Compare commits

...

119 Commits

Author SHA1 Message Date
Tyler Stewart
283be85db3 Merge pull request #38 from trs/improve-connector-stream-handling
Improve connector stream handling
2017-08-18 12:45:32 -06:00
Tyler Stewart
e555ce9230 test(stor): add failure test 2017-08-18 12:06:58 -06:00
Tyler Stewart
e6575808f1 fix(stor): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
a5e58a106e fix(retr): improve event and promise handling 2017-08-18 12:06:42 -06:00
Tyler Stewart
ed086e576a Merge pull request #36 from trs/fix-process-exit
Fix process exit
2017-08-14 17:07:05 -06:00
Tyler Stewart
31f0f3b0dc fix: ensure process exits 2017-08-14 16:46:11 -06:00
Tyler Stewart
d763820c86 refactor: connector socket getter 2017-08-14 16:45:37 -06:00
Tyler Stewart
f3183314cc Merge pull request #34 from trs/add-typings
feat(typings): add TypeScript .d.ts files
2017-07-24 11:00:13 -06:00
Chris Rabl
dde7b36c46 feat(typings): add TypeScript .d.ts files
* created outline for TypeScript declarations
* added basic object shapes for FtpServer and FileSystem
2017-07-24 10:53:41 -06:00
Tyler Stewart
00af9e7e61 Merge pull request #32 from trs/command-args
Command args
2017-07-10 10:02:55 -06:00
Tyler Stewart
99a885cd44 test(commands): update parser tests 2017-07-10 09:58:24 -06:00
Tyler Stewart
443051d753 fix(commands): get flags from ftp command 2017-07-07 17:28:09 -06:00
Tyler Stewart
27ecc4d835 fix(feat): order features alphabetically 2017-07-06 17:51:06 -06:00
Tyler Stewart
c8526be1f4 Merge pull request #30 from trs/fix-utf8
Fix utf8
2017-06-26 19:54:51 -06:00
Tyler Stewart
e0b11ff480 fix: cleanup server 2017-06-26 16:39:00 -06:00
Tyler Stewart
58b9d8db9d chore: update fs readme 2017-06-26 16:39:00 -06:00
Tyler Stewart
fa121ba0fd test(REST): add tests 2017-06-26 16:39:00 -06:00
Tyler Stewart
2e02dc20ad feat(REST): add support for REST command
Allows the client to resume a transfer at the specified bytes
2017-06-26 16:38:59 -06:00
Tyler Stewart
8aeb6976d2 fix(auth): update checks, ensure secure is set using ftps 2017-06-26 16:38:59 -06:00
Tyler Stewart
84a68ae03c chore: dont append branch name to commit 2017-06-26 12:34:21 -06:00
Tyler Stewart
9dfc80b99d test(OPTS): update tests
master
2017-06-26 11:19:42 -06:00
Tyler Stewart
090e3d8105 chore: update log levels 2017-06-26 11:13:55 -06:00
Tyler Stewart
c3b0dbf5b0 feat(OPTS): add opts command handler for utf8
master
2017-06-26 11:13:49 -06:00
Tyler Stewart
69a5133936 fix(FEAT): correctly display feature list 2017-06-26 11:13:00 -06:00
Tyler Stewart
5394908a6b Merge pull request #29 from trs/fix-tls-check
Fix encoding/transfer type
2017-06-20 17:20:23 -06:00
Tyler Stewart
3e7bd5bcf9 chore: update licence format
Still MIT, just updating to ensure GitHub recognizes it
2017-06-20 17:09:49 -06:00
Tyler Stewart
175b422c5f chore: update dependencies 2017-06-20 17:02:05 -06:00
Tyler Stewart
b2a9851204 fix: ensure utf8 support; allows accent characters 2017-06-20 16:56:26 -06:00
Tyler Stewart
977dd1579a fix(TYPE): correctly set types, only use for data connections 2017-06-20 16:55:37 -06:00
Tyler Stewart
176b2b7ca8 fix: actually check _tls 2017-06-20 14:47:44 -06:00
Tyler Stewart
63777c0d74 Merge pull request #26 from trs/test-updates
Test updates
2017-06-16 15:17:31 -06:00
Tyler Stewart
9be8ffa60d test: add and update tests 2017-06-09 15:00:01 -06:00
Tyler Stewart
b8cd6022e1 fix: assign tls options to empty object 2017-06-09 14:40:19 -06:00
Tyler Stewart
0618a3c675 fix(passive): throw error on invalid port range 2017-06-09 14:39:21 -06:00
Tyler Stewart
3c533a5fbc Merge pull request #25 from trs/export-fs
Export fs
2017-06-09 10:31:42 -06:00
Tyler Stewart
3d0a58ca15 docs(readme): update file system override details
master

export-fs

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

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

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

update-logo

update-logo

update-logo

update-logo

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

master

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

explicit-tls-support

explicit-tls-support

explicit-tls-support

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

master

master

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

fix-compatability-bugs

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

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

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

2
.env Executable file
View File

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

1
.gitignore vendored
View File

@@ -2,5 +2,4 @@ node_modules/
dist/
reports/
.env
npm-debug.log

View File

@@ -1,6 +1,11 @@
language: node_js
node_js:
- "6"
- "node"
env:
FTP_URL: ftp://127.0.0.1:8880
PASV_RANGE: 8881
install: npm install
@@ -8,5 +13,13 @@ script:
- npm run verify:js
- npm run test:coverage
after_success:
- if [ $TRAVIS_BRANCH = 'master' ]; then npm run semantic-release; fi
after_script:
- npm run upload-coverage
deploy:
skip_cleanup: true
provider: script
script: npm run semantic-release
on:
branch: master
node: "6"

22
LICENSE
View File

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

263
README.md
View File

@@ -1,150 +1,225 @@
# ftp-srv [![npm version](https://badge.fury.io/js/ftp-srv.svg)](https://badge.fury.io/js/ftp-srv) [![Build Status](https://travis-ci.org/stewarttylerr/ftp-srv.svg?branch=master)](https://travis-ci.org/stewarttylerr/ftp-srv) [![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/)
[![ftp-srv](logo.png)](https://github.com/trs/ftp-srv)
<!--[RM_DESCRIPTION]-->
> Modern, extensible FTP Server
<!--[]-->
[![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)
[![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/)
---
- [Overview](#overview)
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [Events](#events)
- [Supported Commands](#supported-commands)
- [File System](#file-system)
- [Contributing](#contributing)
- [License](#license)
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Supports passive and active connections
- Extensible [file system](#file-system)
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
## Install
`npm install ftp-srv --save`
`yarn add ftp-srv`
`npm install ftp-srv --save`
## Usage
- [Options](#options)
- [Events](#events)
- [File System](#file-system)
```js
const FtpSvr = require('ftp-srv');
const ftpServer = new FtpSvr([url], { [options] ... });
// Quick start
ftpServer.on('...', (data, resolve, reject) => { ... })
const FtpSvr = require('ftp-srv');
const ftpServer = new FtpSvr('ftp://0.0.0.0:9876', { options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
ftpServer.listen()
.then(() => { ... });
```
### Options
__url__ : `ftp://127.0.0.1:21`
> Host and port to listen on and make passive connections to.
Set the hostname to "0.0.0.0" to fetch the external IP automatically: `ftp://0.0.0.0:21`
## API
__pasv_range__ : `22`
> Minimum port or range to use for passive connections.
Provide either a starting integer (`1000`) or a range (`1000-2000`).
### `new FtpSrv(url, [{options}])`
#### url
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
Supported protocols:
- `ftp` Plain FTP
- `ftps` Implicit FTP over TLS
__anonymous__ : `false`
> Set whether a valid username or password combination is required.
If true, will not require the `PASS` command to be sent for login.
_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.
__Default:__ `"ftp://127.0.0.1:21"`
__blacklist__ : `[]`
> Commands listed will not be allowed.
`['RMD', 'RNFR', 'RNTO']`
#### options
__whitelist__ : `[]`
> If set, no other commands are allowed except for those explicitly listed.
`['USER', 'PASS', 'PWD']`
##### `pasv_range`
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
This range is then queried for an available port to use when required.
__Default:__ `22`
__file_format__ : `ls`
> Format to use for [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) responses (such as with the `LIST` command).
Possible values:
`ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
`ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
`function` : pass in your own format function, returning a string:
`function (fileStats) { ... }`
##### `greeting`
A human readable array of lines or string to send when a client connects.
__Default:__ `null`
__log__ : `bunyan`
> A [bunyan logger](https://github.com/trentm/node-bunyan) instance.
##### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false`
### Events
All events emit the same structure: `({data object}, resolve, reject)`
##### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false`
__login__ : `{connection, username, password}`
> Occurs after `PASV` (or `USER` if `options.anonymous`)
##### `blacklist`
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]`
##### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`
##### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
- `function () {}` A custom function returning a format or promise for one.
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
##### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
## Events
The `FtpSvr` 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`
```js
on('login', {connection, username, password}, resolve, reject) => { ... }
```
resolve({
fs, // [optional] custom file system class
cwd, // [optional] initial working directory (if not using custom file system),
blacklist, // [optional] commands to forbid for this connection only
whitelist // [optional] if set, only these commands are allowed for this connection only
})
```
### File System
The file system can be overridden to use your own custom class. This an allow for interacting with files without actually writing them.
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
*Anytime a [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object is used, it must have added `name` property with the file's name.*
`connection` [client class object](src/connection.js)
`username` string of username from `USER` command
`password` string of password from `PASS` command
`resolve` takes an object of arguments:
- `fs`
- Set a custom file system class for this connection to use.
- See [File System](#file-system) for implementation details.
- `root`
- If `fs` is not provided, this will set the root directory for the connection.
- The user cannot traverse lower than this directory.
- `cwd`
- If `fs` is not provided, will set the starting directory for the connection
- This is relative to the `root` directory.
- `blacklist`
- Commands that are forbidden for only this connection
- `whitelist`
- If set, this connection will only be able to use the provided commands
#### Functions
`currentDirectory()`
> Returns a string of the current working directory
`reject` takes an error object
> Used in: `PWD`
### `client-error`
```js
on('client-error', {connection, context, error}) => { ... }
```
`get(fileName)`
> Returns a file stat object of file or directory
Occurs when an error arises in the client connection.
> Used in: `STAT`, `SIZE`, `RNFR`, `MDTM`
`connection` [client class object](src/connection.js)
`context` string of where the error occured
`error` error object
`list(path)`
> Returns array of file and directory stat objects
## Supported Commands
> Used in `LIST`, `STAT`
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
`chdir(path)`
> Returns new directory relative to cwd
## File System
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
> Used in `CWD`, `CDUP`
The default file system is exported and can be extended as needed:
```js
const {FtpSrv, FileSystem} = require('ftp-srv');
`mkdir(path)`
> Return a path to a newly created directory
class MyFileSystem extends FileSystem {
constructor() {
super(...arguments);
}
> Used in `MKD`
get(fileName) {
...
}
}
```
`write(fileName, options)`
> Returns a writable stream
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L29)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L33)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L39)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L56)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L96)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream
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
__Used in:__ `STOR`, `APPE`
> Used in `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
`read(fileName)`
> Returns a readable stream
#### [`delete(path)`](src/fs.js#L87)
Delete a file or directory
__Used in:__ `DELE`
> Used in `RETR`
#### [`rename(from, to)`](src/fs.js#L102)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
`delete(path)`
> Delete a file or directory
#### [`chmod(path)`](src/fs.js#L108)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
> Used in `DELE`
`rename(from, to)`
> Rename a file or directory
> Used in `RNFR`, `RNTO`
`chmod(path)`
> Modify a file or directory's permissions
> Used in `SITE CHMOD`
`getUniqueName()`
> Return a unique file name to write to
> Used in `STOU`
#### [`getUniqueName()`](src/fs.js#L113)
Returns a unique file name to write to
__Used in:__ `STOU`
<!--[RM_CONTRIBUTING]-->
## Contributing

View File

@@ -15,12 +15,7 @@ module.exports = {
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [
{name: 'accounts'},
{name: 'admin'},
{name: 'exampleScope'},
{name: 'changeMe'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
@@ -39,5 +34,5 @@ module.exports = {
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: true
appendBranchNameToCommitMessage: false
};

View File

@@ -1,7 +1,8 @@
// Use JS to support loading of threshold data from external file
var coverageConfig = {
instrumentation: {
root: 'src/'
root: 'src/',
excludes: ['errors.js']
},
check: require('./thresholds.json'),
reporting: {

View File

@@ -1,4 +1,3 @@
test/**/*.spec.js
--reporter list
--no-timeouts
--reporter mocha-pretty-bunyan-nyan
--ui bdd

View File

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

View File

@@ -47,6 +47,7 @@ parserOptions:
<<: *confit-parserOptions
rules:
no-process-exit: 0
max-len:
- warn
- 200 # Line Length

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

@@ -0,0 +1,62 @@
declare class FileSystem {
constructor(connection: any, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
declare class FtpServer {
constructor(url: string, options?: {});
readonly isTLS: boolean;
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>;
emit(action: any, ...data: any[]): void;
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
};
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>;
close(): any;
}
declare const FtpSrv: FtpServer;
export default FtpServer;

6
ftp-srv.js Normal file
View File

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

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

17
logo/generate.js Normal file
View File

@@ -0,0 +1,17 @@
/*
Send Button by Bruno Bosse from the Noun Project
https://thenounproject.com/brunobosse/collection/basics/?i=1054386
*/
const fs = require('fs');
const htmlConvert = require('html-convert');
const convert = htmlConvert();
let ws = fs.createWriteStream('logo.png');
let rs = convert('logo/logo.html', {
width: 350,
height: 76
});
rs.pipe(ws);
ws.on('finish', () => process.exit());

81
logo/icon.svg Normal file
View File

@@ -0,0 +1,81 @@
<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>

After

Width:  |  Height:  |  Size: 21 KiB

47
logo/logo.html Normal file
View File

@@ -0,0 +1,47 @@
<!doctype html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700" rel="stylesheet">
<style>
body {
padding: 0;
margin: 0;
}
h1 {
display: inline-block;
margin: 0;
margin-left: -4px;
padding: 0px;
font-size: 68px;
font-family: 'Source Code Pro', monospace;
font-weight: bold;
line-height: 0.8em;
letter-spacing: -3px;
color: #333;
-webkit-font-smoothing: antialiased;
text-shadow:
1px 0px 0px #ccc, 0px 1px 0px #eee,
2px 1px 0px #ccc, 1px 2px 0px #eee,
3px 2px 0px #ccc, 2px 3px 0px #eee,
4px 3px 0px #ccc;
}
h1 > span {
display: block;
padding: 0;
margin: 0;
padding-top: 6px;
}
img {
display: inline-block;
vertical-align: top;
}
</style>
</head>
<body>
<h1>
<span>ftp-srv</span>
</h1>
<img src="icon.svg" width="76px" height="76px" />
</body>
</html>

4330
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,13 +8,17 @@
"ftp-srv",
"ftp-svr",
"ftpd",
"server"
"server",
"ftpserver"
],
"license": "MIT",
"main": "src/index.js",
"main": "ftp-srv.js",
"files": [
"src"
],
"repository": {
"type": "git",
"url": "https://github.com/stewarttylerr/ftp-srv"
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm-run-all verify test:coverage build ",
@@ -37,6 +41,7 @@
"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"
},
"types": "./ftp-srv.d.ts",
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
@@ -46,33 +51,33 @@
}
},
"dependencies": {
"bunyan": "^1.8.5",
"date-fns": "^1.28.0",
"bunyan": "^1.8.10",
"lodash": "^4.17.4",
"minimist-string": "^1.0.2",
"uuid": "^3.0.1",
"moment": "^2.18.1",
"uuid": "^3.1.0",
"when": "^3.7.8"
},
"devDependencies": {
"chai": "^3.5.0",
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"coveralls": "2.11.15",
"cross-env": "3.1.4",
"cz-customizable": "4.0.0",
"coveralls": "2.13.1",
"cross-env": "5.0.1",
"cz-customizable": "5.0.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "3.14.1",
"eslint-config-google": "0.7.1",
"eslint-plugin-node": "3.0.5",
"eslint": "3.19.0",
"eslint-config-google": "0.8.0",
"eslint-plugin-node": "5.0.0",
"ftp": "^0.3.10",
"husky": "0.13.1",
"html-convert": "^2.1.7",
"husky": "0.13.4",
"istanbul": "0.4.5",
"mocha": "3.2.0",
"npm-run-all": "4.0.1",
"rimraf": "2.5.4",
"semantic-release": "^6.3.2",
"sinon": "^1.17.7",
"sinon-as-promised": "^4.0.2"
"mocha": "3.4.2",
"mocha-pretty-bunyan-nyan": "^1.0.4",
"npm-run-all": "4.0.2",
"rimraf": "2.6.1",
"semantic-release": "^6.3.6",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x",

View File

@@ -11,9 +11,33 @@ class FtpCommands {
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
}
parse(message) {
const strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' ');
const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive: _.chain(directive).trim().toUpper().value(),
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
};
return command;
}
handle(command) {
const log = this.connection.log.child({command});
log.trace('Handle command');
if (typeof command === 'string') command = this.parse(command);
// Obfuscate password from logs
const logCommand = _.clone(command);
if (logCommand.directive === 'PASS') logCommand.arg = '********';
const log = this.connection.log.child({directive: command.directive});
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');

View File

@@ -5,6 +5,6 @@ module.exports = {
handler: function (args) {
return stor.call(this, args);
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Append to a file'
}
};

View File

@@ -1,27 +1,42 @@
const _ = require('lodash');
const tls = require('tls');
module.exports = {
directive: 'AUTH',
handler: function ({command} = {}) {
const method = _.upperCase(command._[1]);
const method = _.upperCase(command.arg);
switch (method) {
case 'TLS': return handleTLS.call(this);
case 'SSL': return handleSSL.call(this);
default: return this.reply(504);
}
},
syntax: '{{cmd}} [type]',
syntax: '{{cmd}} <type>',
description: 'Set authentication mechanism',
flags: {
no_auth: true
no_auth: true,
feat: 'AUTH TLS'
}
}
};
function handleTLS() {
return this.reply(504);
}
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);
function handleSSL() {
return this.reply(504);
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
function forwardEvent() {
this.emit.apply(this, arguments);
}
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
});
this.commandSocket = secureSocket;
this.secure = true;
});
}

View File

@@ -2,10 +2,10 @@ const cwd = require('./cwd').handler;
module.exports = {
directive: ['CDUP', 'XCUP'],
handler: function(args) {
args.command._ = [args.command._[0], '..'];
handler: function (args) {
args.command.arg = '..';
return cwd.call(this, args);
},
syntax: '{{cmd}}',
description: 'Change to Parent Directory'
}
};

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return when(this.fs.chdir(command._[1]))
return when.try(this.fs.chdir.bind(this.fs), command.arg)
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);
@@ -17,6 +17,6 @@ module.exports = {
return this.reply(550, err.message);
});
},
syntax: '{{cmd}}[path]',
syntax: '{{cmd}} <path>',
description: 'Change working directory'
}
};

View File

@@ -6,15 +6,15 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return when(this.fs.delete(command._[1]))
return when.try(this.fs.delete.bind(this.fs), command.arg)
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Delete file'
}
};

View File

@@ -0,0 +1,22 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
const FAMILY = {
1: 4,
2: 6
};
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const [protocol, ip, port] = _.compact(command.arg.split('|'));
const family = FAMILY[protocol];
if (!family) return this.reply(502, 'Unknown network protocol');
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'
};

View File

@@ -0,0 +1,16 @@
const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'EPSV',
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const {port} = server.address();
return this.reply(229, `EPSV OK (|||${port}|)`);
});
},
syntax: '{{cmd}} [<protocol>]',
description: 'Initiate passive mode'
};

View File

@@ -9,13 +9,19 @@ module.exports = {
const feat = _.get(registry[cmd], 'flags.feat', null);
if (feat) return _.concat(feats, feat);
return feats;
}, [])
.map(feat => ` ${feat}`);
return this.reply(211, 'Extensions supported', ...features, 'END');
}, ['UTF8'])
.sort()
.map(feat => ({
message: ` ${feat}`,
raw: true
}));
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');
},
syntax: '{{cmd}}',
description: 'Get the feature list implemented by the server',
flags: {
no_auth: true
}
}
};

View File

@@ -4,7 +4,7 @@ module.exports = {
directive: 'HELP',
handler: function ({command} = {}) {
const registry = require('../registry');
const directive = _.upperCase(command._[1]);
const directive = _.upperCase(command.arg);
if (directive) {
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
@@ -16,9 +16,9 @@ module.exports = {
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
}
},
syntax: '{{cmd}} [command(optional)]',
syntax: '{{cmd}} [<command>]',
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
flags: {
no_auth: true
}
}
};

View File

@@ -6,22 +6,20 @@ const getFileStat = require('../../helpers/file-stat');
// http://cr.yp.to/ftp/list/eplf.html
module.exports = {
directive: 'LIST',
handler: function ({log, command, previous_command} = {}) {
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
const simple = command.directive === 'NLST';
let dataSocket;
const directory = command._[1] || '.';
const path = command.arg || '.';
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.list(directory)))
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.get.bind(this.fs), path))
.then(stat => stat.isDirectory() ? when.try(this.fs.list.bind(this.fs), path) : [stat])
.then(files => {
const getFileMessage = (file) => {
const getFileMessage = file => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
@@ -31,9 +29,9 @@ module.exports = {
return {
raw: true,
message,
socket: dataSocket
socket: this.connector.socket
};
})
});
return this.reply(150)
.then(() => {
if (fileList.length) return this.reply({}, ...fileList);
@@ -48,13 +46,13 @@ module.exports = {
})
.catch(err => {
log.error(err);
return this.reply(err.code || 451, err.message || 'No directory');
return this.reply(451, err.message || 'No directory');
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} [path(optional)]',
syntax: '{{cmd}} [<path>]',
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
}
};

View File

@@ -1,5 +1,5 @@
const when = require('when');
const format = require('date-fns/format');
const moment = require('moment');
module.exports = {
directive: 'MDTM',
@@ -7,19 +7,19 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
return when.try(this.fs.get.bind(this.fs), command.arg)
.then(fileStat => {
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime)
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Return the last-modified time of a specified file',
flags: {
feat: 'MDTM'
}
}
};

View File

@@ -7,16 +7,16 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return when(this.fs.mkdir(command._[1]))
return when.try(this.fs.mkdir.bind(this.fs), command.arg)
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}}[path]',
syntax: '{{cmd}} <path>',
description: 'Make directory'
}
};

View File

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

View File

@@ -5,6 +5,6 @@ module.exports = {
handler: function (args) {
return list.call(this, args);
},
syntax: '{{cmd}} [path(optional)]',
syntax: '{{cmd}} [<path>]',
description: 'Returns a list of file names in a specified directory'
}
};

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

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

View File

@@ -1,15 +1,13 @@
const _ = require('lodash');
module.exports = {
directive: 'PASS',
handler: function ({log, command} = {}) {
if (!this.username) return this.reply(503);
if (this.username && this.authenticated &&
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
if (this.authenticated) return this.reply(202);
// 332 : require account name (ACCT)
const password = command._[1];
const password = command.arg;
if (!password) return this.reply(501, 'Must provide password');
return this.login(this.username, password)
.then(() => {
return this.reply(230);
@@ -19,9 +17,9 @@ module.exports = {
return this.reply(530, err.message || 'Authentication failed');
});
},
syntax: '{{cmd}} [password]',
syntax: '{{cmd}} <password>',
description: 'Authentication password',
flags: {
no_auth: true
}
}
};

View File

@@ -2,7 +2,7 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'PASV',
handler: function ({command} = {}) {
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
@@ -17,4 +17,4 @@ module.exports = {
},
syntax: '{{cmd}}',
description: 'Initiate passive mode'
}
};

View File

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

View File

@@ -1,10 +1,11 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = command._[1].split(',');
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
@@ -12,10 +13,8 @@ module.exports = {
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)
.then(socket => {
return this.reply(200);
})
.then(() => this.reply(200));
},
syntax: '{{cmd}} [x,x,x,x,y,y]',
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,36 +6,38 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.read(command._[1])))
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.read.bind(this.fs), command.arg, {start: this.restByteCount}))
.then(stream => {
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));
this.restByteCount = 0;
stream.on('data', data => dataSocket.write(data, this.encoding));
stream.on('end', () => resolve(this.reply(226)));
stream.on('error', err => reject(err));
this.reply(150).then(() => dataSocket.resume());
const eventsPromise = when.promise((resolve, reject) => {
this.connector.socket.once('error', err => reject(err));
stream.on('data', data => this.connector.socket.write(data, this.transferType));
stream.once('error', err => reject(err));
stream.once('end', () => resolve());
});
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => eventsPromise)
.finally(() => stream.destroy ? stream.destroy() : null);
})
.then(() => this.reply(226))
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(551);
return this.reply(551, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
})
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Retrieve a copy of the file'
}
};

View File

@@ -5,6 +5,6 @@ module.exports = {
handler: function (args) {
return dele.call(this, args);
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Remove a directory'
}
};

View File

@@ -6,17 +6,17 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command._[1];
return when(this.fs.get(fileName))
const fileName = command.arg;
return when.try(this.fs.get.bind(this.fs), fileName)
.then(() => {
this.renameFrom = fileName;
return this.reply(350);
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [name]',
syntax: '{{cmd}} <name>',
description: 'Rename from'
};

View File

@@ -9,20 +9,20 @@ module.exports = {
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
const from = this.renameFrom;
const to = command._[1];
const to = command.arg;
return when(this.fs.rename(from, to))
return when.try(this.fs.rename.bind(this.fs), from, to)
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
})
.finally(() => {
delete this.renameFrom;
})
});
},
syntax: '{{cmd}} [name]',
syntax: '{{cmd}} <name>',
description: 'Rename to'
}
};

View File

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

View File

@@ -1,23 +1,17 @@
const _ = require('lodash');
const when = require('when');
module.exports = {
directive: 'SITE',
handler: function ({log, command} = {}) {
const registry = require('./registry');
let [, subverb, ...subparameters] = command._;
subverb = _.upperCase(subverb);
const subLog = log.child({subverb});
const subCommand = this.commands.parse(command.arg);
const subLog = log.child({subverb: subCommand.directive});
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
const subCommand = {
_: [subverb, ...subparameters],
directive: subverb
}
const handler = registry[subverb].handler.bind(this);
const handler = registry[subCommand.directive].handler.bind(this);
return when.try(handler, { log: subLog, command: subCommand });
},
syntax: '{{cmd}} [subVerb] [subParams]',
syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server'
}
};

View File

@@ -6,18 +6,18 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
return when.try(this.fs.get.bind(this.fs), command.arg)
.then(fileStat => {
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
log.error(err);
return this.reply(550);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Return the size of a file',
flags: {
feat: 'SIZE'
}
}
};

View File

@@ -6,15 +6,17 @@ module.exports = {
directive: 'STAT',
handler: function (args = {}) {
const {log, command} = args;
const path = command._[1];
const path = _.get(command, 'arg');
if (path) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(path))
return when.try(this.fs.get.bind(this.fs), path)
.then(stat => {
if (stat.isDirectory()) {
return when(this.fs.list(path))
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return when.try(this.fs.list.bind(this.fs), path)
.then(files => {
const fileList = files.map(file => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
@@ -22,21 +24,21 @@ module.exports = {
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 this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')));
}
})
.catch(err => {
log.error(err);
return this.reply(450);
})
return this.reply(450, err.message);
});
} else {
return this.reply(211, 'Status OK');
}
},
syntax: '{{cmd}} [path(optional)]',
syntax: '{{cmd}} [<path>]',
description: 'Returns the current status'
}
};

View File

@@ -7,38 +7,47 @@ module.exports = {
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
const append = command.directive === 'APPE';
const fileName = command._[1];
const fileName = command.arg;
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.write(fileName, {append})))
.tap(() => this.commandSocket.pause())
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append, start: this.restByteCount}))
.then(stream => {
return when.promise((resolve, reject) => {
stream.on('error', err => dataSocket.emit('error', err));
this.restByteCount = 0;
dataSocket.on('end', () => stream.end(() => resolve(this.reply(226, fileName))));
dataSocket.on('error', err => reject(err));
dataSocket.on('data', data => stream.write(data, this.encoding));
this.reply(150).then(() => dataSocket.resume());
const streamPromise = when.promise((resolve, reject) => {
stream.once('error', err => reject(err));
stream.once('finish', () => resolve());
});
const socketPromise = when.promise((resolve, reject) => {
this.connector.socket.on('data', data => stream.write(data, this.transferType));
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
resolve();
});
this.connector.socket.once('error', err => reject(err));
});
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => when.join(streamPromise, socketPromise))
.finally(() => stream.destroy ? stream.destroy() : null);
})
.then(() => this.reply(226, fileName))
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(553);
return this.reply(550, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Store data as a file at the server site'
}
};

View File

@@ -1,3 +1,5 @@
const when = require('when');
const stor = require('./stor').handler;
module.exports = {
@@ -6,12 +8,14 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
const fileName = args.command._[1];
return this.fs.get(fileName)
.catch(() => fileName) // does not exist, name is unique
.then(() => this.fs.getUniqueName()) // exists, must create new unique name
const fileName = args.command.arg;
return when.try(() => {
return when.try(this.fs.get.bind(this.fs), fileName)
.then(() => when.try(this.fs.getUniqueName.bind(this.fs)))
.catch(() => when.resolve(fileName));
})
.then(name => {
args.command._[1] = name;
args.command.arg = name;
return stor.call(this, args);
});
},

View File

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

View File

@@ -8,4 +8,4 @@ module.exports = {
flags: {
no_auth: true
}
}
};

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint no-return-assign: 0 */
const commands = [
require('./registration/abor'),
require('./registration/allo'),
@@ -19,6 +20,8 @@ const commands = [
require('./registration/pasv'),
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/rest'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),
@@ -31,7 +34,9 @@ const commands = [
require('./registration/stru'),
require('./registration/syst'),
require('./registration/type'),
require('./registration/user')
require('./registration/user'),
require('./registration/pbsz'),
require('./registration/prot')
];
const registry = commands.reduce((result, cmd) => {

View File

@@ -2,8 +2,6 @@ const _ = require('lodash');
const uuid = require('uuid');
const when = require('when');
const sequence = require('when/sequence');
const parseCommandString = require('minimist-string');
const net = require('net');
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
@@ -14,27 +12,23 @@ const DEFAULT_MESSAGE = require('./messages');
class FtpConnection {
constructor(server, options) {
this.server = server;
this.commandSocket = options.socket;
this.id = uuid.v4();
this.log = options.log.child({id: this.id});
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.encoding = 'utf-8';
this.transferType = 'binary';
this.encoding = 'utf8';
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;
this.connector = new BaseConnector(this);
this.commandSocket = options.socket;
this.commandSocket.on('error', err => {
this.server.server.emit('error', {connection: this, error: err});
});
this.commandSocket.on('data', data => {
const messages = _.compact(data.toString('utf-8').split('\r\n'));
const handleMessage = (message) => {
const command = parseCommandString(message);
command.directive = _.upperCase(command._[0]);
return this.commands.handle(command);
};
return sequence(messages.map(message => handleMessage.bind(this, message)));
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
@@ -42,29 +36,55 @@ class FtpConnection {
});
}
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message)));
}
get ip() {
try {
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress;
} catch (ex) {
return null;
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc;
}
get secure() {
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec;
}
close(code = 421, message = 'Closing connection') {
return when(() => {
if (code) return this.reply(code, message);
})
.then(() => {
if (this.commandSocket) this.commandSocket.end();
});
return when
.resolve(code)
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
return when.try(() => {
const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anoymous) throw new errors.GeneralError('No "login" listener setup', 500);
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emit('login', {connection: this, username, password});
return this.server.emitPromise('login', {connection: this, username, password});
}
})
.then(({fs, cwd, blacklist = [], whitelist = []} = {}) => {
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
this.authenticated = true;
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, {cwd});
this.fs = fs || new FileSystem(this, {root, cwd});
});
}
@@ -73,7 +93,7 @@ class FtpConnection {
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}];
return when.map(letters, (promise, index) => {
return when.map(letters, promise => {
return when(promise)
.then(letter => {
if (!letter) letter = {};
@@ -86,20 +106,20 @@ class FtpConnection {
.then(message => {
letter.message = message;
return letter;
})
});
});
});
}
};
const processLetter = (letter, index) => {
return when.promise((resolve, reject) => {
const seperator = !options.hasOwnProperty('eol') ?
(letters.length - 1 === index ? ' ' : '-') :
(options.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) {
this.log.trace({port: letter.socket.address().port, packet}, 'Reply');
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, packet}, 'Reply');
letter.socket.write(packet + '\r\n', letter.encoding, err => {
if (err) {
this.log.error(err);
@@ -109,10 +129,10 @@ class FtpConnection {
});
} else reject(new errors.SocketError('Socket not writable'));
});
}
};
return satisfyParameters()
.then(letters => sequence(letters.map((letter, index) => processLetter.bind(this, letter, index))))
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index))))
.catch(err => {
this.log.error(err);
});

View File

@@ -1,4 +1,5 @@
const net = require('net');
const {Socket} = require('net');
const tls = require('tls');
const when = require('when');
const Connector = require('./base');
@@ -17,17 +18,27 @@ class Active extends Connector {
.then(() => this.dataSocket);
}
setupConnection(host, port) {
setupConnection(host, port, family = 4) {
const closeExistingServer = () => this.dataSocket ?
when(this.dataSocket.destroy()) :
when.resolve()
when.resolve();
return closeExistingServer()
.then(() => {
this.dataSocket = new net.Socket();
this.dataSocket.setEncoding(this.encoding);
this.dataSocket.connect({ host, port }, () => {
this.dataSocket = new Socket();
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.connect({ host, port, family }, () => {
this.dataSocket.pause();
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
}
this.dataSocket.connected = true;
});
});

View File

@@ -4,14 +4,24 @@ const errors = require('../errors');
class Connector {
constructor(connection) {
this.connection = connection;
this.server = connection.server;
this.log = connection.log;
this.dataSocket = null;
this.dataServer = null;
this.type = false;
}
get log() {
return this.connection.log;
}
get socket() {
return this.dataSocket;
}
get server() {
return this.connection.server;
}
waitForConnection() {
return when.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
}

View File

@@ -1,5 +1,7 @@
const net = require('net');
const tls = require('tls');
const when = require('when');
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
@@ -11,9 +13,7 @@ class Passive extends Connector {
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
if (!this.dataServer) {
return when.reject(new errors.ConnectorError('Passive server not setup'));
}
if (!this.dataServer) return when.reject(new errors.ConnectorError('Passive server not setup'));
return when.iterate(
() => {},
() => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected,
@@ -25,15 +25,12 @@ class Passive extends Connector {
setupServer() {
const closeExistingServer = () => this.dataServer ?
when.promise(resolve => this.dataServer.close(() => resolve())) :
when.resolve()
when.resolve();
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true});
this.dataServer.maxConnections = 1;
this.dataServer.on('connection', socket => {
const connectionHandler = socket => {
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
this.log.error({
pasv_connection: socket.remoteAddress,
@@ -44,18 +41,33 @@ class Passive extends Connector {
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.debug({port}, 'Passive connection fulfilled.');
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket;
if (this.connection.secure) {
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.encoding);
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('close', () => {
this.log.debug('Passive connection closed');
this.log.trace('Passive connection closed');
this.end();
});
});
};
this.dataSocket = null;
this.dataServer = net.createServer({ pauseOnConnect: true }, connectionHandler);
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.log.debug('Passive server closed');
this.log.trace('Passive server closed');
this.dataServer = null;
});
@@ -63,7 +75,7 @@ class Passive extends Connector {
this.dataServer.listen(port, err => {
if (err) reject(err);
else {
this.log.info({port}, 'Passive connection listening');
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
@@ -77,8 +89,9 @@ class Passive extends Connector {
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
} else return undefined;
};
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

@@ -8,11 +8,22 @@ const fs = whenNode.liftAll(syncFs);
const errors = require('./errors');
class FileSystem {
constructor(connection, {
cwd = '/'
} = {}) {
constructor(connection, { root, cwd } = {}) {
this.connection = connection;
this.cwd = cwd;
this.cwd = cwd || nodePath.sep;
this.root = root || process.cwd();
}
_resolvePath(path = '') {
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
const serverPath = nodePath.join(nodePath.sep, cwd, path);
const fsPath = nodePath.join(this.root, serverPath);
return {
serverPath,
fsPath
};
}
currentDirectory() {
@@ -20,17 +31,17 @@ class FileSystem {
}
get(fileName) {
const path = nodePath.resolve(this.cwd, fileName);
return fs.stat(path)
const {fsPath} = this._resolvePath(fileName);
return fs.stat(fsPath)
.then(stat => _.set(stat, 'name', fileName));
}
list(path = '.') {
path = nodePath.resolve(this.cwd, path);
return fs.readdir(path)
const {fsPath} = this._resolvePath(path);
return fs.readdir(fsPath)
.then(fileNames => {
return when.map(fileNames, fileName => {
const filePath = nodePath.join(path, fileName);
const filePath = nodePath.join(fsPath, fileName);
return fs.access(filePath, syncFs.constants.F_OK)
.then(() => {
return fs.stat(filePath)
@@ -43,60 +54,61 @@ class FileSystem {
}
chdir(path = '.') {
path = nodePath.resolve(this.cwd, path);
return fs.stat(path)
const {fsPath, serverPath} = this._resolvePath(path);
return fs.stat(fsPath)
.tap(stat => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = path;
return this.cwd;
this.cwd = serverPath;
return this.currentDirectory();
});
}
write(fileName, {append = false} = {}) {
const path = nodePath.resolve(this.cwd, fileName);
const stream = syncFs.createWriteStream(path, {flags: !append ? 'w+' : 'a+'});
stream.on('error', () => fs.unlink(path));
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlink(fsPath));
stream.once('close', () => stream.end());
return stream;
}
read(fileName) {
const path = nodePath.resolve(this.cwd, fileName);
return fs.stat(path)
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
return fs.stat(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = syncFs.createReadStream(path, {flags: 'r'});
const stream = syncFs.createReadStream(fsPath, {flags: 'r', start});
return stream;
});
}
delete(path) {
path = nodePath.resolve(this.cwd, path);
return fs.stat(path)
const {fsPath} = this._resolvePath(path);
return fs.stat(fsPath)
.then(stat => {
if (stat.isDirectory()) return fs.rmdir(path);
else return fs.unlink(path);
})
if (stat.isDirectory()) return fs.rmdir(fsPath);
else return fs.unlink(fsPath);
});
}
mkdir(path) {
path = nodePath.resolve(this.cwd, path);
return fs.mkdir(path)
.then(() => path);
const {fsPath} = this._resolvePath(path);
return fs.mkdir(fsPath)
.then(() => fsPath);
}
rename(from, to) {
const fromPath = nodePath.resolve(this.cwd, from);
const toPath = nodePath.resolve(this.cwd, to);
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fs.rename(fromPath, toPath);
}
chmod(path, mode) {
path = nodePath.resolve(this.cwd, path);
return fs.chmod(path, mode);
const {fsPath} = this._resolvePath(path);
return fs.chmod(fsPath, mode);
}
getUniqueName() {

View File

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

View File

@@ -1,52 +1,54 @@
const _ = require('lodash');
const format = require('date-fns/format');
const moment = require('moment');
const errors = require('../errors');
const FORMATS = {
ls,
ep
};
module.exports = function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat);
const formats = {
'ls': ls,
'ep': ep
};
if (!formats.hasOwnProperty(format)) {
if (!FORMATS.hasOwnProperty(format)) {
throw new errors.FileSystemError('Bad file stat formatter');
}
return formats[format](fileStat);
}
return FORMATS[format](fileStat);
};
function ls(fileStat) {
const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime));
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
return [
fileStat.mode !== null
? [
fileStat.isDirectory() ? 'd' : '-',
400 & fileStat.mode ? 'r' : '-',
200 & fileStat.mode ? 'w' : '-',
100 & fileStat.mode ? 'x' : '-',
40 & fileStat.mode ? 'r' : '-',
20 & fileStat.mode ? 'w' : '-',
10 & fileStat.mode ? 'x' : '-',
4 & fileStat.mode ? 'r' : '-',
2 & fileStat.mode ? 'w' : '-',
1 & fileStat.mode ? 'x' : '-'
].join('')
: fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
fileStat.mode ? [
fileStat.isDirectory() ? 'd' : '-',
fileStat.mode & 256 ? 'r' : '-',
fileStat.mode & 128 ? 'w' : '-',
fileStat.mode & 64 ? 'x' : '-',
fileStat.mode & 32 ? 'r' : '-',
fileStat.mode & 16 ? 'w' : '-',
fileStat.mode & 8 ? 'x' : '-',
fileStat.mode & 4 ? 'r' : '-',
fileStat.mode & 2 ? 'w' : '-',
fileStat.mode & 1 ? 'x' : '-'
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
'1',
fileStat.uid,
fileStat.gid,
fileStat.uid || 1,
fileStat.gid || 1,
_.padStart(fileStat.size, 12),
_.padStart(format(fileStat.mtime, 'MMM DD HH:mm'), 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ');
}
function ep(fileStat) {
const facts = [
const facts = _.compact([
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
fileStat.size ? `s${fileStat.size}` : null,
fileStat.mtime ? `m${format(fileStat.mtime, 'X')}` : null,
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
fileStat.isDirectory() ? 'r' : '/'
].join(',');
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
fileStat.isDirectory() ? '/' : 'r'
]).join(',');
return `+${facts}\t${fileStat.name}`;
}

View File

@@ -2,19 +2,19 @@ const net = require('net');
const when = require('when');
const errors = require('../errors');
module.exports = function (min = 22, max = undefined) {
module.exports = function (min = 1, max = undefined) {
return when.promise((resolve, reject) => {
let port = min;
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
if (!max || port < max) {
port = port + 1;
portCheckServer.listen(port);
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
})
});
portCheckServer.on('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => {
@@ -22,6 +22,6 @@ module.exports = function (min = 22, max = undefined) {
resolve(port);
});
});
portCheckServer.listen(port);
portCheckServer.listen(checkPort);
});
};

View File

@@ -12,7 +12,7 @@ module.exports = function (hostname) {
if (response.statusCode !== 200) {
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
}
response.setEncoding('utf-8');
response.setEncoding('utf8');
response.on('data', chunk => {
ip += chunk;
});
@@ -21,5 +21,5 @@ module.exports = function (hostname) {
});
});
} else resolve(hostname);
})
}
});
};

View File

@@ -3,6 +3,8 @@ const when = require('when');
const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
@@ -15,28 +17,47 @@ class FtpServer {
pasv_range: 22,
file_format: 'ls',
blacklist: [],
whitelist: []
whitelist: [],
greeting: null,
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting;
delete this.options.tls;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
this.server = net.createServer({pauseOnConnect: true}, socket => {
const serverConnectionHandler = socket => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
socket.on('close', () => this.disconnectClient(connection.id));
const greeting = this.getGreetingMessage();
const features = this.getFeaturesMessage();
return connection.reply(220, greeting, features)
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
});
this.server.on('error', err => {
this.log.error(err);
});
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, { pauseOnConnect: true });
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
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());
process.on('SIGINT', () => this.quit());
process.on('SIGQUIT', () => this.quit());
}
get isTLS() {
return this.url.protocol === 'ftps:' && this._tls;
}
listen() {
@@ -46,60 +67,81 @@ class FtpServer {
return when.promise((resolve, reject) => {
this.server.listen(this.url.port, err => {
if (err) return reject(err);
this.log.info({port: this.url.port}, 'Listening');
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve();
});
});
});
}
emit(action, ...data) {
emitPromise(action, ...data) {
const defer = when.defer();
const params = _.concat(data, [defer.resolve, defer.reject]);
this.server.emit(action, ...params);
return defer.promise;
}
getGreetingMessage() {
return null;
emit(action, ...data) {
this.server.emit(action, ...data);
}
getFeaturesMessage() {
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
});
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
return greeting;
}
setupFeaturesMessage() {
let features = [];
if (this.options.anonymous) features.push('a');
if (features.length) {
features.unshift('Features:');
features.push('.')
features.push('.');
}
return features.length ? features.join(' ') : 'Ready';
}
setGreeting(gretting) {
if (typeof greeting === 'string') {
this.options.greeting = greeting;
} else {
gretting.then(greeting => {
this.options.gretting = greeting;
})
}
}
disconnectClient(id) {
return when.promise((resolve, reject) => {
return when.promise(resolve => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
return client.close(0);
try {
client.close(0);
} catch (err) {
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve();
}
});
}
quit() {
return this.close()
.finally(() => process.exit(0));
}
close() {
this.log.info('Server closing...');
this.server.maxConnections = 0;
return when.map(Object.keys(this.connections), id => this.disconnectClient(id))
.then(() => when.promise((resolve, reject) => {
return when.map(Object.keys(this.connections), id => when.try(this.disconnectClient.bind(this), id))
.then(() => when.promise(resolve => {
this.server.close(err => {
if (err) return reject(err);
if (err) this.log.error(err, 'Error closing server');
resolve();
});
}));

12
test/cert/server.crt Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBtTCCAR4CCQDsFyLCxvy4qzANBgkqhkiG9w0BAQUFADAfMQswCQYDVQQGEwJD
QTEQMA4GA1UECBMHQWxiZXJ0YTAeFw0xNzA1MDgyMzQzMjNaFw0xODA1MDgyMzQz
MjNaMB8xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdBbGJlcnRhMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOia
C4xbwpF6w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/
agkklBnRtdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7
rQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAOkX32keFpo0kKQKpeZgxVYjvn4/Voy6
6oLsj7jJYq3oZts1dX6kHVpLEbF9sWKB2iz7nqz7pSN1ATq0IL/5rcxvNwiL4Idv
F8CCBvsBui+0gwX755NJK/L57a5i8yQ5HC65NujGAA4I5+2x8HlefMVuBpEYjzQ2
6lW2OJJ8xtP/
-----END CERTIFICATE-----

10
test/cert/server.csr Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBXjCByAIBADAfMQswCQYDVQQGEwJDQTEQMA4GA1UECBMHQWxiZXJ0YTCBnzAN
BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA9rjsI1ezW+7mwbCv7R2UOYVKWkPUA/Sa
PKo+wwTomguMW8KResPmQ3+Tp5Eex5d0JXKH25FypC1phtDJucLxQOpGFug/LzE+
Q4u/npLH/2oJJJQZ0bXaAW95bGzG6N/xF4o8B2wim5vrE03u3cBiv8Oqe/Niv5IB
1VpPi+fDu60CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBACzo+Wecs3CTbItrugdL
pP4crsRs+HJljWA0e+WEGKhcd1FjrcLBr4WqzHFQJWHOTz2vM5PiKXPZk9crLxWa
Y8kMhU6eQPnCM6+7Gffm32+VS1ipNlhzHyYsjYpgC3ROElqo0J5M5sas4lbaamr+
FnlyRjrPSUcFdcbPL6ozND3e
-----END CERTIFICATE REQUEST-----

15
test/cert/server.key Normal file
View File

@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOiaC4xbwpF6
w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/agkklBnR
tdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7rQIDAQAB
AoGANRPVYUkVwfpkVFkBj/5kC/fb5g1fiDZQFCr/846Tx8giOv9hssqAOBczGcKD
n6a6iu/XwGnLAvzuDd3O+BKzObrKV36u9HfvCxohKaKvhPg3lBlJ5fFq/UNBoLv8
eHz0GUGGoCxwJBAV43ojV1GdyRZ7vdmYw2hzltsHIp7UDqECQQD7yltCCJm3+gcw
p+Sde0+M9CubkTETPwpd3XPu6Bs5f2nxNVj6RAInPEBr5//5UY7Q9BBEHrDPsq0j
/+gsSlWZAkEA+tjf9qRXk9JHoN3PD0xLNEgUZAsQwDic29jxb3xrGkuUjCebKRuC
FU2sAfNgDp+MyG1iyAoZcySzH2Dp3+v7NQJAaBwBo8oelT2in3GsS5ljCSskpMxh
+E1Gog0hFJWQPDP8wCmIwuI/6a02Def9pT8dyDRCTYhLH3YHtSzo+Pc7cQJBAJ4G
XiD8qwc+o00eLsEOaRoIhn/30JenknmVE5QOJ1KrZmtc0Ax3fd15zvBzp4HO1Vu2
PVKTujClYApWfT9JZDkCQQC3Ne79bb5WSsGNbg4eT+FWde4hkdpheBsWraEDN1Pp
NanupXMPNP0EduAQ1O+oPRiZ5pG38MQYcPZHTtlULoiO
-----END RSA PRIVATE KEY-----

18
test/cert/server.key.org Normal file
View File

@@ -0,0 +1,18 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,A253DC8068106194
jrYyhujaDsASrOfb2kn5Tvb0mRyIRsz7gVdjwlUdF2+lPdA/w6Os/NQBo1BIUAJp
lfS5KDTwiE3QPgrBXUNgpy71Yr+MSzmsYWdonGlXGtchohQKXpxtL3qOpczX3ERR
0AZninOKOYLw9+pe/tLNZI78DHxN1X0qTXS56RFydlW3XZbnl2Ux9CGuaCVq6vwh
yzr9H+XTqeh95wTfdXkRdFRSTyUuJ72cvMsBFRDhz60epDmUDo1XDf844BpXXfcU
kQoXHEtNkWZqzsc4ClOopp3Bgtd7eYoOLQluyovHgXzjtsur0xeMkHn9uTfkJ+IM
cYMS71ZKbMePS7XBt3YPLBvVXNcyYhWUP7VdGYxXTPqd1AVWciDB9S5EvfMxnnZz
O4M4ejxV8S7fF/cGju+sRzXx9oHPfo091Q3XKV1hrsUcF+ULrA8A8rHr64bDJ3wp
luhekzwb+5yFfZDj9XUuGMD6pSWYoWB8Jmk8cxVsdZPtGXbTQHFL9/+UZ65wSGpj
CjTLuFyVhY8pliynZH80vsNeRycdfmx93XoLqfS4xwEmI5v/MGUF24eTpF1/VIa5
oKDrVuERdXAn4JBKeaMratrl6p1BhkPe7VNnMUFw3U+C4x+QHISxbboUJiTcCe1C
pT6+YYkxQJ88rKunSEXkQYt6LeYSDg8Dw5y5Oq68DmW2Rp1m4ptTbqk3+uh83vzV
Ff0JnfNfT80GHD3T5hMgizZal2vV8DeH1WAwPzpNaeV6wTy6MSgRLgQ89cCQ0TXV
0GYnCZCaoXA2ldvbB3vW3fweOTr8Mp7aSl4s8K0R8sT3eief9/SyWA==
-----END RSA PRIVATE KEY-----

View File

@@ -1,48 +0,0 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const CMD = 'AUTH';
describe(CMD, done => {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('TLS // not supported', done => {
CMDFN({command: {_: [CMD, 'TLS'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
done();
})
.catch(done);
});
it('SSL // not supported', done => {
CMDFN({command: {_: [CMD, 'SSL'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
done();
})
.catch(done);
});
it('bad // bad', done => {
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
done();
})
.catch(done);
});
});

View File

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

118
test/commands/index.spec.js Normal file
View File

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

View File

@@ -1,116 +0,0 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'LIST';
describe(CMD, done => {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { list: () => {} },
connector: {
waitForConnection: () => {},
end: () => {}
},
commandSocket: {
resume: () => {},
pause: () => {}
}
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
}]);
});
afterEach(() => {
sandbox.restore();
});
describe('// check', function () {
it('fails on no fs', done => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
it('fails on no fs list command', done => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
it('. // unsuccessful', done => {
mockClient.fs.list.restore();
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
CMDFN({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451);
done();
})
.catch(done);
});
it('. // unsuccessful (timeout)', done => {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects(new when.TimeoutError());
CMDFN({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
done();
})
.catch(done);
});
});

View File

@@ -1,39 +0,0 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const CMD = 'MODE';
describe(CMD, done => {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('S // successful', done => {
CMDFN({command: {_: [CMD, 'S']}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
done();
})
.catch(done);
});
it('Q // unsuccessful', done => {
CMDFN({command: {_: [CMD, 'Q']}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
done();
})
.catch(done);
});
});

View File

@@ -1,66 +0,0 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'NLST';
describe(CMD, done => {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { list: () => {} },
connector: {
waitForConnection: () => {},
end: () => {}
},
commandSocket: {
resume: () => {},
pause: () => {}
}
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/list`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
sandbox.stub(mockClient.fs, 'list').resolves([{
name: 'test1',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
isDirectory: () => false
}]);
});
afterEach(() => {
sandbox.restore();
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1][1]).to.have.property('raw');
expect(mockClient.reply.args[1][1]).to.have.property('message');
expect(mockClient.reply.args[1][1]).to.have.property('socket');
expect(mockClient.reply.args[2][0]).to.equal(226);
done();
})
.catch(done);
});
});

View File

@@ -1,30 +0,0 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const CMD = 'OPTS';
describe(CMD, done => {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// successful', done => {
CMDFN()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501)
done();
})
.catch(done);
})
});

View File

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

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'ALLO';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -19,12 +19,10 @@ describe(CMD, done => {
sandbox.restore();
});
it('// successful', done => {
CMDFN()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
done();
})
.catch(done);
})
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
});

View File

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

View File

@@ -4,7 +4,7 @@ const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CDUP';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
@@ -13,7 +13,7 @@ describe(CMD, done => {
chdir: () => when.resolve()
}
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -25,13 +25,11 @@ describe(CMD, done => {
sandbox.restore();
});
it('.. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
it('.. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
done();
})
.catch(done);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,16 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised')(when.Promise);
const CMD = 'MDTM';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { get: () => {} }
};
const mockSocket = {};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,50 +23,44 @@ describe(CMD, done => {
});
describe('// check', function () {
it('fails on no fs', done => {
it('fails on no fs', () => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs get command', done => {
it('fails on no fs get command', () => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('. // successful', done => {
CMDFN({log, command: {_: [CMD], directive: CMD}})
it('. // successful', () => {
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
done();
})
.catch(done);
});
});
it('. // unsuccessful', done => {
it('. // unsuccessful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
CMDFN({log, command: {_: [CMD], directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon')
const sinon = require('sinon');
const CMD = 'NOOP';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -19,12 +19,10 @@ describe(CMD, done => {
sandbox.restore();
});
it('// successful', done => {
CMDFN()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
done();
})
.catch(done);
})
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});

View File

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

View File

@@ -1,20 +1,18 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'PASS';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
login: () => {},
server: { options: { anonymous: false } },
username: 'user'
username: 'anonymous'
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,61 +24,51 @@ describe(CMD, done => {
sandbox.restore();
});
it('pass // successful', done => {
CMDFN({log, command: {_: [CMD, 'pass'], directive: CMD}})
it('pass // successful', () => {
return cmdFn({log, command: {arg: 'pass', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.args[0]).to.eql(['user', 'pass']);
done();
})
.catch(done);
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass']);
});
});
it('// successful (anonymous)', done => {
it('// successful (already authenticated)', () => {
mockClient.server.options.anonymous = true;
mockClient.authenticated = true;
CMDFN({log, command: {_: [CMD], directive: CMD}})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.reply.args[0][0]).to.equal(202);
expect(mockClient.login.callCount).to.equal(0);
mockClient.server.options.anonymous = false;
mockClient.authenticated = false;
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects('bad');
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects({})
sandbox.stub(mockClient, 'login').rejects({});
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
done();
})
.catch(done);
});
});
it('bad // unsuccessful', done => {
it('bad // unsuccessful', () => {
delete mockClient.username;
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
done();
})
.catch(done);
});
});
});

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
const when = require('when');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'PROT';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve(),
server: {}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
it('// unsuccessful - no bufferSize', () => {
mockClient.server._tls = {};
mockClient.secure = true;
return cmdFn({command: {arg: 'P'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
});
});
it('// successful', () => {
mockClient.bufferSize = 0;
mockClient.secure = true;
return cmdFn({command: {arg: 'p'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
it('// unsuccessful - unsupported', () => {
mockClient.secure = true;
return cmdFn({command: {arg: 'C'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(536);
});
});
it('// unsuccessful - unknown', () => {
mockClient.secure = true;
return cmdFn({command: {arg: 'QQ'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
});

View File

@@ -1,23 +1,21 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'PWD';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { currentDirectory: () => {} }
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
});
afterEach(() => {
@@ -25,61 +23,53 @@ describe(CMD, done => {
});
describe('// check', function () {
it('fails on no fs', done => {
it('fails on no fs', () => {
const badMockClient = { reply: () => {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
it('fails on no fs currentDirectory command', done => {
it('fails on no fs currentDirectory command', () => {
const badMockClient = { reply: () => {}, fs: {} };
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
BADCMDFN()
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
done();
})
.catch(done);
});
});
});
it('// successful', done => {
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
it('// successful', () => {
return cmdFn({log, command: { arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
})
.catch(done);
});
});
it('// successful', done => {
it('// successful', () => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test')
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test');
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
done();
})
.catch(done);
});
});
it('// unsuccessful', done => {
it('// unsuccessful', () => {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'))
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'));
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
})
.catch(done);
});
});
});

View File

@@ -1,17 +1,13 @@
const when = require('when');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
require('sinon-as-promised');
const CMD = 'QUIT';
describe(CMD, done => {
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
close: () => {}
};
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -22,12 +18,10 @@ describe(CMD, done => {
sandbox.restore();
});
it('// successful', done => {
CMDFN()
it('// successful', () => {
return cmdFn()
.then(() => {
expect(mockClient.close.callCount).to.equal(1);
done();
})
.catch(done);
});
});
});

View File

@@ -0,0 +1,58 @@
const {expect} = require('chai');
const sinon = require('sinon');
const when = require('when');
const CMD = 'REST';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => when.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore();
});
it('// unsuccessful', () => {
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('-1 // unsuccessful', () => {
return cmdFn({command: { arg: '-1', directive: CMD } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('bad // unsuccessful', () => {
return cmdFn({command: { arg: 'bad', directive: CMD } })
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('1 // successful', () => {
return cmdFn({command: { arg: '1', directive: CMD } })
.then(() => {
expect(mockClient.restByteCount).to.equal(1);
expect(mockClient.reply.args[0][0]).to.equal(350);
});
});
it('0 // successful', () => {
return cmdFn({command: { arg: '0', directive: CMD } })
.then(() => {
expect(mockClient.restByteCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(350);
});
});
});

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