Compare commits

..

99 Commits

Author SHA1 Message Date
Tyler Stewart
a308a33491 chore: update readme 2018-12-21 16:10:34 -07:00
Tyler Stewart
191ad5507c test: update sinon sandbox creation 2018-12-21 16:07:34 -07:00
Tyler Stewart
281d147b96 chore(package): update scripts 2018-12-21 16:03:13 -07:00
Tyler Stewart
52958ffd9f chore: update packages 2018-12-21 16:00:12 -07:00
Tyler Stewart
3f5d548634 chore(circle): update config 2018-12-21 15:57:04 -07: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
107 changed files with 10984 additions and 1234 deletions

103
.circleci/config.yml Normal file
View File

@@ -0,0 +1,103 @@
version: 2
create-cache-file: &create-cache-file
run:
name: Setup cache
command: echo "$NODE_VERSION" > _cache_node_version
package-json-cache: &package-json-cache
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
base-build: &base-build
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Install
command: npm install
- save_cache:
<<: *package-json-cache
paths:
- node_modules
- run:
name: Lint
command: npm run verify -- --silent
- run:
name: Test
command: npm run test:once
jobs:
test_node_10:
docker:
- image: circleci/node:10
environment:
- NODE_VERSION: 10
<<: *base-build
test_node_8:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
<<: *base-build
test_node_6:
docker:
- image: circleci/node:6
environment:
- NODE_VERSION: 6
<<: *base-build
release:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- deploy:
name: Semantic Release
command: |
npm run semantic-release
workflows:
version: 2
test_and_tag:
jobs:
- test_node_10:
filters:
branches:
only: master
- test_node_8:
filters:
branches:
only: master
- test_node_6:
filters:
branches:
only: master
- release:
requires:
- test_node_6
- test_node_8
- test_node_10
build_and_test:
jobs:
- test_node_10:
filters:
branches:
ignore: master
- test_node_8:
filters:
branches:
ignore: master
- test_node_6:
filters:
branches:
ignore: master

2
.env
View File

@@ -1,2 +0,0 @@
FTP_URL=ftp://127.0.0.1:8880
PASV_RANGE=8881

1
.gitattributes vendored Normal file
View File

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

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ node_modules/
dist/ dist/
reports/ reports/
npm-debug.log npm-debug.log
.nyc_output .nyc_output/
test_tmp/

24
.nycrc
View File

@@ -1,24 +0,0 @@
{
"check-coverage": true,
"per-file": true,
"lines": 40,
"statements": 40,
"functions": 20,
"branches": 40,
"include": [
"src/**/*.js"
],
"exclude": [
"test/**/*.spec.js"
],
"reporter": [
"lcovonly",
"html",
"text",
"cobertura",
"json"
],
"cache": true,
"all": true,
"report-dir": "./reports/coverage/"
}

View File

@@ -1,25 +0,0 @@
language: node_js
node_js:
- "6"
# - "node"
env:
FTP_URL: ftp://127.0.0.1:8880
PASV_RANGE: 8881
install: npm install
script:
- npm run verify:js
- npm run test:coverage
after_script:
- npm run upload-coverage
deploy:
skip_cleanup: true
provider: script
script: npm run semantic-release
on:
branch: master
node: "6"

View File

@@ -1,211 +1,20 @@
<!--[CN_HEADING]--> <p align="center">
# Contributing <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-srv** 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-srv/
├──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:js</pre> | Verify Javascript code style and syntax
<pre>npm run verify:js:fix</pre> | Verify Javascript code style and syntax and fix any errors that can be fixed automatically
<pre>npm run verify:js:watch</pre> | Verify Javascript code style and syntax and watch files for changes
<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 seperate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.

View File

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

196
README.md
View File

@@ -1,12 +1,23 @@
[![ftp-srv](logo.png)](https://github.com/trs/ftp-srv) <p align="center">
<a href="https://github.com/trs/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>
[![npm version](https://badge.fury.io/js/ftp-srv.svg)](https://badge.fury.io/js/ftp-srv) [![Build Status](https://travis-ci.org/trs/ftp-srv.svg?branch=master)](https://travis-ci.org/trs/ftp-srv) <p align="center">
[![Coverage Status](https://coveralls.io/repos/github/trs/ftp-srv/badge.svg?branch=coveralls)](https://coveralls.io/github/trs/ftp-srv?branch=coveralls) [![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/) <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>
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
--- ---
@@ -15,6 +26,7 @@
- [Install](#install) - [Install](#install)
- [Usage](#usage) - [Usage](#usage)
- [API](#api) - [API](#api)
- [CLI](#cli)
- [Events](#events) - [Events](#events)
- [Supported Commands](#supported-commands) - [Supported Commands](#supported-commands)
- [File System](#file-system) - [File System](#file-system)
@@ -24,13 +36,16 @@
## Overview ## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable. `ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
You can use `ftp-srv` to traverse the file system on the server, but it's biggest strength comes from it's customizable file system. This allows you to serve a custom, dynamic, or unique file system to users. You can even server a different system depending on the user connecting.
## Features ## Features
- Extensible [file systems](#file-system) per connection - Extensible [file systems](#file-system) per connection
- Passive and active transfers - Passive and active transfers
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections - [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Promise based API
## Install ## Install
`npm install ftp-srv --save` `npm install ftp-srv --save`
## Usage ## Usage
@@ -38,7 +53,7 @@
// Quick start // Quick start
const FtpSrv = require('ftp-srv'); const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... }); const ftpServer = new FtpSrv({ options ... });
ftpServer.on('login', (data, resolve, reject) => { ... }); ftpServer.on('login', (data, resolve, reject) => { ... });
... ...
@@ -49,49 +64,57 @@ ftpServer.listen()
## API ## API
### `new FtpSrv(url, [{options}])` ### `new FtpSrv({options})`
#### url #### 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. [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: Supported protocols:
- `ftp` Plain FTP - `ftp` Plain FTP
- `ftps` Implicit FTP over TLS - `ftps` Implicit FTP over TLS
_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP. _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"` __Default:__ `"ftp://127.0.0.1:21"`
#### options #### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
##### `pasv_range` _Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections. __Default:__ `"127.0.0.1"`
This range is then queried for an available port to use when required.
__Default:__ `22`
##### `greeting` #### `pasv_min`
Tne 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. A human readable array of lines or string to send when a client connects.
__Default:__ `null` __Default:__ `null`
##### `tls` #### `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. 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` __Default:__ `false`
##### `anonymous` #### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user. 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. 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. The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false` __Default:__ `false`
##### `blacklist` #### `blacklist`
Array of commands that are not allowed. Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands. 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. __Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]` __Default:__ `[]`
##### `whitelist` #### `whitelist`
Array of commands that are only allowed. Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command. Response code `502` is sent to clients sending any other command.
__Default:__ `[]` __Default:__ `[]`
##### `file_format` #### `file_format`
Sets the format to use for file stat queries such as `LIST`. Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"` __Default:__ `"ls"`
__Allowable values:__ __Allowable values:__
@@ -100,16 +123,70 @@ __Allowable values:__
- `function () {}` A custom function returning a format or promise for one. - `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 - 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` #### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default. A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
Piping the output into bunyan will format logs nicely, eg:
```
$ node ./test/start.js | npx bunyan
```
## 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`
#### `--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`.
## Events ## 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`. 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`.
### `login` ### `login`
```js ```js
on('login', {connection, username, password}, resolve, reject) => { ... } 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. Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
@@ -117,7 +194,7 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
`connection` [client class object](src/connection.js) `connection` [client class object](src/connection.js)
`username` string of username from `USER` command `username` string of username from `USER` command
`password` string of password from `PASS` command `password` string of password from `PASS` command
`resolve` takes an object of arguments: `resolve` takes an object of arguments:
- `fs` - `fs`
- Set a custom file system class for this connection to use. - Set a custom file system class for this connection to use.
- See [File System](#file-system) for implementation details. - See [File System](#file-system) for implementation details.
@@ -136,15 +213,35 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
### `client-error` ### `client-error`
```js ```js
on('client-error', {connection, context, error}) => { ... } ftpServer.on('client-error', ({connection, context, error}) => { ... });
``` ```
Occurs when an error arises in the client connection. Occurs when an error arises in the client connection.
`connection` [client class object](src/connection.js) `connection` [client class object](src/connection.js)
`context` string of where the error occured `context` string of where the error occurred
`error` error object `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
## Supported Commands ## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands. See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
@@ -154,7 +251,7 @@ The default [file system](src/fs.js) can be overwritten to use your own implemen
This can allow for virtual file systems, and more. This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user. 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: The default file system is exported and can be extended as needed:
```js ```js
const {FtpSrv, FileSystem} = require('ftp-srv'); const {FtpSrv, FileSystem} = require('ftp-srv');
@@ -172,52 +269,52 @@ class MyFileSystem extends FileSystem {
Custom file systems can implement the following variables depending on the developers needs: Custom file systems can implement the following variables depending on the developers needs:
### Methods ### Methods
#### [`currentDirectory()`](src/fs.js#L29) #### [`currentDirectory()`](src/fs.js#L40)
Returns a string of the current working directory Returns a string of the current working directory
__Used in:__ `PWD` __Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L33) #### [`get(fileName)`](src/fs.js#L44)
Returns a file stat object of file or directory Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM` __Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L39) #### [`list(path)`](src/fs.js#L50)
Returns array of file and directory stat objects Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT` __Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L56) #### [`chdir(path)`](src/fs.js#L67)
Returns new directory relative to current directory Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP` __Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L96) #### [`mkdir(path)`](src/fs.js#L114)
Returns a path to a newly created directory Returns a path to a newly created directory
__Used in:__ `MKD` __Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L68) #### [`write(fileName, {append, start})`](src/fs.js#L79)
Returns a writable stream Returns a writable stream
Options: Options:
`append` if true, append to existing file `append` if true, append to existing file
`start` if set, specifies the byte offset to write to `start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE` __Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L75) #### [`read(fileName, {start})`](src/fs.js#L90)
Returns a readable stream Returns a readable stream
Options: Options:
`start` if set, specifies the byte offset to read from `start` if set, specifies the byte offset to read from
__Used in:__ `RETR` __Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L87) #### [`delete(path)`](src/fs.js#L105)
Delete a file or directory Delete a file or directory
__Used in:__ `DELE` __Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L102) #### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory Renames a file or directory
__Used in:__ `RNFR`, `RNTO` __Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L108) #### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD` __Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L113) #### [`getUniqueName()`](src/fs.js#L131)
Returns a unique file name to write to Returns a unique file name to write to
__Used in:__ `STOU` __Used in:__ `STOU`
@@ -226,12 +323,27 @@ __Used in:__ `STOU`
See [CONTRIBUTING.md](CONTRIBUTING.md). See [CONTRIBUTING.md](CONTRIBUTING.md).
<!--[]--> <!--[]-->
## Contributors
- [OzairP](https://github.com/OzairP)
- [qchar](https://github.com/qchar)
- [jorinvo](https://github.com/jorinvo)
- [voxsoftware](https://github.com/voxsoftware)
- [pkeuter](https://github.com/pkeuter)
- [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [Johnnyrook777](https://github.com/Johnnyrook777)
<!--[RM_LICENSE]--> <!--[RM_LICENSE]-->
## License ## License
This software is licensed under the MIT Licence. See [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)

137
bin/index.js Executable file
View File

@@ -0,0 +1,137 @@
#!/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'
})
.option('pasv_min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
})
.option('pasv_max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
})
.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) {
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,5 +1,5 @@
test/**/*.spec.js test/**/*.spec.js
--reporter mocha-multi-reporters --reporter mocha-multi-reporters
--reporter-options configFile=config/testUnit/reporters.json --reporter-options configFile=config/testUnit/reporters.json
--no-timeouts
--ui bdd --ui bdd
--bail

View File

@@ -1,5 +1,5 @@
{ {
"reporterEnabled": "list, mocha-junit-reporter", "reporterEnabled": "spec",
"mochaJunitReporterReporterOptions": { "mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml" "mochaFile": "reports/junit.xml"
} }

View File

@@ -136,7 +136,7 @@
"comma-dangle": 1, "comma-dangle": 1,
"new-cap": 2, "new-cap": 2,
"new-parens": 2, "new-parens": 2,
"arrow-parens": [2, "as-needed"], "arrow-parens": [2, "always"],
"no-array-constructor": 2, "no-array-constructor": 2,
"array-callback-return": 1, "array-callback-return": 1,
"no-extra-parens": 2, "no-extra-parens": 2,

View File

@@ -1,49 +0,0 @@
generator-confit:
app:
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
buildProfile: Latest
copyrightOwner: Tyler Stewart
license: MIT
projectType: node
publicRepository: true
repositoryType: GitHub
paths:
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
config:
configDir: config/
input:
srcDir: src/
unitTestDir: test/
output:
prodDir: dist/
reportDir: reports/
buildJS:
_version: ead8ce4280b07d696aff499a5fca1a933727582f
framework: []
frameworkScripts: []
outputFormat: ES6
sourceFormat: ES6
entryPoint:
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
entryPoints:
main:
- src/index.js
testUnit:
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
testDependencies: []
testFramework: mocha
verify:
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
jsCodingStandard: none
documentation:
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
generateDocs: false
release:
_version: 47f220593935b502abf17cb34a396f692e453c49
checkCodeCoverage: true
commitMessageFormat: Conventional
useSemantic: true
sampleApp:
_version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba
createSampleApp: false
zzfinish: {}

41
ftp-srv.d.ts vendored
View File

@@ -1,8 +1,14 @@
import * as tls from 'tls' import * as tls from 'tls'
import { Stats } from 'fs' import { Stats } from 'fs'
import { EventEmitter } from 'events';
export interface FileSystem { export class FileSystem {
constructor(connection: any, {root, cwd}?: {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any; root: any;
cwd: any; cwd: any;
}); });
@@ -35,7 +41,7 @@ export interface FileSystem {
getUniqueName(): string; getUniqueName(): string;
} }
export interface FtpConnection { export class FtpConnection extends EventEmitter {
server: FtpServer; server: FtpServer;
id: string; id: string;
log: any; log: any;
@@ -53,18 +59,21 @@ export interface FtpConnection {
} }
export interface FtpServerOptions { export interface FtpServerOptions {
pasv_range?: number | string, url?: string,
greeting?: string, pasv_min?: number,
pasv_max?: number,
pasv_url?: string,
greeting?: string | string[],
tls?: tls.SecureContext | false, tls?: tls.SecureContext | false,
anonymous?: boolean, anonymous?: boolean,
blacklist?: Array<string>, blacklist?: Array<string>,
whitelist?: Array<string>, whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep", file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log: any log?: any,
} }
export interface FtpServer { export class FtpServer extends EventEmitter {
constructor(url: string, options?: FtpServerOptions); constructor(options?: FtpServerOptions);
readonly isTLS: boolean; readonly isTLS: boolean;
@@ -72,7 +81,7 @@ export interface FtpServer {
emitPromise(action: any, ...data: any[]): Promise<any>; emitPromise(action: any, ...data: any[]): Promise<any>;
emit(action: any, ...data: any[]): void; // emit is exported from super class
setupTLS(_tls: boolean): boolean | { setupTLS(_tls: boolean): boolean | {
cert: string; cert: string;
@@ -94,9 +103,15 @@ export interface FtpServer {
username: string, username: string,
password: string password: string
}, },
resolve: (fs?: FileSystem, root?: string, cwd?: string, blacklist?: Array<string>, whitelist?: Array<string>) => void, resolve: (config: {
fs?: FileSystem,
root?: string,
cwd?: string,
blacklist?: Array<string>,
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void reject: (err?: Error) => void
) => void) ) => void): this;
on(event: "client-error", listener: ( on(event: "client-error", listener: (
data: { data: {
@@ -104,8 +119,8 @@ export interface FtpServer {
context: string, context: string,
error: Error, error: Error,
} }
) => void) ) => void): this;
} }
declare const FtpSrv: FtpServer; export {FtpServer as FtpSrv};
export default FtpServer; export default FtpServer;

View File

@@ -1,6 +1,8 @@
const FtpSrv = require('./src'); const FtpSrv = require('./src');
const FileSystem = require('./src/fs'); const FileSystem = require('./src/fs');
const errors = require('./src/errors');
module.exports = FtpSrv; module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv; module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem; module.exports.FileSystem = FileSystem;
module.exports.ftpErrors = errors;

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,17 +1,23 @@
/* const puppeteer = require('puppeteer');
Send Button by Bruno Bosse from the Noun Project const logoPath = `file://${process.cwd()}/logo/logo.html`;
https://thenounproject.com/brunobosse/collection/basics/?i=1054386
*/
const fs = require('fs'); puppeteer.launch()
const htmlConvert = require('html-convert'); .then(browser => {
return browser.newPage()
const convert = htmlConvert(); .then(page => {
let ws = fs.createWriteStream('logo.png'); return page.goto(logoPath)
let rs = convert('logo/logo.html', { .then(() => page);
width: 350, })
height: 76 .then(page => {
return page.setViewport({
width: 600,
height: 250,
deviceScaleFactor: 2
})
.then(() => page.screenshot({
path: 'logo.png',
omitBackground: true
}));
})
.then(() => browser.close());
}); });
rs.pipe(ws);
ws.on('finish', () => process.exit());

View File

@@ -1,81 +0,0 @@
<svg width="566" height="580" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<g display="none" id="svg_1">
<g display="inline" id="svg_2">
<circle cx="337.851" cy="245.093" r="68.103" id="svg_3"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm135.007,301.787c-35.898,35.898 -89.692,42.92 -132.482,20.934l-76.709,76.709c-13.651,13.651 -35.783,13.651 -49.434,0c-13.651,-13.651 -13.651,-35.784 0,-49.435l76.777,-76.777c-21.923,-42.853 -15.06,-96.699 20.761,-132.52c44.483,-44.483 116.448,-44.64 160.931,-0.157c44.482,44.485 44.639,116.763 0.156,161.246z" id="svg_4"/>
</g>
</g>
<g display="none" id="svg_5">
<g display="inline" id="svg_6">
<path d="m282,211.676c-7.801,0 -13,6.324 -13,14.125l0,66.199l-40.682,0c-7.975,0 -14.318,5.978 -14.318,13.953l0,0.597c0,7.975 6.343,14.449 14.318,14.449l54.641,0c0.053,0 -0.495,-0.012 -0.442,-0.013c0.053,0.001 -0.495,0.004 -0.442,0.004c7.801,0 12.925,-6.324 12.925,-14.125l0,-0.315l0,-0.597l0,-80.152c0,-7.801 -5.199,-14.125 -13,-14.125z" id="svg_7"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-139.125,208.04c-0.89,-1.277 -1.569,-2.707 -2.003,-4.244c-3.279,-7.649 -5.111,-16.066 -5.111,-24.915c0,-34.997 28.371,-63.368 63.368,-63.368c14.59,0 28.023,4.937 38.735,13.224c1.019,0.639 1.951,1.4 2.775,2.268c2.431,2.56 3.932,6.012 3.932,9.82c0,5.696 -3.344,10.6 -8.169,12.891c-0.831,0.395 -1.704,0.709 -2.615,0.938c-27.13,10.06 -50.303,28.252 -66.567,51.62c-0.558,1.116 -1.257,2.145 -2.077,3.069c-2.601,2.933 -6.387,4.792 -10.616,4.792c-4.828,0.001 -9.086,-2.414 -11.652,-6.095zm257.1,194.907c-13.91,13.91 -36.463,13.91 -50.373,0l-5.58,-5.58c-18.184,10.084 -39.105,15.833 -61.371,15.833c-22.105,0 -42.885,-5.664 -60.977,-15.613l-5.36,5.36c-13.91,13.91 -36.463,13.91 -50.373,0c-13.91,-13.91 -13.91,-36.464 0,-50.374l5.424,-5.424c-9.849,-18.023 -15.449,-38.698 -15.449,-60.683c0,-69.995 56.741,-126.735 126.735,-126.735s126.735,56.741 126.735,126.735c0,21.823 -5.518,42.358 -15.232,60.286l5.821,5.821c13.91,13.91 13.91,36.464 0,50.374zm24.752,-197.077c-0.402,1.103 -0.924,2.149 -1.573,3.104c-2.536,3.727 -6.81,6.175 -11.658,6.175c-4.483,0 -8.469,-2.099 -11.05,-5.363c-0.508,-0.642 -0.968,-1.323 -1.36,-2.049c-16.153,-23.843 -39.432,-42.457 -66.784,-52.788c-1.048,-0.252 -2.048,-0.618 -2.991,-1.088c-4.671,-2.325 -7.888,-7.133 -7.888,-12.705c0,-3.488 1.263,-6.677 3.349,-9.148c1.025,-1.215 2.254,-2.245 3.628,-3.061c10.753,-8.412 24.288,-13.434 39,-13.434c34.997,0 63.368,28.371 63.368,63.368c-0.001,9.657 -2.176,18.8 -6.041,26.989z" id="svg_8"/>
</g>
</g>
<g display="none" id="svg_9">
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm139.455,375.302c0,7.493 -6.315,13.691 -13.808,13.691l-91.192,0l0,-35.34l0,-20.446l0,-2.871c0.038,-7.37 -6.111,-12.343 -15,-12.343s-39,0 -39,0c-3,-0.005 -2.241,0.161 -2.373,0.161c-7.435,0 -14.625,7.165 -14.625,14.6c0,0.127 -0.002,1.239 -0.002,1.239l0,19.66l0,35.34l-89.837,0l-0.494,0c-7.381,0 -13.669,-6.272 -13.669,-13.685l0,-121.53c0,-2.827 1.062,-5.443 2.568,-7.596c0.888,-1.27 2.164,-2.375 3.431,-3.265l124.012,-124.268l2.154,-2.157c2.255,-1.791 5.106,-2.867 8.209,-2.867c3.004,0 5.772,1.014 7.991,2.702l2.557,2.548l124.987,125.215c-0.054,0.045 -0.047,0.091 -0.101,0.136c2.505,2.415 4.191,5.799 4.191,9.554l0,121.522l0.001,0z" id="svg_10"/>
</g>
<g display="none" id="svg_11">
<g display="inline" id="svg_12">
<path d="m345.15,271.851c1.183,-1.297 2.547,-2.706 3.842,-4.247c1.421,-1.692 2.659,-3.545 3.417,-5.595c0.433,-1.119 0.713,-2.249 0.713,-3.433c0,-0.07 -0.121,-14.058 -0.121,-14.058l0,-40.854c0,-18.056 -14.646,-31.979 -31.875,-31.979c-2.232,0 -4.372,0.235 -6.66,0.724c-0.07,0.013 -0.098,0.032 -0.168,0.045c-9.206,1.965 -18.404,3.611 -27.845,3.643c-10.388,0.038 -20.532,-1.519 -30.667,-3.688c-2.282,-0.489 -4.222,-0.724 -6.453,-0.724c-17.229,0 -31.331,13.923 -31.331,31.979l0,4.447l0,29.426l0,8.558c0,3.935 -0.94,7.942 -0.673,11.865c0.097,1.434 -0.228,2.931 0.286,4.05c0.791,2.056 2.002,3.895 3.397,5.595c2.021,2.463 4.429,4.628 6.529,6.701c3.923,3.878 8.138,7.475 12.48,10.87c8.742,6.828 18.004,12.937 26.886,19.575c4.857,3.63 11.295,5.67 18.034,5.828c6.739,-0.159 13.176,-2.203 18.033,-5.833c9.734,-7.273 20.006,-13.637 29.409,-21.113c4.536,-3.607 8.857,-7.498 12.767,-11.782zm-85.15,-37.557c0,7.909 -7.091,14.317 -15,14.317c-7.909,0 -15,-6.408 -15,-14.317l0,-14.782c0,-7.909 7.091,-14.317 15,-14.317c7.909,0 15,6.408 15,14.317l0,14.782zm59.5,14.318c-7.909,0 -14.5,-6.408 -14.5,-14.317l0,-14.782c0,-7.909 6.591,-14.317 14.5,-14.317c7.909,0 14.5,6.408 14.5,14.317l0,14.782c0,7.908 -6.591,14.317 -14.5,14.317z" id="svg_13"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm108.455,408.03c0,5.283 -4.98,7.963 -10.263,7.963l-103.555,0l-8.55,0l-82.179,0c-5.283,0 -10.453,-2.68 -10.453,-7.963l0,-37.968c0,-11.276 4.59,-23.322 12.77,-35.808c6.836,-10.433 16.918,-21.024 28.32,-30.633c7.486,-6.309 15.463,-11.977 23.502,-16.746c-0.944,-0.673 -1.889,-1.334 -2.831,-2.01c0.055,-0.032 0.106,-0.043 0.16,-0.074c-11.73,-8.489 -19.987,-15.692 -26.141,-21.834c-3.346,-3.388 -5.872,-6.221 -7.879,-9.237c-0.481,-0.51 -0.971,-1.029 -1.433,-1.559c-0.279,-0.321 -0.594,-0.643 -0.863,-0.971c-1.794,-2.187 -3.431,-4.554 -4.449,-7.199c-0.661,-1.441 -1.199,-3.366 -1.325,-5.212c-0.124,-1.817 -0.442,-3.648 -0.429,-5.483c0.024,-3.264 -0.402,-6.543 -0.402,-9.785l0,-18.16l0.549,0.01c0.01,-0.561 0.539,-1.144 0.457,-1.737c-0.82,-5.964 -5.862,-13.494 -7.177,-21.512c-0.104,-0.638 -0.188,-1.279 -0.242,-1.923l0.026,-0.01c-0.401,-4.131 -0.385,-8.308 0.145,-12.457c6.765,-52.927 46.886,-86.808 95.363,-86.808c47.53,0 87.052,32.316 94.942,83.737c0.787,5.139 0.773,10.342 0.025,15.428l0.347,0.109c-0.122,1.478 -0.237,2.94 -0.589,4.384c-1.724,7.058 -5.343,13.637 -6.311,18.973c-0.061,0.336 0.465,0.671 0.465,0.998l0,16.943c0,0 -0.605,6.586 -0.593,11.809c0.008,3.41 -0.606,6.241 -0.606,6.277c0,1.524 -0.42,2.977 -0.977,4.418c-0.975,2.637 -2.594,5.022 -4.423,7.199c-0.278,0.331 -0.559,0.65 -0.841,0.971c-0.657,0.749 -1.313,1.476 -1.963,2.176c-0.905,1.274 -1.887,2.489 -2.972,3.831c-5.301,6.006 -12.847,13.149 -24.136,21.791c-3.328,2.397 -6.672,4.742 -10.056,7.082c8.343,4.984 16.347,10.724 23.081,16.449c0.799,0.68 1.687,1.383 2.49,2.097c19.13,16.999 38.996,43.368 38.996,68.923l0,33.521z" id="svg_14"/>
</g>
</g>
<g display="none" id="svg_15">
<g display="inline" id="svg_16">
<path d="m312.499,160.826c-9.98,2.131 -19.499,5.134 -28.499,8.871l0,0.309c0,-0.051 -0.324,-0.099 -0.449,-0.15c-13.49,5.7 -25.679,13.103 -35.916,21.847c6.85,0.617 12.365,6.309 12.365,13.318l0,13.871c0,7.422 -6.578,13.438 -14,13.438s-14,-6.017 -14,-13.438l0,-12.16c-5,7.508 -10,15.638 -14,24.243l0,10.724c0,11.767 40.019,38.51 46.415,42.736c1.735,0.958 3.005,1.504 3.005,1.504c4.505,2.922 10.252,4.531 16.122,4.582c0.101,-0.005 0.459,-0.014 0.459,-0.014l0,0.023c6,-0.006 13.014,-1.989 17.751,-5.526c0,0 47.249,-30.509 47.249,-43.305l0,-51.547c-0.001,-19.131 -17.791,-33.327 -36.502,-29.326zm15.501,58.072c0,7.422 -6.078,13.438 -13.5,13.438c-7.422,0 -13.5,-6.017 -13.5,-13.438l0,-13.871c0,-7.422 6.078,-13.438 13.5,-13.438c7.416,0 13.5,6.017 13.5,13.438l0,13.871z" id="svg_17"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm105.455,311.424c-9,-3.568 -18.511,-5.577 -28.63,-5.577c-3.612,0 -7.204,0.26 -10.68,0.748c0.319,0.28 0.666,0.55 0.985,0.834c18.856,16.755 38.325,42.746 38.325,67.934l0,33.04c0,5.207 -4.685,8.59 -9.892,8.59l-102.07,0l-8.427,0l-81.001,0c-5.207,0 -9.609,-3.382 -9.609,-8.59l0,-37.423c0,-11.115 4.2,-22.991 12.263,-35.297c6.493,-9.909 15.656,-19.963 26.374,-29.156c-3.329,-0.447 -6.682,-0.68 -10.135,-0.68c-10.119,0 -19.502,2.01 -28.502,5.577l0,-124.877c0,-58.13 47.049,-105.257 105.179,-105.257c0.091,0 0.147,0.001 0.266,0.003c0.074,0 0.175,-0.003 0.249,-0.003c58.13,0 105.306,47.127 105.306,105.257l0,124.877l-0.001,0z" id="svg_18"/>
</g>
</g>
<g display="none" id="svg_19">
<g display="inline" id="svg_20">
<path d="m349.599,245.278l-79.122,40.948c-0.082,0.043 -0.156,0.096 -0.237,0.141c-5.525,2.007 -10.592,5.215 -14.873,9.495c-15.891,15.892 -15.891,41.749 0,57.642c7.698,7.698 17.933,11.938 28.82,11.938s21.122,-4.24 28.82,-11.938c4.63,-4.63 7.901,-10.109 9.832,-15.938l40.798,-78.203c2.104,-4.033 1.351,-8.964 -1.86,-12.185c-3.209,-3.22 -8.138,-3.988 -12.178,-1.9z" id="svg_21"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm157.069,312.993l15.415,0c-1.535,20 -6.792,40.567 -15.092,58.692c-0.006,0.018 -0.01,-0.01 -0.017,0.008c-0.088,0.193 -0.829,1.228 -0.961,1.506c-2.081,3.524 -5.906,4.794 -10.295,4.794l-24.507,0l-267.066,0c-4.166,0 -7.495,-1.264 -9.675,-5.366c-0.216,-0.455 -0.974,-1.57 -0.981,-1.59c-8.851,-19.329 -14.24,-40.044 -15.346,-64.044l15.672,0c5.76,0 10.428,-3.741 10.428,-9.5c0,-5.759 -4.669,-9.5 -10.428,-9.5l-15.463,0c2.802,-38 19.118,-75.51 44.351,-103.289l11.527,11.723c2.036,2.036 4.741,3.054 7.41,3.054c2.669,0 5.41,-1.018 7.446,-3.054c4.073,-4.073 4.217,-10.676 0.144,-14.749l-11.042,-11.331c28.262,-24.456 64.866,-39.891 104.866,-41.796l0,15.959c0,5.759 3.74,10.428 9.5,10.428s9.5,-4.669 9.5,-10.428l0,-15.707c39,2.862 76.041,19.222 103.786,44.5l-10.537,10.895c-4.073,4.073 -4.073,10.688 0,14.761c2.036,2.036 4.705,3.079 7.374,3.079c2.669,0 5.338,-0.969 7.374,-3.004l10.688,-10.588c24.41,28.294 39.792,62.547 41.637,104.547l-15.708,0c-5.76,0 -10.428,4.241 -10.428,10c0,5.759 4.668,10 10.428,10z" id="svg_22"/>
</g>
</g>
<g display="none" id="svg_23">
<g display="inline" id="svg_24">
<path d="m343.258,200.171c-9.089,0.001 -18.289,2.508 -26.521,7.757c-23.007,14.673 -29.763,45.218 -15.09,68.225c9.423,14.775 25.389,22.847 41.704,22.847c9.089,0 18.29,-2.508 26.521,-7.757c23.007,-14.673 29.762,-45.218 15.09,-68.224c-9.423,-14.777 -25.389,-22.849 -41.704,-22.848zm34.669,57.087c-2.05,9.266 -7.655,17.181 -15.657,22.284c-5.724,3.651 -12.455,5.58 -19.199,5.58c-5.983,0 -11.071,-1.48 -17.071,-4.187l0,-40.485c0,-6.296 5.664,-12.45 11.959,-12.45l34.328,0c0.332,2 0.629,1.465 0.94,1.954c5.103,8.001 6.75,18.037 4.7,27.304z" id="svg_25"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-78.545,383.993l-82,0l0,-77.35c0,-6.296 5.466,-10.65 11.762,-10.65l59.472,0c6.295,0 10.766,4.355 10.766,10.65l0,77.35zm104,0l-84,0l0,-228.013c0,-6.296 4.569,-10.987 10.865,-10.987l54.982,0c-21.431,13 -35.968,35.007 -41.526,59.974c-5.613,25.215 -0.753,50.864 13.108,72.597c11.19,17.547 25.571,30.66 46.571,38.006l0,68.423zm17,0l0,-63.149c6,0.938 11.241,1.433 16.863,1.433c4.727,0 9.725,-0.35 14.392,-1.044l37.24,58.161c1.098,1.722 2.327,3.599 3.647,4.599l-72.142,0zm115.213,-4.086c-4.226,2.695 -8.946,3.982 -13.613,3.982c-8.373,0 -16.569,-4.144 -21.405,-11.727l-42.358,-66.416c-6.831,1.782 -13.783,2.653 -20.694,2.653c-27.275,0 -53.884,-13.574 -69.652,-38.297c-24.541,-38.48 -13.102,-89.658 25.378,-114.199c13.747,-8.767 29.081,-12.948 44.238,-12.948c27.271,0 53.959,13.542 69.733,38.275c19.805,31.054 16.076,70.435 -6.202,97.302l42.32,66.357c7.531,11.809 4.063,27.488 -7.745,35.018z" id="svg_26"/>
</g>
</g>
<g display="none" id="svg_27">
<g display="inline" id="svg_28">
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm179.255,300.845c-1.873,8.407 -10.159,13.275 -18.415,10.817l-22.428,-6.677c-2.855,9.611 -6.676,18.805 -11.367,27.462l20.632,11.166c7.575,4.1 9.987,13.398 5.36,20.662c0,0 -10.312,16.191 -23.125,29.004c-12.869,12.869 -29.193,23.244 -29.193,23.244c-7.269,4.621 -16.571,2.203 -20.67,-5.372l-11.112,-20.532c-8.656,4.702 -17.851,8.533 -27.463,11.398l6.674,22.419c2.458,8.255 -2.411,16.535 -10.82,18.401c0,0 -18.741,4.157 -36.861,4.157c-18.199,0 -37.079,-4.206 -37.079,-4.206c-8.407,-1.873 -13.275,-10.16 -10.817,-18.415l6.637,-22.292c-9.623,-2.852 -18.828,-6.674 -27.495,-11.366l-11.091,20.495c-4.1,7.575 -13.398,9.987 -20.662,5.36c0,0 -16.191,-10.312 -29.004,-23.126c-12.869,-12.869 -23.245,-29.193 -23.245,-29.193c-4.62,-7.269 -2.203,-16.57 5.372,-20.67l20.393,-11.036c-4.704,-8.666 -8.536,-17.873 -11.4,-27.496l-22.278,6.632c-8.255,2.458 -16.535,-2.412 -18.4,-10.82c0,0 -4.157,-18.74 -4.157,-36.861c0,-18.199 4.206,-37.079 4.206,-37.079c1.873,-8.407 10.159,-13.275 18.415,-10.817l22.212,6.613c2.863,-9.624 6.695,-18.831 11.399,-27.497l-20.448,-11.066c-7.575,-4.1 -9.987,-13.398 -5.36,-20.662c0,0 10.312,-16.191 23.125,-29.004c12.869,-12.869 29.193,-23.245 29.193,-23.245c7.269,-4.62 16.571,-2.203 20.67,5.372l11.057,20.431c8.667,-4.693 17.874,-8.515 27.497,-11.367l-6.653,-22.348c-2.458,-8.255 2.412,-16.535 10.82,-18.401c0,0 18.741,-4.157 36.861,-4.157c18.199,0 37.079,4.206 37.079,4.206c8.407,1.873 13.275,10.16 10.817,18.415l-6.653,22.348c9.612,2.866 18.808,6.697 27.464,11.4l11.14,-20.585c4.1,-7.575 13.398,-9.987 20.662,-5.36c0,0 16.191,10.312 29.004,23.126c12.869,12.869 23.244,29.193 23.244,29.193c4.62,7.269 2.203,16.57 -5.372,20.67l-20.571,11.133c4.69,8.657 8.512,17.852 11.366,27.463l22.49,-6.695c8.255,-2.458 16.535,2.412 18.4,10.82c0,0 4.157,18.74 4.157,36.861c-0.001,18.197 -4.207,37.077 -4.207,37.077z" id="svg_29"/>
<circle cx="283.779" cy="287.886" r="37.594" id="svg_30"/>
</g>
</g>
<g display="none" id="svg_31">
<g display="inline" id="svg_32">
<rect x="326.113" y="201.398" transform="matrix(-0.7071,-0.7071,0.7071,-0.7071,472.8639,611.4978) " width="73.929" height="12.835" id="svg_33"/>
<polygon points="170.708,383.917 187.339,400.547 225.134,390.423 180.832,346.122 " id="svg_34"/>
<path d="m413.02,192.538c-0.016,-0.268 -0.051,-0.534 -0.072,-0.801c-0.049,-0.602 -0.095,-1.205 -0.173,-1.804c-0.037,-0.284 -0.094,-0.565 -0.138,-0.848c-0.089,-0.58 -0.174,-1.161 -0.291,-1.737c-0.062,-0.305 -0.146,-0.606 -0.215,-0.909c-0.126,-0.55 -0.246,-1.102 -0.397,-1.647c-0.09,-0.325 -0.205,-0.645 -0.304,-0.968c-0.159,-0.518 -0.31,-1.037 -0.492,-1.548c-0.123,-0.346 -0.272,-0.685 -0.406,-1.028c-0.188,-0.48 -0.365,-0.964 -0.574,-1.438c-0.162,-0.367 -0.35,-0.724 -0.525,-1.087c-0.211,-0.44 -0.41,-0.885 -0.64,-1.318c-0.206,-0.388 -0.441,-0.764 -0.661,-1.146c-0.229,-0.396 -0.444,-0.798 -0.688,-1.187c-0.259,-0.413 -0.549,-0.81 -0.826,-1.214c-0.237,-0.345 -0.458,-0.698 -0.708,-1.037c-0.344,-0.466 -0.721,-0.915 -1.089,-1.368c-0.213,-0.262 -0.409,-0.532 -0.63,-0.789c-0.604,-0.703 -1.239,-1.388 -1.905,-2.054l-0.004,0.004c-0.028,-0.03 -0.053,-0.062 -0.083,-0.091c-1.556,-1.556 -4.079,-1.556 -5.634,0c-1.556,1.556 -1.556,4.078 0,5.634c0.042,0.042 0.086,0.078 0.129,0.117c0.172,0.173 0.327,0.356 0.494,0.532c7.992,8.41 9.548,19.621 6.063,30.056c-0.915,1.556 -0.709,3.588 0.626,4.924c1.585,1.585 4.154,1.585 5.739,0c0.537,-0.537 0.888,-1.188 1.06,-1.874c0.08,-0.212 0.141,-0.429 0.218,-0.642c0.211,-0.588 0.421,-1.175 0.601,-1.771c0.072,-0.238 0.128,-0.48 0.195,-0.719c0.17,-0.607 0.339,-1.215 0.478,-1.829c0.054,-0.239 0.091,-0.48 0.141,-0.719c0.127,-0.619 0.253,-1.238 0.348,-1.862c0.036,-0.239 0.056,-0.479 0.088,-0.719c0.083,-0.627 0.166,-1.254 0.217,-1.884c0.02,-0.243 0.022,-0.487 0.037,-0.731c0.039,-0.627 0.077,-1.254 0.084,-1.882c0.003,-0.255 -0.012,-0.509 -0.015,-0.764c-0.007,-0.618 -0.012,-1.236 -0.048,-1.852z" id="svg_35"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm129.63,207.768l-3.086,3.086l-19.966,19.966l-143.453,143.453c-1.009,1.736 -2.674,3.036 -4.666,3.569l-54.971,14.725c-0.002,0 -0.003,0.001 -0.004,0.001l-31.304,8.386c-2.657,0.712 -5.493,-0.048 -7.438,-1.993c-1.945,-1.945 -2.705,-4.781 -1.993,-7.438l8.386,-31.304l14.292,-53.352c0.046,-1.907 0.791,-3.801 2.247,-5.257l144.847,-144.847l19.966,-19.966l3.086,-3.086c20.418,-20.418 53.64,-20.418 74.057,0c20.417,20.418 20.417,53.639 0,74.057z" id="svg_36"/>
</g>
</g>
<g display="none" id="svg_37">
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-62.545,379.174c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm0,-100c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm0,-102c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm212,186.032c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0zm0,-98c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0zm0,-101c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0z" id="svg_38"/>
</g>
<g display="none" id="svg_39">
<g display="inline" id="svg_40">
<rect x="187" y="256" width="192" height="125" id="svg_41"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm39.455,133.723l0,-19.607c0,-11.106 9.394,-20.109 20.5,-20.109s20.5,9.003 20.5,20.109l0,19.607l0,20.612c0,11.106 -9.394,20.109 -20.5,20.109s-20.5,-9.003 -20.5,-20.109l0,-20.612zm-119,0l0,-19.607c0,-11.106 8.394,-20.109 19.5,-20.109s19.5,9.003 19.5,20.109l0,19.607l0,20.612c0,11.106 -8.394,20.109 -19.5,20.109s-19.5,-9.003 -19.5,-20.109l0,-20.612zm217,242.478c0,11.019 -9.318,19.792 -20.337,19.792l-233.587,0c-11.018,0 -21.076,-8.773 -21.076,-19.792l0,-222.527c0,-11.019 10.058,-18.681 21.076,-18.681l19.924,0l0,19.342c0,19.959 17.041,36.197 37,36.197s37,-16.238 37,-36.197l0,-19.342l47,0l0,19.342c0,19.959 15.541,36.197 35.5,36.197s35.5,-16.238 35.5,-36.197l0,-19.342l21.663,0c11.019,0 20.337,7.662 20.337,18.681l0,222.527z" id="svg_42"/>
</g>
</g>
<g display="none" id="svg_43">
<g display="inline" id="svg_44">
<path d="m348,411.767c7.039,0 12,-5.706 12,-12.745l0,-175.1c0,-7.039 -4.961,-12.745 -12,-12.745s-12,5.706 -12,12.745l0,175.1c0,7.038 4.961,12.745 12,12.745z" id="svg_45"/>
<path d="m222,411.767c7.039,0 14,-5.706 14,-12.745l0,-175.1c0,-7.039 -6.961,-12.745 -14,-12.745s-14,5.706 -14,12.745l0,175.1c0,7.038 6.961,12.745 14,12.745z" id="svg_46"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm109.455,185.128l0,212.78c0,12.095 -10.073,23.085 -22.167,23.085l-173.233,0c-12.094,0 -21.6,-10.99 -21.6,-23.085l0,-212.78l0,-32.135l217,0l0,32.135zm0,-47.135l-217,0l0,-10.841c0,-12.095 9.505,-21.159 21.6,-21.159l55.634,0c0.497,-12 10.163,-21 21.942,-21l18.081,0c11.78,0 21.445,9 21.942,21l55.634,0c12.094,0 22.167,9.064 22.167,21.159l0,10.841z" id="svg_47"/>
<path d="m284,411.767c7.039,0 13,-5.706 13,-12.745l0,-175.1c0,-7.039 -5.961,-12.745 -13,-12.745s-13,5.706 -13,12.745l0,175.1c0,7.038 5.961,12.745 13,12.745z" id="svg_48"/>
</g>
</g>
<g id="svg_49">
<g id="svg_50">
<polygon points="196.661,391.813 223.84,340.524 381.46,229.666 196.625,326.192 " id="svg_51" fill="#03a9f4"/>
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm160.376,182.583l-117.664,174.425c-3.558,5.273 -10.504,7.04 -16.15,4.11l-47.144,-24.478l-64.897,56.276c-2.322,2.013 -5.247,3.062 -8.206,3.062c-1.752,0 -3.516,-0.368 -5.173,-1.119c-4.456,-2.023 -7.325,-6.456 -7.345,-11.349l-0.386,-91.883l-70.236,-36.656c-4.681,-2.443 -7.327,-7.562 -6.612,-12.794c0.715,-5.233 4.635,-9.454 9.801,-10.553l321.028,-68.291c4.992,-1.061 10.13,1.012 12.985,5.245c2.854,4.232 2.854,9.773 -0.001,14.005z" id="svg_52" fill="#03a9f4"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,29 +1,44 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Overpass+Mono:700" rel="stylesheet">
<style> <style>
body { body {
padding: 0; padding: 0;
margin: 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 { h1 {
display: inline-block; display: flex;
flex-direction: column;
align-self: center;
text-align: center;
margin: 0; margin: 0;
margin-left: -4px;
padding: 0px; padding: 0px;
width: 75vw;
font-size: 68px; font-size: 68px;
font-family: 'Source Code Pro', monospace; font-family: 'Overpass Mono', monospace;
font-weight: bold; font-weight: bold;
line-height: 0.8em; line-height: 0.8em;
letter-spacing: -3px; letter-spacing: -3px;
color: #333; color: #fff;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-webkit-text-stroke: 1px #0063B1;
text-shadow: text-shadow:
1px 0px 0px #ccc, 0px 1px 0px #eee, 3px 3px 0 #0063B1,
2px 1px 0px #ccc, 1px 2px 0px #eee, -1px -1px 0 #0063B1,
3px 2px 0px #ccc, 2px 3px 0px #eee, 1px -1px 0 #0063B1,
4px 3px 0px #ccc; -1px 1px 0 #0063B1,
1px 1px 0 #0063B1;
} }
h1 > span { h1 > span {
display: block; display: block;
@@ -31,17 +46,23 @@
margin: 0; margin: 0;
padding-top: 6px; padding-top: 6px;
} }
img { h1 > hr {
display: inline-block; padding: 0;
vertical-align: top; margin: 0;
margin-top: 22px;
border: 1px solid #0063B1;
border-radius: 50%;
} }
</style> </style>
</head> </head>
<body> <body>
<h1> <div>
<span>ftp-srv</span> <h1>
</h1> <span>ftp</span>
<img src="icon.svg" width="76px" height="76px" /> <hr />
<span>srv</span>
</h1>
</div>
</body> </body>
</html> </html>

9260
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "ftp-srv", "name": "ftp-srv",
"version": "0.0.0", "version": "0.0.0-development",
"description": "Modern, extensible FTP Server", "description": "Modern, extensible FTP Server",
"keywords": [ "keywords": [
"ftp", "ftp",
@@ -14,34 +14,29 @@
"license": "MIT", "license": "MIT",
"files": [ "files": [
"src", "src",
"bin",
"ftp-srv.d.ts" "ftp-srv.d.ts"
], ],
"main": "ftp-srv.js", "main": "ftp-srv.js",
"bin": "./bin/index.js",
"types": "./ftp-srv.d.ts", "types": "./ftp-srv.d.ts",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/trs/ftp-srv" "url": "https://github.com/trs/ftp-srv"
}, },
"scripts": { "scripts": {
"pre-release": "npm-run-all verify test:coverage build ", "pre-release": "npm run verify",
"build": "cross-env NODE_ENV=production npm run clean:prod",
"clean:prod": "rimraf dist/",
"commitmsg": "cz-customizable-ghooks", "commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch", "dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:coverage --silent", "prepush": "npm run verify && npm run test:once --silent",
"semantic-release": "semantic-release pre && npm publish && semantic-release post", "semantic-release": "semantic-release",
"start": "npm run dev", "start": "npm run dev",
"test": "npm run test:unit", "test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:check-coverage": "nyc check-coverage", "test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent", "verify": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\""
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w", },
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts", "release": {
"upload-coverage": "cat reports/coverage/lcov.info | coveralls", "verifyConditions": "condition-circle"
"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"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
@@ -52,41 +47,37 @@
} }
}, },
"dependencies": { "dependencies": {
"bunyan": "^1.8.10", "bluebird": "^3.5.1",
"lodash": "^4.17.4", "bunyan": "^1.8.12",
"moment": "^2.18.1", "ip": "^1.1.5",
"uuid": "^3.1.0", "lodash": "^4.17.10",
"when": "^3.7.8" "moment": "^2.22.2",
"uuid": "^3.3.2",
"yargs": "^12.0.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.0.2", "@icetee/ftp": "^1.0.3",
"chokidar-cli": "1.2.0", "chai": "^4.1.2",
"coveralls": "2.13.1", "condition-circle": "^2.0.1",
"cross-env": "3.1.4", "cross-env": "5.2.0",
"cz-customizable": "5.2.0", "cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0", "cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0", "eslint": "5.3.0",
"eslint": "4.5.0", "eslint-config-google": "0.9.1",
"eslint-config-google": "0.8.0", "eslint-friendly-formatter": "4.0.1",
"eslint-friendly-formatter": "3.0.0", "eslint-plugin-mocha": "^5.1.0",
"eslint-plugin-mocha": "^4.11.0", "eslint-plugin-node": "7.0.1",
"eslint-plugin-node": "5.1.1", "husky": "0.14.3",
"ftp": "^0.3.10",
"html-convert": "^2.1.7",
"husky": "0.13.3",
"istanbul": "0.4.5", "istanbul": "0.4.5",
"mocha": "3.5.0", "mocha": "^5.2.0",
"mocha-junit-reporter": "1.13.0", "mocha-junit-reporter": "1.18.0",
"mocha-multi-reporters": "1.1.5", "mocha-multi-reporters": "1.1.7",
"mocha-pretty-bunyan-nyan": "^1.0.4", "rimraf": "2.6.2",
"npm-run-all": "4.0.2", "semantic-release": "^15.9.8",
"nyc": "11.1.0", "sinon": "^6.1.5"
"rimraf": "2.6.1",
"semantic-release": "^6.3.6",
"sinon": "^2.3.5"
}, },
"engines": { "engines": {
"node": ">=6.x", "node": ">=6.x",
"npm": ">=3.9.5" "npm": ">=5.x"
} }
} }

View File

@@ -1,27 +1,32 @@
const _ = require('lodash'); const _ = require('lodash');
const when = require('when'); const Promise = require('bluebird');
const REGISTRY = require('./registry'); const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands { class FtpCommands {
constructor(connection) { constructor(connection) {
this.connection = connection; this.connection = connection;
this.previousCommand = {}; this.previousCommand = {};
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd)); this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd)); this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
} }
parse(message) { parse(message) {
const strippedMessage = message.replace(/"/g, ''); const strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' '); 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) => { const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param); if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param); else arg.push(param);
return {arg, flags}; return {arg, flags};
}, {arg: [], flags: []}); }, {arg: [], flags: []});
const command = { const command = {
directive: _.chain(directive).trim().toUpper().value(), directive,
arg: params.arg.length ? params.arg.join(' ') : null, arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags, flags: params.flags,
raw: message raw: message
@@ -62,7 +67,7 @@ class FtpCommands {
} }
const handler = commandRegister.handler.bind(this.connection); const handler = commandRegister.handler.bind(this.connection);
return when.try(handler, {log, command, previous_command: this.previousCommand}) return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => { .finally(() => {
this.previousCommand = _.clone(command); this.previousCommand = _.clone(command);
}); });

View File

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

View File

@@ -20,17 +20,17 @@ module.exports = {
}; };
function handleTLS() { function handleTLS() {
if (!this.server._tls) return this.reply(502); if (!this.server.options.tls) return this.reply(502);
if (this.secure) return this.reply(202); if (this.secure) return this.reply(202);
return this.reply(234) return this.reply(234)
.then(() => { .then(() => {
const secureContext = tls.createSecureContext(this.server._tls); const secureContext = tls.createSecureContext(this.server.options.tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, { const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true, isServer: true,
secureContext secureContext
}); });
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => { ['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
function forwardEvent() { function forwardEvent() {
this.emit.apply(this, arguments); this.emit.apply(this, arguments);
} }

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path'); const escapePath = require('../../helpers/escape-path');
module.exports = { module.exports = {
@@ -7,12 +7,12 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system'); if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.chdir.bind(this.fs), command.arg) return Promise.try(() => this.fs.chdir(command.arg))
.then(cwd => { .then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined; const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path); return this.reply(250, path);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'DELE', directive: 'DELE',
@@ -6,11 +6,11 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system'); if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.delete.bind(this.fs), command.arg) return Promise.try(() => this.fs.delete(command.arg))
.then(() => { .then(() => {
return this.reply(250); return this.reply(250);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -5,7 +5,7 @@ module.exports = {
handler: function () { handler: function () {
this.connector = new PassiveConnector(this); this.connector = new PassiveConnector(this);
return this.connector.setupServer() return this.connector.setupServer()
.then(server => { .then((server) => {
const {port} = server.address(); const {port} = server.address();
return this.reply(229, `EPSV OK (|||${port}|)`); return this.reply(229, `EPSV OK (|||${port}|)`);

View File

@@ -11,7 +11,7 @@ module.exports = {
return feats; return feats;
}, ['UTF8']) }, ['UTF8'])
.sort() .sort()
.map(feat => ({ .map((feat) => ({
message: ` ${feat}`, message: ` ${feat}`,
raw: true raw: true
})); }));

View File

@@ -12,7 +12,7 @@ module.exports = {
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]); const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
return this.reply(214, ...reply); return this.reply(214, ...reply);
} else { } else {
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t')); 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.'); return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
} }
}, },

View File

@@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const when = require('when'); const Promise = require('bluebird');
const getFileStat = require('../../helpers/file-stat'); const getFileStat = require('../../helpers/file-stat');
// http://cr.yp.to/ftp/list.html // http://cr.yp.to/ftp/list.html
@@ -16,35 +16,33 @@ module.exports = {
const path = command.arg || '.'; const path = command.arg || '.';
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.get.bind(this.fs), path)) .then(() => Promise.try(() => this.fs.get(path)))
.then(stat => stat.isDirectory() ? when.try(this.fs.list.bind(this.fs), path) : [stat]) .then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
.then(files => { .then((files) => {
const getFileMessage = file => { const getFileMessage = (file) => {
if (simple) return file.name; if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
}; };
const fileList = files.map(file => { return Promise.try(() => files.map((file) => {
const message = getFileMessage(file); const message = getFileMessage(file);
return { return {
raw: true, raw: true,
message, message,
socket: this.connector.socket socket: this.connector.socket
}; };
}); }));
return this.reply(150)
.then(() => {
if (fileList.length) return this.reply({}, ...fileList);
});
}) })
.then(() => { .tap(() => this.reply(150))
return this.reply(226, 'Transfer OK'); .then((fileList) => {
if (fileList.length) return this.reply({}, ...fileList);
}) })
.catch(when.TimeoutError, err => { .tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(451, err.message || 'No directory'); return this.reply(451, err.message || 'No directory');
}) })

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const moment = require('moment'); const moment = require('moment');
module.exports = { module.exports = {
@@ -7,12 +7,12 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); 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.get) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.get.bind(this.fs), command.arg) return Promise.try(() => this.fs.get(command.arg))
.then(fileStat => { .then((fileStat) => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS'); const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime); return this.reply(213, modificationTime);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path'); const escapePath = require('../../helpers/escape-path');
module.exports = { module.exports = {
@@ -7,12 +7,12 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system'); if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.mkdir.bind(this.fs), command.arg) return Promise.try(() => this.fs.mkdir(command.arg))
.then(dir => { .then((dir) => {
const path = dir ? `"${escapePath(dir)}"` : undefined; const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path); return this.reply(257, path);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -21,14 +21,19 @@ module.exports = {
}; };
function utf8([setting] = []) { function utf8([setting] = []) {
switch (_.toUpper(setting)) { const getEncoding = () => {
case 'ON': switch (_.toUpper(setting)) {
this.encoding = 'utf8'; case 'ON': return 'utf8';
return this.reply(200, 'UTF8 encoding on'); case 'OFF': return 'ascii';
case 'OFF': default: return null;
this.encoding = 'ascii'; }
return this.reply(200, 'UTF8 encoding off'); };
default:
return this.reply(501, 'Unknown setting for option'); const encoding = getEncoding();
} if (!encoding) return this.reply(501, 'Unknown setting for option');
this.encoding = encoding;
if (this.transferType !== 'binary') this.transferType = this.encoding;
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
} }

View File

@@ -12,7 +12,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(230); return this.reply(230);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(530, err.message || 'Authentication failed'); return this.reply(530, err.message || 'Authentication failed');
}); });

View File

@@ -5,8 +5,8 @@ module.exports = {
handler: function () { handler: function () {
this.connector = new PassiveConnector(this); this.connector = new PassiveConnector(this);
return this.connector.setupServer() return this.connector.setupServer()
.then(server => { .then((server) => {
const address = this.server.url.hostname; const address = this.server.options.pasv_url;
const {port} = server.address(); const {port} = server.address();
const host = address.replace(/\./g, ','); const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0; const portByte1 = port / 256 | 0;

View File

@@ -10,7 +10,7 @@ module.exports = {
if (rawConnection.length !== 6) return this.reply(425); if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.'); const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map(p => parseInt(p)); const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1]; const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port) return this.connector.setupConnection(ip, port)

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path'); const escapePath = require('../../helpers/escape-path');
module.exports = { module.exports = {
@@ -7,12 +7,12 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system'); if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.currentDirectory.bind(this.fs)) return Promise.try(() => this.fs.currentDirectory())
.then(cwd => { .then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined; const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path); return this.reply(257, path);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -9,7 +9,7 @@ module.exports = {
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater'); if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
this.restByteCount = byteCount; this.restByteCount = byteCount;
return this.reply(350, `Resarting next transfer at ${byteCount}`); return this.reply(350, `Restarting next transfer at ${byteCount}`);
}, },
syntax: '{{cmd}} <byte-count>', syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE' description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'RETR', directive: 'RETR',
@@ -6,32 +6,52 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system'); if (!this.fs.read) return this.reply(402, 'Not supported by file system');
const filePath = command.arg;
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.read.bind(this.fs), command.arg, {start: this.restByteCount})) .then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then(stream => { .then((fsResponse) => {
this.restByteCount = 0; let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
const eventsPromise = when.promise((resolve, reject) => { const destroyConnection = (connection, reject) => (err) => {
this.connector.socket.once('error', err => reject(err)); if (connection) connection.destroy(err);
reject(err);
};
stream.on('data', data => this.connector.socket const eventsPromise = new Promise((resolve, reject) => {
&& this.connector.socket.write(data, this.transferType)); stream.on('data', (data) => {
stream.once('error', err => reject(err)); if (stream) stream.pause();
if (this.connector.socket) {
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
}
});
stream.once('end', () => resolve()); stream.once('end', () => resolve());
stream.once('error', destroyConnection(this.connector.socket, reject));
this.connector.socket.once('error', destroyConnection(stream, reject));
}); });
return this.reply(150).then(() => this.connector.socket.resume()) this.restByteCount = 0;
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise) .then(() => eventsPromise)
.finally(() => stream.destroy ? stream.destroy() : null); .tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
}) })
.then(() => this.reply(226)) .catch(Promise.TimeoutError, (err) => {
.catch(when.TimeoutError, err => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
this.emit('RETR', err);
return this.reply(551, err.message); return this.reply(551, err.message);
}) })
.finally(() => { .finally(() => {

View File

@@ -1,4 +1,4 @@
const dele = require('./dele').handler; const {handler: dele} = require('./dele');
module.exports = { module.exports = {
directive: ['RMD', 'XRMD'], directive: ['RMD', 'XRMD'],

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'RNFR', directive: 'RNFR',
@@ -7,12 +7,12 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system'); if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command.arg; const fileName = command.arg;
return when.try(this.fs.get.bind(this.fs), fileName) return Promise.try(() => this.fs.get(fileName))
.then(() => { .then(() => {
this.renameFrom = fileName; this.renameFrom = fileName;
return this.reply(350); return this.reply(350);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'RNTO', directive: 'RNTO',
@@ -11,11 +11,11 @@ module.exports = {
const from = this.renameFrom; const from = this.renameFrom;
const to = command.arg; const to = command.arg;
return when.try(this.fs.rename.bind(this.fs), from, to) return Promise.try(() => this.fs.rename(from, to))
.then(() => { .then(() => {
return this.reply(250); return this.reply(250);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}) })

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = function ({log, command} = {}) { module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated'); if (!this.fs) return this.reply(550, 'File system not instantiated');
@@ -6,11 +6,11 @@ module.exports = function ({log, command} = {}) {
const [mode, ...fileNameParts] = command.arg.split(' '); const [mode, ...fileNameParts] = command.arg.split(' ');
const fileName = fileNameParts.join(' '); const fileName = fileNameParts.join(' ');
return when.try(this.fs.chmod.bind(this.fs), fileName, parseInt(mode, 8)) return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
.then(() => { .then(() => {
return this.reply(200); return this.reply(200);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(500); return this.reply(500);
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const _ = require('lodash'); const _ = require('lodash');
const registry = require('./registry'); const registry = require('./registry');
@@ -13,7 +13,7 @@ module.exports = {
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502); if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
const handler = registry[subCommand.directive].handler.bind(this); const handler = registry[subCommand.directive].handler.bind(this);
return when.try(handler, {log: subLog, command: subCommand}); return Promise.resolve(handler({log: subLog, command: subCommand}));
}, },
syntax: '{{cmd}} <subVerb> [...<subParams>]', syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server' description: 'Sends site specific commands to remote server'

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'SIZE', directive: 'SIZE',
@@ -6,11 +6,11 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); 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.get) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.get.bind(this.fs), command.arg) return Promise.try(() => this.fs.get(command.arg))
.then(fileStat => { .then((fileStat) => {
return this.reply(213, {message: fileStat.size}); return this.reply(213, {message: fileStat.size});
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const when = require('when'); const Promise = require('bluebird');
const getFileStat = require('../../helpers/file-stat'); const getFileStat = require('../../helpers/file-stat');
module.exports = { module.exports = {
@@ -11,27 +11,28 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated'); 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.get) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.get.bind(this.fs), path) return Promise.try(() => this.fs.get(path))
.then(stat => { .then((stat) => {
if (stat.isDirectory()) { if (stat.isDirectory()) {
if (!this.fs.list) return this.reply(402, 'Not supported by file system'); if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.list.bind(this.fs), path) return Promise.try(() => this.fs.list(path))
.then(files => { .then((stats) => [213, stats]);
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')));
} }
return [212, [stat]];
}) })
.catch(err => { .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); log.error(err);
return this.reply(450, err.message); return this.reply(450, err.message);
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
module.exports = { module.exports = {
directive: 'STOR', directive: 'STOR',
@@ -11,36 +11,58 @@ module.exports = {
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append, start: this.restByteCount})) .then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then(stream => { .then((fsResponse) => {
this.restByteCount = 0; let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const streamPromise = when.promise((resolve, reject) => { const destroyConnection = (connection, reject) => (err) => {
stream.once('error', err => reject(err)); if (connection) {
if (connection.writeable) connection.end();
connection.destroy(err);
}
reject(err);
};
const streamPromise = new Promise((resolve, reject) => {
stream.once('error', destroyConnection(this.connector.socket, reject));
stream.once('finish', () => resolve()); stream.once('finish', () => resolve());
}); });
const socketPromise = when.promise((resolve, reject) => { const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', data => stream.write(data, this.transferType)); this.connector.socket.on('data', (data) => {
if (this.connector.socket) this.connector.socket.pause();
if (stream) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
}
});
this.connector.socket.once('end', () => { this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close'); if (stream.listenerCount('close')) stream.emit('close');
else stream.end(); else stream.end();
resolve(); resolve();
}); });
this.connector.socket.once('error', err => reject(err)); this.connector.socket.once('error', destroyConnection(stream, reject));
}); });
this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket.resume()) return this.reply(150).then(() => this.connector.socket.resume())
.then(() => when.join(streamPromise, socketPromise)) .then(() => Promise.all([streamPromise, socketPromise]))
.finally(() => stream.destroy ? stream.destroy() : null); .tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
}) })
.then(() => this.reply(226, fileName)) .catch(Promise.TimeoutError, (err) => {
.catch(when.TimeoutError, err => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
this.emit('STOR', err);
return this.reply(550, err.message); return this.reply(550, err.message);
}) })
.finally(() => { .finally(() => {

View File

@@ -1,6 +1,5 @@
const when = require('when'); const Promise = require('bluebird');
const {handler: stor} = require('./stor');
const stor = require('./stor').handler;
module.exports = { module.exports = {
directive: 'STOU', directive: 'STOU',
@@ -9,12 +8,10 @@ module.exports = {
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system'); if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
const fileName = args.command.arg; const fileName = args.command.arg;
return when.try(() => { return Promise.try(() => this.fs.get(fileName))
return when.try(this.fs.get.bind(this.fs), fileName) .then(() => Promise.try(() => this.fs.getUniqueName()))
.then(() => when.try(this.fs.getUniqueName.bind(this.fs))) .catch(() => fileName)
.catch(() => when.resolve(fileName)); .then((name) => {
})
.then(name => {
args.command.arg = name; args.command.arg = name;
return stor.call(this, args); return stor.call(this, args);
}); });

View File

@@ -1,4 +1,3 @@
module.exports = { module.exports = {
directive: 'TYPE', directive: 'TYPE',
handler: function ({command} = {}) { handler: function ({command} = {}) {

View File

@@ -13,7 +13,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(230); return this.reply(230);
}) })
.catch(err => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(530, err.message || 'Authentication failed'); return this.reply(530, err.message || 'Authentication failed');
}); });

View File

@@ -43,7 +43,7 @@ const commands = [
const registry = commands.reduce((result, cmd) => { const registry = commands.reduce((result, cmd) => {
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive]; const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
aliases.forEach(alias => result[alias] = cmd); aliases.forEach((alias) => result[alias] = cmd);
return result; return result;
}, {}); }, {});

View File

@@ -1,7 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const uuid = require('uuid'); const uuid = require('uuid');
const when = require('when'); const Promise = require('bluebird');
const sequence = require('when/sequence'); const EventEmitter = require('events');
const BaseConnector = require('./connector/base'); const BaseConnector = require('./connector/base');
const FileSystem = require('./fs'); const FileSystem = require('./fs');
@@ -9,8 +9,9 @@ const Commands = require('./commands');
const errors = require('./errors'); const errors = require('./errors');
const DEFAULT_MESSAGE = require('./messages'); const DEFAULT_MESSAGE = require('./messages');
class FtpConnection { class FtpConnection extends EventEmitter {
constructor(server, options) { constructor(server, options) {
super();
this.server = server; this.server = server;
this.id = uuid.v4(); this.id = uuid.v4();
this.log = options.log.child({id: this.id, ip: this.ip}); this.log = options.log.child({id: this.id, ip: this.ip});
@@ -24,7 +25,7 @@ class FtpConnection {
this.connector = new BaseConnector(this); this.connector = new BaseConnector(this);
this.commandSocket = options.socket; this.commandSocket = options.socket;
this.commandSocket.on('error', err => { this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error'); this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err}); this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
}); });
@@ -33,18 +34,19 @@ class FtpConnection {
this.commandSocket.on('close', () => { this.commandSocket.on('close', () => {
if (this.connector) this.connector.end(); if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy(); if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
this.removeAllListeners();
}); });
} }
_handleData(data) { _handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n')); const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages); this.log.trace(messages);
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message))); return Promise.mapSeries(messages, (message) => this.commands.handle(message));
} }
get ip() { get ip() {
try { try {
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress; return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
} catch (ex) { } catch (ex) {
return null; return null;
} }
@@ -65,14 +67,13 @@ class FtpConnection {
} }
close(code = 421, message = 'Closing connection') { close(code = 421, message = 'Closing connection') {
return when return Promise.resolve(code)
.resolve(code) .then((_code) => _code && this.reply(_code, message))
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end()); .then(() => this.commandSocket && this.commandSocket.end());
} }
login(username, password) { login(username, password) {
return when.try(() => { return Promise.try(() => {
const loginListeners = this.server.listeners('login'); const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) { if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500); if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
@@ -93,17 +94,21 @@ class FtpConnection {
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters]; if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}]; if (!letters.length) letters = [{}];
return when.map(letters, promise => { return Promise.map(letters, (promise, index) => {
return when(promise) return Promise.resolve(promise)
.then(letter => { .then((letter) => {
if (!letter) letter = {}; if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket; if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information'; if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding; if (!letter.encoding) letter.encoding = this.encoding;
return when(letter.message) // allow passing in a promise as a message return Promise.resolve(letter.message) // allow passing in a promise as a message
.then(message => { .then((message) => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message; letter.message = message;
return letter; return letter;
}); });
@@ -111,16 +116,11 @@ class FtpConnection {
}); });
}; };
const processLetter = (letter, index) => { const processLetter = (letter) => {
return when.promise((resolve, reject) => { return new Promise((resolve, reject) => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
if (letter.socket && letter.socket.writable) { if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, packet}, 'Reply'); this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(packet + '\r\n', letter.encoding, err => { letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
if (err) { if (err) {
this.log.error(err); this.log.error(err);
return reject(err); return reject(err);
@@ -132,8 +132,10 @@ class FtpConnection {
}; };
return satisfyParameters() return satisfyParameters()
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index)))) .then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
.catch(err => { return processLetter(letter, index);
}))
.catch((err) => {
this.log.error(err); this.log.error(err);
}); });
} }

View File

@@ -1,6 +1,6 @@
const {Socket} = require('net'); const {Socket} = require('net');
const tls = require('tls'); const tls = require('tls');
const when = require('when'); const Promise = require('bluebird');
const Connector = require('./base'); const Connector = require('./base');
class Active extends Connector { class Active extends Connector {
@@ -10,29 +10,31 @@ class Active extends Connector {
} }
waitForConnection({timeout = 5000, delay = 250} = {}) { waitForConnection({timeout = 5000, delay = 250} = {}) {
return when.iterate( const checkSocket = () => {
() => {}, if (this.dataSocket && this.dataSocket.connected) {
() => this.dataSocket && this.dataSocket.connected, return Promise.resolve(this.dataSocket);
() => when().delay(delay) }
).timeout(timeout) return Promise.resolve().delay(delay)
.then(() => this.dataSocket); .then(() => checkSocket());
};
return checkSocket().timeout(timeout);
} }
setupConnection(host, port, family = 4) { setupConnection(host, port, family = 4) {
const closeExistingServer = () => this.dataSocket ? const closeExistingServer = () => Promise.resolve(
when(this.dataSocket.destroy()) : this.dataSocket ? this.dataSocket.destroy() : undefined);
when.resolve();
return closeExistingServer() return closeExistingServer()
.then(() => { .then(() => {
this.dataSocket = new Socket(); this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.connection.transferType); this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err})); this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => { this.dataSocket.connect({host, port, family}, () => {
this.dataSocket.pause(); this.dataSocket.pause();
if (this.connection.secure) { if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls); const secureContext = tls.createSecureContext(this.server.options.tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, { const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true, isServer: true,
secureContext secureContext

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const errors = require('../errors'); const errors = require('../errors');
class Connector { class Connector {
@@ -23,15 +23,31 @@ class Connector {
} }
waitForConnection() { waitForConnection() {
return when.reject(new errors.ConnectorError('No connector setup, send PASV or PORT')); return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
} }
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket = null;
}
}
closeServer() {
if (this.dataServer) {
this.dataServer.close();
this.dataServer = null;
}
}
end() { end() {
if (this.dataSocket) this.dataSocket.end(); this.closeSocket();
if (this.dataServer) this.dataServer.close(); this.closeServer();
this.dataSocket = null;
this.dataServer = null;
this.type = false; this.type = false;
this.connection.connector = new Connector(this);
} }
} }
module.exports = Connector; module.exports = Connector;

View File

@@ -1,9 +1,9 @@
const net = require('net'); const net = require('net');
const tls = require('tls'); const tls = require('tls');
const when = require('when'); const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base'); const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors'); const errors = require('../errors');
class Passive extends Connector { class Passive extends Connector {
@@ -12,26 +12,26 @@ class Passive extends Connector {
this.type = 'passive'; this.type = 'passive';
} }
waitForConnection({timeout = 5000, delay = 250} = {}) { waitForConnection({timeout = 5000, delay = 50} = {}) {
if (!this.dataServer) return when.reject(new errors.ConnectorError('Passive server not setup')); if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
return when.iterate(
() => {}, const checkSocket = () => {
() => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected, if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
() => when().delay(delay) return Promise.resolve(this.dataSocket);
).timeout(timeout) }
.then(() => this.dataSocket); return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout);
} }
setupServer() { setupServer() {
const closeExistingServer = () => this.dataServer ? this.closeServer();
when.promise(resolve => this.dataServer.close(() => resolve())) : return this.server.getNextPasvPort()
when.resolve(); .then((port) => {
const connectionHandler = (socket) => {
return closeExistingServer() if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
this.log.error({ this.log.error({
pasv_connection: socket.remoteAddress, pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress cmd_connection: this.connection.commandSocket.remoteAddress
@@ -43,36 +43,35 @@ class Passive extends Connector {
} }
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.'); this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
if (this.connection.secure) { this.dataSocket = socket;
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(socket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
} else {
this.dataSocket = socket;
}
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.transferType); this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err})); this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.trace('Passive connection closed'); if (!this.connection.secure) {
this.end(); this.dataSocket.connected = true;
}); }
}; };
this.dataSocket = null; this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1; this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => { this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.once('close', () => {
this.log.trace('Passive server closed'); this.log.trace('Passive server closed');
this.dataServer = null; this.end();
}); });
return when.promise((resolve, reject) => { if (this.connection.secure) {
this.dataServer.listen(port, err => { this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err); if (err) reject(err);
else { else {
this.log.debug({port}, 'Passive connection listening'); this.log.debug({port}, 'Passive connection listening');
@@ -83,15 +82,5 @@ class Passive extends Connector {
}); });
} }
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
} }
module.exports = Passive; module.exports = Passive;

View File

@@ -1,27 +1,38 @@
const _ = require('lodash'); const _ = require('lodash');
const nodePath = require('path'); const nodePath = require('path');
const uuid = require('uuid'); const uuid = require('uuid');
const when = require('when'); const Promise = require('bluebird');
const whenNode = require('when/node'); const fs = Promise.promisifyAll(require('fs'));
const syncFs = require('fs');
const fs = whenNode.liftAll(syncFs);
const errors = require('./errors'); const errors = require('./errors');
class FileSystem { class FileSystem {
constructor(connection, {root, cwd} = {}) { constructor(connection, {root, cwd} = {}) {
this.connection = connection; this.connection = connection;
this.cwd = cwd || nodePath.sep; this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this.root = root || process.cwd(); this._root = nodePath.resolve(root || process.cwd());
} }
_resolvePath(path = '') { get root() {
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep); return this._root;
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep; }
const serverPath = nodePath.join(nodePath.sep, cwd, path);
const fsPath = nodePath.join(this.root, serverPath); _resolvePath(path = '.') {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
return nodePath.join(resolvedPath);
})();
return { return {
serverPath, clientPath,
fsPath fsPath
}; };
} }
@@ -32,20 +43,20 @@ class FileSystem {
get(fileName) { get(fileName) {
const {fsPath} = this._resolvePath(fileName); const {fsPath} = this._resolvePath(fileName);
return fs.stat(fsPath) return fs.statAsync(fsPath)
.then(stat => _.set(stat, 'name', fileName)); .then((stat) => _.set(stat, 'name', fileName));
} }
list(path = '.') { list(path = '.') {
const {fsPath} = this._resolvePath(path); const {fsPath} = this._resolvePath(path);
return fs.readdir(fsPath) return fs.readdirAsync(fsPath)
.then(fileNames => { .then((fileNames) => {
return when.map(fileNames, fileName => { return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName); const filePath = nodePath.join(fsPath, fileName);
return fs.access(filePath, syncFs.constants.F_OK) return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => { .then(() => {
return fs.stat(filePath) return fs.statAsync(filePath)
.then(stat => _.set(stat, 'name', fileName)); .then((stat) => _.set(stat, 'name', fileName));
}) })
.catch(() => null); .catch(() => null);
}); });
@@ -54,61 +65,67 @@ class FileSystem {
} }
chdir(path = '.') { chdir(path = '.') {
const {fsPath, serverPath} = this._resolvePath(path); const {fsPath, clientPath} = this._resolvePath(path);
return fs.stat(fsPath) return fs.statAsync(fsPath)
.tap(stat => { .tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory'); if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
}) })
.then(() => { .then(() => {
this.cwd = serverPath; this.cwd = clientPath;
return this.currentDirectory(); return this.currentDirectory();
}); });
} }
write(fileName, {append = false, start = undefined} = {}) { write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName); const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start}); const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlink(fsPath)); stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end()); stream.once('close', () => stream.end());
return stream; return {
stream,
clientPath
};
} }
read(fileName, {start = undefined} = {}) { read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName); const {fsPath, clientPath} = this._resolvePath(fileName);
return fs.stat(fsPath) return fs.statAsync(fsPath)
.tap(stat => { .tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory'); if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
}) })
.then(() => { .then(() => {
const stream = syncFs.createReadStream(fsPath, {flags: 'r', start}); const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return stream; return {
stream,
clientPath
};
}); });
} }
delete(path) { delete(path) {
const {fsPath} = this._resolvePath(path); const {fsPath} = this._resolvePath(path);
return fs.stat(fsPath) return fs.statAsync(fsPath)
.then(stat => { .then((stat) => {
if (stat.isDirectory()) return fs.rmdir(fsPath); if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlink(fsPath); else return fs.unlinkAsync(fsPath);
}); });
} }
mkdir(path) { mkdir(path) {
const {fsPath} = this._resolvePath(path); const {fsPath} = this._resolvePath(path);
return fs.mkdir(fsPath) return fs.mkdirAsync(fsPath)
.then(() => fsPath); .then(() => fsPath);
} }
rename(from, to) { rename(from, to) {
const {fsPath: fromPath} = this._resolvePath(from); const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to); const {fsPath: toPath} = this._resolvePath(to);
return fs.rename(fromPath, toPath); return fs.renameAsync(fromPath, toPath);
} }
chmod(path, mode) { chmod(path, mode) {
const {fsPath} = this._resolvePath(path); const {fsPath} = this._resolvePath(path);
return fs.chmod(fsPath, mode); return fs.chmodAsync(fsPath, mode);
} }
getUniqueName() { getUniqueName() {

View File

@@ -18,7 +18,8 @@ module.exports = function (fileStat, format = 'ls') {
function ls(fileStat) { function ls(fileStat) {
const now = moment.utc(); const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime)); const mtime = moment.utc(new Date(fileStat.mtime));
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY'; const timeDiff = now.diff(mtime, 'months');
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
return [ return [
fileStat.mode ? [ fileStat.mode ? [

View File

@@ -1,27 +1,36 @@
const net = require('net'); const net = require('net');
const when = require('when'); const Promise = require('bluebird');
const errors = require('../errors'); const errors = require('../errors');
module.exports = function (min = 1, max = undefined) { function* portNumberGenerator(min, max) {
return when.promise((resolve, reject) => { let current = min;
let checkPort = min; while (true) {
let portCheckServer = net.createServer(); if (current > 65535 || current > max) {
portCheckServer.maxConnections = 0; current = min;
portCheckServer.on('error', () => { }
if (checkPort < 65535 && (!max || checkPort < max)) { yield current++;
checkPort = checkPort + 1; }
portCheckServer.listen(checkPort); }
} else {
reject(new errors.GeneralError('Unable to find open port', 500)); function getNextPortFactory(min, max = Infinity) {
} const nextPortNumber = portNumberGenerator(min, max);
}); const portCheckServer = net.createServer();
portCheckServer.on('listening', () => { portCheckServer.maxConnections = 0;
const {port} = portCheckServer.address(); portCheckServer.on('error', () => {
portCheckServer.close(() => { portCheckServer.listen(nextPortNumber.next().value);
portCheckServer = null;
resolve(port);
});
});
portCheckServer.listen(checkPort);
}); });
return () => new Promise((resolve) => {
portCheckServer.once('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => resolve(port));
});
portCheckServer.listen(nextPortNumber.next().value);
})
.catch(RangeError, (err) => Promise.reject(new errors.ConnectorError(err.message)));
}
module.exports = {
getNextPortFactory,
portNumberGenerator
}; };

View File

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

View File

@@ -1,38 +1,45 @@
const _ = require('lodash'); const _ = require('lodash');
const when = require('when'); const Promise = require('bluebird');
const nodeUrl = require('url'); const nodeUrl = require('url');
const buyan = require('bunyan'); const buyan = require('bunyan');
const net = require('net'); const net = require('net');
const tls = require('tls'); const tls = require('tls');
const fs = require('fs'); const EventEmitter = require('events');
const Connection = require('./connection'); const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host'); const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer { class FtpServer extends EventEmitter {
constructor(url, options = {}) { constructor(options = {}) {
this.options = _.merge({ super();
this.options = Object.assign({
log: buyan.createLogger({name: 'ftp-srv'}), log: buyan.createLogger({name: 'ftp-srv'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false, anonymous: false,
pasv_range: 22,
file_format: 'ls', file_format: 'ls',
blacklist: [], blacklist: [],
whitelist: [], whitelist: [],
greeting: null, greeting: null,
tls: false tls: false
}, options); }, options);
this._greeting = this.setupGreeting(this.options.greeting); this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage(); this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting; delete this.options.greeting;
delete this.options.tls;
this.connections = {}; this.connections = {};
this.log = this.options.log; this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21'); this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const serverConnectionHandler = socket => { const serverConnectionHandler = (socket) => {
let connection = new Connection(this, {log: this.log, socket}); let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection; this.connections[connection.id] = connection;
@@ -43,29 +50,31 @@ class FtpServer {
return connection.reply(220, ...greeting, features) return connection.reply(220, ...greeting, features)
.finally(() => socket.resume()); .finally(() => socket.resume());
}; };
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true}); const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler); this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error')); this.server.on('error', (err) => this.log.error(err, '[Event] error'));
this.on = this.server.on.bind(this.server);
this.once = this.server.once.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);
process.on('SIGTERM', () => this.quit()); const quit = _.debounce(this.quit.bind(this), 100);
process.on('SIGINT', () => this.quit());
process.on('SIGQUIT', () => this.quit()); process.on('SIGTERM', quit);
process.on('SIGINT', quit);
process.on('SIGQUIT', quit);
} }
get isTLS() { get isTLS() {
return this.url.protocol === 'ftps:' && this._tls; return this.url.protocol === 'ftps:' && this.options.tls;
} }
listen() { listen() {
return resolveHost(this.url.hostname) return resolveHost(this.options.pasv_url || this.url.hostname)
.then(hostname => { .then((pasvUrl) => {
this.url.hostname = hostname; this.options.pasv_url = pasvUrl;
return when.promise((resolve, reject) => {
this.server.listen(this.url.port, err => { return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err); if (err) return reject(err);
this.log.info({ this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''), protocol: this.url.protocol.replace(/\W/g, ''),
@@ -79,22 +88,9 @@ class FtpServer {
} }
emitPromise(action, ...data) { emitPromise(action, ...data) {
const defer = when.defer(); return new Promise((resolve, reject) => {
const params = _.concat(data, [defer.resolve, defer.reject]); const params = _.concat(data, [resolve, reject]);
this.server.emit(action, ...params); this.emit.call(this, action, ...params);
return defer.promise;
}
emit(action, ...data) {
this.server.emit(action, ...data);
}
setupTLS(_tls) {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
}); });
} }
@@ -116,7 +112,7 @@ class FtpServer {
} }
disconnectClient(id) { disconnectClient(id) {
return when.promise(resolve => { return new Promise((resolve) => {
const client = this.connections[id]; const client = this.connections[id];
if (!client) return resolve(); if (!client) return resolve();
delete this.connections[id]; delete this.connections[id];
@@ -138,13 +134,14 @@ class FtpServer {
close() { close() {
this.log.info('Server closing...'); this.log.info('Server closing...');
this.server.maxConnections = 0; this.server.maxConnections = 0;
return when.map(Object.keys(this.connections), id => when.try(this.disconnectClient.bind(this), id)) return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => when.promise(resolve => { .then(() => new Promise((resolve) => {
this.server.close(err => { this.server.close((err) => {
if (err) this.log.error(err, 'Error closing server'); if (err) this.log.error(err, 'Error closing server');
resolve('Closed'); resolve('Closed');
}); });
})); }))
.then(() => this.removeAllListeners());
} }
} }

View File

@@ -1,5 +1,5 @@
const {expect} = require('chai'); const {expect} = require('chai');
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -11,7 +11,7 @@ describe('FtpCommands', function () {
let mockConnection = { let mockConnection = {
authenticated: false, authenticated: false,
log: bunyan.createLogger({name: 'FtpCommands'}), log: bunyan.createLogger({name: 'FtpCommands'}),
reply: () => when.resolve({}), reply: () => Promise.resolve({}),
server: { server: {
options: { options: {
blacklist: ['allo'] blacklist: ['allo']
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
}; };
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
commands = new FtpCommands(mockConnection); commands = new FtpCommands(mockConnection);
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => { it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A'); const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
expect(cmd.directive).to.equal('TEST'); expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2'); expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']); expect(cmd.flags).to.deep.equal(['-l', '-A']);
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A'); expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
}); });
@@ -76,6 +76,13 @@ describe('FtpCommands', function () {
expect(cmd.flags).to.deep.equal(['-l']); expect(cmd.flags).to.deep.equal(['-l']);
expect(cmd.raw).to.equal('list -l'); expect(cmd.raw).to.equal('list -l');
}); });
it('does not check for option flags', () => {
const cmd = commands.parse('retr -test');
expect(cmd.directive).to.equal('RETR');
expect(cmd.arg).to.equal('-test');
expect(cmd.flags).to.deep.equal([]);
});
}); });
describe('handle', function () { describe('handle', function () {

View File

@@ -1,21 +1,21 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const CMD = 'ABOR'; const CMD = 'ABOR';
describe(CMD, function () { describe.skip(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
connector: { connector: {
waitForConnection: () => when.resolve(), waitForConnection: () => Promise.resolve(),
end: () => when.resolve() end: () => Promise.resolve()
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection'); sandbox.spy(mockClient.connector, 'waitForConnection');
@@ -33,7 +33,7 @@ describe(CMD, function () {
.then(() => { .then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1); expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(0); expect(mockClient.connector.end.callCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(226); expect(mockClient.reply.args[0][0]).to.equal(225);
}); });
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'ALLO';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,15 +6,17 @@ const CMD = 'AUTH';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
server: { server: {
_tls: {} options: {
tls: {}
}
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -8,15 +8,15 @@ describe(CMD, function () {
let sandbox; let sandbox;
let log = bunyan.createLogger({name: CMD}); let log = bunyan.createLogger({name: CMD});
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
fs: { fs: {
chdir: () => when.resolve() chdir: () => Promise.resolve()
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir'); sandbox.spy(mockClient.fs, 'chdir');

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves(); sandbox.stub(mockClient.fs, 'chdir').resolves();

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'delete').resolves(); sandbox.stub(mockClient.fs, 'delete').resolves();

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -8,12 +8,12 @@ const CMD = 'EPRT';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves(); sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -8,12 +8,12 @@ const CMD = 'EPSV';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({ sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'HELP';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -14,8 +14,8 @@ describe(CMD, function () {
get: () => {} get: () => {}
}, },
connector: { connector: {
waitForConnection: () => when({}), waitForConnection: () => Promise.resolve({}),
end: () => {} end: () => Promise.resolve({})
}, },
commandSocket: { commandSocket: {
resume: () => {}, resume: () => {},
@@ -25,7 +25,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({ sandbox.stub(mockClient.fs, 'get').resolves({
@@ -165,7 +165,7 @@ describe(CMD, function () {
}); });
it('. // unsuccessful (timeout)', () => { it('. // unsuccessful (timeout)', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').returns(when.reject(new when.TimeoutError())); sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
return cmdFn({log, command: {directive: CMD}}) return cmdFn({log, command: {directive: CMD}})
.then(() => { .then(() => {

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'}); sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'mkdir').resolves(); sandbox.stub(mockClient.fs, 'mkdir').resolves();

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'MODE';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -14,8 +14,8 @@ describe(CMD, function () {
list: () => {} list: () => {}
}, },
connector: { connector: {
waitForConnection: () => when({}), waitForConnection: () => Promise.resolve({}),
end: () => {} end: () => Promise.resolve({})
}, },
commandSocket: { commandSocket: {
resume: () => {}, resume: () => {},
@@ -25,7 +25,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({ sandbox.stub(mockClient.fs, 'get').resolves({

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'NOOP';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'OPTS';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -15,7 +15,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'login').resolves(); sandbox.stub(mockClient, 'login').resolves();

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,13 +6,13 @@ const CMD = 'PBSZ';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
server: {} server: {}
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -8,12 +8,12 @@ const CMD = 'PORT';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves(); sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,13 +6,13 @@ const CMD = 'PROT';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
server: {} server: {}
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves(); sandbox.stub(mockClient.fs, 'currentDirectory').resolves();

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'close').resolves(); sandbox.stub(mockClient, 'close').resolves();
}); });

View File

@@ -1,17 +1,17 @@
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const when = require('when'); const Promise = require('bluebird');
const CMD = 'REST'; const CMD = 'REST';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,34 +1,41 @@
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const EventEmitter = require('events');
const CMD = 'RETR'; const CMD = 'RETR';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
let log = bunyan.createLogger({name: CMD}); let log = bunyan.createLogger({name: CMD});
let emitter;
const mockClient = { const mockClient = {
commandSocket: { commandSocket: {
pause: () => {}, pause: () => {},
resume: () => {} resume: () => {}
}, },
reply: () => when.resolve(), reply: () => Promise.resolve(),
connector: { connector: {
waitForConnection: () => when.resolve({ waitForConnection: () => Promise.resolve({
resume: () => {} resume: () => {}
}), }),
end: () => {} end: () => Promise.resolve({})
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
read: () => {} read: () => {}
}; };
emitter = new EventEmitter();
mockClient.emit = emitter.emit.bind(emitter);
mockClient.on = emitter.on.bind(emitter);
mockClient.once = emitter.once.bind(emitter);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });
afterEach(() => sandbox.restore()); afterEach(() => sandbox.restore());
@@ -53,9 +60,10 @@ describe(CMD, function () {
it('// unsuccessful | connector times out', () => { it('// unsuccessful | connector times out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () { sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new when.TimeoutError()); return Promise.reject(new Promise.TimeoutError());
}); });
return cmdFn({log, command: {arg: 'test.txt'}}) return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => { .then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425); expect(mockClient.reply.args[0][0]).to.equal(425);
@@ -64,7 +72,7 @@ describe(CMD, function () {
it('// unsuccessful | connector errors out', () => { it('// unsuccessful | connector errors out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () { sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new Error('test')); return Promise.reject(new Error('test'));
}); });
return cmdFn({log, command: {arg: 'test.txt'}}) return cmdFn({log, command: {arg: 'test.txt'}})
@@ -72,4 +80,20 @@ describe(CMD, function () {
expect(mockClient.reply.args[0][0]).to.equal(551); expect(mockClient.reply.args[0][0]).to.equal(551);
}); });
}); });
it('// unsuccessful | emits error event', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return Promise.reject(new Error('test'));
});
let errorEmitted = false;
emitter.once('RETR', (err) => {
errorEmitted = !!err;
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});
});
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,15 +6,15 @@ const CMD = 'RNFR';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockLog = {error: () => {}}; const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()}; const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.renameFrom = 'test'; mockClient.renameFrom = 'test';
mockClient.fs = { mockClient.fs = {
get: () => when.resolve() get: () => Promise.resolve()
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,16 +6,16 @@ const CMD = 'RNTO';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockLog = {error: () => {}}; const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()}; const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.renameFrom = 'test'; mockClient.renameFrom = 'test';
mockClient.fs = { mockClient.fs = {
get: () => when.resolve(), get: () => Promise.resolve(),
rename: () => when.resolve() rename: () => Promise.resolve()
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,14 +6,14 @@ const CMD = 'CHMOD';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockLog = {error: () => {}}; const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()}; const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient); const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
chmod: () => when.resolve() chmod: () => Promise.resolve()
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
@@ -23,7 +23,7 @@ describe(CMD, function () {
sandbox.restore(); sandbox.restore();
}); });
it('// unsuccessful | no file system', done => { it('// unsuccessful | no file system', (done) => {
delete mockClient.fs; delete mockClient.fs;
cmdFn() cmdFn()
@@ -34,7 +34,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('// unsuccessful | file system does not have functions', done => { it('// unsuccessful | file system does not have functions', (done) => {
mockClient.fs = {}; mockClient.fs = {};
cmdFn() cmdFn()
@@ -45,7 +45,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('777 test // unsuccessful | file chmod fails', done => { it('777 test // unsuccessful | file chmod fails', (done) => {
mockClient.fs.chmod.restore(); mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test')); sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
@@ -57,7 +57,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('777 test // successful', done => { it('777 test // successful', (done) => {
cmdFn({log: mockLog, command: {arg: '777 test'}}) cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => { .then(() => {
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]); expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
@@ -11,13 +11,13 @@ describe(CMD, function () {
let sandbox; let sandbox;
const log = bunyan.createLogger({name: 'site-test'}); const log = bunyan.createLogger({name: 'site-test'});
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
commands: new FtpCommands() commands: new FtpCommands()
}; };
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,14 +6,14 @@ const CMD = 'SIZE';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockLog = {error: () => {}}; const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()}; const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
get: () => when.resolve({size: 1}) get: () => Promise.resolve({size: 1})
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,15 +6,15 @@ const CMD = 'STAT';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockLog = {error: () => {}}; const mockLog = {error: () => {}};
const mockClient = {reply: () => when.resolve()}; const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
get: () => when.resolve({}), get: () => Promise.resolve({}),
list: () => when.resolve([]) list: () => Promise.resolve([])
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,34 +1,41 @@
const when = require('when'); const Promise = require('bluebird');
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const EventEmitter = require('events');
const CMD = 'STOR'; const CMD = 'STOR';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
let log = bunyan.createLogger({name: CMD}); let log = bunyan.createLogger({name: CMD});
let emitter;
const mockClient = { const mockClient = {
commandSocket: { commandSocket: {
pause: () => {}, pause: () => {},
resume: () => {} resume: () => {}
}, },
reply: () => when.resolve(), reply: () => Promise.resolve(),
connector: { connector: {
waitForConnection: () => when.resolve({ waitForConnection: () => Promise.resolve({
resume: () => {} resume: () => {}
}), }),
end: () => {} end: () => Promise.resolve({})
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
write: () => {} write: () => {}
}; };
emitter = new EventEmitter();
mockClient.emit = emitter.emit.bind(emitter);
mockClient.on = emitter.on.bind(emitter);
mockClient.once = emitter.once.bind(emitter);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });
afterEach(() => sandbox.restore()); afterEach(() => sandbox.restore());
@@ -53,7 +60,7 @@ describe(CMD, function () {
it('// unsuccessful | connector times out', () => { it('// unsuccessful | connector times out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () { sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new when.TimeoutError()); return Promise.reject(new Promise.TimeoutError());
}); });
return cmdFn({log, command: {arg: 'test.txt'}}) return cmdFn({log, command: {arg: 'test.txt'}})
@@ -64,7 +71,7 @@ describe(CMD, function () {
it('// unsuccessful | connector errors out', () => { it('// unsuccessful | connector errors out', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () { sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return when.reject(new Error('test')); return Promise.reject(new Error('test'));
}); });
return cmdFn({log, command: {arg: 'test.txt'}}) return cmdFn({log, command: {arg: 'test.txt'}})
@@ -72,4 +79,20 @@ describe(CMD, function () {
expect(mockClient.reply.args[0][0]).to.equal(550); expect(mockClient.reply.args[0][0]).to.equal(550);
}); });
}); });
it('// unsuccessful | emits error event', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
return Promise.reject(new Error('test'));
});
let errorEmitted = false;
emitter.once('STOR', (err) => {
errorEmitted = !!err;
});
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});
});
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -8,16 +8,16 @@ const CMD = 'STOU';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = { mockClient.fs = {
get: () => when.resolve(), get: () => Promise.resolve(),
getUniqueName: () => when.resolve('4') getUniqueName: () => Promise.resolve('4')
}; };
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'STRU';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'SYST';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -6,12 +6,12 @@ const CMD = 'TYPE';
describe(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => when.resolve() reply: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.transferType = null; mockClient.transferType = null;
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -1,4 +1,4 @@
const when = require('when'); const Promise = require('bluebird');
const {expect} = require('chai'); const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -9,14 +9,14 @@ describe(CMD, function () {
error: () => {} error: () => {}
}; };
const mockClient = { const mockClient = {
reply: () => when.resolve(), reply: () => Promise.resolve(),
server: {options: {}}, server: {options: {}},
login: () => when.resolve() login: () => Promise.resolve()
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
delete mockClient.username; delete mockClient.username;
mockClient.server.options = {}; mockClient.server.options = {};

View File

@@ -6,9 +6,10 @@ const net = require('net');
const tls = require('tls'); const tls = require('tls');
const ActiveConnector = require('../../src/connector/active'); const ActiveConnector = require('../../src/connector/active');
const findPort = require('../../src/helpers/find-port'); const {getNextPortFactory} = require('../../src/helpers/find-port');
describe('Connector - Active //', function () { describe('Connector - Active //', function () {
let getNextPort = getNextPortFactory(1024);
let PORT; let PORT;
let active; let active;
let mockConnection = {}; let mockConnection = {};
@@ -18,18 +19,18 @@ describe('Connector - Active //', function () {
before(() => { before(() => {
active = new ActiveConnector(mockConnection); active = new ActiveConnector(mockConnection);
}); });
beforeEach(done => { beforeEach((done) => {
sandbox = sinon.sandbox.create(); sandbox = sinon.createSandbox().usingPromise(Promise);
findPort() getNextPort()
.then(port => { .then((port) => {
PORT = port; PORT = port;
server = net.createServer() server = net.createServer()
.on('connection', socket => socket.destroy()) .on('connection', (socket) => socket.destroy())
.listen(PORT, () => done()); .listen(PORT, () => done());
}); });
}); });
afterEach(done => { afterEach((done) => {
sandbox.restore(); sandbox.restore();
server.close(done); server.close(done);
}); });
@@ -57,7 +58,7 @@ describe('Connector - Active //', function () {
expect(active.dataSocket).to.exist; expect(active.dataSocket).to.exist;
return active.waitForConnection(); return active.waitForConnection();
}) })
.then(dataSocket => { .then((dataSocket) => {
expect(dataSocket.connected).to.equal(true); expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true); expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(false); expect(dataSocket instanceof tls.TLSSocket).to.equal(false);
@@ -66,14 +67,18 @@ describe('Connector - Active //', function () {
it('upgrades to a secure connection', function () { it('upgrades to a secure connection', function () {
mockConnection.secure = true; mockConnection.secure = true;
mockConnection.server = {_tls: {}}; mockConnection.server = {
options: {
tls: {}
}
};
return active.setupConnection('127.0.0.1', PORT) return active.setupConnection('127.0.0.1', PORT)
.then(() => { .then(() => {
expect(active.dataSocket).to.exist; expect(active.dataSocket).to.exist;
return active.waitForConnection(); return active.waitForConnection();
}) })
.then(dataSocket => { .then((dataSocket) => {
expect(dataSocket.connected).to.equal(true); expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true); expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(true); expect(dataSocket instanceof tls.TLSSocket).to.equal(true);

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