Compare commits

...

186 Commits

Author SHA1 Message Date
Tyler Stewart
90b4b78ddc chore: update confit 2018-01-21 15:01:11 -07:00
Tyler Stewart
5e20556a0e WIP(docs): outline docs in own folder 2018-01-21 14:57:49 -07:00
Tyler Stewart
74760189ed WIP(readme): improve docs in readme 2018-01-21 14:57:49 -07:00
Tyler Stewart
c60606971a chore(readme): update badges 2018-01-21 14:55:04 -07:00
Tyler Stewart
bd41b31821 chore(readme): update logo placement 2018-01-21 14:31:27 -07:00
Tyler Stewart
ce1c526c41 chore(logo): new logo design 2018-01-21 14:31:27 -07:00
Tyler Stewart
d822101a07 chore(readme): update coveralls badge 2018-01-10 11:13:28 -07:00
Tyler Stewart
47c8eedd3b chore: fix semantic release 2018-01-10 11:01:39 -07:00
Tyler Stewart
6c08cc2aed refactor: assimilate promises using resolve instead of try 2018-01-10 10:49:02 -07:00
Tyler Stewart
e2a5c78b0a fix(abor): send 225 if no file transfer in progress 2018-01-10 10:49:02 -07:00
Tyler Stewart
2cadac3f7e chore(readme): add reference links 2018-01-10 10:49:02 -07:00
Tyler Stewart
2255be9acd feat(connector): return promise on end 2018-01-10 10:49:02 -07:00
Tyler Stewart
d22c911a36 refactor(connection): completely parse message before handling 2018-01-10 10:49:02 -07:00
Tyler Stewart
5dabbc251b refactor(opts): simplify setting 2018-01-10 10:49:02 -07:00
Tyler Stewart
ef89577627 refactor(list): simplify reply 2018-01-10 10:49:02 -07:00
Tyler Stewart
8fbe750086 refactor(stat): simplify replys 2018-01-10 10:49:02 -07:00
Tyler Stewart
3b33508f44 feat: migrate to bluebird
Replace `when` with `bluebird` promise library
2018-01-10 10:49:02 -07:00
Tyler Stewart
23368b04b9 test: update stor fail test timing 2018-01-10 10:24:41 -07:00
Tyler Stewart
876a061e92 refactor: ensure reject is called on destroyConnection 2018-01-10 10:24:41 -07:00
Tyler Stewart
65b1fd27a0 fix(stor): pause connection to avoid memory build up 2018-01-10 10:24:41 -07:00
James Suárez
286c1063fa fix(retr): pause read stream to avoid memory build up 2018-01-10 10:24:41 -07:00
Tyler Stewart
e87c36d7ff chore: fix semantic releasing 2017-12-08 17:50:16 -07:00
Tyler Stewart
de0aafad2f fix(stat): fix stat on file
Ensure message sent raw
2017-12-08 17:50:16 -07:00
Tyler Stewart
4f80e11745 fix(stat): fix file response
Ensures correct response when `stat`-ing a file

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

export-fs

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

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

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

update-logo

update-logo

update-logo

update-logo

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

master

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

explicit-tls-support

explicit-tls-support

explicit-tls-support

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

master

master

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

fix-compatability-bugs

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

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

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

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

BREAKING CHANGE: name change, removed `disabled_commands`
2017-03-07 18:41:27 -07:00
Tyler Stewart
e5b10c5858 chore: rename to ftp-srv, flows better
master
2017-03-07 18:31:06 -07:00
Tyler Stewart
4a6ab71731 chore: merge commit '43cb87a' 2017-03-06 17:55:12 -07:00
Tyler Stewart
ccc053ac8d chore(readme): update README 2017-03-06 17:49:24 -07:00
163 changed files with 12334 additions and 2035 deletions

140
.circleci/config.yml Normal file
View File

@@ -0,0 +1,140 @@
version: 2
jobs:
build_node_8:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Install
command: npm install
- save_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
paths:
- node_modules
build_node_6:
docker:
- image: circleci/node:6
steps:
- checkout
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Install
command: npm install
- save_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
paths:
- node_modules
lint:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Lint
command: npm run verify:js
test_node_8:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Test Node 8
command: npm run test:coverage
when: always
- store_test_results:
path: reports
- store_artifacts:
path: reports/coverage
prefix: coverage
test_node_6:
docker:
- image: circleci/node:6
steps:
- checkout
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Test Node 6
command: npm run test:coverage
when: always
- store_test_results:
path: reports
- store_artifacts:
path: reports/coverage
prefix: coverage
release:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Update NPM
command: |
npm install npm@5
npm install semantic-release@11
- deploy:
name: Semantic Release
command: |
npm run semantic-release || true
workflows:
version: 2
test_and_tag:
jobs:
- build_node_8:
filters:
branches:
only: master
- build_node_6:
filters:
branches:
only: master
- lint:
requires:
- build_node_8
- test_node_6:
requires:
- build_node_6
- test_node_8:
requires:
- build_node_8
- release:
requires:
- lint
- test_node_6
- test_node_8
build_and_test:
jobs:
- build_node_8:
filters:
branches:
ignore: master
- build_node_6:
filters:
branches:
ignore: master
- lint:
requires:
- build_node_8
- test_node_6:
requires:
- build_node_6
- test_node_8:
requires:
- build_node_8

1
.gitattributes vendored Normal file
View File

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

3
.gitignore vendored
View File

@@ -2,5 +2,6 @@ node_modules/
dist/
reports/
.env
npm-debug.log
.nyc_output/
test_tmp/

24
.nycrc Normal file
View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<!--[CN_HEADING]-->
# Contributing
Welcome! This document explains how you can contribute to making **ftp-svr** even better.
Welcome! This document explains how you can contribute to making **ftp-srv** even better.
<!--[]-->
@@ -26,7 +26,7 @@ npm install
Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks:
```
ftp-svr/
ftp-srv/
├──config/ * configuration files live here (e.g. eslint, verify, testUnit)
├──src/ * source code files should be here
@@ -136,6 +136,9 @@ Command | Description
Command | Description
:------ | :----------
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
<pre>npm run verify:js</pre> | Verify Javascript code style and syntax
<pre>npm run verify:js:fix</pre> | Verify Javascript code style and syntax and fix any errors that can be fixed automatically
<pre>npm run verify:js:watch</pre> | Verify Javascript code style and syntax and watch files for changes
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed

22
LICENSE
View File

@@ -1,9 +1,21 @@
ftp-svr Copyright (c) 2017 Tyler Stewart
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (c) 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.

182
README.md
View File

@@ -1,151 +1,85 @@
# ftp-svr [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
<!--[RM_DESCRIPTION]-->
> Modern, extensible FTP Server
<p align="center">
Modern, extensible FTP Server
</p>
<!--[]-->
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [Contributing](#contributing)
- [License](#license)
<a href="https://circleci.com/gh/trs/ftp-srv">
<img alt="npm" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
<img alt="npm" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
</a>
</p>
---
## Synopsis
`ftp-srv` is an extensible FTP server solution that enables custom file systems per connection allowing the use of virtual file systems. By default, it acts like a regular FTP server. Just include it in your project and start listening.
## Features
- Supports passive and active connections
- Extensible [file system](#file-system)
- Passive and Active transfer support
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Extensible [file systems](#file-system) per connection
- Promise based API
## Install
`npm install ftp-svr --save`
```
$ npm install ftp-srv
```
`yarn add ftp-svr`
## Usage
- [Options](#options)
- [Events](#events)
- [File System](#file-system)
## Quick Start
```js
const FtpSvr = require('ftp-svr');
const ftpServer = new FtpSvr({ [options] ... });
const FtpSrv = require('ftp-srv');
ftpServer.on('...', (data, resolve, reject) => { ... })
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876');
ftpServer.on('login', ({connection, username, password}, resolve, reject) => {
// fetch credentials from database, file, or hard coded
database.users.fetch({username, password})
.then(() => {
connection.on('STOR', (err, file) => console.log(`Uploaded file: ${file}`));
resolve({
root: '/'
});
})
.catch(() => reject);
});
ftpServer.listen()
.then(() => { ... });
.then(() => {
console.log('Waiting for connections!');
});
```
### 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`).
Checkout the [Documentation](/docs).
__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.
__disabled_commands__ : `[]`
> String array of commands to forbid.
`['RMD', 'RNFR', 'RNTO']`
__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) { ... }`
__log__ : `bunyan`
> A [bunyan logger](https://github.com/trentm/node-bunyan) instance.
### Events
All events emit the same structure: `({data object}, resolve, reject)`
__login__ : `{connection, username, password}`
> Occurs after `PASV` (or `USER` if `options.anonymous`)
```
resolve({
fs, // [optional] custom file system class
cwd // [optional] initial working directory (if not using custom file system)
})
```
### 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.
*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.*
#### Functions
`currentDirectory()`
> Returns a string of the current working directory
> Used in: `PWD`
`get(fileName)`
> Returns a file stat object of file or directory
> Used in: `STAT`, `SIZE`, `RNFR`, `MDTM`
`list(path)`
> Returns array of file and directory stat objects
> Used in `LIST`, `STAT`
`chdir(path)`
> Returns new directory relative to cwd
> Used in `CWD`, `CDUP`
`mkdir(path)`
> Return a path to a newly created directory
> Used in `MKD`
`write(fileName, options)`
> Returns a writable stream
Options:
`append` if true, append to existing file
> Used in `STOR`, `APPE`
`read(fileName)`
> Returns a readable stream
> Used in `RETR`
`delete(path)`
> Delete a file or directory
> 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`
<!--[RM_CONTRIBUTING]-->
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
<!--[]-->
<!--[RM_LICENSE]-->
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
<!--[]-->
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

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

View File

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

View File

@@ -0,0 +1,6 @@
{
"reporterEnabled": "mocha-pretty-bunyan-nyan",
"mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml"
}
}

View File

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

View File

@@ -1,60 +1,162 @@
# START_CONFIT_GENERATED_CONTENT
confit:
extends: &confit-extends
- plugin:node/recommended
plugins: &confit-plugins
- node
env: &confit-env
commonjs: true # For Webpack, CommonJS
node: true
mocha: true
es6: true
globals: &confit-globals {}
parser: &confit-parser espree
parserOptions: &confit-parserOptions
ecmaVersion: 6
sourceType: module
ecmaFeatures:
globalReturn: false
impliedStrict: true
jsx: false
# END_CONFIT_GENERATED_CONTENT
# Customise this section to meet your needs...
extends: *confit-extends
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-extends" from the line above
# <<: *confit-extends
plugins: *confit-plugins
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-plugins" from the line above
# <<: *confit-extends
env:
<<: *confit-env
globals:
<<: *confit-globals
parser: *confit-parser
parserOptions:
<<: *confit-parserOptions
rules:
max-len:
- warn
- 200 # Line Length
node/no-unpublished-require:
- 2
- allowModules:
- chai
- dotenv
- ftp
- sinon
- sinon-as-promised
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

View File

@@ -1,6 +1,6 @@
generator-confit:
app:
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
buildProfile: Latest
copyrightOwner: Tyler Stewart
license: MIT
@@ -8,7 +8,7 @@ generator-confit:
publicRepository: true
repositoryType: GitHub
paths:
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
config:
configDir: config/
input:
@@ -18,22 +18,23 @@ generator-confit:
prodDir: dist/
reportDir: reports/
buildJS:
_version: df428a706d926204228c5d9ebdbd7b49908926d9
_version: ead8ce4280b07d696aff499a5fca1a933727582f
framework: []
frameworkScripts: []
outputFormat: ES6
sourceFormat: ES6
entryPoint:
_version: de20402bf85c703080ef6daf21e35325a3b9d604
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
entryPoints:
main:
- src/index.js
- ftp-srv.js
testUnit:
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
testDependencies: []
testFramework: mocha
verify:
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
jsCodingStandard: eslint
jsCodingStandard: none
documentation:
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
generateDocs: false

2
docs/README.md Normal file
View File

@@ -0,0 +1,2 @@
## Documentation

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

@@ -0,0 +1,122 @@
import * as tls from 'tls'
import { Stats } from 'fs'
export class FileSystem {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
export class FtpConnection {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>
}
export interface FtpServerOptions {
pasv_range?: number | string,
greeting?: string | string[],
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any
}
export class FtpServer {
constructor(url: string, options?: FtpServerOptions);
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;
on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
},
resolve: (config: {
fs?: FileSystem,
root?: string,
cwd?: string,
blacklist?: Array<string>,
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void)
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void)
}
export {FtpServer as FtpSrv};
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: 22 KiB

23
logo/generate.js Normal file
View File

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

68
logo/logo.html Normal file
View File

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

7495
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,26 @@
{
"name": "ftp-svr",
"version": "0.0.0",
"name": "ftp-srv",
"version": "0.0.0-development",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
"ftp-server",
"ftp-srv",
"ftp-svr",
"ftpd",
"ftpserver",
"server"
],
"license": "MIT",
"main": "src/index.js",
"files": [
"src",
"ftp-srv.d.ts"
],
"main": "ftp-srv.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/stewarttylerr/ftp-svr"
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm-run-all verify test:coverage build ",
@@ -22,14 +29,14 @@
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:coverage --silent",
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
"semantic-release": "semantic-release",
"start": "npm run dev",
"test": "npm run test:unit",
"test:check-coverage": "cross-env NODE_ENV=test istanbul check-coverage reports/coverage/coverage.json --config config/testUnit/istanbul.js",
"test:check-coverage": "nyc check-coverage",
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
"test:unit": "chokidar 'src/**/*.js' 'test/**/*.js' -c 'npm run test:unit:once' --initial --silent",
"test:unit:once": "cross-env NODE_ENV=test istanbul cover --config config/testUnit/istanbul.js _mocha -- --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov.info | coveralls",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
@@ -45,36 +52,44 @@
}
},
"dependencies": {
"bunyan": "^1.8.5",
"date-fns": "^1.28.0",
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"lodash": "^4.17.4",
"minimist-string": "^1.0.2",
"uuid": "^3.0.1",
"when": "^3.7.8"
"moment": "^2.19.1",
"uuid": "^3.1.0"
},
"devDependencies": {
"chai": "^3.5.0",
"@icetee/ftp": "^0.3.15",
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"coveralls": "2.11.15",
"condition-circle": "^1.6.0",
"coveralls": "2.13.1",
"cross-env": "3.1.4",
"cz-customizable": "4.0.0",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "3.14.1",
"eslint-config-google": "0.7.1",
"eslint-plugin-node": "3.0.5",
"ftp": "^0.3.10",
"husky": "0.13.1",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "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.5.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"mocha-pretty-bunyan-nyan": "^1.0.4",
"npm-run-all": "4.0.2",
"nyc": "11.1.0",
"rimraf": "2.6.1",
"semantic-release": "^11.0.2",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x",
"npm": ">=3.9.5"
},
"release": {
"verifyConditions": "condition-circle"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return when(this.fs.delete(command._[1]))
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

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

View File

@@ -1,16 +0,0 @@
const _ = require('lodash');
module.exports = function ({command} = {}) {
const registry = require('./registry');
const directive = _.upperCase(command._[1]);
if (directive) {
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
const {syntax, help, obsolete} = registry[directive];
const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]);
return this.reply(214, ...reply);
} else {
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
}
};

View File

@@ -1,29 +1,60 @@
const _ = require('lodash');
const when = require('when');
const Promise = require('bluebird');
const REGISTRY = require('./registry');
class FtpCommands {
constructor(connection) {
this.connection = connection;
this.registry = require('./registry');
this.previousCommand = {};
this.disabledCommands = _.get(this.connection, 'server.options.disabled_commands', []).map(cmd => _.upperCase(cmd));
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
}
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);
if (!this.registry.hasOwnProperty(command.directive)) {
// 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');
}
if (_.includes(this.disabledCommands, command.directive)) {
return this.connection.reply(502, 'Command forbidden');
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, 'Command blacklisted');
}
const commandRegister = this.registry[command.directive];
if (!commandRegister.no_auth && !this.connection.authenticated) {
return this.connection.reply(530);
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, 'Command not whitelisted');
}
const commandRegister = REGISTRY[command.directive];
const commandFlags = _.get(commandRegister, 'flags', {});
if (!commandFlags.no_auth && !this.connection.authenticated) {
return this.connection.reply(530, 'Command requires authentication');
}
if (!commandRegister.handler) {
@@ -31,7 +62,7 @@ class FtpCommands {
}
const handler = commandRegister.handler.bind(this.connection);
return when.try(handler, { log, command, previous_command: this.previousCommand })
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => {
this.previousCommand = _.clone(command);
});

View File

@@ -1,53 +0,0 @@
const _ = require('lodash');
const when = require('when');
const getFileStat = require('../helpers/file-stat');
// http://cr.yp.to/ftp/list.html
// http://cr.yp.to/ftp/list/eplf.html
module.exports = function ({log, command, previous_command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
const simple = command.directive === 'NLST';
let dataSocket;
const directory = command._[1] || '.';
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.list(directory)))
.then(files => {
const getFileMessage = (file) => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
const fileList = files.map(file => {
const message = getFileMessage(file);
return {
raw: true,
message,
socket: dataSocket
};
})
return this.reply(150)
.then(() => this.reply(...fileList));
})
.then(() => {
return this.reply(226, 'Transfer OK');
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(err.code || 451, err.message || 'No directory');
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const format = require('date-fns/format');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
.then(fileStat => {
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime)
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
const escapePath = require('../helpers/escape-path');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return when(this.fs.mkdir(command._[1]))
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
const ActiveConnector = require('../connector/active');
module.exports = function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = command._[1].split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)
.then(socket => {
return this.reply(200);
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
const _ = require('lodash');
const tls = require('tls');
module.exports = {
directive: 'AUTH',
handler: function ({command} = {}) {
const method = _.upperCase(command.arg);
switch (method) {
case 'TLS': return handleTLS.call(this);
default: return this.reply(504);
}
},
syntax: '{{cmd}} <type>',
description: 'Set authentication mechanism',
flags: {
no_auth: true,
feat: 'AUTH TLS'
}
};
function handleTLS() {
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);
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

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
const FAMILY = {
1: 4,
2: 6
};
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this);
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,21 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)
.then(() => this.reply(200));
},
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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.read(command._[1])))
.then(stream => {
return when.promise((resolve, reject) => {
dataSocket.on('error', err => stream.emit('error', err));
stream.on('data', data => dataSocket.write(data, this.encoding));
stream.on('end', () => resolve(this.reply(226)));
stream.on('error', err => reject(err));
this.reply(150).then(() => dataSocket.resume());
});
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(551);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

@@ -1,17 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command._[1];
return when(this.fs.get(fileName))
.then(() => {
this.renameFrom = fileName;
return this.reply(350);
})
.catch(err => {
log.error(err);
return this.reply(550);
})
}

View File

@@ -1,23 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.renameFrom) return this.reply(503);
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
const from = this.renameFrom;
const to = command._[1];
return when(this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550);
})
.finally(() => {
delete this.renameFrom;
});
}

View File

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

View File

@@ -1,15 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return when(this.fs.get(command._[1]))
.then(fileStat => {
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
log.error(err);
return this.reply(550);
});
}

View File

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

View File

@@ -1,38 +0,0 @@
const when = require('when');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
const append = command.directive === 'APPE';
let dataSocket;
return this.connector.waitForConnection()
.then(socket => {
this.commandSocket.pause();
dataSocket = socket;
})
.then(() => when(this.fs.write(command._[1], {append})))
.then(stream => {
return when.promise((resolve, reject) => {
stream.on('error', err => dataSocket.emit('error', err));
dataSocket.on('end', () => resolve(this.reply(226)));
dataSocket.on('error', err => reject(err));
dataSocket.on('data', data => stream.write(data, this.encoding));
this.reply(150).then(() => dataSocket.resume());
});
})
.catch(when.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
log.error(err);
return this.reply(553);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
}

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
module.exports = function ({log, command} = {}) {
if (this.username) return this.reply(530, 'Username already set');
this.username = command._[1];
if (this.server.options.anonymous === true) {
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(331);
};

View File

@@ -1,9 +1,6 @@
const _ = require('lodash');
const uuid = require('uuid');
const when = require('when');
const sequence = require('when/sequence');
const parseSentence = require('minimist-string');
const net = require('net');
const Promise = require('bluebird');
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
@@ -14,57 +11,78 @@ 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({ftp_session_id: this.commandSocket.ftp_session_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 => {
console.log('error', err)
});
this.commandSocket.on('data', data => {
const messages = _.compact(data.toString('utf-8').split('\r\n'));
const handleMessage = (message) => {
const command = parseSentence(message);
command.directive = _.upperCase(command._[0]);
return this.commands.handle(command);
};
return sequence(messages.map(message => handleMessage.bind(this, message)));
});
this.commandSocket.on('timeout', () => {
console.log('timeout')
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
});
}
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, message => this.commands.handle(message));
}
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 Promise.resolve(code)
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
return when.try(() => {
return Promise.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} = {}) => {
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
this.authenticated = true;
this.fs = fs || new FileSystem(this, {cwd});
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, {root, cwd});
});
}
@@ -73,8 +91,8 @@ 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(promise)
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise)
.then(letter => {
if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
@@ -82,25 +100,24 @@ class FtpConnection {
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
return when(letter.message) // allow passing in a promise as a message
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then(message => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
return letter;
})
});
});
});
}
const processLetter = (letter, index) => {
return when.promise((resolve, reject) => {
const seperator = !options.hasOwnProperty('eol') ?
(letters.length - 1 === index ? ' ' : '-') :
(options.eol ? ' ' : '-');
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
};
const processLetter = letter => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, packet}, 'Reply');
letter.socket.write(packet + '\r\n', letter.encoding, err => {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
if (err) {
this.log.error(err);
return reject(err);
@@ -109,10 +126,12 @@ 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 => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch(err => {
this.log.error(err);
});

View File

@@ -1,5 +1,6 @@
const net = require('net');
const when = require('when');
const {Socket} = require('net');
const tls = require('tls');
const Promise = require('bluebird');
const Connector = require('./base');
class Active extends Connector {
@@ -8,26 +9,38 @@ class Active extends Connector {
this.type = 'active';
}
waitForConnection() {
return when.iterate(
() => {},
() => this.dataSocket && this.dataSocket.connected,
() => when().delay(250)
).timeout(5000)
.then(() => this.dataSocket);
waitForConnection({timeout = 5000, delay = 250} = {}) {
const checkSocket = () => {
if (this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout);
}
setupConnection(host, port) {
const closeExistingServer = () => this.dataSocket ?
when(this.dataSocket.destroy()) :
when.resolve()
setupConnection(host, port, family = 4) {
const closeExistingServer = () => Promise.resolve(
this.dataSocket ? this.dataSocket.destroy() : undefined);
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 && 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

@@ -1,28 +1,47 @@
const when = require('when');
const Promise = require('bluebird');
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'));
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
}
end() {
if (this.dataSocket) this.dataSocket.end();
if (this.dataServer) this.dataServer.close();
this.dataSocket = null;
this.dataServer = null;
const closeDataSocket = new Promise(resolve => {
if (this.dataSocket) this.dataSocket.end();
else resolve();
});
const closeDataServer = new Promise(resolve => {
if (this.dataServer) this.dataServer.close(() => resolve());
else resolve();
});
this.connection.connector = new Connector(this.connection);
return Promise.all([closeDataSocket, closeDataServer])
.then(() => {
this.dataSocket = null;
this.dataServer = null;
this.type = false;
});
}
}
module.exports = Connector;

View File

@@ -1,5 +1,7 @@
const net = require('net');
const when = require('when');
const tls = require('tls');
const Promise = require('bluebird');
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
@@ -10,30 +12,29 @@ class Passive extends Connector {
this.type = 'passive';
}
waitForConnection() {
if (!this.dataServer) {
return when.reject(new errors.ConnectorError('Passive server not setup'));
}
return when.iterate(
() => {},
() => this.dataServer && this.dataServer.listening && this.dataSocket,
() => when().delay(250)
).timeout(5000)
.then(() => this.dataSocket);
waitForConnection({timeout = 5000, delay = 250} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout);
}
setupServer() {
const closeExistingServer = () => this.dataServer ?
when.promise(resolve => this.dataServer.close(() => resolve())) :
when.resolve()
new Promise(resolve => this.dataServer.close(() => resolve())) :
Promise.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,26 +45,41 @@ class Passive extends Connector {
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.info({port}, 'Passive connection fulfilled.');
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket;
this.dataSocket.setEncoding(this.connection.encoding);
this.dataSocket.on('data', data => {
});
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.transferType);
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.trace('Passive connection closed');
this.end();
});
});
};
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.trace('Passive server closed');
this.dataServer = null;
});
return when.promise((resolve, reject) => {
return new Promise((resolve, reject) => {
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 +93,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

@@ -1,17 +1,27 @@
const _ = require('lodash');
const nodePath = require('path');
const when = require('when');
const whenNode = require('when/node');
const syncFs = require('fs');
const fs = whenNode.liftAll(syncFs);
const uuid = require('uuid');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
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() {
@@ -19,20 +29,20 @@ class FileSystem {
}
get(fileName) {
const path = nodePath.resolve(this.cwd, fileName);
return fs.stat(path)
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(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.readdirAsync(fsPath)
.then(fileNames => {
return when.map(fileNames, fileName => {
const filePath = nodePath.join(path, fileName);
return fs.access(filePath, syncFs.constants.F_OK)
return Promise.map(fileNames, fileName => {
const filePath = nodePath.join(fsPath, fileName);
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fs.stat(filePath)
return fs.statAsync(filePath)
.then(stat => _.set(stat, 'name', fileName));
})
.catch(() => null);
@@ -42,60 +52,65 @@ class FileSystem {
}
chdir(path = '.') {
path = nodePath.resolve(this.cwd, path);
return fs.stat(path)
const {fsPath, serverPath} = this._resolvePath(path);
return fs.statAsync(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 = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(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.statAsync(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = syncFs.createReadStream(path, {flags: 'r'});
const stream = fs.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.statAsync(fsPath)
.then(stat => {
if (stat.isDirectory()) return fs.rmdir(path);
else return fs.unlink(path);
})
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});
}
mkdir(path) {
path = nodePath.resolve(this.cwd, path);
return fs.mkdir(path)
.then(() => path);
const {fsPath} = this._resolvePath(path);
return fs.mkdirAsync(fsPath)
.then(() => fsPath);
}
rename(from, to) {
const fromPath = nodePath.resolve(this.cwd, from);
const toPath = nodePath.resolve(this.cwd, to);
return fs.rename(fromPath, toPath);
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fs.renameAsync(fromPath, toPath);
}
chmod(path, mode) {
path = nodePath.resolve(this.cwd, path);
return fs.chmod(path, mode);
const {fsPath} = this._resolvePath(path);
return fs.chmodAsync(fsPath, mode);
}
getUniqueName() {
return uuid.v4().replace(/\W/g, '');
}
}
module.exports = FileSystem;

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