Compare commits
42 Commits
update-pac
...
v4.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e449e75219 | ||
|
|
296573ae01 | ||
|
|
be579f65d0 | ||
|
|
c440050945 | ||
|
|
ca576acf2e | ||
|
|
649d582f30 | ||
|
|
e26caad783 | ||
|
|
2470db6482 | ||
|
|
d78fae0ce6 | ||
|
|
c59e191a39 | ||
|
|
b2b1b2a0d3 | ||
|
|
81fa7fcb89 | ||
|
|
a18841d770 | ||
|
|
0dbb7f9070 | ||
|
|
0b9167e1e4 | ||
|
|
484409d2eb | ||
|
|
5ffcef3312 | ||
|
|
290769a042 | ||
|
|
a1c7f2ffda | ||
|
|
7153ffab4d | ||
|
|
c0e132b70e | ||
|
|
e661bd10e2 | ||
|
|
bece42a0c9 | ||
|
|
b1fe56826c | ||
|
|
16dbc7895c | ||
|
|
94f0b893e4 | ||
|
|
79d7bd9062 | ||
|
|
44999c714d | ||
|
|
0d9131c370 | ||
|
|
eafaf6f642 | ||
|
|
ea99e6ebbd | ||
|
|
342911eb36 | ||
|
|
0094614ee3 | ||
|
|
0773967ba5 | ||
|
|
e5820688c4 | ||
|
|
a3fa1a2f71 | ||
|
|
e540822d5b | ||
|
|
36f331d15d | ||
|
|
03ff982959 | ||
|
|
8c8f3922a3 | ||
|
|
eca26ee86a | ||
|
|
811db7b1a7 |
@@ -1,103 +1,97 @@
|
||||
version: 2
|
||||
|
||||
create-cache-file: &create-cache-file
|
||||
run:
|
||||
name: Setup cache
|
||||
command: echo "$NODE_VERSION" > _cache_node_version
|
||||
|
||||
package-json-cache: &package-json-cache
|
||||
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
|
||||
|
||||
base-build: &base-build
|
||||
steps:
|
||||
- checkout
|
||||
- <<: *create-cache-file
|
||||
- restore_cache:
|
||||
<<: *package-json-cache
|
||||
- run:
|
||||
name: Install
|
||||
command: npm install
|
||||
- save_cache:
|
||||
<<: *package-json-cache
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: Lint
|
||||
command: npm run verify -- --silent
|
||||
- run:
|
||||
name: Test
|
||||
command: npm run test:once
|
||||
|
||||
jobs:
|
||||
test_node_10:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
environment:
|
||||
- NODE_VERSION: 10
|
||||
<<: *base-build
|
||||
- image: &node-image circleci/node:lts
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- &npm-cache-key npm-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- npm-cache-{{ .Branch }}
|
||||
- npm-cache
|
||||
- run:
|
||||
name: Install
|
||||
command: npm ci
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- node_modules
|
||||
- save_cache:
|
||||
key: *npm-cache-key
|
||||
paths:
|
||||
- ~/.npm/_cacache
|
||||
|
||||
test_node_8:
|
||||
lint:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
environment:
|
||||
- NODE_VERSION: 8
|
||||
<<: *base-build
|
||||
- image: *node-image
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Lint
|
||||
command: npm run verify
|
||||
|
||||
test_node_6:
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:6
|
||||
environment:
|
||||
- NODE_VERSION: 6
|
||||
<<: *base-build
|
||||
- image: *node-image
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- deploy:
|
||||
name: Test
|
||||
command: |
|
||||
npm run test
|
||||
|
||||
release_dry_run:
|
||||
docker:
|
||||
- image: *node-image
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- deploy:
|
||||
name: Dry Release
|
||||
command: |
|
||||
npm run semantic-release -- --dry-run
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
environment:
|
||||
- NODE_VERSION: 8
|
||||
- image: *node-image
|
||||
steps:
|
||||
- checkout
|
||||
- <<: *create-cache-file
|
||||
- restore_cache:
|
||||
<<: *package-json-cache
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- deploy:
|
||||
name: Semantic Release
|
||||
name: Release
|
||||
command: |
|
||||
npm run semantic-release
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test_and_tag:
|
||||
publish:
|
||||
jobs:
|
||||
- test_node_10:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- test_node_8:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- test_node_6:
|
||||
- build
|
||||
- lint:
|
||||
requires:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- release_dry_run:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
requires:
|
||||
- test
|
||||
- lint
|
||||
- hold_release:
|
||||
type: approval
|
||||
requires:
|
||||
- release_dry_run
|
||||
- release:
|
||||
requires:
|
||||
- test_node_6
|
||||
- test_node_8
|
||||
- test_node_10
|
||||
|
||||
build_and_test:
|
||||
jobs:
|
||||
- test_node_10:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- test_node_8:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- test_node_6:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- hold_release
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
# START_CONFIT_GENERATED_CONTENT
|
||||
# Common folders to ignore
|
||||
node_modules/*
|
||||
bower_components/*
|
||||
|
||||
# Config folder (optional - you might want to lint this...)
|
||||
config/*
|
||||
|
||||
# END_CONFIT_GENERATED_CONTENT
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
test_tmp/
|
||||
|
||||
node_modules/
|
||||
|
||||
dist/
|
||||
reports/
|
||||
npm-debug.log
|
||||
.nyc_output/
|
||||
test_tmp/
|
||||
@@ -16,5 +16,10 @@
|
||||
|
||||
- Any new fixes are features should include new or updated [tests](/test).
|
||||
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
|
||||
- Submit your pull requests to the `master` branch, these will normally be merged into a seperate branch for any finally changes before being merged into `master`.
|
||||
- Submit any bugs or requests to the issues page in Github.
|
||||
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
|
||||
- Submit any bugs or requests to the issues page in Github.
|
||||
|
||||
## Setup
|
||||
|
||||
- Clone the repository `git clone`
|
||||
- Install dependencies `npm install`
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Tyler Stewart
|
||||
Copyright (c) 2019 Tyler Stewart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
76
README.md
76
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/trs/ftp-srv">
|
||||
<a href="https://github.com/autovance/ftp-srv">
|
||||
<img alt="ftp-srv" src="logo.png" width="600px" />
|
||||
</a>
|
||||
</p>
|
||||
@@ -14,8 +14,8 @@
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
|
||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
|
||||
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
|
||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
## Overview
|
||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
||||
|
||||
You can use `ftp-srv` to traverse the file system on the server, but it's biggest strength comes from it's customizable file system. This allows you to serve a custom, dynamic, or unique file system to users. You can even server a different system depending on the user connecting.
|
||||
|
||||
## Features
|
||||
- Extensible [file systems](#file-system) per connection
|
||||
- Passive and active transfers
|
||||
@@ -75,10 +73,8 @@ _Note:_ The hostname must be the external IP address to accept external connecti
|
||||
__Default:__ `"ftp://127.0.0.1:21"`
|
||||
|
||||
#### `pasv_url`
|
||||
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
|
||||
|
||||
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
|
||||
__Default:__ `"127.0.0.1"`
|
||||
The hostname to provide a client when attempting a passive connection (`PASV`).
|
||||
If not provided, clients can only connect using an `Active` connection.
|
||||
|
||||
#### `pasv_min`
|
||||
Tne starting port to accept passive connections.
|
||||
@@ -126,10 +122,9 @@ __Allowable values:__
|
||||
#### `log`
|
||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||
|
||||
Piping the output into bunyan will format logs nicely, eg:
|
||||
```
|
||||
$ node ./test/start.js | npx bunyan
|
||||
```
|
||||
#### `timeout`
|
||||
Sets the timeout (in ms) after that an idle connection is closed by the server
|
||||
__Default:__ `0`
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -144,19 +139,29 @@ $ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
|
||||
```
|
||||
|
||||
#### `url`
|
||||
|
||||
Set the listening URL.
|
||||
|
||||
Defaults to `ftp://127.0.0.1:21`
|
||||
|
||||
#### `--root` / `-r`
|
||||
#### `--pasv_url`
|
||||
The hostname to provide a client when attempting a passive connection (`PASV`).
|
||||
If not provided, clients can only connect using an `Active` connection.
|
||||
|
||||
#### `--pasv_min`
|
||||
The starting port to accept passive connections.
|
||||
__Default:__ `1024`
|
||||
|
||||
#### `--pasv_max`
|
||||
The ending port to accept passive connections.
|
||||
The range is then queried for an available port to use when required.
|
||||
__Default:__ `65535`
|
||||
|
||||
#### `--root` / `-r`
|
||||
Set the default root directory for users.
|
||||
|
||||
Defaults to the current directory.
|
||||
|
||||
#### `--credentials` / `-c`
|
||||
|
||||
Set the path to a json credentials file.
|
||||
|
||||
Format:
|
||||
@@ -173,13 +178,14 @@ Format:
|
||||
```
|
||||
|
||||
#### `--username`
|
||||
|
||||
Set the username for the only user. Do not provide an argument to allow anonymous login.
|
||||
|
||||
#### `--password`
|
||||
|
||||
Set the password for the given `username`.
|
||||
|
||||
#### `--read-only`
|
||||
Disable write actions such as upload, delete, etc.
|
||||
|
||||
## Events
|
||||
|
||||
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
|
||||
@@ -242,6 +248,16 @@ Occurs when a file is uploaded.
|
||||
`error` if successful, will be `null`
|
||||
`fileName` name of the file that was uploaded
|
||||
|
||||
### `RNTO`
|
||||
```js
|
||||
connection.on('RNTO', (error, fileName) => { ... });
|
||||
```
|
||||
|
||||
Occurs when a file is renamed.
|
||||
|
||||
`error` if successful, will be `null`
|
||||
`fileName` name of the file that was renamed
|
||||
|
||||
## Supported Commands
|
||||
|
||||
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
||||
@@ -318,32 +334,34 @@ __Used in:__ `SITE CHMOD`
|
||||
Returns a unique file name to write to
|
||||
__Used in:__ `STOU`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
<!--[]-->
|
||||
|
||||
## Contributors
|
||||
|
||||
- [OzairP](https://github.com/OzairP)
|
||||
- [qchar](https://github.com/qchar)
|
||||
- [jorinvo](https://github.com/jorinvo)
|
||||
- [voxsoftware](https://github.com/voxsoftware)
|
||||
- [pkeuter](https://github.com/pkeuter)
|
||||
- [TimLuq](https://github.com/TimLuq)
|
||||
- [edin-mg](https://github.com/edin-m)
|
||||
- [crabl](https://github.com/crabl)
|
||||
- [hirviid](https://github.com/hirviid)
|
||||
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
|
||||
- [edin-m](https://github.com/edin-m)
|
||||
- [voxsoftware](https://github.com/voxsoftware)
|
||||
- [jorinvo](https://github.com/jorinvo)
|
||||
- [Johnnyrook777](https://github.com/Johnnyrook777)
|
||||
- [qchar](https://github.com/qchar)
|
||||
- [mikejestes](https://github.com/mikejestes)
|
||||
- [pkeuter](https://github.com/pkeuter)
|
||||
- [qiansc](https://github.com/qiansc)
|
||||
- [broofa](https://github.com/broofa)
|
||||
- [lafin](https://github.com/lafin)
|
||||
- [alancnet](https://github.com/alancnet)
|
||||
- [zgwit](https://github.com/zgwit)
|
||||
|
||||
<!--[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)
|
||||
|
||||
25
bin/index.js
25
bin/index.js
@@ -37,19 +37,22 @@ function setupYargs() {
|
||||
boolean: true,
|
||||
default: false
|
||||
})
|
||||
.option('pasv_url', {
|
||||
.option('pasv-url', {
|
||||
describe: 'URL to provide for passive connections',
|
||||
type: 'string'
|
||||
type: 'string',
|
||||
alias: 'pasv_url'
|
||||
})
|
||||
.option('pasv_min', {
|
||||
.option('pasv-min', {
|
||||
describe: 'Starting point to use when creating passive connections',
|
||||
type: 'number',
|
||||
default: 1024
|
||||
default: 1024,
|
||||
alias: 'pasv_min'
|
||||
})
|
||||
.option('pasv_max', {
|
||||
.option('pasv-max', {
|
||||
describe: 'Ending port to use when creating passive connections',
|
||||
type: 'number',
|
||||
default: 65535
|
||||
default: 65535,
|
||||
alias: 'pasv_max'
|
||||
})
|
||||
.parse();
|
||||
}
|
||||
@@ -113,11 +116,15 @@ function setupState(_args) {
|
||||
}
|
||||
|
||||
function startFtpServer(_state) {
|
||||
// Remove null/undefined options so they get set to defaults, below
|
||||
for (const key in _state) {
|
||||
if (_state[key] === undefined) delete _state[key];
|
||||
}
|
||||
|
||||
function checkLogin(data, resolve, reject) {
|
||||
const user = _state.credentials[data.username]
|
||||
if (_state.anonymous || (user && user.password === data.password)) {
|
||||
return resolve({root: (user && user.root) || _state.root});
|
||||
const user = _state.credentials[data.username];
|
||||
if (_state.anonymous || user && user.password === data.password) {
|
||||
return resolve({root: user && user.root || _state.root});
|
||||
}
|
||||
|
||||
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
types: [
|
||||
{value: 'feat', name: 'feat: A new feature'},
|
||||
{value: 'fix', name: 'fix: A bug fix'},
|
||||
{value: 'docs', name: 'docs: Documentation only changes'},
|
||||
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
|
||||
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
|
||||
{value: 'perf', name: 'perf: A code change that improves performance'},
|
||||
{value: 'test', name: 'test: Adding missing tests'},
|
||||
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
|
||||
{value: 'revert', name: 'revert: Revert to a commit'},
|
||||
{value: 'WIP', name: 'WIP: Work in progress'}
|
||||
],
|
||||
|
||||
scopes: [],
|
||||
|
||||
// it needs to match the value for field type. Eg.: 'fix'
|
||||
/*
|
||||
scopeOverrides: {
|
||||
fix: [
|
||||
|
||||
{name: 'merge'},
|
||||
{name: 'style'},
|
||||
{name: 'e2eTest'},
|
||||
{name: 'unitTest'}
|
||||
]
|
||||
},
|
||||
*/
|
||||
|
||||
allowCustomScopes: true,
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
|
||||
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
|
||||
appendBranchNameToCommitMessage: false
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
test/**/*.spec.js
|
||||
--reporter mocha-multi-reporters
|
||||
--reporter-options configFile=config/testUnit/reporters.json
|
||||
--ui bdd
|
||||
--bail
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"reporterEnabled": "spec",
|
||||
"mochaJunitReporterReporterOptions": {
|
||||
"mochaFile": "reports/junit.xml"
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
{
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"es6": true
|
||||
},
|
||||
"plugins": [
|
||||
"mocha",
|
||||
"node"
|
||||
],
|
||||
"rules": {
|
||||
"mocha/no-exclusive-tests": 2,
|
||||
"no-warning-comments": [
|
||||
1,
|
||||
{
|
||||
"terms": ["todo", "fixme", "xxx"],
|
||||
"location": "start"
|
||||
},
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"array-bracket-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"brace-style": [
|
||||
2,
|
||||
"1tbs"
|
||||
],
|
||||
"consistent-return": 0,
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"MemberExpression": "off"
|
||||
}
|
||||
],
|
||||
"no-multiple-empty-lines": [
|
||||
2,
|
||||
{
|
||||
"max": 2
|
||||
}
|
||||
],
|
||||
"no-use-before-define": [
|
||||
2,
|
||||
"nofunc"
|
||||
],
|
||||
"one-var": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"quote-props": [
|
||||
2,
|
||||
"as-needed"
|
||||
],
|
||||
"quotes": [
|
||||
2,
|
||||
"single"
|
||||
],
|
||||
"keyword-spacing": 2,
|
||||
"space-before-function-paren": [
|
||||
2,
|
||||
{
|
||||
"anonymous": "always",
|
||||
"named": "never"
|
||||
}
|
||||
],
|
||||
"space-in-parens": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"strict": [
|
||||
2,
|
||||
"global"
|
||||
],
|
||||
"curly": [
|
||||
2,
|
||||
"multi-line"
|
||||
],
|
||||
"eol-last": 2,
|
||||
"key-spacing": [
|
||||
2,
|
||||
{
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
}
|
||||
],
|
||||
"no-eval": 2,
|
||||
"no-with": 2,
|
||||
"space-infix-ops": 2,
|
||||
"dot-notation": [
|
||||
2,
|
||||
{
|
||||
"allowKeywords": true
|
||||
}
|
||||
],
|
||||
"eqeqeq": 2,
|
||||
"no-alert": 2,
|
||||
"no-caller": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-iterator": 2,
|
||||
"no-label-var": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-native-reassign": 2,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-proto": 2,
|
||||
"no-return-assign": 2,
|
||||
"no-script-url": 2,
|
||||
"no-sequences": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"yoda": 2,
|
||||
"no-shadow": 2,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-console": 1,
|
||||
"camelcase": [
|
||||
0,
|
||||
{
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"comma-spacing": 2,
|
||||
"comma-dangle": 1,
|
||||
"new-cap": 2,
|
||||
"new-parens": 2,
|
||||
"arrow-parens": [2, "always"],
|
||||
"no-array-constructor": 2,
|
||||
"array-callback-return": 1,
|
||||
"no-extra-parens": 2,
|
||||
"no-new-object": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-fallthrough": 0,
|
||||
"semi": 2,
|
||||
"semi-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"emcaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"impliedStrict": true
|
||||
}
|
||||
}
|
||||
12
ftp-srv.d.ts
vendored
12
ftp-srv.d.ts
vendored
@@ -64,12 +64,13 @@ export interface FtpServerOptions {
|
||||
pasv_max?: number,
|
||||
pasv_url?: string,
|
||||
greeting?: string | string[],
|
||||
tls?: tls.SecureContext | false,
|
||||
tls?: tls.SecureContextOptions | false,
|
||||
anonymous?: boolean,
|
||||
blacklist?: Array<string>,
|
||||
whitelist?: Array<string>,
|
||||
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
||||
log?: any,
|
||||
log?: any,
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export class FtpServer extends EventEmitter {
|
||||
@@ -111,6 +112,13 @@ export class FtpServer extends EventEmitter {
|
||||
whitelist?: Array<string>
|
||||
}) => void,
|
||||
reject: (err?: Error) => void
|
||||
) => void): this;
|
||||
|
||||
on(event: "disconnect", listener: (
|
||||
data: {
|
||||
connection: FtpConnection,
|
||||
id: string
|
||||
}
|
||||
) => void): this;
|
||||
|
||||
on(event: "client-error", listener: (
|
||||
|
||||
27
meta/contributors/contributors.js
Normal file
27
meta/contributors/contributors.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const {get} = require('https');
|
||||
|
||||
get('https://api.github.com/repos/trs/ftp-srv/contributors', {
|
||||
headers: {
|
||||
'User-Agent': 'Chrome'
|
||||
}
|
||||
}, (res) => {
|
||||
let response = '';
|
||||
res.on('data', (data) => {
|
||||
response += data;
|
||||
});
|
||||
res.on('end', () => {
|
||||
const contributors = JSON.parse(response)
|
||||
.filter((contributor) => contributor.type === 'User');
|
||||
|
||||
for (const contributor of contributors) {
|
||||
const url = contributor.html_url;
|
||||
const username = contributor.login;
|
||||
|
||||
const markdown = `- [${username}](${url})\n`;
|
||||
|
||||
process.stdout.write(markdown);
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
process.stderr.write(err);
|
||||
});
|
||||
7632
package-lock.json
generated
7632
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ftp-srv",
|
||||
"version": "0.0.0-development",
|
||||
"version": "0.0.0",
|
||||
"description": "Modern, extensible FTP Server",
|
||||
"keywords": [
|
||||
"ftp",
|
||||
@@ -22,62 +22,70 @@
|
||||
"types": "./ftp-srv.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/trs/ftp-srv"
|
||||
"url": "https://github.com/autovance/ftp-srv"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-release": "npm run verify",
|
||||
"commitmsg": "cz-customizable-ghooks",
|
||||
"dev": "cross-env NODE_ENV=development npm run verify:watch",
|
||||
"prepush": "npm run verify && npm run test:once --silent",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "npm run dev",
|
||||
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
|
||||
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
|
||||
"verify": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\""
|
||||
"test": "mocha test/**/*.spec.js test/*.spec.js --ui bdd",
|
||||
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
|
||||
},
|
||||
"release": {
|
||||
"verifyConditions": "condition-circle"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"es6": true
|
||||
},
|
||||
"cz-customizable": {
|
||||
"config": "config/release/commitMessageConfig.js"
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"uuid": "^3.3.2",
|
||||
"yargs": "^12.0.1"
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.22.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@icetee/ftp": "^1.0.3",
|
||||
"chai": "^4.1.2",
|
||||
"condition-circle": "^2.0.1",
|
||||
"cross-env": "5.2.0",
|
||||
"cz-customizable": "5.2.0",
|
||||
"cz-customizable-ghooks": "1.5.0",
|
||||
"eslint": "5.3.0",
|
||||
"eslint-config-google": "0.9.1",
|
||||
"eslint-friendly-formatter": "4.0.1",
|
||||
"eslint-plugin-mocha": "^5.1.0",
|
||||
"eslint-plugin-node": "7.0.1",
|
||||
"husky": "0.14.3",
|
||||
"istanbul": "0.4.5",
|
||||
"@commitlint/cli": "^8.1.0",
|
||||
"@commitlint/config-conventional": "^8.1.0",
|
||||
"@icetee/ftp": "^1.0.2",
|
||||
"chai": "^4.2.0",
|
||||
"condition-circle": "^2.0.2",
|
||||
"eslint": "^5.14.1",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.4",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "1.18.0",
|
||||
"mocha-multi-reporters": "1.1.7",
|
||||
"rimraf": "2.6.2",
|
||||
"semantic-release": "^15.9.8",
|
||||
"sinon": "^6.1.5"
|
||||
"rimraf": "^2.6.1",
|
||||
"semantic-release": "^15.13.24",
|
||||
"sinon": "^2.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
"npm": ">=5.x"
|
||||
"node": ">=6.x"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,25 +45,25 @@ class FtpCommands {
|
||||
log.trace({command: logCommand}, 'Handle command');
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
return this.connection.reply(502, `Command not allowed: ${command.directive}`);
|
||||
}
|
||||
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command blacklisted');
|
||||
return this.connection.reply(502, `Command blacklisted: ${command.directive}`);
|
||||
}
|
||||
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command not whitelisted');
|
||||
return this.connection.reply(502, `Command not whitelisted: ${command.directive}`);
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530, 'Command requires authentication');
|
||||
return this.connection.reply(530, `Command requires authentication: ${command.directive}`);
|
||||
}
|
||||
|
||||
if (!commandRegister.handler) {
|
||||
return this.connection.reply(502, 'Handler not set on command');
|
||||
return this.connection.reply(502, `Handler not set on command: ${command.directive}`);
|
||||
}
|
||||
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
|
||||
@@ -8,14 +8,18 @@ const FAMILY = {
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPRT',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function ({log, command} = {}) {
|
||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||
const family = FAMILY[protocol];
|
||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||
|
||||
this.connector = new ActiveConnector(this);
|
||||
return this.connector.setupConnection(ip, port, family)
|
||||
.then(() => this.reply(200));
|
||||
.then(() => this.reply(200))
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 425, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPSV',
|
||||
handler: function () {
|
||||
handler: function ({log}) {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then((server) => {
|
||||
const {port} = server.address();
|
||||
|
||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 425, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<protocol>]',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const _ = require('lodash');
|
||||
const registry = require('../registry');
|
||||
|
||||
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);
|
||||
|
||||
@@ -36,6 +36,7 @@ module.exports = {
|
||||
.tap(() => this.reply(150))
|
||||
.then((fileList) => {
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
return this.reply({socket: this.connector.socket, useEmptyMessage: true});
|
||||
})
|
||||
.tap(() => this.reply(226))
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ module.exports = {
|
||||
const [_option, ...args] = command.arg.split(' ');
|
||||
const option = _.toUpper(_option);
|
||||
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command');
|
||||
return OPTIONS[option].call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
@@ -33,7 +33,6 @@ function utf8([setting] = []) {
|
||||
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)}`);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
const PassiveConnector = require('../../connector/passive');
|
||||
const {isLocalIP} = require('../../helpers/is-local');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function () {
|
||||
handler: function ({log} = {}) {
|
||||
if (!this.server.options.pasv_url) {
|
||||
return this.reply(502);
|
||||
}
|
||||
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then((server) => {
|
||||
const address = this.server.options.pasv_url;
|
||||
let address = this.server.options.pasv_url;
|
||||
// Allow connecting from local
|
||||
if (isLocalIP(this.ip)) {
|
||||
address = this.ip;
|
||||
}
|
||||
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})`);
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 425, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -3,7 +3,7 @@ const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function ({log, command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
|
||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||
@@ -14,7 +14,11 @@ module.exports = {
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(() => this.reply(200));
|
||||
.then(() => this.reply(200))
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 425, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
stream.on('data', (data) => {
|
||||
if (stream) stream.pause();
|
||||
if (this.connector.socket) {
|
||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
||||
this.connector.socket.write(data, () => stream && stream.resume());
|
||||
}
|
||||
});
|
||||
stream.once('end', () => resolve());
|
||||
|
||||
@@ -15,8 +15,10 @@ module.exports = {
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.tap(() => this.emit('RNTO', null, to))
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
this.emit('RNTO', err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -21,11 +21,14 @@ module.exports = {
|
||||
const serverPath = stream.path || fileName;
|
||||
|
||||
const destroyConnection = (connection, reject) => (err) => {
|
||||
if (connection) {
|
||||
if (connection.writeable) connection.end();
|
||||
connection.destroy(err);
|
||||
try {
|
||||
if (connection) {
|
||||
if (connection.writable) connection.end();
|
||||
connection.destroy(err);
|
||||
}
|
||||
} finally {
|
||||
reject(err);
|
||||
}
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
@@ -36,8 +39,8 @@ module.exports = {
|
||||
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());
|
||||
if (stream && stream.writable) {
|
||||
stream.write(data, () => this.connector.socket && this.connector.socket.resume());
|
||||
}
|
||||
});
|
||||
this.connector.socket.once('end', () => {
|
||||
|
||||
@@ -30,7 +30,10 @@ class FtpConnection extends EventEmitter {
|
||||
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('timeout', () => {
|
||||
this.log.trace('Client timeout');
|
||||
this.close();
|
||||
});
|
||||
this.commandSocket.on('close', () => {
|
||||
if (this.connector) this.connector.end();
|
||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||
@@ -69,7 +72,7 @@ class FtpConnection extends EventEmitter {
|
||||
close(code = 421, message = 'Closing connection') {
|
||||
return Promise.resolve(code)
|
||||
.then((_code) => _code && this.reply(_code, message))
|
||||
.then(() => this.commandSocket && this.commandSocket.end());
|
||||
.finally(() => this.commandSocket && this.commandSocket.destroy());
|
||||
}
|
||||
|
||||
login(username, password) {
|
||||
@@ -101,15 +104,21 @@ class FtpConnection extends EventEmitter {
|
||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||
|
||||
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||
if (!letter.encoding) letter.encoding = this.encoding;
|
||||
if (!options.useEmptyMessage) {
|
||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||
if (!letter.encoding) letter.encoding = this.encoding;
|
||||
}
|
||||
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
||||
.then((message) => {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
letters.length - 1 === index ? ' ' : '-' :
|
||||
options.eol ? ' ' : '-';
|
||||
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
|
||||
letter.message = message;
|
||||
if (!options.useEmptyMessage) {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
letters.length - 1 === index ? ' ' : '-' :
|
||||
options.eol ? ' ' : '-';
|
||||
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
|
||||
letter.message = message;
|
||||
} else {
|
||||
letter.message = '';
|
||||
}
|
||||
return letter;
|
||||
});
|
||||
});
|
||||
@@ -127,7 +136,10 @@ class FtpConnection extends EventEmitter {
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else reject(new errors.SocketError('Socket not writable'));
|
||||
} else {
|
||||
this.log.trace({message: letter.message}, 'Could not write message');
|
||||
reject(new errors.SocketError('Socket not writable'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const {Socket} = require('net');
|
||||
const tls = require('tls');
|
||||
const ip = require('ip');
|
||||
const Promise = require('bluebird');
|
||||
const Connector = require('./base');
|
||||
const {SocketError} = require('../errors');
|
||||
|
||||
class Active extends Connector {
|
||||
constructor(connection) {
|
||||
@@ -27,8 +29,11 @@ class Active extends Connector {
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => {
|
||||
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
|
||||
throw new SocketError('The given address is not yours', 500);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -29,7 +29,7 @@ class Connector {
|
||||
closeSocket() {
|
||||
if (this.dataSocket) {
|
||||
const socket = this.dataSocket;
|
||||
this.dataSocket.end(() => socket.destroy());
|
||||
this.dataSocket.end(() => socket && socket.destroy());
|
||||
this.dataSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ const Promise = require('bluebird');
|
||||
const Connector = require('./base');
|
||||
const errors = require('../errors');
|
||||
|
||||
const CONNECT_TIMEOUT = 30 * 1000;
|
||||
|
||||
class Passive extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
@@ -30,6 +32,9 @@ class Passive extends Connector {
|
||||
this.closeServer();
|
||||
return this.server.getNextPasvPort()
|
||||
.then((port) => {
|
||||
this.dataSocket = null;
|
||||
let idleServerTimeout;
|
||||
|
||||
const connectionHandler = (socket) => {
|
||||
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
||||
this.log.error({
|
||||
@@ -41,19 +46,19 @@ class Passive extends Connector {
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
clearTimeout(idleServerTimeout);
|
||||
|
||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||
|
||||
this.dataSocket = socket;
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.once('close', () => this.closeServer());
|
||||
|
||||
if (!this.connection.secure) {
|
||||
this.dataSocket.connected = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.dataSocket = null;
|
||||
|
||||
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
|
||||
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
|
||||
this.dataServer.maxConnections = 1;
|
||||
@@ -74,6 +79,8 @@ class Passive extends Connector {
|
||||
this.dataServer.listen(port, this.server.url.hostname, (err) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT);
|
||||
|
||||
this.log.debug({port}, 'Passive connection listening');
|
||||
resolve(this.dataServer);
|
||||
}
|
||||
|
||||
39
src/fs.js
39
src/fs.js
@@ -2,13 +2,14 @@ const _ = require('lodash');
|
||||
const nodePath = require('path');
|
||||
const uuid = require('uuid');
|
||||
const Promise = require('bluebird');
|
||||
const fs = Promise.promisifyAll(require('fs'));
|
||||
const {createReadStream, createWriteStream, constants} = require('fs');
|
||||
const fsAsync = require('./helpers/fs-async');
|
||||
const errors = require('./errors');
|
||||
|
||||
class FileSystem {
|
||||
constructor(connection, {root, cwd} = {}) {
|
||||
this.connection = connection;
|
||||
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
|
||||
this.cwd = nodePath.normalize(cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep);
|
||||
this._root = nodePath.resolve(root || process.cwd());
|
||||
}
|
||||
|
||||
@@ -27,8 +28,8 @@ class FileSystem {
|
||||
})();
|
||||
|
||||
const fsPath = (() => {
|
||||
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
|
||||
return nodePath.join(resolvedPath);
|
||||
const resolvedPath = nodePath.join(this.root, clientPath);
|
||||
return nodePath.resolve(nodePath.normalize(nodePath.join(resolvedPath)));
|
||||
})();
|
||||
|
||||
return {
|
||||
@@ -43,19 +44,19 @@ class FileSystem {
|
||||
|
||||
get(fileName) {
|
||||
const {fsPath} = this._resolvePath(fileName);
|
||||
return fs.statAsync(fsPath)
|
||||
return fsAsync.stat(fsPath)
|
||||
.then((stat) => _.set(stat, 'name', fileName));
|
||||
}
|
||||
|
||||
list(path = '.') {
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.readdirAsync(fsPath)
|
||||
return fsAsync.readdir(fsPath)
|
||||
.then((fileNames) => {
|
||||
return Promise.map(fileNames, (fileName) => {
|
||||
const filePath = nodePath.join(fsPath, fileName);
|
||||
return fs.accessAsync(filePath, fs.constants.F_OK)
|
||||
return fsAsync.access(filePath, constants.F_OK)
|
||||
.then(() => {
|
||||
return fs.statAsync(filePath)
|
||||
return fsAsync.stat(filePath)
|
||||
.then((stat) => _.set(stat, 'name', fileName));
|
||||
})
|
||||
.catch(() => null);
|
||||
@@ -66,7 +67,7 @@ class FileSystem {
|
||||
|
||||
chdir(path = '.') {
|
||||
const {fsPath, clientPath} = this._resolvePath(path);
|
||||
return fs.statAsync(fsPath)
|
||||
return fsAsync.stat(fsPath)
|
||||
.tap((stat) => {
|
||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
||||
})
|
||||
@@ -78,8 +79,8 @@ class FileSystem {
|
||||
|
||||
write(fileName, {append = false, start = undefined} = {}) {
|
||||
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
||||
stream.once('error', () => fs.unlinkAsync(fsPath));
|
||||
const stream = createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
||||
stream.once('error', () => fsAsync.unlink(fsPath));
|
||||
stream.once('close', () => stream.end());
|
||||
return {
|
||||
stream,
|
||||
@@ -89,12 +90,12 @@ class FileSystem {
|
||||
|
||||
read(fileName, {start = undefined} = {}) {
|
||||
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||
return fs.statAsync(fsPath)
|
||||
return fsAsync.stat(fsPath)
|
||||
.tap((stat) => {
|
||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
||||
})
|
||||
.then(() => {
|
||||
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
|
||||
const stream = createReadStream(fsPath, {flags: 'r', start});
|
||||
return {
|
||||
stream,
|
||||
clientPath
|
||||
@@ -104,28 +105,28 @@ class FileSystem {
|
||||
|
||||
delete(path) {
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.statAsync(fsPath)
|
||||
return fsAsync.stat(fsPath)
|
||||
.then((stat) => {
|
||||
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
|
||||
else return fs.unlinkAsync(fsPath);
|
||||
if (stat.isDirectory()) return fsAsync.rmdir(fsPath);
|
||||
else return fsAsync.unlink(fsPath);
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.mkdirAsync(fsPath)
|
||||
return fsAsync.mkdir(fsPath)
|
||||
.then(() => fsPath);
|
||||
}
|
||||
|
||||
rename(from, to) {
|
||||
const {fsPath: fromPath} = this._resolvePath(from);
|
||||
const {fsPath: toPath} = this._resolvePath(to);
|
||||
return fs.renameAsync(fromPath, toPath);
|
||||
return fsAsync.rename(fromPath, toPath);
|
||||
}
|
||||
|
||||
chmod(path, mode) {
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.chmodAsync(fsPath, mode);
|
||||
return fsAsync.chmod(fsPath, mode);
|
||||
}
|
||||
|
||||
getUniqueName() {
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
const net = require('net');
|
||||
const Promise = require('bluebird');
|
||||
const errors = require('../errors');
|
||||
|
||||
function* portNumberGenerator(min, max) {
|
||||
const MAX_PORT = 65535;
|
||||
const MAX_PORT_CHECK_ATTEMPT = 5;
|
||||
|
||||
function* portNumberGenerator(min, max = MAX_PORT) {
|
||||
let current = min;
|
||||
while (true) {
|
||||
if (current > 65535 || current > max) {
|
||||
if (current > MAX_PORT || current > max) {
|
||||
current = min;
|
||||
}
|
||||
yield current++;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextPortFactory(min, max = Infinity) {
|
||||
const nextPortNumber = portNumberGenerator(min, max);
|
||||
const portCheckServer = net.createServer();
|
||||
portCheckServer.maxConnections = 0;
|
||||
portCheckServer.on('error', () => {
|
||||
portCheckServer.listen(nextPortNumber.next().value);
|
||||
});
|
||||
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
|
||||
const nextPortNumber = portNumberGenerator(portMin, portMax);
|
||||
|
||||
return () => new Promise((resolve) => {
|
||||
portCheckServer.once('listening', () => {
|
||||
const {port} = portCheckServer.address();
|
||||
portCheckServer.close(() => resolve(port));
|
||||
});
|
||||
portCheckServer.listen(nextPortNumber.next().value);
|
||||
})
|
||||
.catch(RangeError, (err) => Promise.reject(new errors.ConnectorError(err.message)));
|
||||
return () => new Promise((resolve, reject) => {
|
||||
const portCheckServer = net.createServer();
|
||||
portCheckServer.maxConnections = 0;
|
||||
|
||||
let attemptCount = 0;
|
||||
const tryGetPort = () => {
|
||||
attemptCount++;
|
||||
if (attemptCount > maxAttempts) {
|
||||
reject(new errors.ConnectorError('Unable to find valid port'));
|
||||
return;
|
||||
}
|
||||
|
||||
const {value: port} = nextPortNumber.next();
|
||||
|
||||
portCheckServer.removeAllListeners();
|
||||
portCheckServer.once('error', (err) => {
|
||||
if (['EADDRINUSE'].includes(err.code)) {
|
||||
tryGetPort();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
portCheckServer.once('listening', () => {
|
||||
portCheckServer.removeAllListeners();
|
||||
portCheckServer.close(() => resolve(port));
|
||||
});
|
||||
|
||||
try {
|
||||
portCheckServer.listen(port, host);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
tryGetPort();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
18
src/helpers/fs-async.js
Normal file
18
src/helpers/fs-async.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const fs = require('fs');
|
||||
const {promisify} = require('bluebird');
|
||||
|
||||
const methods = [
|
||||
'stat',
|
||||
'readdir',
|
||||
'access',
|
||||
'unlink',
|
||||
'rmdir',
|
||||
'mkdir',
|
||||
'rename',
|
||||
'chmod'
|
||||
];
|
||||
|
||||
module.exports = methods.reduce((obj, method) => {
|
||||
obj[method] = promisify(fs[method]);
|
||||
return obj;
|
||||
}, {});
|
||||
3
src/helpers/is-local.js
Normal file
3
src/helpers/is-local.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports.isLocalIP = function(ip) {
|
||||
return ip === '127.0.0.1' || ip == '::1';
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
const http = require('http');
|
||||
const Promise = require('bluebird');
|
||||
const errors = require('../errors');
|
||||
|
||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
||||
|
||||
module.exports = function (hostname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!hostname || hostname === '0.0.0.0') {
|
||||
let ip = '';
|
||||
http.get(IP_WEBSITE, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
|
||||
}
|
||||
response.setEncoding('utf8');
|
||||
response.on('data', (chunk) => {
|
||||
ip += chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
resolve(ip);
|
||||
});
|
||||
});
|
||||
} else resolve(hostname);
|
||||
});
|
||||
};
|
||||
41
src/index.js
41
src/index.js
@@ -7,7 +7,6 @@ const tls = require('tls');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const Connection = require('./connection');
|
||||
const resolveHost = require('./helpers/resolve-host');
|
||||
const {getNextPortFactory} = require('./helpers/find-port');
|
||||
|
||||
class FtpServer extends EventEmitter {
|
||||
@@ -24,7 +23,8 @@ class FtpServer extends EventEmitter {
|
||||
blacklist: [],
|
||||
whitelist: [],
|
||||
greeting: null,
|
||||
tls: false
|
||||
tls: false,
|
||||
timeout: 0
|
||||
}, options);
|
||||
|
||||
this._greeting = this.setupGreeting(this.options.greeting);
|
||||
@@ -36,19 +36,25 @@ class FtpServer extends EventEmitter {
|
||||
this.log = this.options.log;
|
||||
this.url = nodeUrl.parse(this.options.url);
|
||||
this.getNextPasvPort = getNextPortFactory(
|
||||
_.get(this, 'url.hostname'),
|
||||
_.get(this, 'options.pasv_min'),
|
||||
_.get(this, 'options.pasv_max'));
|
||||
|
||||
const timeout = Number(this.options.timeout);
|
||||
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
|
||||
|
||||
const serverConnectionHandler = (socket) => {
|
||||
this.options.timeout > 0 && socket.setTimeout(this.options.timeout);
|
||||
let connection = new Connection(this, {log: this.log, socket});
|
||||
this.connections[connection.id] = connection;
|
||||
|
||||
socket.on('close', () => this.disconnectClient(connection.id));
|
||||
socket.once('close', () => this.emit('disconnect', {connection, id: connection.id}));
|
||||
|
||||
const greeting = this._greeting || [];
|
||||
const features = this._features || 'Ready';
|
||||
return connection.reply(220, ...greeting, features)
|
||||
.finally(() => socket.resume());
|
||||
.finally(() => socket.resume());
|
||||
};
|
||||
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
|
||||
|
||||
@@ -67,22 +73,21 @@ class FtpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
listen() {
|
||||
return resolveHost(this.options.pasv_url || this.url.hostname)
|
||||
.then((pasvUrl) => {
|
||||
this.options.pasv_url = pasvUrl;
|
||||
if (!this.options.pasv_url) {
|
||||
this.log.warn('Passive URL not set. Passive connections not available.');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.once('error', reject);
|
||||
this.server.listen(this.url.port, this.url.hostname, (err) => {
|
||||
this.server.removeListener('error', reject);
|
||||
if (err) return reject(err);
|
||||
this.log.info({
|
||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
||||
ip: this.url.hostname,
|
||||
port: this.url.port
|
||||
}, 'Listening');
|
||||
resolve('Listening');
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.once('error', reject);
|
||||
this.server.listen(this.url.port, this.url.hostname, (err) => {
|
||||
this.server.removeListener('error', reject);
|
||||
if (err) return reject(err);
|
||||
this.log.info({
|
||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
||||
ip: this.url.hostname,
|
||||
port: this.url.port
|
||||
}, 'Listening');
|
||||
resolve('Listening');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
commands = new FtpCommands(mockConnection);
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('FtpCommands', function () {
|
||||
return commands.handle('bad')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(402);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe.skip(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.fs, 'chdir');
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'delete').resolves();
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||
@@ -23,7 +23,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful | no argument', () => {
|
||||
return cmdFn()
|
||||
return cmdFn({})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful IPv4', () => {
|
||||
return cmdFn()
|
||||
return cmdFn({})
|
||||
.then(() => {
|
||||
const [code, message] = mockClient.reply.args[0];
|
||||
expect(code).to.equal(229);
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').resolves();
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe(CMD, function () {
|
||||
it('BAD // unsuccessful', () => {
|
||||
return cmdFn({command: {arg: 'BAD', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(500);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient, 'login').resolves();
|
||||
|
||||
@@ -12,7 +12,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||
|
||||
@@ -12,7 +12,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
|
||||
|
||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'close').resolves();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
read: () => {}
|
||||
|
||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.renameFrom = 'test';
|
||||
mockClient.fs = {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const CMD = 'RNTO';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
let emitter;
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.renameFrom = 'test';
|
||||
mockClient.fs = {
|
||||
@@ -18,6 +20,9 @@ describe(CMD, function () {
|
||||
rename: () => Promise.resolve()
|
||||
};
|
||||
|
||||
emitter = new EventEmitter();
|
||||
mockClient.emit = emitter.emit.bind(emitter);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.fs, 'rename');
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
chmod: () => Promise.resolve()
|
||||
|
||||
@@ -17,7 +17,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve({size: 1})
|
||||
|
||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve({}),
|
||||
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
write: () => {}
|
||||
|
||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve(),
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
mockClient.transferType = null;
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
|
||||
@@ -16,7 +16,7 @@ describe(CMD, function () {
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
delete mockClient.username;
|
||||
mockClient.server.options = {};
|
||||
|
||||
@@ -9,18 +9,21 @@ const ActiveConnector = require('../../src/connector/active');
|
||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||
|
||||
describe('Connector - Active //', function () {
|
||||
let getNextPort = getNextPortFactory(1024);
|
||||
const host = '127.0.0.1';
|
||||
let getNextPort = getNextPortFactory(host, 1024);
|
||||
let PORT;
|
||||
let active;
|
||||
let mockConnection = {};
|
||||
let mockConnection = {
|
||||
commandSocket: {
|
||||
remoteAddress: '::ffff:127.0.0.1'
|
||||
}
|
||||
};
|
||||
let sandbox;
|
||||
let server;
|
||||
|
||||
before(() => {
|
||||
active = new ActiveConnector(mockConnection);
|
||||
});
|
||||
beforeEach((done) => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
active = new ActiveConnector(mockConnection);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
|
||||
getNextPort()
|
||||
.then((port) => {
|
||||
@@ -30,30 +33,47 @@ describe('Connector - Active //', function () {
|
||||
.listen(PORT, () => done());
|
||||
});
|
||||
});
|
||||
afterEach((done) => {
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
server.close(done);
|
||||
server.close();
|
||||
active.end();
|
||||
|
||||
});
|
||||
|
||||
it('sets up a connection', function () {
|
||||
return active.setupConnection('127.0.0.1', PORT)
|
||||
return active.setupConnection(host, PORT)
|
||||
.then(() => {
|
||||
expect(active.dataSocket).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects alternative host', function () {
|
||||
return active.setupConnection('123.45.67.89', PORT)
|
||||
.catch((err) => {
|
||||
expect(err.code).to.equal(500);
|
||||
expect(err.message).to.equal('The given address is not yours');
|
||||
})
|
||||
.finally(() => {
|
||||
expect(active.dataSocket).not.to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('destroys existing connection, then sets up a connection', function () {
|
||||
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
|
||||
|
||||
return active.setupConnection('127.0.0.1', PORT)
|
||||
return active.setupConnection(host, PORT)
|
||||
.then(() => {
|
||||
expect(destroyFnSpy.callCount).to.equal(1);
|
||||
expect(active.dataSocket).to.exist;
|
||||
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
|
||||
|
||||
return active.setupConnection(host, PORT)
|
||||
.then(() => {
|
||||
expect(destroyFnSpy.callCount).to.equal(1);
|
||||
expect(active.dataSocket).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('waits for connection', function () {
|
||||
return active.setupConnection('127.0.0.1', PORT)
|
||||
return active.setupConnection(host, PORT)
|
||||
.then(() => {
|
||||
expect(active.dataSocket).to.exist;
|
||||
return active.waitForConnection();
|
||||
@@ -73,7 +93,7 @@ describe('Connector - Active //', function () {
|
||||
}
|
||||
};
|
||||
|
||||
return active.setupConnection('127.0.0.1', PORT)
|
||||
return active.setupConnection(host, PORT)
|
||||
.then(() => {
|
||||
expect(active.dataSocket).to.exist;
|
||||
return active.waitForConnection();
|
||||
|
||||
@@ -10,6 +10,7 @@ const PassiveConnector = require('../../src/connector/passive');
|
||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||
|
||||
describe('Connector - Passive //', function () {
|
||||
const host = '127.0.0.1';
|
||||
let mockConnection = {
|
||||
reply: () => Promise.resolve({}),
|
||||
close: () => Promise.resolve({}),
|
||||
@@ -20,7 +21,7 @@ describe('Connector - Passive //', function () {
|
||||
},
|
||||
server: {
|
||||
url: '',
|
||||
getNextPasvPort: getNextPortFactory(1024)
|
||||
getNextPasvPort: getNextPortFactory(host, 1024)
|
||||
}
|
||||
};
|
||||
let sandbox;
|
||||
@@ -48,7 +49,7 @@ describe('Connector - Passive //', function () {
|
||||
|
||||
describe('setup', function () {
|
||||
before(function () {
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory());
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(host));
|
||||
});
|
||||
|
||||
it('no pasv range provided', function (done) {
|
||||
@@ -56,7 +57,7 @@ describe('Connector - Passive //', function () {
|
||||
passive.setupServer()
|
||||
.catch((err) => {
|
||||
try {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
expect(err.name).to.contain('RangeError');
|
||||
done();
|
||||
} catch (ex) {
|
||||
done(ex);
|
||||
@@ -68,7 +69,7 @@ describe('Connector - Passive //', function () {
|
||||
describe('setup', function () {
|
||||
let connection;
|
||||
before(function () {
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(-1, -1));
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(host, -1, -1));
|
||||
|
||||
connection = new PassiveConnector(mockConnection);
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe('Connector - Passive //', function () {
|
||||
it('has invalid pasv range', function (done) {
|
||||
connection.setupServer()
|
||||
.catch((err) => {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
expect(err.name).to.contain('RangeError');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('helpers // file-stat', function () {
|
||||
let sandbox;
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
});
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
|
||||
@@ -1,52 +1,55 @@
|
||||
/* eslint no-unused-expressions: 0 */
|
||||
const {expect} = require('chai');
|
||||
const net = require('net');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||
const {getNextPortFactory, portNumberGenerator} = require('../../src/helpers/find-port');
|
||||
|
||||
describe('helpers // find-port', function () {
|
||||
let sandbox;
|
||||
let getNextPort;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
getNextPort = getNextPortFactory(1, 2);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('finds a port', () => {
|
||||
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) {
|
||||
this.address = () => ({port});
|
||||
setImmediate(() => this.emit('listening'));
|
||||
});
|
||||
|
||||
return getNextPort()
|
||||
.then((port) => {
|
||||
expect(port).to.equal(1);
|
||||
describe('getNextPortFactory', function () {
|
||||
describe('portNumberGenerator', () => {
|
||||
it('loops through given set of numbers', () => {
|
||||
const nextNumber = portNumberGenerator(1, 5);
|
||||
expect(nextNumber.next().value).to.equal(1);
|
||||
expect(nextNumber.next().value).to.equal(2);
|
||||
expect(nextNumber.next().value).to.equal(3);
|
||||
expect(nextNumber.next().value).to.equal(4);
|
||||
expect(nextNumber.next().value).to.equal(5);
|
||||
expect(nextNumber.next().value).to.equal(1);
|
||||
expect(nextNumber.next().value).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('restarts count', () => {
|
||||
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) {
|
||||
this.address = () => ({port});
|
||||
setImmediate(() => this.emit('listening'));
|
||||
describe('keeps trying new ports', () => {
|
||||
let getNextPort;
|
||||
let serverAlreadyRunning;
|
||||
beforeEach((done) => {
|
||||
getNextPort = getNextPortFactory('::', 8821);
|
||||
|
||||
serverAlreadyRunning = net.createServer();
|
||||
serverAlreadyRunning.listen(8821, () => done());
|
||||
});
|
||||
|
||||
return getNextPort()
|
||||
.then((port) => {
|
||||
expect(port).to.equal(1);
|
||||
})
|
||||
.then(() => getNextPort())
|
||||
.then((port) => {
|
||||
expect(port).to.equal(2);
|
||||
})
|
||||
.then(() => getNextPort())
|
||||
.then((port) => {
|
||||
expect(port).to.equal(1);
|
||||
afterEach((done) => {
|
||||
serverAlreadyRunning.close(() => done());
|
||||
});
|
||||
|
||||
it('test', () => {
|
||||
return getNextPort()
|
||||
.then((port) => {
|
||||
expect(port).to.equal(8822);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('finds ports concurrently', () => {
|
||||
const portStart = 10000;
|
||||
const getCount = 100;
|
||||
|
||||
const getNextPort = getNextPortFactory('::', portStart);
|
||||
const portFinders = new Array(getCount).fill().map(() => getNextPort());
|
||||
return Promise.all(portFinders)
|
||||
.then((ports) => {
|
||||
expect(ports.length).to.equal(getCount);
|
||||
expect(ports).to.eql(new Array(getCount).fill().map((v, i) => i + portStart));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const resolveHost = require('../../src/helpers/resolve-host');
|
||||
|
||||
describe('helpers //resolve-host', function () {
|
||||
this.timeout(4000);
|
||||
|
||||
let sandbox;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
});
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
it('fetches ip address', () => {
|
||||
const hostname = '0.0.0.0';
|
||||
return resolveHost(hostname)
|
||||
.then((resolvedHostname) => {
|
||||
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches ip address', () => {
|
||||
const hostname = null;
|
||||
return resolveHost(hostname)
|
||||
.then((resolvedHostname) => {
|
||||
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('does nothing', () => {
|
||||
const hostname = '127.0.0.1';
|
||||
return resolveHost(hostname)
|
||||
.then((resolvedHostname) => {
|
||||
expect(resolvedHostname).to.equal(hostname);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on getting hostname', () => {
|
||||
sandbox.stub(require('http'), 'get').callsFake(function (url, cb) {
|
||||
cb({
|
||||
statusCode: 420
|
||||
});
|
||||
});
|
||||
|
||||
return resolveHost(null)
|
||||
.then(() => expect(1).to.equal(2))
|
||||
.catch((err) => {
|
||||
expect(err.code).to.equal(420);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ describe('Integration', function () {
|
||||
return startServer({url: 'ftp://127.0.0.1:8880'});
|
||||
});
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
});
|
||||
afterEach(() => sandbox.restore());
|
||||
after(() => server.close());
|
||||
@@ -48,6 +48,7 @@ describe('Integration', function () {
|
||||
function startServer(options = {}) {
|
||||
server = new FtpServer(_.assign({
|
||||
log,
|
||||
pasv_url: '127.0.0.1',
|
||||
pasv_min: 8881,
|
||||
greeting: ['hello', 'world'],
|
||||
anonymous: true
|
||||
@@ -178,7 +179,7 @@ describe('Integration', function () {
|
||||
const fsPath = `${clientDirectory}/${name}/fail.txt`;
|
||||
|
||||
sandbox.stub(connection.fs, 'write').callsFake(function () {
|
||||
const stream = fs.createWriteStream(fsPath, {flags: 'w+'});
|
||||
const stream = fs.createWriteStream(fsPath, {flags: 'w+', autoClose: false});
|
||||
stream.on('error', () => fs.existsSync(fsPath) && fs.unlinkSync(fsPath));
|
||||
stream.on('close', () => stream.end());
|
||||
setImmediate(() => stream.emit('error', new Error('STOR fail test')));
|
||||
@@ -186,9 +187,9 @@ describe('Integration', function () {
|
||||
});
|
||||
|
||||
client.put(buffer, 'fail.txt', (err) => {
|
||||
expect(err).to.exist;
|
||||
setImmediate(() => {
|
||||
const fileExists = fs.existsSync(fsPath);
|
||||
expect(err).to.exist;
|
||||
expect(fileExists).to.equal(false);
|
||||
done();
|
||||
});
|
||||
@@ -216,6 +217,24 @@ describe('Integration', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('STOR logo.png', (done) => {
|
||||
const logo = `${__dirname}/../logo.png`;
|
||||
const fsPath = `${clientDirectory}/${name}/logo.png`;
|
||||
|
||||
client.put(logo, 'logo.png', (err) => {
|
||||
expect(err).to.not.exist;
|
||||
setImmediate(() => {
|
||||
expect(fs.existsSync(fsPath)).to.equal(true);
|
||||
|
||||
const logoContents = fs.readFileSync(logo);
|
||||
const transferedContects = fs.readFileSync(fsPath);
|
||||
|
||||
expect(logoContents.equals(transferedContects));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('APPE tést.txt', (done) => {
|
||||
const buffer = Buffer.from(', awesome!');
|
||||
const fsPath = `${clientDirectory}/${name}/tést.txt`;
|
||||
@@ -350,6 +369,39 @@ describe('Integration', function () {
|
||||
});
|
||||
}
|
||||
|
||||
describe('Server events', function () {
|
||||
const disconnect = sinon.spy();
|
||||
const login = sinon.spy();
|
||||
|
||||
before(() => {
|
||||
server.on('login', login);
|
||||
server.on('disconnect', disconnect);
|
||||
return connectClient({
|
||||
host: server.url.hostname,
|
||||
port: server.url.port,
|
||||
user: 'test',
|
||||
password: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
server.off('login', login);
|
||||
server.off('disconnect', disconnect);
|
||||
})
|
||||
|
||||
it('should fire a login event on connect', () => {
|
||||
expect(login.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire a close event on disconnect', (done) => {
|
||||
client.end();
|
||||
setTimeout(() => {
|
||||
expect(disconnect.calledOnce).to.be.true;
|
||||
done();
|
||||
}, 100)
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ASCII', function () {
|
||||
before(() => {
|
||||
return connectClient({
|
||||
@@ -403,7 +455,7 @@ describe('Integration', function () {
|
||||
readFile(`${process.cwd()}/test/cert/server.csr`)
|
||||
]))
|
||||
.then(([key, cert, ca]) => startServer({
|
||||
url: 'ftp://127.0.0.1:8880',
|
||||
url: 'ftp://127.0.0.1:8881',
|
||||
tls: {key, cert, ca}
|
||||
}))
|
||||
.then(() => {
|
||||
@@ -431,7 +483,7 @@ describe('Integration', function () {
|
||||
readFile(`${process.cwd()}/test/cert/server.csr`)
|
||||
]))
|
||||
.then(([key, cert, ca]) => startServer({
|
||||
url: 'ftps://127.0.0.1:8880',
|
||||
url: 'ftps://127.0.0.1:8882',
|
||||
tls: {key, cert, ca}
|
||||
}))
|
||||
.then(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ const FtpServer = require('../src');
|
||||
const server = new FtpServer({
|
||||
log: bunyan.createLogger({name: 'test', level: 'trace'}),
|
||||
url: 'ftp://127.0.0.1:8880',
|
||||
pasv_url: '192.168.1.1',
|
||||
pasv_min: 8881,
|
||||
greeting: ['Welcome', 'to', 'the', 'jungle!'],
|
||||
tls: {
|
||||
|
||||
Reference in New Issue
Block a user