Compare commits

...

352 Commits
v1.1.0 ... 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
Tyler Stewart
7acf861a4d fix(cli): correctly setup server, add pasv options (#130) 2018-11-19 09:48:24 -07:00
Tyler Stewart
4801ecc0cc fix: correct timeouts around TLS data connection (#128)
* fix: timeouts when using tls

* fix: correct tls connection

* fix(connector): dont prematurely destroy socket

* fix(passive): set connected if not tls

* refactor: dont return promises on connector end

Since we're not waiting, we don't need to return promises
2018-11-12 03:19:57 +00:00
Qian.Sicheng
8e34e4c71a fix: correct type definitions (#125)
* fix types of server options

* fix usage of server options
2018-11-07 08:29:37 -07:00
Tyler Stewart
0afd578683 chore: update semantic-release (#124) 2018-10-30 01:48:36 +00:00
Tyler Stewart
46b0d52ff2 fix: improve uploading of files 2018-10-30 01:38:59 +00:00
Tyler Stewart
185e473edc chore: update test example 2018-10-30 01:38:59 +00:00
Tyler Stewart
92a323f3dd chore: update fs method links 2018-10-30 01:38:59 +00:00
Tyler Stewart
f67e487306 test: update tests 2018-10-30 01:38:59 +00:00
Tyler Stewart
2716123da7 chore: add Johnnyrook777 to contributors 2018-10-30 01:38:59 +00:00
Tyler Stewart
ef207f60c1 feat: replace tls options with actual tls SecureContext
https://github.com/trs/ftp-srv/pull/108

BREAKING CHANGE: tls options no longer assume file paths are passed for key, ca, and cert. Options are passed directly to `tls.createServer`. See: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
2018-10-30 01:38:59 +00:00
Tyler Stewart
4d8cf42ad0 chore: update readme with changed options 2018-10-30 01:38:59 +00:00
Tyler Stewart
50c6b92d12 fix: improve connection close events and handlers (#123)
* (fix) base: set datasocket to null once destroyed (#119)

Ensure new sockets get created when uploading files by setting the datasocket to null after its destroyed

*  fix(stor): use the close event (#118)

The 'end' event is not called when using SSL. Should use the 'close' event

* refactor(list): chained promises with less depth (#117)

* Update list.js

Tidy up of the List function.

Makes the intent of the method clearer, as there are less nested promises

* refactor(list): chained promises with less depth

* refactor: fixup formatting

* test: update test file config
2018-10-25 10:25:52 -06:00
Tyler Stewart
a2103e5a3c fix: command parse ignores flags on RETR and SIZE (#122)
* fix: command parse ignores flags on RETR and SIZE (#121)

fixes #120

* refactor: upper directive before checking flag parse

* test: add test for command option parse

* feat: restrict command flags to single dash, single character

Only parse `-x` style flags.
2018-10-25 10:25:32 -06:00
Mark Wheeldon
2302b749fa fix(connector): gracefully end data socket (#116)
Fixes GnuTLS error -110: The TLS connection was non-properly terminated issue
2018-09-21 13:54:59 -06:00
Tyler Stewart
27b43d702b chore: update scripts 2018-09-02 16:20:48 +00:00
Tyler Stewart
fae003e644 test(passive): correctly close test connections 2018-09-02 16:20:48 +00:00
Tyler Stewart
a51678ae70 feat: start pasv port selection from last checked port (#111)
* Update find-port.js

Reuse the Port Number Generator

* feat(passive): initialize port factory in server

Ensures that each passive connection does not have to start from the beginning of the pasv port range

https://github.com/trs/ftp-srv/pull/110

* fix(connector): ensure data socket is destroyed and resolved

If there was a data socket, this would never resolve

* test: use bluebird promises for tests, fix stubs

* fix(commands): ensure connector is ended before resuming
2018-09-02 16:20:48 +00:00
Tyler Stewart
bc26886a0d fix(fs): wrap fs methods in try
This handles throwing better than `resolve`
2018-09-02 16:20:48 +00:00
Tyler Stewart
c9b4371579 feat: respond with full paths in STOR/RETR (#95)
Emit and reply with the full paths to the files.

BREAKING CHANGE: fs expects an object `{stream, clientPath}` in response to `.read()` and `.write()`.

This is implemented in a backwards compatible way, and works with the old `fs` implementation. But since this may break builds in unintented ways.
2018-09-02 16:20:48 +00:00
Tyler Stewart
95471bdd15 test: update tests with refactors 2018-09-02 16:20:48 +00:00
Tyler Stewart
5a36a6685d refactor(passive): increment passive connection ports
Using a generator, will loop through min and max ports in order

This should be faster and more efficient since it starts after the last valid port meaning it has a higher chance of being valid
2018-09-02 16:20:48 +00:00
Tyler Stewart
90a7419661 feat: change server options
BREAKING CHANGE: some options have moved or been renamed
2018-09-02 16:20:48 +00:00
Tyler Stewart
29cb035f66 chore: migration guide v2 to v3 2018-09-02 16:20:48 +00:00
Tyler Stewart
66fc66ed80 fix(connector): build passive server correctly for tls (#106)
* fix(connector): build passive server correctly for tls

Fixes an issue where passive tls connections would never be fulfilled.

This uses `tls` to create a server if the connection is secure, which allows an ftp client to connect correctly

* test: stop skipping explicit tests
2018-08-08 19:07:06 -06:00
Tyler Stewart
c970a42132 chore(package): update package lock (#107)
Fixes vulnerabilities in packages
2018-08-08 19:04:38 -06:00
Tim Lundqvist
30ae54a952 fix: better typings for EventEmitter inheritance (#105) 2018-07-24 11:46:03 -06:00
Tyler Stewart
91be338ebd fix: expose errors as export 2018-07-22 18:47:23 -06:00
Tyler Stewart
2a5013447c fix: fix event emitter extension typing 2018-07-22 18:44:15 -06:00
alancnet
1f15af0fb6 fix(cli): resolve authentication bug (#94)
cli would reject all logins with `530 Cannot destructure property `password` of 'undefined' or 'null'.` because the credentials object was being indexed with `[object Object]`. Even with that fixed, if the username was not found, it would produce that error.
2018-06-08 01:52:51 +00:00
Tyler Stewart
1cf1f750f4 chore(readme): update contributors 2018-06-02 18:51:27 +00:00
Diego Rodríguez Baquero
442490d713 chore(readme): correct word and improve events (#91) 2018-06-02 18:51:27 +00:00
Tyler Stewart
58b9ba27d9 test(fs): add fs tests 2018-05-31 14:28:10 +00:00
Tyler Stewart
87a2138cb3 fix(fs): improve path resolution 2018-05-31 14:28:10 +00:00
Tyler Stewart
9fd423c745 docs: fix circle ci badge
Links only to master branch
2018-05-25 17:34:25 -06:00
Tyler Stewart
363839ec8f chore: fix markdown newlines 2018-05-25 22:50:01 +00:00
Tyler Stewart
d9fc0c9cac feat: pasv_url option to send to client
This has passive connections to listen on the same hostname as the server.
But allows this to be customized via the `pasv_url` option.

Hostnames are no longer resolved if given `0.0.0.0`, except when being given to the client via `PASV`
2018-05-25 22:50:01 +00:00
Tyler Stewart
b0463d65b6 fix(passive): listen on server hostname 2018-05-25 22:50:01 +00:00
Tyler Stewart
47b2cc0593 chore(readme): add cli documentation 2018-05-18 21:21:43 +00:00
Tyler Stewart
e5f24f991d refactor(cli): update state management, call ftp methods 2018-05-18 21:21:43 +00:00
Edin Mujagic
87f3ae79a1 feat(cli): add cli support for ftp-srv 2018-05-18 21:21:43 +00:00
Tyler Stewart
3a7b3d4570 chore(circle): update circle config 2018-05-18 20:53:01 +00:00
Tyler Stewart
dc040eaabd chore: remove unused dev packages/settings 2018-05-18 20:53:01 +00:00
Tyler Stewart
fecec961e1 docs(contributing): update contribution guide
This was an auto generated document, actually make it
2018-05-18 20:53:01 +00:00
Tyler Stewart
5ff677ce42 fix(passive): improve remoteAddress comparisons
Instead of a direct equal comparison, use the `ip` package
2018-05-18 20:53:01 +00:00
Tyler Stewart
cc0f2a5cd3 feat: set hostname to listen on
By default, `net.Server` listens on `0.0.0.0`, which is any available address. This ensures that ftp-srv only listens on the set hostname.
2018-05-18 20:53:01 +00:00
Tim Lundqvist
414433a56e fix: reject on server listen error
See trs/ftp-srv#84
2018-05-18 20:53:01 +00:00
Tyler Stewart
a794f1e5b3 test: fix tests 2018-05-07 11:08:16 -06:00
Mateusz Kucharczyk
1b5d22a3ca fix(typings): updated typescript typings to allow compiling with noImplicitAny option enabled 2018-05-07 10:07:31 -06:00
Jorin Vogel
4205caf7ac fix(rest): small typo 2018-03-29 23:27:33 -06:00
Tyler Stewart
a468d4ffd0 chore(readme): add file events to docs 2018-02-09 19:38:04 -07:00
Tyler Stewart
40b08893ac refactor(connection): only return IP of commandSocket
Since the dataSocket and commandSocket must be on the same IP for a data channel to open, it is redundant to check the dataSocket IP.
2018-02-09 19:38:04 -07:00
Tyler Stewart
8a2454ceea fix(connection): remove listeners on close 2018-02-09 19:38:04 -07:00
Tyler Stewart
0c7cc4fe6e test: new events 2018-02-09 19:38:04 -07:00
Tyler Stewart
6ea6baceb0 feat(server): extend event emitter 2018-02-09 19:38:04 -07:00
Tyler Stewart
b07e0189ee feat(connection): extend event emitter
Allow custom events
2018-02-09 19:38:04 -07:00
Tyler Stewart
ec30a5a4f3 feat(stor): emit connection event on upload success/failure 2018-02-09 19:38:04 -07:00
Tyler Stewart
6020409979 feat(retr): emit connection event on success/failure 2018-02-09 19:38:04 -07:00
Tyler Stewart
c60606971a chore(readme): update badges 2018-01-21 14:55:04 -07:00
Tyler Stewart
bd41b31821 chore(readme): update logo placement 2018-01-21 14:31:27 -07:00
Tyler Stewart
ce1c526c41 chore(logo): new logo design 2018-01-21 14:31:27 -07:00
Tyler Stewart
d822101a07 chore(readme): update coveralls badge 2018-01-10 11:13:28 -07:00
Tyler Stewart
47c8eedd3b chore: fix semantic release 2018-01-10 11:01:39 -07:00
Tyler Stewart
6c08cc2aed refactor: assimilate promises using resolve instead of try 2018-01-10 10:49:02 -07:00
Tyler Stewart
e2a5c78b0a fix(abor): send 225 if no file transfer in progress 2018-01-10 10:49:02 -07:00
Tyler Stewart
2cadac3f7e chore(readme): add reference links 2018-01-10 10:49:02 -07:00
Tyler Stewart
2255be9acd feat(connector): return promise on end 2018-01-10 10:49:02 -07:00
Tyler Stewart
d22c911a36 refactor(connection): completely parse message before handling 2018-01-10 10:49:02 -07:00
Tyler Stewart
5dabbc251b refactor(opts): simplify setting 2018-01-10 10:49:02 -07:00
Tyler Stewart
ef89577627 refactor(list): simplify reply 2018-01-10 10:49:02 -07:00
Tyler Stewart
8fbe750086 refactor(stat): simplify replys 2018-01-10 10:49:02 -07:00
Tyler Stewart
3b33508f44 feat: migrate to bluebird
Replace `when` with `bluebird` promise library
2018-01-10 10:49:02 -07:00
Tyler Stewart
23368b04b9 test: update stor fail test timing 2018-01-10 10:24:41 -07:00
Tyler Stewart
876a061e92 refactor: ensure reject is called on destroyConnection 2018-01-10 10:24:41 -07:00
Tyler Stewart
65b1fd27a0 fix(stor): pause connection to avoid memory build up 2018-01-10 10:24:41 -07:00
James Suárez
286c1063fa fix(retr): pause read stream to avoid memory build up 2018-01-10 10:24:41 -07:00
Tyler Stewart
e87c36d7ff chore: fix semantic releasing 2017-12-08 17:50:16 -07:00
Tyler Stewart
de0aafad2f fix(stat): fix stat on file
Ensure message sent raw
2017-12-08 17:50:16 -07:00
Tyler Stewart
4f80e11745 fix(stat): fix file response
Ensures correct response when `stat`-ing a file

```
212-Status begin
...
212 Status end
```
2017-12-08 17:40:10 -07:00
Tyler Stewart
6bbd905379 test: update integration tests 2017-11-12 20:50:11 -07:00
Tyler Stewart
de50f55457 test(circleci): fix workflow 2017-11-12 18:01:26 -07:00
Ozair Patel
32cdedd163 chore: add public properties to typings 2017-11-12 17:50:51 -07:00
Tyler Stewart
6c2c1a87dc test: use mock fs 2017-11-12 17:50:51 -07:00
Tyler Stewart
9e83143690 chore: improve tests 2017-11-12 17:50:51 -07:00
Tyler Stewart
0238529edf chore: change dev dependencies 2017-11-12 17:50:51 -07:00
Tyler Stewart
d0c204eb81 chore: migrate to circle ci 2.0 2017-11-12 17:50:51 -07:00
Tyler Stewart
cdebe9a464 chore: remove .env 2017-11-12 17:50:51 -07:00
Tyler Stewart
eeb8f9ab4d chore(mocha): use pretty bunyan nyan reporter 2017-11-12 17:50:51 -07:00
Tyler Stewart
60d06c21c8 test: update test flow
Wrap each difference in a describe block
2017-11-12 17:50:51 -07:00
Tyler Stewart
8609b1d02e chore(package): update dependenices
Adds back package-lock
2017-11-12 17:50:51 -07:00
Tyler Stewart
80b05215ff chore(config): add timeouts to tests 2017-11-12 17:50:51 -07:00
Peter Keuter
37f0a15549 fix: updated typings 2017-11-06 10:21:38 -07:00
Tyler Stewart
1ba67034b1 chore(README): fix class name references 2017-10-31 16:18:36 -06:00
Tyler Stewart
0a331c5998 fix(package): correct main file
This was changed with the confit update
2017-10-31 16:18:36 -06:00
Tyler Stewart
a7103ded7e fix: add values to resolved promises 2017-10-30 18:48:42 -06:00
Tyler Stewart
d787d4cab6 test(site): test site registry call 2017-10-30 17:52:05 -06:00
Tyler Stewart
154cd5a5d7 chore: eslint --fix 2017-10-30 17:52:05 -06:00
Tyler Stewart
5fc59b50b1 chore: update package versions 2017-10-30 17:52:05 -06:00
Tyler Stewart
043c97c80f chore: update build with confit 2017-10-30 17:52:05 -06:00
Tyler Stewart
772fe5ca06 feat: add eprt and epsv for IPv6 support 2017-10-30 17:52:05 -06:00
Ozair Patel
e272802525 feat(typings): swapped declare class for export interface 2017-10-25 21:46:24 -06:00
Ozair Patel
7589322abc feat(typings): removed Server extension for FtpServer 2017-10-25 21:46:24 -06:00
Ozair Patel
fae5564041 feat(typings): removed typed dependency and fixed import error 2017-10-25 21:46:24 -06:00
Ozair Patel
e9b4a6385d feat(typings): ypdated typescript typings 2017-10-25 21:46:24 -06:00
Tyler Stewart
71621aae4f Merge pull request #44 from trs/include-types-file-on-install
fix(package): include types file
2017-10-25 12:22:57 -06:00
Tyler Stewart
0eaa0f8743 chore(travis): only test v6
Node 8 fails for some reason, will look into in the future
2017-10-25 12:18:14 -06:00
Tyler Stewart
8828a4ea09 fix(package): include types file
Should ensure types are inluded on an install
2017-10-25 12:00:20 -06:00
Tyler Stewart
b33659320f Merge pull request #40 from trs/fix-socket-ref
fix(retr): check for connector socket
2017-09-30 11:14:31 -06:00
Tyler Stewart
6a6b949d3b fix(retr): check for connector socket
Ensures socket still exists and client hasn't disconnected
2017-09-30 11:09:30 -06:00
Tyler Stewart
283be85db3 Merge pull request #38 from trs/improve-connector-stream-handling
Improve connector stream handling
2017-08-18 12:45:32 -06:00
Tyler Stewart
e555ce9230 test(stor): add failure test 2017-08-18 12:06:58 -06:00
Tyler Stewart
e6575808f1 fix(stor): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
a5e58a106e fix(retr): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
ed086e576a Merge pull request #36 from trs/fix-process-exit
Fix process exit
2017-08-14 17:07:05 -06:00
Tyler Stewart
31f0f3b0dc fix: ensure process exits 2017-08-14 16:46:11 -06:00
Tyler Stewart
d763820c86 refactor: connector socket getter 2017-08-14 16:45:37 -06:00
Tyler Stewart
f3183314cc Merge pull request #34 from trs/add-typings
feat(typings): add TypeScript .d.ts files
2017-07-24 11:00:13 -06:00
Chris Rabl
dde7b36c46 feat(typings): add TypeScript .d.ts files
* created outline for TypeScript declarations
* added basic object shapes for FtpServer and FileSystem
2017-07-24 10:53:41 -06:00
Tyler Stewart
00af9e7e61 Merge pull request #32 from trs/command-args
Command args
2017-07-10 10:02:55 -06:00
Tyler Stewart
99a885cd44 test(commands): update parser tests 2017-07-10 09:58:24 -06:00
Tyler Stewart
443051d753 fix(commands): get flags from ftp command 2017-07-07 17:28:09 -06:00
Tyler Stewart
27ecc4d835 fix(feat): order features alphabetically 2017-07-06 17:51:06 -06:00
Tyler Stewart
c8526be1f4 Merge pull request #30 from trs/fix-utf8
Fix utf8
2017-06-26 19:54:51 -06:00
Tyler Stewart
e0b11ff480 fix: cleanup server 2017-06-26 16:39:00 -06:00
Tyler Stewart
58b9d8db9d chore: update fs readme 2017-06-26 16:39:00 -06:00
Tyler Stewart
fa121ba0fd test(REST): add tests 2017-06-26 16:39:00 -06:00
Tyler Stewart
2e02dc20ad feat(REST): add support for REST command
Allows the client to resume a transfer at the specified bytes
2017-06-26 16:38:59 -06:00
Tyler Stewart
8aeb6976d2 fix(auth): update checks, ensure secure is set using ftps 2017-06-26 16:38:59 -06:00
Tyler Stewart
84a68ae03c chore: dont append branch name to commit 2017-06-26 12:34:21 -06:00
Tyler Stewart
9dfc80b99d test(OPTS): update tests
master
2017-06-26 11:19:42 -06:00
Tyler Stewart
090e3d8105 chore: update log levels 2017-06-26 11:13:55 -06:00
Tyler Stewart
c3b0dbf5b0 feat(OPTS): add opts command handler for utf8
master
2017-06-26 11:13:49 -06:00
Tyler Stewart
69a5133936 fix(FEAT): correctly display feature list 2017-06-26 11:13:00 -06:00
Tyler Stewart
5394908a6b Merge pull request #29 from trs/fix-tls-check
Fix encoding/transfer type
2017-06-20 17:20:23 -06:00
Tyler Stewart
3e7bd5bcf9 chore: update licence format
Still MIT, just updating to ensure GitHub recognizes it
2017-06-20 17:09:49 -06:00
Tyler Stewart
175b422c5f chore: update dependencies 2017-06-20 17:02:05 -06:00
Tyler Stewart
b2a9851204 fix: ensure utf8 support; allows accent characters 2017-06-20 16:56:26 -06:00
Tyler Stewart
977dd1579a fix(TYPE): correctly set types, only use for data connections 2017-06-20 16:55:37 -06:00
Tyler Stewart
176b2b7ca8 fix: actually check _tls 2017-06-20 14:47:44 -06:00
Tyler Stewart
63777c0d74 Merge pull request #26 from trs/test-updates
Test updates
2017-06-16 15:17:31 -06:00
Tyler Stewart
9be8ffa60d test: add and update tests 2017-06-09 15:00:01 -06:00
Tyler Stewart
b8cd6022e1 fix: assign tls options to empty object 2017-06-09 14:40:19 -06:00
Tyler Stewart
0618a3c675 fix(passive): throw error on invalid port range 2017-06-09 14:39:21 -06:00
Tyler Stewart
3c533a5fbc Merge pull request #25 from trs/export-fs
Export fs
2017-06-09 10:31:42 -06:00
Tyler Stewart
3d0a58ca15 docs(readme): update file system override details
master

export-fs

export-fs
2017-06-09 10:28:03 -06:00
Tyler Stewart
4b4c809af8 feat: export FileSystem and FtpSrv
Non breaking, as it still exports FtpSrv by default
2017-06-09 10:28:03 -06:00
Tyler Stewart
a234534de0 Merge pull request #23 from trs/write-stream-close
feat(fs): close stream before ending
2017-06-08 17:35:58 -06:00
Tyler Stewart
635fb35341 fix(stor): end stream if no close listener present 2017-06-08 17:31:02 -06:00
Tyler Stewart
51a6448ac2 fix(stor): ensure stream is destroyed once used up 2017-06-08 17:23:14 -06:00
Tyler Stewart
4d8a69615c feat(fs): close stream before ending
This allows processing of the received data before a response is sent to the client.

write-stream-close
2017-06-08 17:17:09 -06:00
Tyler Stewart
ab5a2e9641 chore(travis): dont cleanup on deploy 2017-06-08 15:19:35 -06:00
Tyler Stewart
a7f25accd2 Merge pull request #21 from trs/list-fix
List fix
2017-06-08 15:16:09 -06:00
Tyler Stewart
c49a361c36 chore(package): add package-lock 2017-06-08 14:10:56 -06:00
Tyler Stewart
d3d65aa5cf chore(package): update package dependency versions 2017-06-08 14:09:44 -06:00
Tyler Stewart
e53848f881 chore(travis): test multiple node versions, update deployment script
list-fix

HEAD
2017-06-08 14:03:03 -06:00
Tyler Stewart
b5cf75b09f chore(package): specify npm files 2017-06-08 13:54:22 -06:00
Tyler Stewart
dd0a790519 docs(readme): update fs docs to reflect list changes 2017-06-08 13:40:41 -06:00
Tyler Stewart
e25ee55865 test: update tests to reflect list changes 2017-06-08 13:40:27 -06:00
Tyler Stewart
0433bb48cd fix(stat): allow a file as an argument
If a file is passed as an argument, will send only that file's stat.
Otherwise, send the stats for all items in directory (current or provided)
2017-06-08 13:38:00 -06:00
Tyler Stewart
350c2b3e81 chore(readme): add coveralls badge 2017-05-25 23:24:26 -06:00
Tyler Stewart
887bf1fa58 chore: upload test coverage to coveralls 2017-05-25 23:02:37 -06:00
Tyler Stewart
70a62f1da1 Merge pull request #17 from trs/update-logo
fix: update logo
2017-05-20 21:34:19 -06:00
Tyler Stewart
3a1afdb694 fix: update logo
update-logo

update-logo

update-logo

update-logo

update-logo

update-logo
2017-05-20 21:31:22 -06:00
Tyler Stewart
10127b32e5 chore(readme): move badges under description 2017-05-19 13:08:34 -06:00
Tyler Stewart
e1aad3e021 Merge pull request #16 from trs/fix-secure-check
Fix secure check
2017-05-19 13:01:35 -06:00
Tyler Stewart
a254e6c5f3 chore: update logo, generate png
master

master

master
2017-05-19 12:28:29 -06:00
Tyler Stewart
c46d6086ea fix: simplifiy secure check 2017-05-19 12:28:29 -06:00
Tyler Stewart
88f02cd498 Merge pull request #14 from trs/explicit-tls-support
feat: add explicit TLS support with AUTH
2017-05-15 21:38:44 -06:00
Tyler Stewart
2cc5d54d7f chore: add logo
explicit-tls-support

explicit-tls-support

explicit-tls-support

explicit-tls-support

explicit-tls-support
2017-05-15 21:35:56 -06:00
Tyler Stewart
f127d0e7b6 fix: correct user/pass tests 2017-05-15 15:55:34 -06:00
Tyler Stewart
13048a96bd docs(commands): fix command sytanx structure 2017-05-15 15:55:34 -06:00
Tyler Stewart
f6355e66c3 fix: correctly set _tls to false if not set
explicit-tls-support
2017-05-15 15:55:34 -06:00
Tyler Stewart
6e79e958cc fix: improve anonymous login
Only initiate anonymous login if username is anonymous
2017-05-15 15:55:34 -06:00
Tyler Stewart
db7d88f411 docs(readme): add explicit connections to features 2017-05-15 15:55:33 -06:00
Tyler Stewart
323ee62110 test: test explicit TLS 2017-05-15 15:55:33 -06:00
Tyler Stewart
1e446a7801 fix: add no_auth flag to secure commands 2017-05-15 15:55:33 -06:00
Tyler Stewart
977fbd4190 chore(package): fix username change 2017-05-15 15:55:33 -06:00
Tyler Stewart
d5d1b98b04 test: test new secure commands 2017-05-12 11:50:58 -06:00
Tyler Stewart
df0a4d640c fix: update secure checks 2017-05-12 11:50:43 -06:00
Tyler Stewart
73274191fe feat: add explicit tls support for active transfer
explicit-tls-support
2017-05-12 11:50:37 -06:00
Tyler Stewart
37c3da3a62 fix: correctly reference connection in client-error event 2017-05-12 11:32:54 -06:00
Tyler Stewart
9bece5f946 feat: add explicit TLS support with AUTH 2017-05-11 18:59:54 -06:00
Tyler Stewart
83947142df Merge pull request #13 from stewarttylerr/implicit-tls-feats
Implicit tls feats
2017-05-11 17:34:14 -06:00
Tyler Stewart
c54045e0b9 fix: correct ls and ep file stat formats
master

master

master

master
2017-05-11 16:48:08 -06:00
Tyler Stewart
cf71243729 fix: get dataSocket ip if available 2017-05-11 16:38:14 -06:00
Tyler Stewart
7fb43a5790 test: add test certs 2017-05-11 15:49:28 -06:00
Tyler Stewart
e99059125e feat(connection): add client-error events 2017-05-11 15:49:28 -06:00
Tyler Stewart
954e9a1252 feat(server): add greeting and implicit TLS 2017-05-11 15:45:28 -06:00
Tyler Stewart
2b9e163958 chore: exclude errors file from test coverage 2017-05-11 15:39:36 -06:00
Tyler Stewart
c6a49d2191 docs(readme): update readme readability 2017-05-11 15:39:14 -06:00
Tyler Stewart
14e5f87cc3 fix: allow error messages to be set via catch 2017-05-11 15:36:49 -06:00
Tyler Stewart
580b8d6eae Merge pull request #12 from stewarttylerr/fix-compatability-bugs
Fix compatability bugs
2017-05-05 18:29:00 -06:00
Tyler Stewart
a75d63df92 fix(fs): resolve paths correctly
Should solve win32 issues

fix-compatability-bugs

fix-compatability-bugs
2017-05-05 18:08:47 -06:00
Tyler Stewart
301ae110e8 test: update tests and directory structure 2017-05-05 18:08:30 -06:00
Tyler Stewart
4d69b48466 chore: update mocha reporter 2017-05-05 18:08:30 -06:00
Tyler Stewart
ec010697bb feat(commands): remove minimist command parser for basic parsing
Tokenizes command string based on spaces, concats remaining arguments

This allows filesystem commands to handle directories/files with spaces.

fix-compatability-bugs
2017-05-05 18:08:30 -06:00
Tyler Stewart
cf3d543f1a fix(commands): correctly clone command for log 2017-05-05 18:04:54 -06:00
Tyler Stewart
69bec2b01c fix(fs): normalize fs paths
- Attempting to fix compatability on windows
2017-05-04 17:43:46 -06:00
Tyler Stewart
2eac41d127 test: update test setup
master
2017-05-04 17:43:41 -06:00
Tyler Stewart
eb32f93fc6 feat: close server on SIGINT (ctrl+c) 2017-05-04 17:42:47 -06:00
Tyler Stewart
095423606e fix(find-port): stop check at 65535 2017-05-04 17:42:10 -06:00
Tyler Stewart
61cf1bda39 feat(connection): add helper get for socket remote address 2017-05-04 17:40:37 -06:00
Tyler Stewart
75f847ed5d feat(commands): obfuscate password from logs 2017-05-04 17:40:01 -06:00
Tyler Stewart
ad4b32fc13 chore: add .env to github for tests 2017-05-04 17:39:15 -06:00
Tyler Stewart
be3c57bed0 Merge pull request #10 from stewarttylerr/sandbotorg-master
Sandbotorg master
2017-04-27 13:27:46 -06:00
Tyler Stewart
dc7dd1075c test(passive): merge master 2017-04-27 13:22:57 -06:00
salper
543e6cc1cc chore(readme): fix grammar 2017-04-27 13:22:39 -06:00
salper
5c1f8f7a65 fix: plug QUIT command
master
2017-04-27 13:19:29 -06:00
Tyler Stewart
557995a1a9 test(travis): add env variables 2017-04-27 13:17:01 -06:00
Tyler Stewart
45eca5afe0 test(passive): fix test 2017-04-27 12:49:55 -06:00
Tyler Stewart
695e594d97 Merge pull request #6 from stewarttylerr/migate-to-moment
feat: migrate to moment from date-fns, fix ls format
2017-03-31 17:14:44 -06:00
Tyler Stewart
97b55fc92c feat: migrate to moment from date-fns, fix ls format
Date-fns is great, but too early for use
2017-03-31 17:12:57 -06:00
Tyler Stewart
577066850b fix: improve getting current directory 2017-03-30 12:26:04 -06:00
Tyler Stewart
0ec989cf1e docs: update login event for new root option 2017-03-29 10:20:22 -06:00
Tyler Stewart
568833e216 Merge pull request #4 from stewarttylerr/set-fs-root
feat(fs): allow default file system root to be set
2017-03-29 10:15:33 -06:00
Tyler Stewart
6b0c06e588 fix: update tests 2017-03-29 10:13:32 -06:00
Tyler Stewart
acd485a571 test: more tests
set-fs-root
2017-03-28 14:27:40 -06:00
Tyler Stewart
2b2ca45673 test: update and add tests 2017-03-27 17:57:03 -06:00
Tyler Stewart
a62b6f9559 fix: resolve disconnectClient promise, linting 2017-03-27 17:51:10 -06:00
Tyler Stewart
84d54cbc2b fix(TYPE): correctly set encoding 2017-03-27 17:51:10 -06:00
Tyler Stewart
ef6134d91b fix: wrap fs calls with when, linting 2017-03-27 17:51:10 -06:00
Tyler Stewart
043d9369cc chore(readme): minor text fixes 2017-03-27 17:51:10 -06:00
Tyler Stewart
6b81748fd7 chore: minor text fixes 2017-03-27 17:51:10 -06:00
Tyler Stewart
0f4f5cdbd7 chore(readme): update api explainations
master
2017-03-27 17:51:10 -06:00
Tyler Stewart
0293752635 chore(readme): minor text fixes 2017-03-27 13:57:54 -06:00
Tyler Stewart
aa278105f9 chore: minor text fixes 2017-03-27 13:56:51 -06:00
Tyler Stewart
bbe0bf2942 chore(readme): update api explainations
master
2017-03-16 14:09:31 -06:00
Tyler Stewart
846df72e24 feat(fs): allow default file system root to be set
Enables users to only access portions of the file system (eg /home/user)
2017-03-13 13:29:15 -06:00
Tyler Stewart
8227c512dd Merge pull request #1 from stewarttylerr/feat-abor-stou
Feat abor stou
2017-03-10 14:08:47 -07:00
Tyler Stewart
b7e17af99e feat: allow STRU to take name suggesstion 2017-03-10 13:43:37 -07:00
Tyler Stewart
9276f7f448 feat: add STOU command
master
2017-03-10 13:36:09 -07:00
Tyler Stewart
99a0ebd536 feat: add ABOR command
master
2017-03-10 13:35:19 -07:00
Tyler Stewart
0c5f8562d5 fix: log cleanup
master
2017-03-10 13:35:19 -07:00
Tyler Stewart
5154743a3a chore: correct example 2017-03-08 20:55:16 -07:00
Tyler Stewart
6654f2c25c fix(commands.list): fix first file not sending 2017-03-08 16:16:32 -07:00
Tyler Stewart
83540d268a fix: update feat and help commands
master
2017-03-08 12:45:18 -07:00
Tyler Stewart
795c3d7c65 refactor(commands): commands now store all info about themselves
Makes it easier to modify a command

Each command exports an object:
{
  directive: string or array of commands that call this handler
  handler: function to process the command
  syntax: string of how to call the command
  description: human readable explaination of command
  flags: optional object of flags
}
2017-03-08 12:31:44 -07:00
Tyler Stewart
f6d1a3828a feat: add black/white list for commands
Allow black/white list to be set for individual connections

BREAKING CHANGE: name change, removed `disabled_commands`
2017-03-07 18:41:27 -07:00
Tyler Stewart
e5b10c5858 chore: rename to ftp-srv, flows better
master
2017-03-07 18:31:06 -07:00
Tyler Stewart
4a6ab71731 chore: merge commit '43cb87a' 2017-03-06 17:55:12 -07:00
Tyler Stewart
ccc053ac8d chore(readme): update README 2017-03-06 17:49:24 -07:00
Tyler Stewart
43cb87a660 chore(readme): update README 2017-03-06 17:29:31 -07:00
Tyler Stewart
1d07ff86d5 feat: allow commands to be disabled via options 2017-03-06 17:25:09 -07:00
182 changed files with 24329 additions and 2987 deletions

112
.circleci/config.yml Normal file
View File

@@ -0,0 +1,112 @@
version: 2.1
orbs:
node: circleci/node@5.0.2
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
executors:
node-lts:
parameters:
node-version:
type: string
default: lts
docker:
- image: cimg/node:<< parameters.node-version >>
jobs:
lint:
executor: node-lts
steps:
- checkout
- 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: Dry Release
command: |
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
release_scheduled:
triggers:
# 6:03 UTC (mornings) 1 monday
- schedule:
cron: "3 6 * * 1"
filters:
branches:
only:
- main
jobs:
- 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:
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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
package-lock.json binary

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,5 +1,6 @@
test_tmp/
node_modules/
dist/
reports/
.env
npm-debug.log

View File

@@ -1,12 +0,0 @@
language: node_js
node_js:
- "6"
install: npm install
script:
- npm run verify:js
- npm run test:coverage
after_success:
- if [ $TRAVIS_BRANCH = 'master' ]; then npm run semantic-release; fi

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

@@ -1,208 +1,25 @@
<!--[CN_HEADING]-->
# Contributing
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="400px" />
</a>
</p>
Welcome! This document explains how you can contribute to making **ftp-svr** even better.
<h1 align="center">
Contributing Guide
</h1>
## Welcome
<!--[]-->
- Thank you for your eagerness to contribute, pull requests are encouraged!
<!--[CN_GETTING_STARTED]-->
# Getting Started
## Installation
```
git clone <this repo>
npm install -g commitizen
npm install -g semantic-release-cli
npm install
```
<!--[]-->
<!--[RM_DIR_STRUCTURE]-->
## Directory Structure
Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks:
```
ftp-svr/
├──config/ * configuration files live here (e.g. eslint, verify, testUnit)
├──src/ * source code files should be here
├──dist/ * production-build code should live here
├──reports/ * test reports appear here
├──test/ * unit test specifications live here
├──confit.yml * the project config file generated by 'yo confit'
├──CONTRIBUTING.md * how to contribute to the project
├──README.md * this file
└──package.json * NPM package description file
```
<!--[]-->
<!--[CN_GITFLOW_PROCESS]-->
# GitFlow Development Process
This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/index.html) workflow.
## Create a branch
When you're working on a project, you're going to have a bunch of different features or ideas in progress at any given time some of which are ready to go, and others which are not. Branching exists to help you manage this workflow.
When you create a branch in your project, you're creating an environment where you can try out new ideas. Changes you make on a branch don't affect the `master` branch, so you're free to experiment and commit changes, safe in the knowledge that your branch won't be merged until it's ready to be reviewed by someone you're collaborating with.
###ProTip
Branching is a core concept in Git, and the entire GitHub Flow is based upon it. There's only one rule: anything in the `master` branch is always deployable.
Because of this, it's extremely important that your new branch is created off of `master` when working on a feature or a fix. Your branch name should be descriptive (e.g., `refactor-authentication`, `user-content-cache-key`, `make-retina-avatars`), so that others can see what is being worked on.
## Add commits
Once your branch has been created, it's time to start making changes. Whenever you add, edit, or delete a file, you're making a commit, and adding them to your branch. This process of adding commits keeps track of your progress as you work on a feature branch.
Commits also create a transparent history of your work that others can follow to understand what you've done and why. Each commit has an associated commit message, which is a description explaining why a particular change was made. Furthermore, each commit is considered a separate unit of change. This lets you roll back changes if a bug is found, or if you decide to head in a different direction.
###ProTip
Commit messages are important, especially since Git tracks your changes and then displays them as commits once they're pushed to the server. By writing clear commit messages, you can make it easier for other people to follow along and provide feedback.
## Open a pull request
Pull Requests initiate discussion about your commits. Because they're tightly integrated with the underlying Git repository, anyone can see exactly what changes would be merged if they accept your request.
You can open a Pull Request at any point during the development process: when you have little or no code but want to share some screenshots or general ideas, when you're stuck and need help or advice, or when you're ready for someone to review your work. By using GitHub's @mention system in your Pull Request message, you can ask for feedback from specific people or teams, whether they're down the hall or ten time zones away.
###ProTip
Pull Requests are useful for contributing to open source projects and for managing changes to shared repositories. If you're using a Fork & Pull Model, Pull Requests provide a way to notify project maintainers about the changes you'd like them to consider. If you're using a Shared Repository Model, Pull Requests help start code review and conversation about proposed changes before they're merged into the `master` branch.
## Discuss and review your code
Once a Pull Request has been opened, the person or team reviewing your changes may have questions or comments. Perhaps the coding style doesn't match project guidelines, the change is missing unit tests, or maybe everything looks great and props are in order. Pull Requests are designed to encourage and capture this type of conversation.
You can also continue to push to your branch in light of discussion and feedback about your commits. If someone comments that you forgot to do something or if there is a bug in the code, you can fix it in your branch and push up the change. GitHub will show your new commits and any additional feedback you may receive in the unified Pull Request view.
###ProTip
Pull Request comments are written in Markdown, so you can embed images and emoji, use pre-formatted text blocks, and other lightweight formatting.
## Merge to `master`
Once your PR has passed any the integration tests and received approval to merge, it is time to merge your code into the `master` branch.
Once merged, Pull Requests preserve a record of the historical changes to your code. Because they're searchable, they let anyone go back in time to understand why and how a decision was made.
###ProTip
By incorporating certain keywords into the text of your Pull Request, you can associate issues with code. When your Pull Request is merged, the related issues are also closed. For example, entering the phrase Closes #32 would close issue number 32 in the repository. For more information, check out our help article.
<!--[]-->
<!--[CN_BUILD_TASKS]-->
## Build Tasks
Command | Description
:------ | :----------
<pre>npm run build</pre> | Generate production build into [dist/](dist/) folder
<pre>npm run dev</pre> | Run project in development mode (verify code, and re-verify when code is changed)
<pre>npm start</pre> | Alias for `npm run dev` task
<!--[]-->
<!--[CN_TEST_TASKS]-->
## Test Tasks
Command | Description
:------ | :----------
<pre>npm test</pre> | Alias for `npm run test:unit` task
<pre>npm run test:coverage</pre> | Run instrumented unit tests then verify coverage meets defined thresholds<ul><li>Returns non-zero exit code when coverage does not meet thresholds (as defined in istanbul.js)</li></ul>
<pre>npm run test:unit</pre> | Run unit tests whenever JS source or tests change<ul><li>Uses Mocha</li><li>Code coverage</li><li>Runs continuously (best to run in a separate window)</li></ul>
<pre>npm run test:unit:once</pre> | Run unit tests once<ul><li>Uses Mocha</li><li>Code coverage</li></ul>
<!--[]-->
<!--[CN_VERIFY_TASKS]-->
## Verification (Linting) Tasks
Command | Description
:------ | :----------
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed
<!--[]-->
<!--[CN_COMMIT_TASKS]-->
## Commit Tasks
Command | Description
:------ | :----------
<pre>git status</pre> | Lists the current branch and the status of changed files
<pre>git log</pre> | Displays the commit log (press Q to quit viewing)
<pre>git add .</pre> | Stages all modified & untracked files, ready to be committed
<pre>git cz</pre> | Commit changes to local repository using Commitizen.<ul><li>Asks questions about the change to generate a valid conventional commit message</li><li>Can be customised by modifying [config/release/commitMessageConfig.js](config/release/commitMessageConfig.js)</li></ul>
<pre>git push</pre> | Push local repository changes to remote repository
<!--[]-->
<!--[CN_DOCUMENTATION_TASKS]-->
<!--[]-->
<!--[CN_RELEASE_TASKS]-->
## Release Tasks
Command | Description
:------ | :----------
<pre>npm run commitmsg</pre> | Git commit message hook that validates the commit message conforms to your commit message conventions.
<pre>npm run pre-release</pre> | Verify code, run unit tests, check test coverage, build software. This task is designed to be run before
the `semantic-release` task.
<ul><li>Run `semantic-release-cli setup` once you have a remote repository. See https://github.com/semantic-release/cli for details.</li><li>Semantic-release integrates with Travis CI (or similar tools) to generate release notes
for each release (which appears in the "Releases" section in GitHub) and
publishes the package to NPM (when all the tests are successful) with a semantic version number.
</li></ul>
<pre>npm run prepush</pre> | Git pre-push hook that verifies code and checks unit test coverage meet minimum thresholds.
<pre>npm run upload-coverage</pre> | Uploads code-coverage metrics to Coveralls.io<ul><li>Setup - https://coveralls.zendesk.com/hc/en-us/articles/201347419-Coveralls-currently-supports</li><li>Define an environment variable called COVERALLS_REPO_TOKEN in your build environment with the repo token from https://coveralls.io/github/<repo-name>/settings</li><li>In your CI configuration (e.g. `travis.yml`), call `npm run upload-coverage` if the build is successful.</li></ul>
<!--[]-->
<!--[CN_CHANGING_BUILD_TOOL_CONFIG]-->
## Changing build-tool configuration
There are 3 ways you can change the build-tool configuration for this project:
1. BEST: Modify the Confit configuration file ([confit.yml](confit.yml)) by hand, then re-run `yo confit` and tell it to use the existing configuration.
1. OK: Re-run `yo confit` and provide new answers to the questions. **Confit will attempt to overwrite your existing configuration (it will prompt for confirmation), so make sure you have committed your code to a source control (e.g. git) first**.
There are certain configuration settings which can **only** be specified by hand, in which case the first approach is still best.
1. RISKY: Modify the generated build-tool config by hand. Be aware that if you re-run `yo confit` it will attempt to overwrite your changes. So commit your changes to source control first.
Additionally, the **currently-generated** configuration can be extended in the following ways:
- The task configuration is defined in [package.json](package.json). It is possible to change the task definitions to add your own sub-tasks.
You can also use the `pre...` and `post...` script-name prefixes to run commands before (pre) and after (post) the generated commands.
- The `entryPoint.entryPoints` string in [confit.yml](confit.yml) is designed to be edited manually. It represents the starting-point(s) of the application (like a `main()` function). A NodeJS application has one entry point. E.g. `src/index.js`
<!--[]-->
## Guidelines
- 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 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`

22
LICENSE
View File

@@ -1,9 +1,21 @@
ftp-svr Copyright (c) 2017 Tyler Stewart
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (c) 2019 Tyler Stewart
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

415
README.md
View File

@@ -1,39 +1,416 @@
# ftp-svr [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
<p align="center">
<a href="https://github.com/autovance/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
<!--[RM_DESCRIPTION]-->
> Modern, extensible FTP Server
<p align="center">
Modern, extensible FTP Server
</p>
<!--[]-->
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [Contributing](#contributing)
- [License](#license)
<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
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Promise based API
## Install
`npm install ftp-svr --save`
`yarn add ftp-svr`
`npm install ftp-srv --save`
## Usage
```js
// Quick start, create an active ftp server.
const FtpSrv = require('ftp-srv');
const port=21;
const ftpServer = new FtpSrv({
url: "ftp://0.0.0.0:" + port,
anonymous: true
});
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
### `new FtpSrv({options})`
#### url
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
Supported protocols:
- `ftp` Plain FTP
- `ftps` Implicit FTP over TLS
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
__Default:__ `"ftp://127.0.0.1:21"`
#### `pasv_url`
`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');
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`
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`
#### `greeting`
A human readable array of lines or string to send when a client connects.
__Default:__ `null`
#### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false`
#### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false`
#### `blacklist`
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]`
#### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`
#### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
- `function () {}` A custom function returning a format or promise for one.
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
#### `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.
```bash
$ ftp-srv [url] [options]
```
```bash
$ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--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:
```js
[
{
"username": "...",
"password": "...",
"root": "..." // Root directory
},
...
]
```
#### `--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) => { ... });
```
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
`connection` [client class object](src/connection.js)
`username` string of username from `USER` command
`password` string of password from `PASS` command
`resolve` takes an object of arguments:
- `fs`
- Set a custom file system class for this connection to use.
- See [File System](#file-system) for implementation details.
- `root`
- If `fs` is not provided, this will set the root directory for the connection.
- The user cannot traverse lower than this directory.
- `cwd`
- If `fs` is not provided, will set the starting directory for the connection
- This is relative to the `root` directory.
- `blacklist`
- Commands that are forbidden for only this connection
- `whitelist`
- If set, this connection will only be able to use the provided commands
`reject` takes an error object
### `server-error`
```js
ftpServer.on('server-error', ({error}) => { ... });
```
Occurs when an error arises in the FTP server.
`error` error object
### `RETR`
```js
connection.on('RETR', (error, filePath) => { ... });
```
Occurs when a file is downloaded.
`error` if successful, will be `null`
`filePath` location to which file was downloaded
### `STOR`
```js
connection.on('STOR', (error, fileName) => { ... });
```
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.
## File System
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
The default file system is exported and can be extended as needed:
```js
const {FtpSrv, FileSystem} = require('ftp-srv');
class MyFileSystem extends FileSystem {
constructor() {
super(...arguments);
}
get(fileName) {
...
}
}
```
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L40)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`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).
<!--[]-->
<!--[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).

144
bin/index.js Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env node
const yargs = require('yargs')
const path = require('path')
const FtpSrv = require('../src')
const errors = require('../src/errors')
const args = setupYargs()
const state = setupState(args)
startFtpServer(state)
function setupYargs() {
return yargs
.option('credentials', {
alias: 'c',
describe: 'Load user & pass from json file',
normalize: true
})
.option('username', {
describe: 'Blank for anonymous',
type: 'string',
default: ''
})
.option('password', {
describe: 'Password for given username',
type: 'string'
})
.option('root', {
alias: 'r',
describe: 'Default root directory for users',
type: 'string',
normalize: true
})
.option('read-only', {
describe: 'Disable write actions such as upload, delete, etc',
boolean: true,
default: false
})
.option('pasv-url', {
describe: 'URL to provide for passive connections',
type: 'string',
alias: 'pasv_url'
})
.option('pasv-min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024,
alias: 'pasv_min'
})
.option('pasv-max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535,
alias: 'pasv_max'
})
.parse()
}
function setupState(_args) {
const _state = {}
function setupOptions() {
if (_args._ && _args._.length > 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 === ''
}
function setupRoot() {
const dirPath = _args.root
if (dirPath) {
_state.root = dirPath
} else {
_state.root = process.cwd()
}
}
function setupCredentials() {
_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)
for (const cred of credentials) {
setCredentials(cred.username, cred.password, cred.root)
}
} else if (_args.username) {
setCredentials(_args.username, _args.password)
}
}
function setupCommandBlacklist() {
if (_args.readOnly) {
_state.blacklist = ['ALLO', 'APPE', 'DELE', 'MKD', 'RMD', 'RNRF', 'RNTO', 'STOR', 'STRU']
}
}
setupOptions()
setupRoot()
setupCredentials()
setupCommandBlacklist()
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 reject(new errors.GeneralError('Invalid username or password', 401))
}
const ftpServer = new FtpSrv({
url: _state.url,
pasv_url: _state.pasv_url,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
anonymous: _state.anonymous,
blacklist: _state.blacklist
})
ftpServer.on('login', checkLogin)
ftpServer.listen()
}

View File

@@ -0,0 +1,44 @@
# Migration Guide - v2 to v3
The `FtpServer` constructor has been changed to only take one object option. Combining the two just made sense.
### From:
```js
const server = new FtpServer('ftp://0.0.0.0:21');
```
### To:
```js
const server = new FtpServer({
url: 'ftp://0.0.0.0:21'
});
```
----
The `pasv_range` option has been changed to separate integer variables: `pasv_min`, `pasv_max`.
### From:
```js
const server = new FtpServer(..., {
pasv_range: '1000-2000'
});
```
### To:
```js
const server = new FtpServer({
pasv_min: 1000,
pasv_max: 2000
})
```
----
The default passive port range has been changed to `1024` - `65535`
----

View File

@@ -1,43 +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: [
{name: 'accounts'},
{name: 'admin'},
{name: 'exampleScope'},
{name: 'changeMe'}
],
// 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: true
};

View File

@@ -1,34 +0,0 @@
// Use JS to support loading of threshold data from external file
var coverageConfig = {
instrumentation: {
root: 'src/'
},
check: require('./thresholds.json'),
reporting: {
print: 'both',
dir: 'reports/coverage/',
reports: [
'cobertura',
'html',
'lcovonly',
'html',
'json'
],
'report-config': {
cobertura: {
file: 'cobertura/coverage.xml'
},
json: {
file: 'json/coverage.json'
},
lcovonly: {
file: 'lcov/lcov.info'
},
text: {
file: null
}
}
}
};
module.exports = coverageConfig;

View File

@@ -1,4 +0,0 @@
test/**/*.spec.js
--reporter list
--no-timeouts
--ui bdd

View File

@@ -1,14 +0,0 @@
{
"global": {
"statements": 70,
"branches": 60,
"functions": 80,
"lines": 80
},
"each": {
"statements": 0,
"branches": 0,
"functions": 0,
"lines": 0
}
}

View File

@@ -1,60 +0,0 @@
# START_CONFIT_GENERATED_CONTENT
confit:
extends: &confit-extends
- plugin:node/recommended
plugins: &confit-plugins
- node
env: &confit-env
commonjs: true # For Webpack, CommonJS
node: true
mocha: true
es6: true
globals: &confit-globals {}
parser: &confit-parser espree
parserOptions: &confit-parserOptions
ecmaVersion: 6
sourceType: module
ecmaFeatures:
globalReturn: false
impliedStrict: true
jsx: false
# END_CONFIT_GENERATED_CONTENT
# Customise this section to meet your needs...
extends: *confit-extends
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-extends" from the line above
# <<: *confit-extends
plugins: *confit-plugins
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-plugins" from the line above
# <<: *confit-extends
env:
<<: *confit-env
globals:
<<: *confit-globals
parser: *confit-parser
parserOptions:
<<: *confit-parserOptions
rules:
max-len:
- warn
- 200 # Line Length
node/no-unpublished-require:
- 2
- allowModules:
- chai
- dotenv
- ftp
- sinon
- sinon-as-promised

View File

@@ -1,48 +0,0 @@
generator-confit:
app:
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
buildProfile: Latest
copyrightOwner: Tyler Stewart
license: MIT
projectType: node
publicRepository: true
repositoryType: GitHub
paths:
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
config:
configDir: config/
input:
srcDir: src/
unitTestDir: test/
output:
prodDir: dist/
reportDir: reports/
buildJS:
_version: df428a706d926204228c5d9ebdbd7b49908926d9
framework: []
frameworkScripts: []
outputFormat: ES6
sourceFormat: ES6
entryPoint:
_version: de20402bf85c703080ef6daf21e35325a3b9d604
entryPoints:
main:
- src/index.js
testUnit:
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
testDependencies: []
verify:
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
jsCodingStandard: eslint
documentation:
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
generateDocs: false
release:
_version: 47f220593935b502abf17cb34a396f692e453c49
checkCodeCoverage: true
commitMessageFormat: Conventional
useSemantic: true
sampleApp:
_version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba
createSampleApp: false
zzfinish: {}

179
ftp-srv.d.ts vendored Normal file
View File

@@ -0,0 +1,179 @@
import type { Server } from 'node:net'
const EventEmitter = import('events').EventEmitter
export class FileSystem {
readonly connection: FtpConnection
readonly root: string
readonly cwd: string
constructor(
connection: FtpConnection,
{
root,
cwd
}?: {
root: any
cwd: any
}
)
currentDirectory(): string
get(fileName: string): Promise<any>
list(path?: string): Promise<any>
chdir(path?: string): Promise<string>
write(
fileName: string,
{
append,
start
}?: {
append?: boolean
start?: any
}
): any
read(
fileName: string,
{
start
}?: {
start?: any
}
): Promise<any>
delete(path: string): Promise<any>
mkdir(path: string): Promise<any>
rename(from: string, to: string): Promise<any>
chmod(path: string, mode: string): Promise<any>
getUniqueName(fileName: string): 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>
}
export interface FtpServerOptions {
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 {
server: Server
constructor(options?: FtpServerOptions)
readonly isTLS: boolean
listen(): any
emitPromise(action: any, ...data: any[]): Promise<any>
// emit is exported from super class
setupTLS(_tls: boolean):
| boolean
| {
cert: string
key: string
ca: string
}
setupGreeting(greet: string): string[]
setupFeaturesMessage(): string
disconnectClient(id: string): Promise<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: '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

6
ftp-srv.js Normal file
View File

@@ -0,0 +1,6 @@
import FtpSrv from './src/index.js'
import FileSystem from './src/fs.js'
import ftpErrors from './src/errors.js'
export default FtpSrv
export { FtpSrv, FileSystem, ftpErrors }

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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())
})

68
meta/logo/logo.html Normal file
View File

@@ -0,0 +1,68 @@
<!doctype html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Overpass+Mono:700" rel="stylesheet">
<style>
body {
padding: 0;
margin: 0;
display: flex;
height: 100vh;
flex-direction: row;
justify-content: center;
}
div {
display: flex;
flex-direction: column;
justify-content: center;
width: 100vw;
}
h1 {
display: flex;
flex-direction: column;
align-self: center;
text-align: center;
margin: 0;
padding: 0px;
width: 75vw;
font-size: 68px;
font-family: 'Overpass Mono', monospace;
font-weight: bold;
line-height: 0.8em;
letter-spacing: -3px;
color: #fff;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke: 1px #0063B1;
text-shadow:
3px 3px 0 #0063B1,
-1px -1px 0 #0063B1,
1px -1px 0 #0063B1,
-1px 1px 0 #0063B1,
1px 1px 0 #0063B1;
}
h1 > span {
display: block;
padding: 0;
margin: 0;
padding-top: 6px;
}
h1 > hr {
padding: 0;
margin: 0;
margin-top: 22px;
border: 1px solid #0063B1;
border-radius: 50%;
}
</style>
</head>
<body>
<div>
<h1>
<span>ftp</span>
<hr />
<span>srv</span>
</h1>
</div>
</body>
</html>

17768
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +1,94 @@
{
"name": "ftp-svr",
"name": "@tabshift/ftp-srv",
"version": "0.0.0",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
"ftp-server",
"ftp-srv",
"ftp-svr",
"ftpd",
"ftpserver",
"server"
],
"license": "MIT",
"main": "src/index.js",
"files": [
"src",
"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/stewarttylerr/ftp-svr"
"url": "https://github.com/autovance/ftp-srv"
},
"scripts": {
"pre-release": "npm-run-all verify test:coverage build ",
"build": "cross-env NODE_ENV=production npm run clean:prod",
"clean:prod": "rimraf dist/",
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:coverage --silent",
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
"start": "npm run dev",
"test": "npm run test:unit",
"test:check-coverage": "cross-env NODE_ENV=test istanbul check-coverage reports/coverage/coverage.json --config config/testUnit/istanbul.js",
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
"test:unit": "chokidar 'src/**/*.js' 'test/**/*.js' -c 'npm run test:unit:once' --initial --silent",
"test:unit:once": "cross-env NODE_ENV=test istanbul cover --config config/testUnit/istanbul.js _mocha -- --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"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",
"verify:js:watch": "chokidar 'src/**/*.js' 'test/**/*.js' 'config/**/*.js' -c 'npm run verify:js:fix' --initial --silent",
"verify:watch": "npm run verify:js:watch --silent"
"pre-release": "npm run verify",
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
"release": {
"verifyConditions": "condition-circle",
"branch": "main",
"branches": [
"main"
]
},
"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": {
"bunyan": "^1.8.5",
"date-fns": "^1.28.0",
"lodash": "^4.17.4",
"minimist-string": "^1.0.2",
"uuid": "^3.0.1",
"when": "^3.7.8"
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.15",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^15.4.1"
},
"devDependencies": {
"chai": "^3.5.0",
"chokidar-cli": "1.2.0",
"coveralls": "2.11.15",
"cross-env": "3.1.4",
"cz-customizable": "4.0.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "3.14.1",
"eslint-config-google": "0.7.1",
"eslint-plugin-node": "3.0.5",
"ftp": "^0.3.10",
"husky": "0.13.1",
"istanbul": "0.4.5",
"mocha": "3.2.0",
"npm-run-all": "4.0.1",
"rimraf": "2.5.4",
"semantic-release": "^6.3.2",
"sinon": "^1.17.7",
"sinon-as-promised": "^4.0.2"
"@commitlint/cli": "^10.0.0",
"@commitlint/config-conventional": "^16.2.1",
"@icetee/ftp": "^1.0.2",
"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": ">=3.9.5"
"node": ">=12"
}
}

View File

@@ -1,3 +0,0 @@
module.exports = function () {
return this.reply(202);
}

View File

@@ -1,19 +0,0 @@
const _ = require('lodash');
module.exports = function ({command} = {}) {
const method = _.upperCase(command._[1]);
switch (method) {
case 'TLS': return handleTLS.call(this);
case 'SSL': return handleSSL.call(this);
default: return this.reply(504);
}
}
function handleTLS() {
return this.reply(504);
}
function handleSSL() {
return this.reply(504);
}

View File

@@ -1,6 +0,0 @@
const cwd = require('./cwd');
module.exports = function(args) {
args.command._ = [args.command._[0], '..'];
return cwd.call(this, args);
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const escapePath = require('../helpers/escape-path');
module.exports = 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 when(this.fs.chdir(command._[1]))
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);
})
.catch(err => {
log.error(err);
return this.reply(550, err.message);
});
}

View File

@@ -1,15 +0,0 @@
const when = require('when');
module.exports = 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 when(this.fs.delete(command._[1]))
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

@@ -1,10 +0,0 @@
const _ = require('lodash');
module.exports = function () {
const registry = require('./registry');
const features = Object.keys(registry)
.filter(cmd => registry[cmd].hasOwnProperty('feat'))
.reduce((feats, cmd) => _.concat(feats, registry[cmd].feat), [])
.map(feat => ` ${feat}`);
return this.reply(211, 'Extensions supported', ...features, 'END');
}

View File

@@ -1,16 +0,0 @@
const _ = require('lodash');
module.exports = function ({command} = {}) {
const registry = require('./registry');
const directive = _.upperCase(command._[1]);
if (directive) {
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
const {syntax, help, obsolete} = registry[directive];
const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]);
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.');
}
};

View File

@@ -1,35 +1,76 @@
const _ = require('lodash');
const when = require('when');
import _ from 'lodash'
import Promise from 'bluebird'
import REGISTRY from './registry.js'
class FtpCommands {
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/)
export default class FtpCommands {
constructor(connection) {
this.connection = connection;
this.registry = require('./registry');
this.previousCommand = {};
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 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
}
handle(command) {
const log = this.connection.log.child({command});
log.trace('Handle command');
if (typeof command === 'string') command = this.parse(command)
if (!this.registry.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');
// Obfuscate password from logs
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')
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(502, `Command not allowed: ${command.directive}`)
}
const commandRegister = this.registry[command.directive];
if (!commandRegister.no_auth && !this.connection.authenticated) {
return this.connection.reply(530);
if (_.includes(this.blacklist, command.directive)) {
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: ${command.directive}`)
}
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: ${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 when.try(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,53 +0,0 @@
const _ = require('lodash');
const when = require('when');
const getFileStat = require('../helpers/file-stat');
// http://cr.yp.to/ftp/list.html
// http://cr.yp.to/ftp/list/eplf.html
module.exports = function ({log, command, previous_command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
const simple = command.directive === 'NLST';
let dataSocket;
const directory = command._[1] || '.';
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.list(directory)))
.then(files => {
const getFileMessage = (file) => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
const fileList = files.map(file => {
const message = getFileMessage(file);
return {
raw: true,
message,
socket: dataSocket
};
})
return this.reply(150)
.then(() => this.reply(...fileList));
})
.then(() => {
return this.reply(226, 'Transfer OK');
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(err.code || 451, err.message || 'No directory');
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const format = require('date-fns/format');
module.exports = 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 when(this.fs.get(command._[1]))
.then(fileStat => {
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime)
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const escapePath = require('../helpers/escape-path');
module.exports = 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 when(this.fs.mkdir(command._[1]))
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

@@ -1,3 +0,0 @@
module.exports = function ({command} = {}) {
return this.reply(command._[1] === 'S' ? 200 : 504);
}

View File

@@ -1,3 +0,0 @@
module.exports = function () {
return this.reply(200);
}

View File

@@ -1,3 +0,0 @@
module.exports = function () {
return this.reply(501);
}

View File

@@ -1,19 +0,0 @@
const _ = require('lodash');
module.exports = function ({log, command} = {}) {
if (!this.username) return this.reply(503);
if (this.username && this.authenticated &&
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
// 332 : require account name (ACCT)
const password = command._[1];
return this.login(this.username, password)
.then(() => {
return this.reply(230);
})
.catch(err => {
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
};

View File

@@ -1,15 +0,0 @@
const PassiveConnector = require('../connector/passive');
module.exports = function ({command} = {}) {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const address = this.server.url.hostname;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
});
}

View File

@@ -1,16 +0,0 @@
const ActiveConnector = require('../connector/active');
module.exports = function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = command._[1].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];
return this.connector.setupConnection(ip, port)
.then(socket => {
return this.reply(200);
})
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const escapePath = require('../helpers/escape-path');
module.exports = function ({log, command} = {}) {
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 when(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);
});
}

View File

@@ -1,3 +0,0 @@
module.exports = function () {
return this.close(221);
}

View File

@@ -0,0 +1,14 @@
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))
.then(() => this.connector.end())
},
syntax: '{{cmd}}',
description: 'Abort an active file transfer'
}

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
import _ from 'lodash'
import tls from 'node:tls'
export default {
directive: 'AUTH',
handler: function ({ command } = {}) {
const method = _.upperCase(command.arg)
switch (method) {
case 'TLS':
return handleTLS.call(this)
default:
return this.reply(504)
}
},
syntax: '{{cmd}} <type>',
description: 'Set authentication mechanism',
flags: {
no_auth: true,
feat: 'AUTH TLS'
}
}
function handleTLS() {
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)
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
})
;['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
function forwardEvent() {
this.emit.apply(this, arguments)
}
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event))
})
this.commandSocket = secureSocket
this.secure = true
})
}

View File

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

View File

@@ -0,0 +1,22 @@
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
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')
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)
})
},
syntax: '{{cmd}} <path>',
description: 'Change working directory'
}

View File

@@ -0,0 +1,20 @@
import Promise from 'bluebird'
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')
return Promise.try(() => this.fs.delete(command.arg))
.then(() => {
return this.reply(250)
})
.catch((err) => {
log.error(err)
return this.reply(550, err.message)
})
},
syntax: '{{cmd}} <path>',
description: 'Delete file'
}

View File

@@ -0,0 +1,27 @@
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
const FAMILY = {
1: 4,
2: 6
}
export default {
directive: 'EPRT',
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))
.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

@@ -0,0 +1,21 @@
import PassiveConnector from '../../connector/passive.js'
export default {
directive: 'EPSV',
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}|)`)
})
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
},
syntax: '{{cmd}} [<protocol>]',
description: 'Initiate passive mode'
}

View File

@@ -0,0 +1,30 @@
import _ from 'lodash'
export default {
directive: 'FEAT',
handler: function () {
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']
)
.sort()
.map((feat) => ({
message: ` ${feat}`,
raw: true
}))
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features')
},
syntax: '{{cmd}}',
description: 'Get the feature list implemented by the server',
flags: {
no_auth: true
}
}

View File

@@ -0,0 +1,30 @@
import _ from 'lodash'
export default {
directive: 'HELP',
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}.`)
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.'
)
}
},
syntax: '{{cmd}} [<command>]',
description:
'Returns usage documentation on a command if specified, else a general help document is returned',
flags: {
no_auth: true
}
}

View File

@@ -0,0 +1,61 @@
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
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')
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'))
}
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'
}

View File

@@ -0,0 +1,25 @@
import Promise from 'bluebird'
import moment from 'moment'
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')
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)
})
},
syntax: '{{cmd}} <path>',
description: 'Return the last-modified time of a specified file',
flags: {
feat: 'MDTM'
}
}

View File

@@ -0,0 +1,22 @@
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
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')
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

@@ -0,0 +1,11 @@
export default {
directive: 'MODE',
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

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

View File

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

View File

@@ -0,0 +1,41 @@
import _ from 'lodash'
const OPTIONS = {
UTF8: utf8,
'UTF-8': utf8
}
export default {
directive: 'OPTS',
handler: function ({ command } = {}) {
if (!_.has(command, 'arg')) return this.reply(501)
const [_option, ...args] = command.arg.split(' ')
const option = _.toUpper(_option)
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
}
}
const encoding = getEncoding()
if (!encoding) return this.reply(501, 'Unknown setting for option')
this.encoding = encoding
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`)
}

View File

@@ -0,0 +1,25 @@
export default {
directive: 'PASS',
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')
return this.login(this.username, password)
.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

@@ -0,0 +1,39 @@
import Promise from 'bluebird'
import PassiveConnector from '../../connector/passive.js'
import { isLocalIP } from '../../helpers/is-local.js'
export default {
directive: 'PASV',
handler: function ({ log } = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502)
}
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

@@ -0,0 +1,14 @@
export default {
directive: 'PBSZ',
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',
flags: {
no_auth: true,
feat: 'PBSZ'
}
}

View File

@@ -0,0 +1,29 @@
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
export default {
directive: 'PORT',
handler: function ({ log, command } = {}) {
this.connector = new ActiveConnector(this)
const rawConnection = _.get(command, 'arg', '').split(',')
if (rawConnection.length !== 6) return this.reply(425)
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))
.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

@@ -0,0 +1,26 @@
import _ from 'lodash'
export default {
directive: 'PROT',
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 'C':
case 'S':
case 'E':
return this.reply(536, 'Not supported')
default:
return this.reply(504)
}
},
syntax: '{{cmd}}',
description: 'Data Channel Protection Level',
flags: {
no_auth: true,
feat: 'PROT'
}
}

View File

@@ -0,0 +1,22 @@
import Promise from 'bluebird'
import escapePath from '../../helpers/escape-path.js'
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')
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)
})
},
syntax: '{{cmd}}',
description: 'Print current working directory'
}

View File

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

View File

@@ -0,0 +1,16 @@
import _ from 'lodash'
export default {
directive: 'REST',
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')
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

@@ -0,0 +1,66 @@
import Promise from 'bluebird'
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')
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
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, () => stream && stream.resume())
}
})
stream.once('end', () => resolve())
stream.once('error', destroyConnection(this.connector.socket, reject))
this.connector.socket.once('error', destroyConnection(stream, reject))
})
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))
.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

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

View File

@@ -0,0 +1,22 @@
import Promise from 'bluebird'
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')
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)
})
},
syntax: '{{cmd}} <name>',
description: 'Rename from'
}

View File

@@ -0,0 +1,30 @@
import Promise from 'bluebird'
export default {
directive: 'RNTO',
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')
const from = this.renameFrom
const to = command.arg
return Promise.try(() => this.fs.rename(from, to))
.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

@@ -0,0 +1,17 @@
import Promise from 'bluebird'
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(' ')
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
.then(() => {
return this.reply(200)
})
.catch((err) => {
log.error(err)
return this.reply(500)
})
}

View File

@@ -0,0 +1,20 @@
import Promise from 'bluebird'
import _ from 'lodash'
import registry from './registry.js'
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 })
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 }))
},
syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server'
}

View File

@@ -0,0 +1,6 @@
import chmod from './chmod.js'
export default {
CHMOD: {
handler: chmod
}
}

View File

@@ -0,0 +1,23 @@
import Promise from 'bluebird'
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')
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)
})
},
syntax: '{{cmd}} <path>',
description: 'Return the size of a file',
flags: {
feat: 'SIZE'
}
}

View File

@@ -0,0 +1,43 @@
import _ from 'lodash'
import Promise from 'bluebird'
import getFileStat from '../../helpers/file-stat.js'
export default {
directive: 'STAT',
handler: function (args = {}) {
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')
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')
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)
})
} else {
return this.reply(211, 'Status OK')
}
},
syntax: '{{cmd}} [<path>]',
description: 'Returns the current status'
}

View File

@@ -0,0 +1,75 @@
import Promise from 'bluebird'
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')
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) => {
try {
if (connection) {
if (connection.writable) connection.end()
connection.destroy(err)
}
} finally {
reject(err)
}
}
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.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

@@ -0,0 +1,21 @@
import Promise from 'bluebird'
import stor from './stor.js'
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')
const fileName = args.command.arg
return Promise.try(() => this.fs.get(fileName))
.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

@@ -0,0 +1,11 @@
export default {
directive: 'STRU',
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

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

View File

@@ -0,0 +1,18 @@
export default {
directive: 'TYPE',
handler: function ({ command } = {}) {
if (/^A[0-9]?$/i.test(command.arg)) {
this.transferType = 'ascii'
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
this.transferType = 'binary'
} else {
return this.reply(501)
}
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

@@ -0,0 +1,30 @@
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)
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
) {
return this.login(this.username, '@anonymous')
.then(() => {
return this.reply(230)
})
.catch((err) => {
log.error(err)
return this.reply(530, err.message || 'Authentication failed')
})
}
return this.reply(331)
},
syntax: '{{cmd}} <username>',
description: 'Authentication username',
flags: {
no_auth: true
}
}

View File

@@ -1,200 +1,90 @@
module.exports = {
AUTH: {
handler: require('./auth'),
syntax: 'AUTH [type]',
help: 'Not supported',
no_auth: true
},
USER: {
handler: require('./user'),
syntax: 'USER [username]',
help: 'Authentication username',
no_auth: true
},
PASS: {
handler: require('./pass'),
syntax: 'PASS [password]',
help: 'Authentication password',
no_auth: true
},
SYST: {
handler: require('./syst'),
syntax: 'SYST',
help: 'Return system type',
no_auth: true
},
FEAT: {
handler: require('./feat'),
syntax: 'FEAT',
help: 'Get the feature list implemented by the server',
no_auth: true
},
PWD: {
handler: require('./pwd'),
syntax: 'PWD',
help: 'Print current working directory'
},
XPWD: {
handler: require('./pwd'),
syntax: 'XPWD',
help: 'Print current working directory'
},
TYPE: {
handler: require('./type'),
syntax: 'TYPE',
help: 'Set the transfer mode'
},
PASV: {
handler: require('./pasv'),
syntax: 'PASV',
help: 'Initiate passive mode'
},
PORT: {
handler: require('./port'),
syntax: 'PORT [x,x,x,x,y,y]',
help: 'Specifies an address and port to which the server should connect'
},
LIST: {
handler: require('./list'),
syntax: 'LIST [path(optional)]',
help: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
},
NLST: {
handler: require('./list'),
syntax: 'NLST [path(optional)]',
help: 'Returns a list of file names in a specified directory'
},
CWD: {
handler: require('./cwd'),
syntax: 'CWD [path]',
help: 'Change working directory'
},
XCWD: {
handler: require('./cwd'),
syntax: 'XCWD [path]',
help: 'Change working directory'
},
CDUP: {
handler: require('./cdup'),
syntax: 'CDUP',
help: 'Change to Parent Directory'
},
XCUP: {
handler: require('./cdup'),
syntax: 'XCUP',
help: 'Change to Parent Directory'
},
STOR: {
handler: require('./stor'),
syntax: 'STOR [path]',
help: 'Accept the data and to store the data as a file at the server site'
},
APPE: {
handler: require('./stor'),
syntax: 'APPE [path]',
help: 'Append to file'
},
RETR: {
handler: require('./retr'),
syntax: 'RETR [path]',
help: 'Retrieve a copy of the file'
},
DELE: {
handler: require('./dele'),
syntax: 'DELE [path]',
help: 'Delete file'
},
RMD: {
handler: require('./dele'),
syntax: 'RMD [path]',
help: 'Remove a directory'
},
XRMD: {
handler: require('./dele'),
syntax: 'XRMD [path]',
help: 'Remove a directory'
},
HELP: {
handler: require('./help'),
syntax: 'HELP [command(optional)]',
help: 'Returns usage documentation on a command if specified, else a general help document is returned'
},
MDTM: {
handler: require('./mdtm'),
syntax: 'MDTM [path]',
help: 'Return the last-modified time of a specified file',
feat: 'MDTM'
},
MKD: {
handler: require('./mkd'),
syntax: 'MKD [path]',
help: 'Make directory'
},
XMKD: {
handler: require('./mkd'),
syntax: 'XMKD [path]',
help: 'Make directory'
},
NOOP: {
handler: require('./noop'),
syntax: 'NOOP',
help: 'No operation',
no_auth: true
},
QUIT: {
handler: require('./quit'),
syntax: 'QUIT',
help: 'Disconnect',
no_auth: true
},
RNFR: {
handler: require('./rnfr'),
syntax: 'RNFR [name]',
help: 'Rename from'
},
RNTO: {
handler: require('./rnto'),
syntax: 'RNTO [name]',
help: 'Rename to'
},
SIZE: {
handler: require('./size'),
syntax: 'SIZE [path]',
help: 'Return the size of a file',
feat: 'SIZE'
},
STAT: {
handler: require('./stat'),
syntax: 'SIZE [path(optional)]',
help: 'Returns the current status'
},
SITE: {
handler: require('./site'),
syntax: 'SITE [subVerb] [subParams]',
help: 'Sends site specific commands to remote server'
},
OPTS: {
handler: require('./opts'),
syntax: 'OPTS',
help: 'Select options for a feature'
},
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'
STRU: {
handler: require('./stru'),
syntax: 'STRU [structure]',
help: 'Set file transfer structure',
obsolete: true
},
ALLO: {
handler: require('./allo'),
syntax: 'ALLO',
help: 'Allocate sufficient disk space to receive a file',
obsolete: true
},
MODE: {
handler: require('./mode'),
syntax: 'MODE [mode]',
help: 'Sets the transfer mode (Stream, Block, or Compressed)',
obsolete: true
}
};
/* eslint no-return-assign: 0 */
const commands = [
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
}, {})
export default registry

View File

@@ -1,36 +0,0 @@
const when = require('when');
module.exports = 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');
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.read(command._[1])))
.then(stream => {
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));
stream.on('data', data => dataSocket.write(data, this.encoding));
stream.on('end', () => resolve(this.reply(226)));
stream.on('error', err => reject(err));
this.reply(150).then(() => dataSocket.resume());
});
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(551);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
module.exports = 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._[1];
return when(this.fs.get(fileName))
.then(() => {
this.renameFrom = fileName;
return this.reply(350);
})
.catch(err => {
log.error(err);
return this.reply(550);
})
}

View File

@@ -1,23 +0,0 @@
const when = require('when');
module.exports = 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');
const from = this.renameFrom;
const to = command._[1];
return when(this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
})
.finally(() => {
delete this.renameFrom;
});
}

View File

@@ -1,14 +0,0 @@
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');
const [, mode, fileName] = command._;
return this.fs.chmod(fileName, parseInt(mode, 8))
.then(() => {
return this.reply(200);
})
.catch(err => {
log.error(err);
return this.reply(500);
})
};

View File

@@ -1,18 +0,0 @@
const _ = require('lodash');
const when = require('when');
const registry = require('./registry');
module.exports = function ({log, command} = {}) {
let [, subverb, ...subparameters] = command._;
subverb = _.upperCase(subverb);
const subLog = log.child({subverb});
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
const subCommand = {
_: [subverb, ...subparameters],
directive: subverb
}
const handler = registry[subverb].handler.bind(this);
return when.try(handler, { log: subLog, command: subCommand });
}

View File

@@ -1,5 +0,0 @@
module.exports = {
CHMOD: {
handler: require('./chmod')
}
};

View File

@@ -1,15 +0,0 @@
const when = require('when');
module.exports = 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 when(this.fs.get(command._[1]))
.then(fileStat => {
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

@@ -1,37 +0,0 @@
const _ = require('lodash');
const when = require('when');
const getFileStat = require('../helpers/file-stat');
module.exports = function (args = {}) {
const {log, command} = args;
const path = command._[1];
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');
return when(this.fs.get(path))
.then(stat => {
if (stat.isDirectory()) {
return when(this.fs.list(path))
.then(files => {
const fileList = files.map(file => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return {
raw: true,
message
};
})
return this.reply(213, 'Status begin', ...fileList, 'Status end');
})
} else {
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')))
}
})
.catch(err => {
log.error(err);
return this.reply(450);
})
} else {
return this.reply(211, 'Status OK');
}
}

View File

@@ -1,38 +0,0 @@
const when = require('when');
module.exports = 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';
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.write(command._[1], {append})))
.then(stream => {
return when.promise((resolve, reject) => {
stream.on('error', err => dataSocket.emit('error', err));
dataSocket.on('end', () => resolve(this.reply(226)));
dataSocket.on('error', err => reject(err));
dataSocket.on('data', data => stream.write(data, this.encoding));
this.reply(150).then(() => dataSocket.resume());
});
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(553);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

@@ -1,3 +0,0 @@
module.exports = function ({command} = {}) {
return this.reply(command._[1] === 'F' ? 200 : 504);
}

View File

@@ -1,3 +0,0 @@
module.exports = function () {
return this.reply(215);
}

View File

@@ -1,15 +0,0 @@
const _ = require('lodash');
module.exports = function ({command} = {}) {
const encoding = _.upperCase(command._[1]);
switch (encoding) {
case 'A':
this.encoding = 'utf-8';
case 'I':
case 'L':
this.encoding = 'binary';
return this.reply(200);
default:
return this.reply(501);
}
}

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