Compare commits
208 Commits
improve-lo
...
v4.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af62a7c4f | ||
|
|
0b65a22296 | ||
|
|
f59857e34a | ||
|
|
1ad45fc757 | ||
|
|
bc8abb14da | ||
|
|
0b55b3f79d | ||
|
|
4f4a6c25a5 | ||
|
|
45a4bf15bf | ||
|
|
a1be4416a7 | ||
|
|
32a0750e2c | ||
|
|
4eb17015f1 | ||
|
|
d2566e7745 | ||
|
|
f8cd1e8f64 | ||
|
|
e0e676e7e9 | ||
|
|
a7775a46ae | ||
|
|
f9c81b162a | ||
|
|
e1f1aa09cd | ||
|
|
b0174bb24e | ||
|
|
5852851ded | ||
|
|
1c5db00a5e | ||
|
|
02227d653e | ||
|
|
bf44cbba58 | ||
|
|
80ff71655e | ||
|
|
eb8e3d837f | ||
|
|
dc37c9c435 | ||
|
|
6a94a20f64 | ||
|
|
02355eda28 | ||
|
|
8b32be7acc | ||
|
|
5daaa9883c | ||
|
|
02798e094f | ||
|
|
720c93d088 | ||
|
|
bffcc35299 | ||
|
|
78de22f518 | ||
|
|
2b140ecb0d | ||
|
|
57ddfb5e08 | ||
|
|
beef19af30 | ||
|
|
e7c5f83311 | ||
|
|
1ab793c04e | ||
|
|
24f7126acf | ||
|
|
457b859450 | ||
|
|
722da60a82 | ||
|
|
9f95d60916 | ||
|
|
4cd88b129c | ||
|
|
db49063b0d | ||
|
|
b55557292e | ||
|
|
c87ce2fef6 | ||
|
|
05a68cfb08 | ||
|
|
31290fc964 | ||
|
|
a598fab03c | ||
|
|
87e8ac6ca8 | ||
|
|
75e34988f4 | ||
|
|
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 | ||
|
|
ab085a1bca | ||
|
|
a5f26480e5 | ||
|
|
e41b04be46 | ||
|
|
7acf861a4d | ||
|
|
4801ecc0cc | ||
|
|
8e34e4c71a | ||
|
|
0afd578683 | ||
|
|
46b0d52ff2 | ||
|
|
185e473edc | ||
|
|
92a323f3dd | ||
|
|
f67e487306 | ||
|
|
2716123da7 | ||
|
|
ef207f60c1 | ||
|
|
4d8cf42ad0 | ||
|
|
50c6b92d12 | ||
|
|
a2103e5a3c | ||
|
|
2302b749fa | ||
|
|
27b43d702b | ||
|
|
fae003e644 | ||
|
|
a51678ae70 | ||
|
|
bc26886a0d | ||
|
|
c9b4371579 | ||
|
|
95471bdd15 | ||
|
|
5a36a6685d | ||
|
|
90a7419661 | ||
|
|
29cb035f66 | ||
|
|
66fc66ed80 | ||
|
|
c970a42132 | ||
|
|
30ae54a952 | ||
|
|
91be338ebd | ||
|
|
2a5013447c | ||
|
|
1f15af0fb6 | ||
|
|
1cf1f750f4 | ||
|
|
442490d713 | ||
|
|
58b9ba27d9 | ||
|
|
87a2138cb3 | ||
|
|
9fd423c745 | ||
|
|
363839ec8f | ||
|
|
d9fc0c9cac | ||
|
|
b0463d65b6 | ||
|
|
47b2cc0593 | ||
|
|
e5f24f991d | ||
|
|
87f3ae79a1 | ||
|
|
3a7b3d4570 | ||
|
|
dc040eaabd | ||
|
|
fecec961e1 | ||
|
|
5ff677ce42 | ||
|
|
cc0f2a5cd3 | ||
|
|
414433a56e | ||
|
|
a794f1e5b3 | ||
|
|
1b5d22a3ca | ||
|
|
4205caf7ac | ||
|
|
a468d4ffd0 | ||
|
|
40b08893ac | ||
|
|
8a2454ceea | ||
|
|
0c7cc4fe6e | ||
|
|
6ea6baceb0 | ||
|
|
b07e0189ee | ||
|
|
ec30a5a4f3 | ||
|
|
6020409979 | ||
|
|
c60606971a | ||
|
|
bd41b31821 | ||
|
|
ce1c526c41 | ||
|
|
d822101a07 | ||
|
|
47c8eedd3b | ||
|
|
6c08cc2aed | ||
|
|
e2a5c78b0a | ||
|
|
2cadac3f7e | ||
|
|
2255be9acd | ||
|
|
d22c911a36 | ||
|
|
5dabbc251b | ||
|
|
ef89577627 | ||
|
|
8fbe750086 | ||
|
|
3b33508f44 | ||
|
|
23368b04b9 | ||
|
|
876a061e92 | ||
|
|
65b1fd27a0 | ||
|
|
286c1063fa | ||
|
|
e87c36d7ff | ||
|
|
de0aafad2f | ||
|
|
4f80e11745 | ||
|
|
6bbd905379 | ||
|
|
de50f55457 | ||
|
|
32cdedd163 | ||
|
|
6c2c1a87dc | ||
|
|
9e83143690 | ||
|
|
0238529edf | ||
|
|
d0c204eb81 | ||
|
|
cdebe9a464 | ||
|
|
eeb8f9ab4d | ||
|
|
60d06c21c8 | ||
|
|
8609b1d02e | ||
|
|
80b05215ff | ||
|
|
37f0a15549 | ||
|
|
1ba67034b1 | ||
|
|
0a331c5998 | ||
|
|
a7103ded7e | ||
|
|
d787d4cab6 | ||
|
|
154cd5a5d7 | ||
|
|
5fc59b50b1 | ||
|
|
043c97c80f | ||
|
|
772fe5ca06 | ||
|
|
e272802525 | ||
|
|
7589322abc | ||
|
|
fae5564041 | ||
|
|
e9b4a6385d | ||
|
|
71621aae4f | ||
|
|
0eaa0f8743 | ||
|
|
8828a4ea09 | ||
|
|
b33659320f | ||
|
|
6a6b949d3b | ||
|
|
283be85db3 | ||
|
|
e555ce9230 | ||
|
|
e6575808f1 | ||
|
|
a5e58a106e |
112
.circleci/config.yml
Normal file
112
.circleci/config.yml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@5.0.2
|
||||||
|
|
||||||
|
commands:
|
||||||
|
setup_git_bot:
|
||||||
|
description: set up the bot git user to make changes
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: "Git: Botovance"
|
||||||
|
command: |
|
||||||
|
git config --global user.name "Bot Vance"
|
||||||
|
git config --global user.email bot@autovance.com
|
||||||
|
|
||||||
|
executors:
|
||||||
|
node-lts:
|
||||||
|
parameters:
|
||||||
|
node-version:
|
||||||
|
type: string
|
||||||
|
default: lts
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:<< parameters.node-version >>
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
executor: node-lts
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- node/install-packages
|
||||||
|
- run:
|
||||||
|
name: Lint
|
||||||
|
command: npm run verify
|
||||||
|
|
||||||
|
release_dry_run:
|
||||||
|
executor: node-lts
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- node/install-packages
|
||||||
|
- setup_git_bot
|
||||||
|
- deploy:
|
||||||
|
name: Dry Release
|
||||||
|
command: |
|
||||||
|
git branch -u "origin/${CIRCLE_BRANCH}"
|
||||||
|
npx semantic-release --dry-run
|
||||||
|
|
||||||
|
release:
|
||||||
|
executor: node-lts
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- node/install-packages
|
||||||
|
- setup_git_bot
|
||||||
|
- deploy:
|
||||||
|
name: Release
|
||||||
|
command: |
|
||||||
|
git branch -u "origin/${CIRCLE_BRANCH}"
|
||||||
|
npx semantic-release
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
release_scheduled:
|
||||||
|
triggers:
|
||||||
|
# 6:03 UTC (mornings) 1 monday
|
||||||
|
- schedule:
|
||||||
|
cron: "3 6 * * 1"
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
- lint
|
||||||
|
- node/test:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
version:
|
||||||
|
- '12.22'
|
||||||
|
- '14.19'
|
||||||
|
- '16.14'
|
||||||
|
- 'current'
|
||||||
|
- release:
|
||||||
|
context: npm-deploy-av
|
||||||
|
requires:
|
||||||
|
- node/test
|
||||||
|
- lint
|
||||||
|
|
||||||
|
test:
|
||||||
|
jobs:
|
||||||
|
- lint
|
||||||
|
- node/test:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
version:
|
||||||
|
- '12.22'
|
||||||
|
- '14.19'
|
||||||
|
- '16.14'
|
||||||
|
- 'current'
|
||||||
|
- release_dry_run:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: main
|
||||||
|
requires:
|
||||||
|
- node/test
|
||||||
|
- lint
|
||||||
|
- hold_release:
|
||||||
|
type: approval
|
||||||
|
requires:
|
||||||
|
- release_dry_run
|
||||||
|
- release:
|
||||||
|
context: npm-deploy-av
|
||||||
|
requires:
|
||||||
|
- hold_release
|
||||||
@@ -1,9 +1,2 @@
|
|||||||
# START_CONFIT_GENERATED_CONTENT
|
|
||||||
# Common folders to ignore
|
|
||||||
node_modules/*
|
node_modules/*
|
||||||
bower_components/*
|
bower_components/*
|
||||||
|
|
||||||
# Config folder (optional - you might want to lint this...)
|
|
||||||
config/*
|
|
||||||
|
|
||||||
# END_CONFIT_GENERATED_CONTENT
|
|
||||||
|
|||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package-lock.json binary
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
|
test_tmp/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
reports/
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|||||||
25
.travis.yml
25
.travis.yml
@@ -1,25 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "6"
|
|
||||||
- "node"
|
|
||||||
|
|
||||||
env:
|
|
||||||
FTP_URL: ftp://127.0.0.1:8880
|
|
||||||
PASV_RANGE: 8881
|
|
||||||
|
|
||||||
install: npm install
|
|
||||||
|
|
||||||
script:
|
|
||||||
- npm run verify:js
|
|
||||||
- npm run test:coverage
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
- npm run upload-coverage
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
skip_cleanup: true
|
|
||||||
provider: script
|
|
||||||
script: npm run semantic-release
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
node: "6"
|
|
||||||
5
CODEOWNERS
Normal file
5
CODEOWNERS
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Lines starting with '#' are comments.
|
||||||
|
# Each line is a file pattern followed by one or more owners.
|
||||||
|
# Order is important. The last matching pattern has the most precedence.
|
||||||
|
|
||||||
|
* @quorumdms/team-gbt
|
||||||
219
CONTRIBUTING.md
219
CONTRIBUTING.md
@@ -1,208 +1,25 @@
|
|||||||
<!--[CN_HEADING]-->
|
<p align="center">
|
||||||
# Contributing
|
<a href="https://github.com/trs/ftp-srv">
|
||||||
|
<img alt="ftp-srv" src="logo.png" width="400px" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Welcome! This document explains how you can contribute to making **ftp-srv** even better.
|
<h1 align="center">
|
||||||
|
Contributing Guide
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
## Welcome
|
||||||
|
|
||||||
<!--[]-->
|
- Thank you for your eagerness to contribute, pull requests are encouraged!
|
||||||
|
|
||||||
<!--[CN_GETTING_STARTED]-->
|
## Guidelines
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone <this repo>
|
|
||||||
npm install -g commitizen
|
|
||||||
npm install -g semantic-release-cli
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[RM_DIR_STRUCTURE]-->
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks:
|
|
||||||
```
|
|
||||||
ftp-srv/
|
|
||||||
├──config/ * configuration files live here (e.g. eslint, verify, testUnit)
|
|
||||||
│
|
|
||||||
├──src/ * source code files should be here
|
|
||||||
│
|
|
||||||
├──dist/ * production-build code should live here
|
|
||||||
│
|
|
||||||
├──reports/ * test reports appear here
|
|
||||||
│
|
|
||||||
├──test/ * unit test specifications live here
|
|
||||||
│
|
|
||||||
├──confit.yml * the project config file generated by 'yo confit'
|
|
||||||
├──CONTRIBUTING.md * how to contribute to the project
|
|
||||||
├──README.md * this file
|
|
||||||
└──package.json * NPM package description file
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_GITFLOW_PROCESS]-->
|
|
||||||
# GitFlow Development Process
|
|
||||||
|
|
||||||
This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/index.html) workflow.
|
|
||||||
|
|
||||||
## Create a branch
|
|
||||||
When you're working on a project, you're going to have a bunch of different features or ideas in progress at any given time – some of which are ready to go, and others which are not. Branching exists to help you manage this workflow.
|
|
||||||
|
|
||||||
When you create a branch in your project, you're creating an environment where you can try out new ideas. Changes you make on a branch don't affect the `master` branch, so you're free to experiment and commit changes, safe in the knowledge that your branch won't be merged until it's ready to be reviewed by someone you're collaborating with.
|
|
||||||
|
|
||||||
###ProTip
|
|
||||||
|
|
||||||
Branching is a core concept in Git, and the entire GitHub Flow is based upon it. There's only one rule: anything in the `master` branch is always deployable.
|
|
||||||
|
|
||||||
Because of this, it's extremely important that your new branch is created off of `master` when working on a feature or a fix. Your branch name should be descriptive (e.g., `refactor-authentication`, `user-content-cache-key`, `make-retina-avatars`), so that others can see what is being worked on.
|
|
||||||
|
|
||||||
## Add commits
|
|
||||||
Once your branch has been created, it's time to start making changes. Whenever you add, edit, or delete a file, you're making a commit, and adding them to your branch. This process of adding commits keeps track of your progress as you work on a feature branch.
|
|
||||||
|
|
||||||
Commits also create a transparent history of your work that others can follow to understand what you've done and why. Each commit has an associated commit message, which is a description explaining why a particular change was made. Furthermore, each commit is considered a separate unit of change. This lets you roll back changes if a bug is found, or if you decide to head in a different direction.
|
|
||||||
|
|
||||||
###ProTip
|
|
||||||
|
|
||||||
Commit messages are important, especially since Git tracks your changes and then displays them as commits once they're pushed to the server. By writing clear commit messages, you can make it easier for other people to follow along and provide feedback.
|
|
||||||
|
|
||||||
## Open a pull request
|
|
||||||
|
|
||||||
Pull Requests initiate discussion about your commits. Because they're tightly integrated with the underlying Git repository, anyone can see exactly what changes would be merged if they accept your request.
|
|
||||||
|
|
||||||
You can open a Pull Request at any point during the development process: when you have little or no code but want to share some screenshots or general ideas, when you're stuck and need help or advice, or when you're ready for someone to review your work. By using GitHub's @mention system in your Pull Request message, you can ask for feedback from specific people or teams, whether they're down the hall or ten time zones away.
|
|
||||||
|
|
||||||
###ProTip
|
|
||||||
|
|
||||||
Pull Requests are useful for contributing to open source projects and for managing changes to shared repositories. If you're using a Fork & Pull Model, Pull Requests provide a way to notify project maintainers about the changes you'd like them to consider. If you're using a Shared Repository Model, Pull Requests help start code review and conversation about proposed changes before they're merged into the `master` branch.
|
|
||||||
|
|
||||||
## Discuss and review your code
|
|
||||||
Once a Pull Request has been opened, the person or team reviewing your changes may have questions or comments. Perhaps the coding style doesn't match project guidelines, the change is missing unit tests, or maybe everything looks great and props are in order. Pull Requests are designed to encourage and capture this type of conversation.
|
|
||||||
|
|
||||||
You can also continue to push to your branch in light of discussion and feedback about your commits. If someone comments that you forgot to do something or if there is a bug in the code, you can fix it in your branch and push up the change. GitHub will show your new commits and any additional feedback you may receive in the unified Pull Request view.
|
|
||||||
|
|
||||||
###ProTip
|
|
||||||
|
|
||||||
Pull Request comments are written in Markdown, so you can embed images and emoji, use pre-formatted text blocks, and other lightweight formatting.
|
|
||||||
|
|
||||||
## Merge to `master`
|
|
||||||
|
|
||||||
Once your PR has passed any the integration tests and received approval to merge, it is time to merge your code into the `master` branch.
|
|
||||||
|
|
||||||
Once merged, Pull Requests preserve a record of the historical changes to your code. Because they're searchable, they let anyone go back in time to understand why and how a decision was made.
|
|
||||||
|
|
||||||
###ProTip
|
|
||||||
|
|
||||||
By incorporating certain keywords into the text of your Pull Request, you can associate issues with code. When your Pull Request is merged, the related issues are also closed. For example, entering the phrase Closes #32 would close issue number 32 in the repository. For more information, check out our help article.
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_BUILD_TASKS]-->
|
|
||||||
## Build Tasks
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
:------ | :----------
|
|
||||||
<pre>npm run build</pre> | Generate production build into [dist/](dist/) folder
|
|
||||||
<pre>npm run dev</pre> | Run project in development mode (verify code, and re-verify when code is changed)
|
|
||||||
<pre>npm start</pre> | Alias for `npm run dev` task
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_TEST_TASKS]-->
|
|
||||||
## Test Tasks
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
:------ | :----------
|
|
||||||
<pre>npm test</pre> | Alias for `npm run test:unit` task
|
|
||||||
<pre>npm run test:coverage</pre> | Run instrumented unit tests then verify coverage meets defined thresholds<ul><li>Returns non-zero exit code when coverage does not meet thresholds (as defined in istanbul.js)</li></ul>
|
|
||||||
<pre>npm run test:unit</pre> | Run unit tests whenever JS source or tests change<ul><li>Uses Mocha</li><li>Code coverage</li><li>Runs continuously (best to run in a separate window)</li></ul>
|
|
||||||
<pre>npm run test:unit:once</pre> | Run unit tests once<ul><li>Uses Mocha</li><li>Code coverage</li></ul>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_VERIFY_TASKS]-->
|
|
||||||
## Verification (Linting) Tasks
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
:------ | :----------
|
|
||||||
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
|
|
||||||
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_COMMIT_TASKS]-->
|
|
||||||
## Commit Tasks
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
:------ | :----------
|
|
||||||
<pre>git status</pre> | Lists the current branch and the status of changed files
|
|
||||||
<pre>git log</pre> | Displays the commit log (press Q to quit viewing)
|
|
||||||
<pre>git add .</pre> | Stages all modified & untracked files, ready to be committed
|
|
||||||
<pre>git cz</pre> | Commit changes to local repository using Commitizen.<ul><li>Asks questions about the change to generate a valid conventional commit message</li><li>Can be customised by modifying [config/release/commitMessageConfig.js](config/release/commitMessageConfig.js)</li></ul>
|
|
||||||
<pre>git push</pre> | Push local repository changes to remote repository
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_DOCUMENTATION_TASKS]-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[CN_RELEASE_TASKS]-->
|
|
||||||
## Release Tasks
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
:------ | :----------
|
|
||||||
<pre>npm run commitmsg</pre> | Git commit message hook that validates the commit message conforms to your commit message conventions.
|
|
||||||
<pre>npm run pre-release</pre> | Verify code, run unit tests, check test coverage, build software. This task is designed to be run before
|
|
||||||
the `semantic-release` task.
|
|
||||||
<ul><li>Run `semantic-release-cli setup` once you have a remote repository. See https://github.com/semantic-release/cli for details.</li><li>Semantic-release integrates with Travis CI (or similar tools) to generate release notes
|
|
||||||
for each release (which appears in the "Releases" section in GitHub) and
|
|
||||||
publishes the package to NPM (when all the tests are successful) with a semantic version number.
|
|
||||||
</li></ul>
|
|
||||||
<pre>npm run prepush</pre> | Git pre-push hook that verifies code and checks unit test coverage meet minimum thresholds.
|
|
||||||
<pre>npm run upload-coverage</pre> | Uploads code-coverage metrics to Coveralls.io<ul><li>Setup - https://coveralls.zendesk.com/hc/en-us/articles/201347419-Coveralls-currently-supports</li><li>Define an environment variable called COVERALLS_REPO_TOKEN in your build environment with the repo token from https://coveralls.io/github/<repo-name>/settings</li><li>In your CI configuration (e.g. `travis.yml`), call `npm run upload-coverage` if the build is successful.</li></ul>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
|
|
||||||
<!--[CN_CHANGING_BUILD_TOOL_CONFIG]-->
|
|
||||||
## Changing build-tool configuration
|
|
||||||
|
|
||||||
There are 3 ways you can change the build-tool configuration for this project:
|
|
||||||
|
|
||||||
1. BEST: Modify the Confit configuration file ([confit.yml](confit.yml)) by hand, then re-run `yo confit` and tell it to use the existing configuration.
|
|
||||||
1. OK: Re-run `yo confit` and provide new answers to the questions. **Confit will attempt to overwrite your existing configuration (it will prompt for confirmation), so make sure you have committed your code to a source control (e.g. git) first**.
|
|
||||||
There are certain configuration settings which can **only** be specified by hand, in which case the first approach is still best.
|
|
||||||
1. RISKY: Modify the generated build-tool config by hand. Be aware that if you re-run `yo confit` it will attempt to overwrite your changes. So commit your changes to source control first.
|
|
||||||
|
|
||||||
Additionally, the **currently-generated** configuration can be extended in the following ways:
|
|
||||||
|
|
||||||
- The task configuration is defined in [package.json](package.json). It is possible to change the task definitions to add your own sub-tasks.
|
|
||||||
You can also use the `pre...` and `post...` script-name prefixes to run commands before (pre) and after (post) the generated commands.
|
|
||||||
|
|
||||||
- The `entryPoint.entryPoints` string in [confit.yml](confit.yml) is designed to be edited manually. It represents the starting-point(s) of the application (like a `main()` function). A NodeJS application has one entry point. E.g. `src/index.js`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
|
- Any new fixes are features should include new or updated [tests](/test).
|
||||||
|
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
|
||||||
|
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
|
||||||
|
- Submit any bugs or requests to the issues page in Github.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Clone the repository `git clone`
|
||||||
|
- Install dependencies `npm install`
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Tyler Stewart
|
Copyright (c) 2019 Tyler Stewart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
265
README.md
265
README.md
@@ -1,26 +1,26 @@
|
|||||||
[](https://github.com/trs/ftp-srv)
|
<p align="center">
|
||||||
|
<a href="https://github.com/autovance/ftp-srv">
|
||||||
|
<img alt="ftp-srv" src="logo.png" width="600px" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<!--[RM_DESCRIPTION]-->
|
|
||||||
> Modern, extensible FTP Server
|
|
||||||
|
|
||||||
<!--[]-->
|
<p align="center">
|
||||||
|
Modern, extensible FTP Server
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://badge.fury.io/js/ftp-srv) [](https://travis-ci.org/trs/ftp-srv)
|
<p align="center">
|
||||||
[](https://coveralls.io/github/trs/ftp-srv?branch=coveralls) [](https://github.com/semantic-release/semantic-release) [](http://commitizen.github.io/cz-cli/)
|
<a href="https://www.npmjs.com/package/ftp-srv">
|
||||||
|
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
|
||||||
|
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Features](#features)
|
|
||||||
- [Install](#install)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [API](#api)
|
|
||||||
- [Events](#events)
|
|
||||||
- [Supported Commands](#supported-commands)
|
|
||||||
- [File System](#file-system)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
- Extensible [file systems](#file-system) per connection
|
- Extensible [file systems](#file-system) per connection
|
||||||
- Passive and active transfers
|
- Passive and active transfers
|
||||||
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
|
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
|
||||||
|
- Promise based API
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
`npm install ftp-srv --save`
|
`npm install ftp-srv --save`
|
||||||
@@ -35,63 +36,116 @@
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Quick start
|
// Quick start, create an active ftp server.
|
||||||
|
const FtpSrv = require('ftp-srv');
|
||||||
|
|
||||||
const FtpSvr = require('ftp-srv');
|
const port=21;
|
||||||
const ftpServer = new FtpSvr('ftp://0.0.0.0:9876', { options ... });
|
const ftpServer = new FtpSrv({
|
||||||
|
url: "ftp://0.0.0.0:" + port,
|
||||||
|
anonymous: true
|
||||||
|
});
|
||||||
|
|
||||||
ftpServer.on('login', (data, resolve, reject) => { ... });
|
ftpServer.on('login', (data, resolve, reject) => {
|
||||||
...
|
if(data.username === 'anonymous' && data.password === 'anonymous'){
|
||||||
|
return resolve({ root:"/" });
|
||||||
|
}
|
||||||
|
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||||
|
});
|
||||||
|
|
||||||
ftpServer.listen()
|
ftpServer.listen().then(() => {
|
||||||
.then(() => { ... });
|
console.log('Ftp server is starting...')
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `new FtpSrv(url, [{options}])`
|
### `new FtpSrv({options})`
|
||||||
#### url
|
#### url
|
||||||
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
|
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
|
||||||
Supported protocols:
|
Supported protocols:
|
||||||
- `ftp` Plain FTP
|
- `ftp` Plain FTP
|
||||||
- `ftps` Implicit FTP over TLS
|
- `ftps` Implicit FTP over TLS
|
||||||
|
|
||||||
_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP.
|
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
|
||||||
__Default:__ `"ftp://127.0.0.1:21"`
|
__Default:__ `"ftp://127.0.0.1:21"`
|
||||||
|
|
||||||
#### options
|
#### `pasv_url`
|
||||||
|
`FTP-srv` provides an IP address to the client when a `PASV` command is received in the handshake for a passive connection. Reference [PASV verb](https://cr.yp.to/ftp/retr.html#pasv). This can be one of two options:
|
||||||
|
- A function which takes one parameter containing the remote IP address of the FTP client. This can be useful when the user wants to return a different IP address depending if the user is connecting from Internet or from an LAN address.
|
||||||
|
Example:
|
||||||
|
```js
|
||||||
|
const { networkInterfaces } = require('os');
|
||||||
|
const { Netmask } = require('netmask');
|
||||||
|
|
||||||
##### `pasv_range`
|
const nets = networkInterfaces();
|
||||||
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
|
function getNetworks() {
|
||||||
This range is then queried for an available port to use when required.
|
let networks = {};
|
||||||
__Default:__ `22`
|
for (const name of Object.keys(nets)) {
|
||||||
|
for (const net of nets[name]) {
|
||||||
|
if (net.family === 'IPv4' && !net.internal) {
|
||||||
|
networks[net.address + "/24"] = net.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return networks;
|
||||||
|
}
|
||||||
|
|
||||||
##### `greeting`
|
const resolverFunction = (address) => {
|
||||||
|
// const networks = {
|
||||||
|
// '$GATEWAY_IP/32': `${public_ip}`,
|
||||||
|
// '10.0.0.0/8' : `${lan_ip}`
|
||||||
|
// }
|
||||||
|
const networks = getNetworks();
|
||||||
|
for (const network in networks) {
|
||||||
|
if (new Netmask(network).contains(address)) {
|
||||||
|
return networks[network];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "127.0.0.1";
|
||||||
|
}
|
||||||
|
|
||||||
|
new FtpSrv({pasv_url: resolverFunction});
|
||||||
|
```
|
||||||
|
|
||||||
|
- A static IP address (ie. an external WAN **IP address** that the FTP server is bound to). In this case, only connections from localhost are handled differently returning `127.0.0.1` to the client.
|
||||||
|
|
||||||
|
If not provided, clients can only connect using an `Active` connection.
|
||||||
|
|
||||||
|
#### `pasv_min`
|
||||||
|
The starting port to accept passive connections.
|
||||||
|
__Default:__ `1024`
|
||||||
|
|
||||||
|
#### `pasv_max`
|
||||||
|
The ending port to accept passive connections.
|
||||||
|
The range is then queried for an available port to use when required.
|
||||||
|
__Default:__ `65535`
|
||||||
|
|
||||||
|
#### `greeting`
|
||||||
A human readable array of lines or string to send when a client connects.
|
A human readable array of lines or string to send when a client connects.
|
||||||
__Default:__ `null`
|
__Default:__ `null`
|
||||||
|
|
||||||
##### `tls`
|
#### `tls`
|
||||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
||||||
__Default:__ `false`
|
__Default:__ `false`
|
||||||
|
|
||||||
##### `anonymous`
|
#### `anonymous`
|
||||||
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
||||||
Can also set as a string which allows users to authenticate using the username provided.
|
Can also set as a string which allows users to authenticate using the username provided.
|
||||||
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
||||||
__Default:__ `false`
|
__Default:__ `false`
|
||||||
|
|
||||||
##### `blacklist`
|
#### `blacklist`
|
||||||
Array of commands that are not allowed.
|
Array of commands that are not allowed.
|
||||||
Response code `502` is sent to clients sending one of these commands.
|
Response code `502` is sent to clients sending one of these commands.
|
||||||
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
||||||
__Default:__ `[]`
|
__Default:__ `[]`
|
||||||
|
|
||||||
##### `whitelist`
|
#### `whitelist`
|
||||||
Array of commands that are only allowed.
|
Array of commands that are only allowed.
|
||||||
Response code `502` is sent to clients sending any other command.
|
Response code `502` is sent to clients sending any other command.
|
||||||
__Default:__ `[]`
|
__Default:__ `[]`
|
||||||
|
|
||||||
##### `file_format`
|
#### `file_format`
|
||||||
Sets the format to use for file stat queries such as `LIST`.
|
Sets the format to use for file stat queries such as `LIST`.
|
||||||
__Default:__ `"ls"`
|
__Default:__ `"ls"`
|
||||||
__Allowable values:__
|
__Allowable values:__
|
||||||
@@ -100,16 +154,80 @@ __Allowable values:__
|
|||||||
- `function () {}` A custom function returning a format or promise for one.
|
- `function () {}` A custom function returning a format or promise for one.
|
||||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
||||||
|
|
||||||
##### `log`
|
#### `log`
|
||||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||||
|
|
||||||
|
#### `timeout`
|
||||||
|
Sets the timeout (in ms) after that an idle connection is closed by the server
|
||||||
|
__Default:__ `0`
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
`ftp-srv` also comes with a builtin CLI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ftp-srv [url] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `url`
|
||||||
|
Set the listening URL.
|
||||||
|
|
||||||
|
Defaults to `ftp://127.0.0.1:21`
|
||||||
|
|
||||||
|
#### `--pasv_url`
|
||||||
|
The hostname to provide a client when attempting a passive connection (`PASV`).
|
||||||
|
If not provided, clients can only connect using an `Active` connection.
|
||||||
|
|
||||||
|
#### `--pasv_min`
|
||||||
|
The starting port to accept passive connections.
|
||||||
|
__Default:__ `1024`
|
||||||
|
|
||||||
|
#### `--pasv_max`
|
||||||
|
The ending port to accept passive connections.
|
||||||
|
The range is then queried for an available port to use when required.
|
||||||
|
__Default:__ `65535`
|
||||||
|
|
||||||
|
#### `--root` / `-r`
|
||||||
|
Set the default root directory for users.
|
||||||
|
|
||||||
|
Defaults to the current directory.
|
||||||
|
|
||||||
|
#### `--credentials` / `-c`
|
||||||
|
Set the path to a json credentials file.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "...",
|
||||||
|
"password": "...",
|
||||||
|
"root": "..." // Root directory
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `--username`
|
||||||
|
Set the username for the only user. Do not provide an argument to allow anonymous login.
|
||||||
|
|
||||||
|
#### `--password`
|
||||||
|
Set the password for the given `username`.
|
||||||
|
|
||||||
|
#### `--read-only`
|
||||||
|
Disable write actions such as upload, delete, etc.
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
The `FtpSvr` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
|
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
|
||||||
|
|
||||||
### `login`
|
### `login`
|
||||||
```js
|
```js
|
||||||
on('login', {connection, username, password}, resolve, reject) => { ... }
|
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
|
||||||
```
|
```
|
||||||
|
|
||||||
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
|
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
|
||||||
@@ -136,15 +254,45 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
|
|||||||
|
|
||||||
### `client-error`
|
### `client-error`
|
||||||
```js
|
```js
|
||||||
on('client-error', {connection, context, error}) => { ... }
|
ftpServer.on('client-error', ({connection, context, error}) => { ... });
|
||||||
```
|
```
|
||||||
|
|
||||||
Occurs when an error arises in the client connection.
|
Occurs when an error arises in the client connection.
|
||||||
|
|
||||||
`connection` [client class object](src/connection.js)
|
`connection` [client class object](src/connection.js)
|
||||||
`context` string of where the error occured
|
`context` string of where the error occurred
|
||||||
`error` error object
|
`error` error object
|
||||||
|
|
||||||
|
### `RETR`
|
||||||
|
```js
|
||||||
|
connection.on('RETR', (error, filePath) => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
Occurs when a file is downloaded.
|
||||||
|
|
||||||
|
`error` if successful, will be `null`
|
||||||
|
`filePath` location to which file was downloaded
|
||||||
|
|
||||||
|
### `STOR`
|
||||||
|
```js
|
||||||
|
connection.on('STOR', (error, fileName) => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
Occurs when a file is uploaded.
|
||||||
|
|
||||||
|
`error` if successful, will be `null`
|
||||||
|
`fileName` name of the file that was uploaded
|
||||||
|
|
||||||
|
### `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
|
## Supported Commands
|
||||||
|
|
||||||
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
||||||
@@ -172,66 +320,63 @@ class MyFileSystem extends FileSystem {
|
|||||||
Custom file systems can implement the following variables depending on the developers needs:
|
Custom file systems can implement the following variables depending on the developers needs:
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
#### [`currentDirectory()`](src/fs.js#L29)
|
#### [`currentDirectory()`](src/fs.js#L40)
|
||||||
Returns a string of the current working directory
|
Returns a string of the current working directory
|
||||||
__Used in:__ `PWD`
|
__Used in:__ `PWD`
|
||||||
|
|
||||||
#### [`get(fileName)`](src/fs.js#L33)
|
#### [`get(fileName)`](src/fs.js#L44)
|
||||||
Returns a file stat object of file or directory
|
Returns a file stat object of file or directory
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||||
|
|
||||||
#### [`list(path)`](src/fs.js#L39)
|
#### [`list(path)`](src/fs.js#L50)
|
||||||
Returns array of file and directory stat objects
|
Returns array of file and directory stat objects
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`
|
__Used in:__ `LIST`, `NLST`, `STAT`
|
||||||
|
|
||||||
#### [`chdir(path)`](src/fs.js#L56)
|
#### [`chdir(path)`](src/fs.js#L67)
|
||||||
Returns new directory relative to current directory
|
Returns new directory relative to current directory
|
||||||
__Used in:__ `CWD`, `CDUP`
|
__Used in:__ `CWD`, `CDUP`
|
||||||
|
|
||||||
#### [`mkdir(path)`](src/fs.js#L96)
|
#### [`mkdir(path)`](src/fs.js#L114)
|
||||||
Returns a path to a newly created directory
|
Returns a path to a newly created directory
|
||||||
__Used in:__ `MKD`
|
__Used in:__ `MKD`
|
||||||
|
|
||||||
#### [`write(fileName, {append, start})`](src/fs.js#L68)
|
#### [`write(fileName, {append, start})`](src/fs.js#L79)
|
||||||
Returns a writable stream
|
Returns a writable stream
|
||||||
Options:
|
Options:
|
||||||
`append` if true, append to existing file
|
`append` if true, append to existing file
|
||||||
`start` if set, specifies the byte offset to write to
|
`start` if set, specifies the byte offset to write to
|
||||||
__Used in:__ `STOR`, `APPE`
|
__Used in:__ `STOR`, `APPE`
|
||||||
|
|
||||||
#### [`read(fileName, {start})`](src/fs.js#L75)
|
#### [`read(fileName, {start})`](src/fs.js#L90)
|
||||||
Returns a readable stream
|
Returns a readable stream
|
||||||
Options:
|
Options:
|
||||||
`start` if set, specifies the byte offset to read from
|
`start` if set, specifies the byte offset to read from
|
||||||
__Used in:__ `RETR`
|
__Used in:__ `RETR`
|
||||||
|
|
||||||
#### [`delete(path)`](src/fs.js#L87)
|
#### [`delete(path)`](src/fs.js#L105)
|
||||||
Delete a file or directory
|
Delete a file or directory
|
||||||
__Used in:__ `DELE`
|
__Used in:__ `DELE`
|
||||||
|
|
||||||
#### [`rename(from, to)`](src/fs.js#L102)
|
#### [`rename(from, to)`](src/fs.js#L120)
|
||||||
Renames a file or directory
|
Renames a file or directory
|
||||||
__Used in:__ `RNFR`, `RNTO`
|
__Used in:__ `RNFR`, `RNTO`
|
||||||
|
|
||||||
#### [`chmod(path)`](src/fs.js#L108)
|
#### [`chmod(path)`](src/fs.js#L126)
|
||||||
Modifies a file or directory's permissions
|
Modifies a file or directory's permissions
|
||||||
__Used in:__ `SITE CHMOD`
|
__Used in:__ `SITE CHMOD`
|
||||||
|
|
||||||
#### [`getUniqueName()`](src/fs.js#L113)
|
#### [`getUniqueName(fileName)`](src/fs.js#L131)
|
||||||
Returns a unique file name to write to
|
Returns a unique file name to write to. Client requested filename available if you want to base your function on it.
|
||||||
__Used in:__ `STOU`
|
__Used in:__ `STOU`
|
||||||
|
|
||||||
<!--[RM_CONTRIBUTING]-->
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[RM_LICENSE]-->
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
<!--[]-->
|
## References
|
||||||
|
|
||||||
|
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
||||||
|
|||||||
16
SECURITY.md
Normal file
16
SECURITY.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 4.x | :white_check_mark: |
|
||||||
|
| 3.x | :white_check_mark: |
|
||||||
|
| < 3.0 | :x: |
|
||||||
|
|
||||||
|
__Critical vulnerabilities will be ported as far back as possible.__
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Report a security vulnerability directly to the maintainers by sending an email to security@autovance.com
|
||||||
|
or by reporting a vulnerability to the [NPM and Github security teams](https://docs.npmjs.com/reporting-a-vulnerability-in-an-npm-package).
|
||||||
144
bin/index.js
Executable file
144
bin/index.js
Executable file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const yargs = require('yargs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const FtpSrv = require('../src');
|
||||||
|
const errors = require('../src/errors');
|
||||||
|
|
||||||
|
const args = setupYargs();
|
||||||
|
const state = setupState(args);
|
||||||
|
startFtpServer(state);
|
||||||
|
|
||||||
|
function setupYargs() {
|
||||||
|
return yargs
|
||||||
|
.option('credentials', {
|
||||||
|
alias: 'c',
|
||||||
|
describe: 'Load user & pass from json file',
|
||||||
|
normalize: true
|
||||||
|
})
|
||||||
|
.option('username', {
|
||||||
|
describe: 'Blank for anonymous',
|
||||||
|
type: 'string',
|
||||||
|
default: ''
|
||||||
|
})
|
||||||
|
.option('password', {
|
||||||
|
describe: 'Password for given username',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
.option('root', {
|
||||||
|
alias: 'r',
|
||||||
|
describe: 'Default root directory for users',
|
||||||
|
type: 'string',
|
||||||
|
normalize: true
|
||||||
|
})
|
||||||
|
.option('read-only', {
|
||||||
|
describe: 'Disable write actions such as upload, delete, etc',
|
||||||
|
boolean: true,
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
.option('pasv-url', {
|
||||||
|
describe: 'URL to provide for passive connections',
|
||||||
|
type: 'string',
|
||||||
|
alias: 'pasv_url'
|
||||||
|
})
|
||||||
|
.option('pasv-min', {
|
||||||
|
describe: 'Starting point to use when creating passive connections',
|
||||||
|
type: 'number',
|
||||||
|
default: 1024,
|
||||||
|
alias: 'pasv_min'
|
||||||
|
})
|
||||||
|
.option('pasv-max', {
|
||||||
|
describe: 'Ending port to use when creating passive connections',
|
||||||
|
type: 'number',
|
||||||
|
default: 65535,
|
||||||
|
alias: 'pasv_max'
|
||||||
|
})
|
||||||
|
.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupState(_args) {
|
||||||
|
const _state = {};
|
||||||
|
|
||||||
|
function setupOptions() {
|
||||||
|
if (_args._ && _args._.length > 0) {
|
||||||
|
_state.url = _args._[0];
|
||||||
|
}
|
||||||
|
_state.pasv_url = _args.pasv_url;
|
||||||
|
_state.pasv_min = _args.pasv_min;
|
||||||
|
_state.pasv_max = _args.pasv_max;
|
||||||
|
_state.anonymous = _args.username === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRoot() {
|
||||||
|
const dirPath = _args.root;
|
||||||
|
if (dirPath) {
|
||||||
|
_state.root = dirPath;
|
||||||
|
} else {
|
||||||
|
_state.root = process.cwd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCredentials() {
|
||||||
|
_state.credentials = {};
|
||||||
|
|
||||||
|
const setCredentials = (username, password, root = null) => {
|
||||||
|
_state.credentials[username] = {
|
||||||
|
password,
|
||||||
|
root
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_args.credentials) {
|
||||||
|
const credentialsFile = path.resolve(_args.credentials);
|
||||||
|
const credentials = require(credentialsFile);
|
||||||
|
|
||||||
|
for (const cred of credentials) {
|
||||||
|
setCredentials(cred.username, cred.password, cred.root);
|
||||||
|
}
|
||||||
|
} else if (_args.username) {
|
||||||
|
setCredentials(_args.username, _args.password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCommandBlacklist() {
|
||||||
|
if (_args.readOnly) {
|
||||||
|
_state.blacklist = ['ALLO', 'APPE', 'DELE', 'MKD', 'RMD', 'RNRF', 'RNTO', 'STOR', 'STRU'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupOptions();
|
||||||
|
setupRoot();
|
||||||
|
setupCredentials();
|
||||||
|
setupCommandBlacklist();
|
||||||
|
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startFtpServer(_state) {
|
||||||
|
// Remove null/undefined options so they get set to defaults, below
|
||||||
|
for (const key in _state) {
|
||||||
|
if (_state[key] === undefined) delete _state[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLogin(data, resolve, reject) {
|
||||||
|
const user = _state.credentials[data.username];
|
||||||
|
if (_state.anonymous || user && user.password === data.password) {
|
||||||
|
return resolve({root: user && user.root || _state.root});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ftpServer = new FtpSrv({
|
||||||
|
url: _state.url,
|
||||||
|
pasv_url: _state.pasv_url,
|
||||||
|
pasv_min: _state.pasv_min,
|
||||||
|
pasv_max: _state.pasv_max,
|
||||||
|
anonymous: _state.anonymous,
|
||||||
|
blacklist: _state.blacklist
|
||||||
|
});
|
||||||
|
|
||||||
|
ftpServer.on('login', checkLogin);
|
||||||
|
ftpServer.listen();
|
||||||
|
}
|
||||||
44
changelog/v2_to_v3_migation.md
Normal file
44
changelog/v2_to_v3_migation.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Migration Guide - v2 to v3
|
||||||
|
|
||||||
|
The `FtpServer` constructor has been changed to only take one object option. Combining the two just made sense.
|
||||||
|
|
||||||
|
### From:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer('ftp://0.0.0.0:21');
|
||||||
|
```
|
||||||
|
|
||||||
|
### To:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer({
|
||||||
|
url: 'ftp://0.0.0.0:21'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
The `pasv_range` option has been changed to separate integer variables: `pasv_min`, `pasv_max`.
|
||||||
|
|
||||||
|
### From:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer(..., {
|
||||||
|
pasv_range: '1000-2000'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### To:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer({
|
||||||
|
pasv_min: 1000,
|
||||||
|
pasv_max: 2000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
The default passive port range has been changed to `1024` - `65535`
|
||||||
|
|
||||||
|
----
|
||||||
@@ -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,35 +0,0 @@
|
|||||||
// Use JS to support loading of threshold data from external file
|
|
||||||
var coverageConfig = {
|
|
||||||
instrumentation: {
|
|
||||||
root: 'src/',
|
|
||||||
excludes: ['errors.js']
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
test/**/*.spec.js
|
|
||||||
--reporter mocha-pretty-bunyan-nyan
|
|
||||||
--ui bdd
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"global": {
|
|
||||||
"statements": 90,
|
|
||||||
"branches": 80,
|
|
||||||
"functions": 90,
|
|
||||||
"lines": 90
|
|
||||||
},
|
|
||||||
"each": {
|
|
||||||
"statements": 70,
|
|
||||||
"branches": 40,
|
|
||||||
"functions": 60,
|
|
||||||
"lines": 70
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# START_CONFIT_GENERATED_CONTENT
|
|
||||||
confit:
|
|
||||||
extends: &confit-extends
|
|
||||||
- plugin:node/recommended
|
|
||||||
|
|
||||||
plugins: &confit-plugins
|
|
||||||
- node
|
|
||||||
|
|
||||||
env: &confit-env
|
|
||||||
commonjs: true # For Webpack, CommonJS
|
|
||||||
node: true
|
|
||||||
mocha: true
|
|
||||||
es6: true
|
|
||||||
|
|
||||||
globals: &confit-globals {}
|
|
||||||
parser: &confit-parser espree
|
|
||||||
|
|
||||||
parserOptions: &confit-parserOptions
|
|
||||||
ecmaVersion: 6
|
|
||||||
sourceType: module
|
|
||||||
ecmaFeatures:
|
|
||||||
globalReturn: false
|
|
||||||
impliedStrict: true
|
|
||||||
jsx: false
|
|
||||||
|
|
||||||
# END_CONFIT_GENERATED_CONTENT
|
|
||||||
|
|
||||||
# Customise this section to meet your needs...
|
|
||||||
|
|
||||||
extends: *confit-extends
|
|
||||||
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-extends" from the line above
|
|
||||||
# <<: *confit-extends
|
|
||||||
|
|
||||||
plugins: *confit-plugins
|
|
||||||
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-plugins" from the line above
|
|
||||||
# <<: *confit-extends
|
|
||||||
|
|
||||||
env:
|
|
||||||
<<: *confit-env
|
|
||||||
|
|
||||||
globals:
|
|
||||||
<<: *confit-globals
|
|
||||||
|
|
||||||
parser: *confit-parser
|
|
||||||
|
|
||||||
parserOptions:
|
|
||||||
<<: *confit-parserOptions
|
|
||||||
|
|
||||||
rules:
|
|
||||||
no-process-exit: 0
|
|
||||||
max-len:
|
|
||||||
- warn
|
|
||||||
- 200 # Line Length
|
|
||||||
node/no-unpublished-require:
|
|
||||||
- 2
|
|
||||||
- allowModules:
|
|
||||||
- chai
|
|
||||||
- dotenv
|
|
||||||
- ftp
|
|
||||||
- sinon
|
|
||||||
- sinon-as-promised
|
|
||||||
48
confit.yml
48
confit.yml
@@ -1,48 +0,0 @@
|
|||||||
generator-confit:
|
|
||||||
app:
|
|
||||||
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
|
|
||||||
buildProfile: Latest
|
|
||||||
copyrightOwner: Tyler Stewart
|
|
||||||
license: MIT
|
|
||||||
projectType: node
|
|
||||||
publicRepository: true
|
|
||||||
repositoryType: GitHub
|
|
||||||
paths:
|
|
||||||
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
|
|
||||||
config:
|
|
||||||
configDir: config/
|
|
||||||
input:
|
|
||||||
srcDir: src/
|
|
||||||
unitTestDir: test/
|
|
||||||
output:
|
|
||||||
prodDir: dist/
|
|
||||||
reportDir: reports/
|
|
||||||
buildJS:
|
|
||||||
_version: df428a706d926204228c5d9ebdbd7b49908926d9
|
|
||||||
framework: []
|
|
||||||
frameworkScripts: []
|
|
||||||
outputFormat: ES6
|
|
||||||
sourceFormat: ES6
|
|
||||||
entryPoint:
|
|
||||||
_version: de20402bf85c703080ef6daf21e35325a3b9d604
|
|
||||||
entryPoints:
|
|
||||||
main:
|
|
||||||
- src/index.js
|
|
||||||
testUnit:
|
|
||||||
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
|
|
||||||
testDependencies: []
|
|
||||||
verify:
|
|
||||||
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
|
|
||||||
jsCodingStandard: eslint
|
|
||||||
documentation:
|
|
||||||
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
|
|
||||||
generateDocs: false
|
|
||||||
release:
|
|
||||||
_version: 47f220593935b502abf17cb34a396f692e453c49
|
|
||||||
checkCodeCoverage: true
|
|
||||||
commitMessageFormat: Conventional
|
|
||||||
useSemantic: true
|
|
||||||
sampleApp:
|
|
||||||
_version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba
|
|
||||||
createSampleApp: false
|
|
||||||
zzfinish: {}
|
|
||||||
86
ftp-srv.d.ts
vendored
86
ftp-srv.d.ts
vendored
@@ -1,5 +1,14 @@
|
|||||||
declare class FileSystem {
|
import * as tls from 'tls'
|
||||||
constructor(connection: any, {root, cwd}?: {
|
import { Stats } from 'fs'
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export class FileSystem {
|
||||||
|
|
||||||
|
readonly connection: FtpConnection;
|
||||||
|
readonly root: string;
|
||||||
|
readonly cwd: string;
|
||||||
|
|
||||||
|
constructor(connection: FtpConnection, {root, cwd}?: {
|
||||||
root: any;
|
root: any;
|
||||||
cwd: any;
|
cwd: any;
|
||||||
});
|
});
|
||||||
@@ -29,11 +38,43 @@ declare class FileSystem {
|
|||||||
|
|
||||||
chmod(path: string, mode: string): Promise<any>;
|
chmod(path: string, mode: string): Promise<any>;
|
||||||
|
|
||||||
getUniqueName(): string;
|
getUniqueName(fileName: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class FtpServer {
|
export class FtpConnection extends EventEmitter {
|
||||||
constructor(url: string, options?: {});
|
server: FtpServer;
|
||||||
|
id: string;
|
||||||
|
log: any;
|
||||||
|
transferType: string;
|
||||||
|
encoding: string;
|
||||||
|
bufferSize: boolean;
|
||||||
|
readonly ip: string;
|
||||||
|
restByteCount: number | undefined;
|
||||||
|
secure: boolean
|
||||||
|
|
||||||
|
close (code: number, message: number): Promise<any>
|
||||||
|
login (username: string, password: string): Promise<any>
|
||||||
|
reply (options: number | Object, ...letters: Array<any>): Promise<any>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FtpServerOptions {
|
||||||
|
url?: string,
|
||||||
|
pasv_min?: number,
|
||||||
|
pasv_max?: number,
|
||||||
|
pasv_url?: string,
|
||||||
|
greeting?: string | string[],
|
||||||
|
tls?: tls.SecureContextOptions | false,
|
||||||
|
anonymous?: boolean,
|
||||||
|
blacklist?: Array<string>,
|
||||||
|
whitelist?: Array<string>,
|
||||||
|
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
||||||
|
log?: any,
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FtpServer extends EventEmitter {
|
||||||
|
constructor(options?: FtpServerOptions);
|
||||||
|
|
||||||
readonly isTLS: boolean;
|
readonly isTLS: boolean;
|
||||||
|
|
||||||
@@ -41,7 +82,7 @@ declare class FtpServer {
|
|||||||
|
|
||||||
emitPromise(action: any, ...data: any[]): Promise<any>;
|
emitPromise(action: any, ...data: any[]): Promise<any>;
|
||||||
|
|
||||||
emit(action: any, ...data: any[]): void;
|
// emit is exported from super class
|
||||||
|
|
||||||
setupTLS(_tls: boolean): boolean | {
|
setupTLS(_tls: boolean): boolean | {
|
||||||
cert: string;
|
cert: string;
|
||||||
@@ -56,7 +97,38 @@ declare class FtpServer {
|
|||||||
disconnectClient(id: string): Promise<any>;
|
disconnectClient(id: string): Promise<any>;
|
||||||
|
|
||||||
close(): any;
|
close(): any;
|
||||||
|
|
||||||
|
on(event: "login", listener: (
|
||||||
|
data: {
|
||||||
|
connection: FtpConnection,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
},
|
||||||
|
resolve: (config: {
|
||||||
|
fs?: FileSystem,
|
||||||
|
root?: string,
|
||||||
|
cwd?: string,
|
||||||
|
blacklist?: Array<string>,
|
||||||
|
whitelist?: Array<string>
|
||||||
|
}) => void,
|
||||||
|
reject: (err?: Error) => void
|
||||||
|
) => void): this;
|
||||||
|
|
||||||
|
on(event: "disconnect", listener: (
|
||||||
|
data: {
|
||||||
|
connection: FtpConnection,
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
) => void): this;
|
||||||
|
|
||||||
|
on(event: "client-error", listener: (
|
||||||
|
data: {
|
||||||
|
connection: FtpConnection,
|
||||||
|
context: string,
|
||||||
|
error: Error,
|
||||||
|
}
|
||||||
|
) => void): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const FtpSrv: FtpServer;
|
export {FtpServer as FtpSrv};
|
||||||
export default FtpServer;
|
export default FtpServer;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const FtpSrv = require('./src');
|
const FtpSrv = require('./src');
|
||||||
const FileSystem = require('./src/fs');
|
const FileSystem = require('./src/fs');
|
||||||
|
const errors = require('./src/errors');
|
||||||
|
|
||||||
module.exports = FtpSrv;
|
module.exports = FtpSrv;
|
||||||
module.exports.FtpSrv = FtpSrv;
|
module.exports.FtpSrv = FtpSrv;
|
||||||
module.exports.FileSystem = FileSystem;
|
module.exports.FileSystem = FileSystem;
|
||||||
|
module.exports.ftpErrors = errors;
|
||||||
|
|||||||
BIN
logo.png
BIN
logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 22 KiB |
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Send Button by Bruno Bosse from the Noun Project
|
|
||||||
https://thenounproject.com/brunobosse/collection/basics/?i=1054386
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const htmlConvert = require('html-convert');
|
|
||||||
|
|
||||||
const convert = htmlConvert();
|
|
||||||
let ws = fs.createWriteStream('logo.png');
|
|
||||||
let rs = convert('logo/logo.html', {
|
|
||||||
width: 350,
|
|
||||||
height: 76
|
|
||||||
});
|
|
||||||
|
|
||||||
rs.pipe(ws);
|
|
||||||
ws.on('finish', () => process.exit());
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<svg width="566" height="580" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<g>
|
|
||||||
<title>Layer 1</title>
|
|
||||||
<g display="none" id="svg_1">
|
|
||||||
<g display="inline" id="svg_2">
|
|
||||||
<circle cx="337.851" cy="245.093" r="68.103" id="svg_3"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm135.007,301.787c-35.898,35.898 -89.692,42.92 -132.482,20.934l-76.709,76.709c-13.651,13.651 -35.783,13.651 -49.434,0c-13.651,-13.651 -13.651,-35.784 0,-49.435l76.777,-76.777c-21.923,-42.853 -15.06,-96.699 20.761,-132.52c44.483,-44.483 116.448,-44.64 160.931,-0.157c44.482,44.485 44.639,116.763 0.156,161.246z" id="svg_4"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_5">
|
|
||||||
<g display="inline" id="svg_6">
|
|
||||||
<path d="m282,211.676c-7.801,0 -13,6.324 -13,14.125l0,66.199l-40.682,0c-7.975,0 -14.318,5.978 -14.318,13.953l0,0.597c0,7.975 6.343,14.449 14.318,14.449l54.641,0c0.053,0 -0.495,-0.012 -0.442,-0.013c0.053,0.001 -0.495,0.004 -0.442,0.004c7.801,0 12.925,-6.324 12.925,-14.125l0,-0.315l0,-0.597l0,-80.152c0,-7.801 -5.199,-14.125 -13,-14.125z" id="svg_7"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-139.125,208.04c-0.89,-1.277 -1.569,-2.707 -2.003,-4.244c-3.279,-7.649 -5.111,-16.066 -5.111,-24.915c0,-34.997 28.371,-63.368 63.368,-63.368c14.59,0 28.023,4.937 38.735,13.224c1.019,0.639 1.951,1.4 2.775,2.268c2.431,2.56 3.932,6.012 3.932,9.82c0,5.696 -3.344,10.6 -8.169,12.891c-0.831,0.395 -1.704,0.709 -2.615,0.938c-27.13,10.06 -50.303,28.252 -66.567,51.62c-0.558,1.116 -1.257,2.145 -2.077,3.069c-2.601,2.933 -6.387,4.792 -10.616,4.792c-4.828,0.001 -9.086,-2.414 -11.652,-6.095zm257.1,194.907c-13.91,13.91 -36.463,13.91 -50.373,0l-5.58,-5.58c-18.184,10.084 -39.105,15.833 -61.371,15.833c-22.105,0 -42.885,-5.664 -60.977,-15.613l-5.36,5.36c-13.91,13.91 -36.463,13.91 -50.373,0c-13.91,-13.91 -13.91,-36.464 0,-50.374l5.424,-5.424c-9.849,-18.023 -15.449,-38.698 -15.449,-60.683c0,-69.995 56.741,-126.735 126.735,-126.735s126.735,56.741 126.735,126.735c0,21.823 -5.518,42.358 -15.232,60.286l5.821,5.821c13.91,13.91 13.91,36.464 0,50.374zm24.752,-197.077c-0.402,1.103 -0.924,2.149 -1.573,3.104c-2.536,3.727 -6.81,6.175 -11.658,6.175c-4.483,0 -8.469,-2.099 -11.05,-5.363c-0.508,-0.642 -0.968,-1.323 -1.36,-2.049c-16.153,-23.843 -39.432,-42.457 -66.784,-52.788c-1.048,-0.252 -2.048,-0.618 -2.991,-1.088c-4.671,-2.325 -7.888,-7.133 -7.888,-12.705c0,-3.488 1.263,-6.677 3.349,-9.148c1.025,-1.215 2.254,-2.245 3.628,-3.061c10.753,-8.412 24.288,-13.434 39,-13.434c34.997,0 63.368,28.371 63.368,63.368c-0.001,9.657 -2.176,18.8 -6.041,26.989z" id="svg_8"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_9">
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm139.455,375.302c0,7.493 -6.315,13.691 -13.808,13.691l-91.192,0l0,-35.34l0,-20.446l0,-2.871c0.038,-7.37 -6.111,-12.343 -15,-12.343s-39,0 -39,0c-3,-0.005 -2.241,0.161 -2.373,0.161c-7.435,0 -14.625,7.165 -14.625,14.6c0,0.127 -0.002,1.239 -0.002,1.239l0,19.66l0,35.34l-89.837,0l-0.494,0c-7.381,0 -13.669,-6.272 -13.669,-13.685l0,-121.53c0,-2.827 1.062,-5.443 2.568,-7.596c0.888,-1.27 2.164,-2.375 3.431,-3.265l124.012,-124.268l2.154,-2.157c2.255,-1.791 5.106,-2.867 8.209,-2.867c3.004,0 5.772,1.014 7.991,2.702l2.557,2.548l124.987,125.215c-0.054,0.045 -0.047,0.091 -0.101,0.136c2.505,2.415 4.191,5.799 4.191,9.554l0,121.522l0.001,0z" id="svg_10"/>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_11">
|
|
||||||
<g display="inline" id="svg_12">
|
|
||||||
<path d="m345.15,271.851c1.183,-1.297 2.547,-2.706 3.842,-4.247c1.421,-1.692 2.659,-3.545 3.417,-5.595c0.433,-1.119 0.713,-2.249 0.713,-3.433c0,-0.07 -0.121,-14.058 -0.121,-14.058l0,-40.854c0,-18.056 -14.646,-31.979 -31.875,-31.979c-2.232,0 -4.372,0.235 -6.66,0.724c-0.07,0.013 -0.098,0.032 -0.168,0.045c-9.206,1.965 -18.404,3.611 -27.845,3.643c-10.388,0.038 -20.532,-1.519 -30.667,-3.688c-2.282,-0.489 -4.222,-0.724 -6.453,-0.724c-17.229,0 -31.331,13.923 -31.331,31.979l0,4.447l0,29.426l0,8.558c0,3.935 -0.94,7.942 -0.673,11.865c0.097,1.434 -0.228,2.931 0.286,4.05c0.791,2.056 2.002,3.895 3.397,5.595c2.021,2.463 4.429,4.628 6.529,6.701c3.923,3.878 8.138,7.475 12.48,10.87c8.742,6.828 18.004,12.937 26.886,19.575c4.857,3.63 11.295,5.67 18.034,5.828c6.739,-0.159 13.176,-2.203 18.033,-5.833c9.734,-7.273 20.006,-13.637 29.409,-21.113c4.536,-3.607 8.857,-7.498 12.767,-11.782zm-85.15,-37.557c0,7.909 -7.091,14.317 -15,14.317c-7.909,0 -15,-6.408 -15,-14.317l0,-14.782c0,-7.909 7.091,-14.317 15,-14.317c7.909,0 15,6.408 15,14.317l0,14.782zm59.5,14.318c-7.909,0 -14.5,-6.408 -14.5,-14.317l0,-14.782c0,-7.909 6.591,-14.317 14.5,-14.317c7.909,0 14.5,6.408 14.5,14.317l0,14.782c0,7.908 -6.591,14.317 -14.5,14.317z" id="svg_13"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm108.455,408.03c0,5.283 -4.98,7.963 -10.263,7.963l-103.555,0l-8.55,0l-82.179,0c-5.283,0 -10.453,-2.68 -10.453,-7.963l0,-37.968c0,-11.276 4.59,-23.322 12.77,-35.808c6.836,-10.433 16.918,-21.024 28.32,-30.633c7.486,-6.309 15.463,-11.977 23.502,-16.746c-0.944,-0.673 -1.889,-1.334 -2.831,-2.01c0.055,-0.032 0.106,-0.043 0.16,-0.074c-11.73,-8.489 -19.987,-15.692 -26.141,-21.834c-3.346,-3.388 -5.872,-6.221 -7.879,-9.237c-0.481,-0.51 -0.971,-1.029 -1.433,-1.559c-0.279,-0.321 -0.594,-0.643 -0.863,-0.971c-1.794,-2.187 -3.431,-4.554 -4.449,-7.199c-0.661,-1.441 -1.199,-3.366 -1.325,-5.212c-0.124,-1.817 -0.442,-3.648 -0.429,-5.483c0.024,-3.264 -0.402,-6.543 -0.402,-9.785l0,-18.16l0.549,0.01c0.01,-0.561 0.539,-1.144 0.457,-1.737c-0.82,-5.964 -5.862,-13.494 -7.177,-21.512c-0.104,-0.638 -0.188,-1.279 -0.242,-1.923l0.026,-0.01c-0.401,-4.131 -0.385,-8.308 0.145,-12.457c6.765,-52.927 46.886,-86.808 95.363,-86.808c47.53,0 87.052,32.316 94.942,83.737c0.787,5.139 0.773,10.342 0.025,15.428l0.347,0.109c-0.122,1.478 -0.237,2.94 -0.589,4.384c-1.724,7.058 -5.343,13.637 -6.311,18.973c-0.061,0.336 0.465,0.671 0.465,0.998l0,16.943c0,0 -0.605,6.586 -0.593,11.809c0.008,3.41 -0.606,6.241 -0.606,6.277c0,1.524 -0.42,2.977 -0.977,4.418c-0.975,2.637 -2.594,5.022 -4.423,7.199c-0.278,0.331 -0.559,0.65 -0.841,0.971c-0.657,0.749 -1.313,1.476 -1.963,2.176c-0.905,1.274 -1.887,2.489 -2.972,3.831c-5.301,6.006 -12.847,13.149 -24.136,21.791c-3.328,2.397 -6.672,4.742 -10.056,7.082c8.343,4.984 16.347,10.724 23.081,16.449c0.799,0.68 1.687,1.383 2.49,2.097c19.13,16.999 38.996,43.368 38.996,68.923l0,33.521z" id="svg_14"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_15">
|
|
||||||
<g display="inline" id="svg_16">
|
|
||||||
<path d="m312.499,160.826c-9.98,2.131 -19.499,5.134 -28.499,8.871l0,0.309c0,-0.051 -0.324,-0.099 -0.449,-0.15c-13.49,5.7 -25.679,13.103 -35.916,21.847c6.85,0.617 12.365,6.309 12.365,13.318l0,13.871c0,7.422 -6.578,13.438 -14,13.438s-14,-6.017 -14,-13.438l0,-12.16c-5,7.508 -10,15.638 -14,24.243l0,10.724c0,11.767 40.019,38.51 46.415,42.736c1.735,0.958 3.005,1.504 3.005,1.504c4.505,2.922 10.252,4.531 16.122,4.582c0.101,-0.005 0.459,-0.014 0.459,-0.014l0,0.023c6,-0.006 13.014,-1.989 17.751,-5.526c0,0 47.249,-30.509 47.249,-43.305l0,-51.547c-0.001,-19.131 -17.791,-33.327 -36.502,-29.326zm15.501,58.072c0,7.422 -6.078,13.438 -13.5,13.438c-7.422,0 -13.5,-6.017 -13.5,-13.438l0,-13.871c0,-7.422 6.078,-13.438 13.5,-13.438c7.416,0 13.5,6.017 13.5,13.438l0,13.871z" id="svg_17"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm105.455,311.424c-9,-3.568 -18.511,-5.577 -28.63,-5.577c-3.612,0 -7.204,0.26 -10.68,0.748c0.319,0.28 0.666,0.55 0.985,0.834c18.856,16.755 38.325,42.746 38.325,67.934l0,33.04c0,5.207 -4.685,8.59 -9.892,8.59l-102.07,0l-8.427,0l-81.001,0c-5.207,0 -9.609,-3.382 -9.609,-8.59l0,-37.423c0,-11.115 4.2,-22.991 12.263,-35.297c6.493,-9.909 15.656,-19.963 26.374,-29.156c-3.329,-0.447 -6.682,-0.68 -10.135,-0.68c-10.119,0 -19.502,2.01 -28.502,5.577l0,-124.877c0,-58.13 47.049,-105.257 105.179,-105.257c0.091,0 0.147,0.001 0.266,0.003c0.074,0 0.175,-0.003 0.249,-0.003c58.13,0 105.306,47.127 105.306,105.257l0,124.877l-0.001,0z" id="svg_18"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_19">
|
|
||||||
<g display="inline" id="svg_20">
|
|
||||||
<path d="m349.599,245.278l-79.122,40.948c-0.082,0.043 -0.156,0.096 -0.237,0.141c-5.525,2.007 -10.592,5.215 -14.873,9.495c-15.891,15.892 -15.891,41.749 0,57.642c7.698,7.698 17.933,11.938 28.82,11.938s21.122,-4.24 28.82,-11.938c4.63,-4.63 7.901,-10.109 9.832,-15.938l40.798,-78.203c2.104,-4.033 1.351,-8.964 -1.86,-12.185c-3.209,-3.22 -8.138,-3.988 -12.178,-1.9z" id="svg_21"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm157.069,312.993l15.415,0c-1.535,20 -6.792,40.567 -15.092,58.692c-0.006,0.018 -0.01,-0.01 -0.017,0.008c-0.088,0.193 -0.829,1.228 -0.961,1.506c-2.081,3.524 -5.906,4.794 -10.295,4.794l-24.507,0l-267.066,0c-4.166,0 -7.495,-1.264 -9.675,-5.366c-0.216,-0.455 -0.974,-1.57 -0.981,-1.59c-8.851,-19.329 -14.24,-40.044 -15.346,-64.044l15.672,0c5.76,0 10.428,-3.741 10.428,-9.5c0,-5.759 -4.669,-9.5 -10.428,-9.5l-15.463,0c2.802,-38 19.118,-75.51 44.351,-103.289l11.527,11.723c2.036,2.036 4.741,3.054 7.41,3.054c2.669,0 5.41,-1.018 7.446,-3.054c4.073,-4.073 4.217,-10.676 0.144,-14.749l-11.042,-11.331c28.262,-24.456 64.866,-39.891 104.866,-41.796l0,15.959c0,5.759 3.74,10.428 9.5,10.428s9.5,-4.669 9.5,-10.428l0,-15.707c39,2.862 76.041,19.222 103.786,44.5l-10.537,10.895c-4.073,4.073 -4.073,10.688 0,14.761c2.036,2.036 4.705,3.079 7.374,3.079c2.669,0 5.338,-0.969 7.374,-3.004l10.688,-10.588c24.41,28.294 39.792,62.547 41.637,104.547l-15.708,0c-5.76,0 -10.428,4.241 -10.428,10c0,5.759 4.668,10 10.428,10z" id="svg_22"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_23">
|
|
||||||
<g display="inline" id="svg_24">
|
|
||||||
<path d="m343.258,200.171c-9.089,0.001 -18.289,2.508 -26.521,7.757c-23.007,14.673 -29.763,45.218 -15.09,68.225c9.423,14.775 25.389,22.847 41.704,22.847c9.089,0 18.29,-2.508 26.521,-7.757c23.007,-14.673 29.762,-45.218 15.09,-68.224c-9.423,-14.777 -25.389,-22.849 -41.704,-22.848zm34.669,57.087c-2.05,9.266 -7.655,17.181 -15.657,22.284c-5.724,3.651 -12.455,5.58 -19.199,5.58c-5.983,0 -11.071,-1.48 -17.071,-4.187l0,-40.485c0,-6.296 5.664,-12.45 11.959,-12.45l34.328,0c0.332,2 0.629,1.465 0.94,1.954c5.103,8.001 6.75,18.037 4.7,27.304z" id="svg_25"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-78.545,383.993l-82,0l0,-77.35c0,-6.296 5.466,-10.65 11.762,-10.65l59.472,0c6.295,0 10.766,4.355 10.766,10.65l0,77.35zm104,0l-84,0l0,-228.013c0,-6.296 4.569,-10.987 10.865,-10.987l54.982,0c-21.431,13 -35.968,35.007 -41.526,59.974c-5.613,25.215 -0.753,50.864 13.108,72.597c11.19,17.547 25.571,30.66 46.571,38.006l0,68.423zm17,0l0,-63.149c6,0.938 11.241,1.433 16.863,1.433c4.727,0 9.725,-0.35 14.392,-1.044l37.24,58.161c1.098,1.722 2.327,3.599 3.647,4.599l-72.142,0zm115.213,-4.086c-4.226,2.695 -8.946,3.982 -13.613,3.982c-8.373,0 -16.569,-4.144 -21.405,-11.727l-42.358,-66.416c-6.831,1.782 -13.783,2.653 -20.694,2.653c-27.275,0 -53.884,-13.574 -69.652,-38.297c-24.541,-38.48 -13.102,-89.658 25.378,-114.199c13.747,-8.767 29.081,-12.948 44.238,-12.948c27.271,0 53.959,13.542 69.733,38.275c19.805,31.054 16.076,70.435 -6.202,97.302l42.32,66.357c7.531,11.809 4.063,27.488 -7.745,35.018z" id="svg_26"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_27">
|
|
||||||
<g display="inline" id="svg_28">
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm179.255,300.845c-1.873,8.407 -10.159,13.275 -18.415,10.817l-22.428,-6.677c-2.855,9.611 -6.676,18.805 -11.367,27.462l20.632,11.166c7.575,4.1 9.987,13.398 5.36,20.662c0,0 -10.312,16.191 -23.125,29.004c-12.869,12.869 -29.193,23.244 -29.193,23.244c-7.269,4.621 -16.571,2.203 -20.67,-5.372l-11.112,-20.532c-8.656,4.702 -17.851,8.533 -27.463,11.398l6.674,22.419c2.458,8.255 -2.411,16.535 -10.82,18.401c0,0 -18.741,4.157 -36.861,4.157c-18.199,0 -37.079,-4.206 -37.079,-4.206c-8.407,-1.873 -13.275,-10.16 -10.817,-18.415l6.637,-22.292c-9.623,-2.852 -18.828,-6.674 -27.495,-11.366l-11.091,20.495c-4.1,7.575 -13.398,9.987 -20.662,5.36c0,0 -16.191,-10.312 -29.004,-23.126c-12.869,-12.869 -23.245,-29.193 -23.245,-29.193c-4.62,-7.269 -2.203,-16.57 5.372,-20.67l20.393,-11.036c-4.704,-8.666 -8.536,-17.873 -11.4,-27.496l-22.278,6.632c-8.255,2.458 -16.535,-2.412 -18.4,-10.82c0,0 -4.157,-18.74 -4.157,-36.861c0,-18.199 4.206,-37.079 4.206,-37.079c1.873,-8.407 10.159,-13.275 18.415,-10.817l22.212,6.613c2.863,-9.624 6.695,-18.831 11.399,-27.497l-20.448,-11.066c-7.575,-4.1 -9.987,-13.398 -5.36,-20.662c0,0 10.312,-16.191 23.125,-29.004c12.869,-12.869 29.193,-23.245 29.193,-23.245c7.269,-4.62 16.571,-2.203 20.67,5.372l11.057,20.431c8.667,-4.693 17.874,-8.515 27.497,-11.367l-6.653,-22.348c-2.458,-8.255 2.412,-16.535 10.82,-18.401c0,0 18.741,-4.157 36.861,-4.157c18.199,0 37.079,4.206 37.079,4.206c8.407,1.873 13.275,10.16 10.817,18.415l-6.653,22.348c9.612,2.866 18.808,6.697 27.464,11.4l11.14,-20.585c4.1,-7.575 13.398,-9.987 20.662,-5.36c0,0 16.191,10.312 29.004,23.126c12.869,12.869 23.244,29.193 23.244,29.193c4.62,7.269 2.203,16.57 -5.372,20.67l-20.571,11.133c4.69,8.657 8.512,17.852 11.366,27.463l22.49,-6.695c8.255,-2.458 16.535,2.412 18.4,10.82c0,0 4.157,18.74 4.157,36.861c-0.001,18.197 -4.207,37.077 -4.207,37.077z" id="svg_29"/>
|
|
||||||
<circle cx="283.779" cy="287.886" r="37.594" id="svg_30"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_31">
|
|
||||||
<g display="inline" id="svg_32">
|
|
||||||
<rect x="326.113" y="201.398" transform="matrix(-0.7071,-0.7071,0.7071,-0.7071,472.8639,611.4978) " width="73.929" height="12.835" id="svg_33"/>
|
|
||||||
<polygon points="170.708,383.917 187.339,400.547 225.134,390.423 180.832,346.122 " id="svg_34"/>
|
|
||||||
<path d="m413.02,192.538c-0.016,-0.268 -0.051,-0.534 -0.072,-0.801c-0.049,-0.602 -0.095,-1.205 -0.173,-1.804c-0.037,-0.284 -0.094,-0.565 -0.138,-0.848c-0.089,-0.58 -0.174,-1.161 -0.291,-1.737c-0.062,-0.305 -0.146,-0.606 -0.215,-0.909c-0.126,-0.55 -0.246,-1.102 -0.397,-1.647c-0.09,-0.325 -0.205,-0.645 -0.304,-0.968c-0.159,-0.518 -0.31,-1.037 -0.492,-1.548c-0.123,-0.346 -0.272,-0.685 -0.406,-1.028c-0.188,-0.48 -0.365,-0.964 -0.574,-1.438c-0.162,-0.367 -0.35,-0.724 -0.525,-1.087c-0.211,-0.44 -0.41,-0.885 -0.64,-1.318c-0.206,-0.388 -0.441,-0.764 -0.661,-1.146c-0.229,-0.396 -0.444,-0.798 -0.688,-1.187c-0.259,-0.413 -0.549,-0.81 -0.826,-1.214c-0.237,-0.345 -0.458,-0.698 -0.708,-1.037c-0.344,-0.466 -0.721,-0.915 -1.089,-1.368c-0.213,-0.262 -0.409,-0.532 -0.63,-0.789c-0.604,-0.703 -1.239,-1.388 -1.905,-2.054l-0.004,0.004c-0.028,-0.03 -0.053,-0.062 -0.083,-0.091c-1.556,-1.556 -4.079,-1.556 -5.634,0c-1.556,1.556 -1.556,4.078 0,5.634c0.042,0.042 0.086,0.078 0.129,0.117c0.172,0.173 0.327,0.356 0.494,0.532c7.992,8.41 9.548,19.621 6.063,30.056c-0.915,1.556 -0.709,3.588 0.626,4.924c1.585,1.585 4.154,1.585 5.739,0c0.537,-0.537 0.888,-1.188 1.06,-1.874c0.08,-0.212 0.141,-0.429 0.218,-0.642c0.211,-0.588 0.421,-1.175 0.601,-1.771c0.072,-0.238 0.128,-0.48 0.195,-0.719c0.17,-0.607 0.339,-1.215 0.478,-1.829c0.054,-0.239 0.091,-0.48 0.141,-0.719c0.127,-0.619 0.253,-1.238 0.348,-1.862c0.036,-0.239 0.056,-0.479 0.088,-0.719c0.083,-0.627 0.166,-1.254 0.217,-1.884c0.02,-0.243 0.022,-0.487 0.037,-0.731c0.039,-0.627 0.077,-1.254 0.084,-1.882c0.003,-0.255 -0.012,-0.509 -0.015,-0.764c-0.007,-0.618 -0.012,-1.236 -0.048,-1.852z" id="svg_35"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm129.63,207.768l-3.086,3.086l-19.966,19.966l-143.453,143.453c-1.009,1.736 -2.674,3.036 -4.666,3.569l-54.971,14.725c-0.002,0 -0.003,0.001 -0.004,0.001l-31.304,8.386c-2.657,0.712 -5.493,-0.048 -7.438,-1.993c-1.945,-1.945 -2.705,-4.781 -1.993,-7.438l8.386,-31.304l14.292,-53.352c0.046,-1.907 0.791,-3.801 2.247,-5.257l144.847,-144.847l19.966,-19.966l3.086,-3.086c20.418,-20.418 53.64,-20.418 74.057,0c20.417,20.418 20.417,53.639 0,74.057z" id="svg_36"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_37">
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm-62.545,379.174c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm0,-100c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm0,-102c0,11.498 -9.321,20.819 -20.819,20.819l-29.363,0c-11.497,0 -20.818,-9.321 -20.818,-20.819l0,-29.363c0,-11.498 9.321,-20.819 20.819,-20.819l29.363,0c11.498,0 20.819,9.321 20.819,20.819l0,29.363l-0.001,0zm212,186.032c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0zm0,-98c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0zm0,-101c0,11.48 -9.307,20.787 -20.787,20.787l-138.426,0c-11.48,0 -20.787,-9.307 -20.787,-20.787l0,-0.425c0,-11.481 9.307,-20.787 20.787,-20.787l138.425,0c11.481,0 20.787,9.307 20.787,20.787l0,0.425l0.001,0z" id="svg_38"/>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_39">
|
|
||||||
<g display="inline" id="svg_40">
|
|
||||||
<rect x="187" y="256" width="192" height="125" id="svg_41"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm39.455,133.723l0,-19.607c0,-11.106 9.394,-20.109 20.5,-20.109s20.5,9.003 20.5,20.109l0,19.607l0,20.612c0,11.106 -9.394,20.109 -20.5,20.109s-20.5,-9.003 -20.5,-20.109l0,-20.612zm-119,0l0,-19.607c0,-11.106 8.394,-20.109 19.5,-20.109s19.5,9.003 19.5,20.109l0,19.607l0,20.612c0,11.106 -8.394,20.109 -19.5,20.109s-19.5,-9.003 -19.5,-20.109l0,-20.612zm217,242.478c0,11.019 -9.318,19.792 -20.337,19.792l-233.587,0c-11.018,0 -21.076,-8.773 -21.076,-19.792l0,-222.527c0,-11.019 10.058,-18.681 21.076,-18.681l19.924,0l0,19.342c0,19.959 17.041,36.197 37,36.197s37,-16.238 37,-36.197l0,-19.342l47,0l0,19.342c0,19.959 15.541,36.197 35.5,36.197s35.5,-16.238 35.5,-36.197l0,-19.342l21.663,0c11.019,0 20.337,7.662 20.337,18.681l0,222.527z" id="svg_42"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g display="none" id="svg_43">
|
|
||||||
<g display="inline" id="svg_44">
|
|
||||||
<path d="m348,411.767c7.039,0 12,-5.706 12,-12.745l0,-175.1c0,-7.039 -4.961,-12.745 -12,-12.745s-12,5.706 -12,12.745l0,175.1c0,7.038 4.961,12.745 12,12.745z" id="svg_45"/>
|
|
||||||
<path d="m222,411.767c7.039,0 14,-5.706 14,-12.745l0,-175.1c0,-7.039 -6.961,-12.745 -14,-12.745s-14,5.706 -14,12.745l0,175.1c0,7.038 6.961,12.745 14,12.745z" id="svg_46"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm109.455,185.128l0,212.78c0,12.095 -10.073,23.085 -22.167,23.085l-173.233,0c-12.094,0 -21.6,-10.99 -21.6,-23.085l0,-212.78l0,-32.135l217,0l0,32.135zm0,-47.135l-217,0l0,-10.841c0,-12.095 9.505,-21.159 21.6,-21.159l55.634,0c0.497,-12 10.163,-21 21.942,-21l18.081,0c11.78,0 21.445,9 21.942,21l55.634,0c12.094,0 22.167,9.064 22.167,21.159l0,10.841z" id="svg_47"/>
|
|
||||||
<path d="m284,411.767c7.039,0 13,-5.706 13,-12.745l0,-175.1c0,-7.039 -5.961,-12.745 -13,-12.745s-13,5.706 -13,12.745l0,175.1c0,7.038 5.961,12.745 13,12.745z" id="svg_48"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="svg_49">
|
|
||||||
<g id="svg_50">
|
|
||||||
<polygon points="196.661,391.813 223.84,340.524 381.46,229.666 196.625,326.192 " id="svg_51" fill="#03a9f4"/>
|
|
||||||
<path d="m283.545,24.007c-145.682,0 -263.781,118.099 -263.781,263.781s118.099,263.781 263.781,263.781s263.781,-118.099 263.781,-263.781s-118.099,-263.781 -263.781,-263.781zm160.376,182.583l-117.664,174.425c-3.558,5.273 -10.504,7.04 -16.15,4.11l-47.144,-24.478l-64.897,56.276c-2.322,2.013 -5.247,3.062 -8.206,3.062c-1.752,0 -3.516,-0.368 -5.173,-1.119c-4.456,-2.023 -7.325,-6.456 -7.345,-11.349l-0.386,-91.883l-70.236,-36.656c-4.681,-2.443 -7.327,-7.562 -6.612,-12.794c0.715,-5.233 4.635,-9.454 9.801,-10.553l321.028,-68.291c4.992,-1.061 10.13,1.012 12.985,5.245c2.854,4.232 2.854,9.773 -0.001,14.005z" id="svg_52" fill="#03a9f4"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,47 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: -4px;
|
|
||||||
padding: 0px;
|
|
||||||
font-size: 68px;
|
|
||||||
font-family: 'Source Code Pro', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 0.8em;
|
|
||||||
letter-spacing: -3px;
|
|
||||||
color: #333;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
text-shadow:
|
|
||||||
1px 0px 0px #ccc, 0px 1px 0px #eee,
|
|
||||||
2px 1px 0px #ccc, 1px 2px 0px #eee,
|
|
||||||
3px 2px 0px #ccc, 2px 3px 0px #eee,
|
|
||||||
4px 3px 0px #ccc;
|
|
||||||
}
|
|
||||||
h1 > span {
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>
|
|
||||||
<span>ftp-srv</span>
|
|
||||||
</h1>
|
|
||||||
<img src="icon.svg" width="76px" height="76px" />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
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);
|
||||||
|
});
|
||||||
23
meta/logo/generate.js
Normal file
23
meta/logo/generate.js
Normal 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
meta/logo/logo.html
Normal file
68
meta/logo/logo.html
Normal 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>
|
||||||
20132
package-lock.json
generated
20132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
119
package.json
119
package.json
@@ -8,79 +8,86 @@
|
|||||||
"ftp-srv",
|
"ftp-srv",
|
||||||
"ftp-svr",
|
"ftp-svr",
|
||||||
"ftpd",
|
"ftpd",
|
||||||
"server",
|
"ftpserver",
|
||||||
"ftpserver"
|
"server"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "ftp-srv.js",
|
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"src",
|
||||||
|
"bin",
|
||||||
|
"ftp-srv.d.ts"
|
||||||
],
|
],
|
||||||
|
"main": "ftp-srv.js",
|
||||||
|
"bin": "./bin/index.js",
|
||||||
|
"types": "./ftp-srv.d.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/trs/ftp-srv"
|
"url": "https://github.com/autovance/ftp-srv"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pre-release": "npm-run-all verify test:coverage build ",
|
"pre-release": "npm run verify",
|
||||||
"build": "cross-env NODE_ENV=production npm run clean:prod",
|
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
|
||||||
"clean:prod": "rimraf dist/",
|
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
|
||||||
"commitmsg": "cz-customizable-ghooks",
|
|
||||||
"dev": "cross-env NODE_ENV=development npm run verify:watch",
|
|
||||||
"prepush": "npm-run-all verify test:coverage --silent",
|
|
||||||
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
|
|
||||||
"start": "npm run dev",
|
|
||||||
"test": "npm run test:unit",
|
|
||||||
"test:check-coverage": "cross-env NODE_ENV=test istanbul check-coverage reports/coverage/coverage.json --config config/testUnit/istanbul.js",
|
|
||||||
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
|
|
||||||
"test:unit": "chokidar 'src/**/*.js' 'test/**/*.js' -c 'npm run test:unit:once' --initial --silent",
|
|
||||||
"test:unit:once": "cross-env NODE_ENV=test istanbul cover --config config/testUnit/istanbul.js _mocha -- --opts config/testUnit/mocha.opts",
|
|
||||||
"upload-coverage": "cat reports/coverage/lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
|
||||||
"verify": "npm run verify:js --silent",
|
|
||||||
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
|
|
||||||
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
|
|
||||||
"verify:js:watch": "chokidar 'src/**/*.js' 'test/**/*.js' 'config/**/*.js' -c 'npm run verify:js:fix' --initial --silent",
|
|
||||||
"verify:watch": "npm run verify:js:watch --silent"
|
|
||||||
},
|
},
|
||||||
"types": "./ftp-srv.d.ts",
|
"release": {
|
||||||
"config": {
|
"verifyConditions": "condition-circle",
|
||||||
"commitizen": {
|
"branch": "main",
|
||||||
"path": "node_modules/cz-customizable"
|
"branches": [
|
||||||
|
"main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged",
|
||||||
|
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.js": [
|
||||||
|
"eslint --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"commitlint": {
|
||||||
|
"extends": [
|
||||||
|
"@commitlint/config-conventional"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"mocha": true,
|
||||||
|
"es6": true
|
||||||
},
|
},
|
||||||
"cz-customizable": {
|
"parserOptions": {
|
||||||
"config": "config/release/commitMessageConfig.js"
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bunyan": "^1.8.10",
|
"bluebird": "^3.5.1",
|
||||||
"lodash": "^4.17.4",
|
"bunyan": "^1.8.12",
|
||||||
"moment": "^2.18.1",
|
"ip": "^1.1.5",
|
||||||
"uuid": "^3.1.0",
|
"lodash": "^4.17.15",
|
||||||
"when": "^3.7.8"
|
"moment": "^2.22.1",
|
||||||
|
"uuid": "^3.2.1",
|
||||||
|
"yargs": "^15.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.0.2",
|
"@commitlint/cli": "^10.0.0",
|
||||||
"chokidar-cli": "1.2.0",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"coveralls": "2.13.1",
|
"@icetee/ftp": "^1.0.2",
|
||||||
"cross-env": "5.0.1",
|
"chai": "^4.2.0",
|
||||||
"cz-customizable": "5.0.0",
|
"condition-circle": "^2.0.2",
|
||||||
"cz-customizable-ghooks": "1.5.0",
|
"eslint": "^5.14.1",
|
||||||
"dotenv": "^4.0.0",
|
"husky": "^1.3.1",
|
||||||
"eslint": "3.19.0",
|
"lint-staged": "^12.3.7",
|
||||||
"eslint-config-google": "0.8.0",
|
"mocha": "^9.2.2",
|
||||||
"eslint-plugin-node": "5.0.0",
|
"rimraf": "^2.6.1",
|
||||||
"ftp": "^0.3.10",
|
"semantic-release": "^19.0.2",
|
||||||
"html-convert": "^2.1.7",
|
|
||||||
"husky": "0.13.4",
|
|
||||||
"istanbul": "0.4.5",
|
|
||||||
"mocha": "3.4.2",
|
|
||||||
"mocha-pretty-bunyan-nyan": "^1.0.4",
|
|
||||||
"npm-run-all": "4.0.2",
|
|
||||||
"rimraf": "2.6.1",
|
|
||||||
"semantic-release": "^6.3.6",
|
|
||||||
"sinon": "^2.3.5"
|
"sinon": "^2.3.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.x",
|
"node": ">=12"
|
||||||
"npm": ">=3.9.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
const REGISTRY = require('./registry');
|
const REGISTRY = require('./registry');
|
||||||
|
|
||||||
|
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
|
||||||
|
|
||||||
class FtpCommands {
|
class FtpCommands {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.previousCommand = {};
|
this.previousCommand = {};
|
||||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
|
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
|
||||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
|
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(message) {
|
parse(message) {
|
||||||
const strippedMessage = message.replace(/"/g, '');
|
const strippedMessage = message.replace(/"/g, '');
|
||||||
const [directive, ...args] = strippedMessage.split(' ');
|
let [directive, ...args] = strippedMessage.split(' ');
|
||||||
|
directive = _.chain(directive).trim().toUpper().value();
|
||||||
|
|
||||||
|
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
|
||||||
const params = args.reduce(({arg, flags}, param) => {
|
const params = args.reduce(({arg, flags}, param) => {
|
||||||
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
|
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
|
||||||
else arg.push(param);
|
else arg.push(param);
|
||||||
return {arg, flags};
|
return {arg, flags};
|
||||||
}, {arg: [], flags: []});
|
}, {arg: [], flags: []});
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
directive: _.chain(directive).trim().toUpper().value(),
|
directive,
|
||||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||||
flags: params.flags,
|
flags: params.flags,
|
||||||
raw: message
|
raw: message
|
||||||
@@ -40,29 +45,29 @@ class FtpCommands {
|
|||||||
log.trace({command: logCommand}, 'Handle command');
|
log.trace({command: logCommand}, 'Handle command');
|
||||||
|
|
||||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
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)) {
|
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)) {
|
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 commandRegister = REGISTRY[command.directive];
|
||||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
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) {
|
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);
|
const handler = commandRegister.handler.bind(this.connection);
|
||||||
return when.try(handler, { log, command, previous_command: this.previousCommand })
|
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.previousCommand = _.clone(command);
|
this.previousCommand = _.clone(command);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
directive: 'ABOR',
|
directive: 'ABOR',
|
||||||
handler: function () {
|
handler: function () {
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.then(socket => {
|
.then((socket) => {
|
||||||
return this.reply(426, {socket})
|
return this.reply(426, {socket})
|
||||||
.then(() => this.connector.end());
|
.then(() => this.reply(226));
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => this.reply(225))
|
||||||
.then(() => this.reply(226));
|
.finally(() => this.connector.end());
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
description: 'Abort an active file transfer'
|
description: 'Abort an active file transfer'
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function handleTLS() {
|
function handleTLS() {
|
||||||
if (!this.server._tls) return this.reply(502);
|
if (!this.server.options.tls) return this.reply(502);
|
||||||
if (this.secure) return this.reply(202);
|
if (this.secure) return this.reply(202);
|
||||||
|
|
||||||
return this.reply(234)
|
return this.reply(234)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
||||||
isServer: true,
|
isServer: true,
|
||||||
secureContext
|
secureContext
|
||||||
});
|
});
|
||||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
|
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
|
||||||
function forwardEvent() {
|
function forwardEvent() {
|
||||||
this.emit.apply(this, arguments);
|
this.emit.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const escapePath = require('../../helpers/escape-path');
|
const escapePath = require('../../helpers/escape-path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.chdir.bind(this.fs), command.arg)
|
return Promise.try(() => this.fs.chdir(command.arg))
|
||||||
.then(cwd => {
|
.then((cwd) => {
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||||
return this.reply(250, path);
|
return this.reply(250, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'DELE',
|
directive: 'DELE',
|
||||||
@@ -6,11 +6,11 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.delete.bind(this.fs), command.arg)
|
return Promise.try(() => this.fs.delete(command.arg))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(250);
|
return this.reply(250);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ const FAMILY = {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'EPRT',
|
directive: 'EPRT',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({log, command} = {}) {
|
||||||
this.connector = new ActiveConnector(this);
|
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||||
const [protocol, ip, port] = _.compact(command.arg.split('|'));
|
|
||||||
const family = FAMILY[protocol];
|
const family = FAMILY[protocol];
|
||||||
if (!family) return this.reply(502, 'Unknown network protocol');
|
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||||
|
|
||||||
|
this.connector = new ActiveConnector(this);
|
||||||
return this.connector.setupConnection(ip, port, family)
|
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>|',
|
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
description: 'Specifies an address and port to which the server should connect'
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'EPSV',
|
directive: 'EPSV',
|
||||||
handler: function () {
|
handler: function ({log}) {
|
||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then(server => {
|
.then((server) => {
|
||||||
const {port} = server.address();
|
const {port} = server.address();
|
||||||
|
|
||||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.error(err);
|
||||||
|
return this.reply(err.code || 425, err.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} [<protocol>]',
|
syntax: '{{cmd}} [<protocol>]',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
return feats;
|
return feats;
|
||||||
}, ['UTF8'])
|
}, ['UTF8'])
|
||||||
.sort()
|
.sort()
|
||||||
.map(feat => ({
|
.map((feat) => ({
|
||||||
message: ` ${feat}`,
|
message: ` ${feat}`,
|
||||||
raw: true
|
raw: true
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
||||||
return this.reply(214, ...reply);
|
return this.reply(214, ...reply);
|
||||||
} else {
|
} else {
|
||||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'));
|
||||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const getFileStat = require('../../helpers/file-stat');
|
const getFileStat = require('../../helpers/file-stat');
|
||||||
|
|
||||||
// http://cr.yp.to/ftp/list.html
|
// http://cr.yp.to/ftp/list.html
|
||||||
@@ -16,35 +16,34 @@ module.exports = {
|
|||||||
const path = command.arg || '.';
|
const path = command.arg || '.';
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => when.try(this.fs.get.bind(this.fs), path))
|
.then(() => Promise.try(() => this.fs.get(path)))
|
||||||
.then(stat => stat.isDirectory() ? when.try(this.fs.list.bind(this.fs), path) : [stat])
|
.then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
|
||||||
.then(files => {
|
.then((files) => {
|
||||||
const getFileMessage = file => {
|
const getFileMessage = (file) => {
|
||||||
if (simple) return file.name;
|
if (simple) return file.name;
|
||||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileList = files.map(file => {
|
return Promise.try(() => files.map((file) => {
|
||||||
const message = getFileMessage(file);
|
const message = getFileMessage(file);
|
||||||
return {
|
return {
|
||||||
raw: true,
|
raw: true,
|
||||||
message,
|
message,
|
||||||
socket: this.connector.socket
|
socket: this.connector.socket
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
return this.reply(150)
|
|
||||||
.then(() => {
|
|
||||||
if (fileList.length) return this.reply({}, ...fileList);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.tap(() => this.reply(150))
|
||||||
return this.reply(226, 'Transfer OK');
|
.then((fileList) => {
|
||||||
|
if (fileList.length) return this.reply({}, ...fileList);
|
||||||
|
return this.reply({socket: this.connector.socket, useEmptyMessage: true});
|
||||||
})
|
})
|
||||||
.catch(when.TimeoutError, err => {
|
.tap(() => this.reply(226))
|
||||||
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(451, err.message || 'No directory');
|
return this.reply(451, err.message || 'No directory');
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.get.bind(this.fs), command.arg)
|
return Promise.try(() => this.fs.get(command.arg))
|
||||||
.then(fileStat => {
|
.then((fileStat) => {
|
||||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||||
return this.reply(213, modificationTime);
|
return this.reply(213, modificationTime);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const escapePath = require('../../helpers/escape-path');
|
const escapePath = require('../../helpers/escape-path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.mkdir.bind(this.fs), command.arg)
|
return Promise.try(() => this.fs.mkdir(command.arg, { recursive: true }))
|
||||||
.then(dir => {
|
.then((dir) => {
|
||||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||||
return this.reply(257, path);
|
return this.reply(257, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module.exports = {
|
|||||||
const [_option, ...args] = command.arg.split(' ');
|
const [_option, ...args] = command.arg.split(' ');
|
||||||
const option = _.toUpper(_option);
|
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);
|
return OPTIONS[option].call(this, args);
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
@@ -21,14 +21,18 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function utf8([setting] = []) {
|
function utf8([setting] = []) {
|
||||||
switch (_.toUpper(setting)) {
|
const getEncoding = () => {
|
||||||
case 'ON':
|
switch (_.toUpper(setting)) {
|
||||||
this.encoding = 'utf8';
|
case 'ON': return 'utf8';
|
||||||
return this.reply(200, 'UTF8 encoding on');
|
case 'OFF': return 'ascii';
|
||||||
case 'OFF':
|
default: return null;
|
||||||
this.encoding = 'ascii';
|
}
|
||||||
return this.reply(200, 'UTF8 encoding off');
|
};
|
||||||
default:
|
|
||||||
return this.reply(501, 'Unknown setting for option');
|
const encoding = getEncoding();
|
||||||
}
|
if (!encoding) return this.reply(501, 'Unknown setting for option');
|
||||||
|
|
||||||
|
this.encoding = encoding;
|
||||||
|
|
||||||
|
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(230);
|
return this.reply(230);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(530, err.message || 'Authentication failed');
|
return this.reply(530, err.message || 'Authentication failed');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
|
const Promise = require('bluebird');
|
||||||
const PassiveConnector = require('../../connector/passive');
|
const PassiveConnector = require('../../connector/passive');
|
||||||
|
const {isLocalIP} = require('../../helpers/is-local');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PASV',
|
directive: 'PASV',
|
||||||
handler: function () {
|
handler: function ({log} = {}) {
|
||||||
|
if (!this.server.options.pasv_url) {
|
||||||
|
return this.reply(502);
|
||||||
|
}
|
||||||
|
|
||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then(server => {
|
.then((server) => {
|
||||||
const address = this.server.url.hostname;
|
|
||||||
const {port} = server.address();
|
const {port} = server.address();
|
||||||
|
let pasvAddress = this.server.options.pasv_url;
|
||||||
|
if (typeof pasvAddress === "function") {
|
||||||
|
return Promise.try(() => pasvAddress(this.ip))
|
||||||
|
.then((address) => ({address, port}));
|
||||||
|
}
|
||||||
|
// Allow connecting from local
|
||||||
|
if (isLocalIP(this.ip)) pasvAddress = this.ip;
|
||||||
|
return {address: pasvAddress, port};
|
||||||
|
})
|
||||||
|
.then(({address, port}) => {
|
||||||
const host = address.replace(/\./g, ',');
|
const host = address.replace(/\./g, ',');
|
||||||
const portByte1 = port / 256 | 0;
|
const portByte1 = port / 256 | 0;
|
||||||
const portByte2 = port % 256;
|
const portByte2 = port % 256;
|
||||||
|
|
||||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.error(err);
|
||||||
|
return this.reply(err.code || 425, err.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PBSZ',
|
directive: 'PBSZ',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({command} = {}) {
|
||||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
if (!this.secure) return this.reply(202, 'Not supported');
|
||||||
this.bufferSize = parseInt(command.arg, 10);
|
this.bufferSize = parseInt(command.arg, 10);
|
||||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ const ActiveConnector = require('../../connector/active');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PORT',
|
directive: 'PORT',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({log, command} = {}) {
|
||||||
this.connector = new ActiveConnector(this);
|
this.connector = new ActiveConnector(this);
|
||||||
|
|
||||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||||
if (rawConnection.length !== 6) return this.reply(425);
|
if (rawConnection.length !== 6) return this.reply(425);
|
||||||
|
|
||||||
const ip = rawConnection.slice(0, 4).join('.');
|
const ip = rawConnection.slice(0, 4).map((b) => parseInt(b)).join('.');
|
||||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
|
||||||
const port = portBytes[0] * 256 + portBytes[1];
|
const port = portBytes[0] * 256 + portBytes[1];
|
||||||
|
|
||||||
return this.connector.setupConnection(ip, port)
|
return this.connector.setupConnection(ip, port)
|
||||||
.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>',
|
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
description: 'Specifies an address and port to which the server should connect'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const _ = require('lodash');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PROT',
|
directive: 'PROT',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({command} = {}) {
|
||||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
if (!this.secure) return this.reply(202, 'Not supported');
|
||||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
||||||
|
|
||||||
switch (_.toUpper(command.arg)) {
|
switch (_.toUpper(command.arg)) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const escapePath = require('../../helpers/escape-path');
|
const escapePath = require('../../helpers/escape-path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.currentDirectory.bind(this.fs))
|
return Promise.try(() => this.fs.currentDirectory())
|
||||||
.then(cwd => {
|
.then((cwd) => {
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||||
return this.reply(257, path);
|
return this.reply(257, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module.exports = {
|
|||||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
||||||
|
|
||||||
this.restByteCount = byteCount;
|
this.restByteCount = byteCount;
|
||||||
return this.reply(350, `Resarting next transfer at ${byteCount}`);
|
return this.reply(350, `Restarting next transfer at ${byteCount}`);
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} <byte-count>',
|
syntax: '{{cmd}} <byte-count>',
|
||||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'RETR',
|
directive: 'RETR',
|
||||||
@@ -6,26 +6,52 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
|
const filePath = command.arg;
|
||||||
|
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => when.try(this.fs.read.bind(this.fs), command.arg, {start: this.restByteCount}))
|
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
|
||||||
.then(stream => {
|
.then((fsResponse) => {
|
||||||
this.restByteCount = 0;
|
let {stream, clientPath} = fsResponse;
|
||||||
return when.promise((resolve, reject) => {
|
if (!stream && !clientPath) {
|
||||||
this.connector.socket.on('error', err => stream.emit('error', err));
|
stream = fsResponse;
|
||||||
|
clientPath = filePath;
|
||||||
|
}
|
||||||
|
const serverPath = stream.path || filePath;
|
||||||
|
|
||||||
stream.on('data', data => this.connector.socket.write(data, this.transferType));
|
const destroyConnection = (connection, reject) => (err) => {
|
||||||
stream.on('end', () => resolve(this.reply(226)));
|
if (connection) connection.destroy(err);
|
||||||
stream.on('error', err => reject(err));
|
reject(err);
|
||||||
this.reply(150).then(() => this.connector.socket.resume());
|
};
|
||||||
|
|
||||||
|
const eventsPromise = new Promise((resolve, reject) => {
|
||||||
|
stream.on('data', (data) => {
|
||||||
|
if (stream) stream.pause();
|
||||||
|
if (this.connector.socket) {
|
||||||
|
this.connector.socket.write(data, () => stream && stream.resume());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.once('end', () => resolve());
|
||||||
|
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||||
|
|
||||||
|
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.restByteCount = 0;
|
||||||
|
|
||||||
|
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
||||||
|
.then(() => eventsPromise)
|
||||||
|
.tap(() => this.emit('RETR', null, serverPath))
|
||||||
|
.then(() => this.reply(226, clientPath))
|
||||||
|
.finally(() => stream.destroy && stream.destroy());
|
||||||
})
|
})
|
||||||
.catch(when.TimeoutError, err => {
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
this.emit('RETR', err);
|
||||||
return this.reply(551, err.message);
|
return this.reply(551, err.message);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const dele = require('./dele').handler;
|
const {handler: dele} = require('./dele');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: ['RMD', 'XRMD'],
|
directive: ['RMD', 'XRMD'],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'RNFR',
|
directive: 'RNFR',
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
const fileName = command.arg;
|
const fileName = command.arg;
|
||||||
return when.try(this.fs.get.bind(this.fs), fileName)
|
return Promise.try(() => this.fs.get(fileName))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.renameFrom = fileName;
|
this.renameFrom = fileName;
|
||||||
return this.reply(350);
|
return this.reply(350);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'RNTO',
|
directive: 'RNTO',
|
||||||
@@ -11,12 +11,14 @@ module.exports = {
|
|||||||
const from = this.renameFrom;
|
const from = this.renameFrom;
|
||||||
const to = command.arg;
|
const to = command.arg;
|
||||||
|
|
||||||
return when.try(this.fs.rename.bind(this.fs), from, to)
|
return Promise.try(() => this.fs.rename(from, to))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(250);
|
return this.reply(250);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.tap(() => this.emit('RNTO', null, to))
|
||||||
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
this.emit('RNTO', err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = function ({log, command} = {}) {
|
module.exports = function ({log, command} = {}) {
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
@@ -6,11 +6,11 @@ module.exports = function ({log, command} = {}) {
|
|||||||
|
|
||||||
const [mode, ...fileNameParts] = command.arg.split(' ');
|
const [mode, ...fileNameParts] = command.arg.split(' ');
|
||||||
const fileName = fileNameParts.join(' ');
|
const fileName = fileNameParts.join(' ');
|
||||||
return when.try(this.fs.chmod.bind(this.fs), fileName, parseInt(mode, 8))
|
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(200);
|
return this.reply(200);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(500);
|
return this.reply(500);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const registry = require('./registry');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'SITE',
|
directive: 'SITE',
|
||||||
handler: function ({log, command} = {}) {
|
handler: function ({log, command} = {}) {
|
||||||
const registry = require('./registry');
|
const rawSubCommand = _.get(command, 'arg', '');
|
||||||
const subCommand = this.commands.parse(command.arg);
|
const subCommand = this.commands.parse(rawSubCommand);
|
||||||
const subLog = log.child({subverb: subCommand.directive});
|
const subLog = log.child({subverb: subCommand.directive});
|
||||||
|
|
||||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
||||||
|
|
||||||
const handler = registry[subCommand.directive].handler.bind(this);
|
const handler = registry[subCommand.directive].handler.bind(this);
|
||||||
return when.try(handler, { log: subLog, command: subCommand });
|
return Promise.resolve(handler({log: subLog, command: subCommand}));
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} <subVerb> [...<subParams>]',
|
syntax: '{{cmd}} <subVerb> [...<subParams>]',
|
||||||
description: 'Sends site specific commands to remote server'
|
description: 'Sends site specific commands to remote server'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'SIZE',
|
directive: 'SIZE',
|
||||||
@@ -6,11 +6,11 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.get.bind(this.fs), command.arg)
|
return Promise.try(() => this.fs.get(command.arg))
|
||||||
.then(fileStat => {
|
.then((fileStat) => {
|
||||||
return this.reply(213, {message: fileStat.size});
|
return this.reply(213, {message: fileStat.size});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const getFileStat = require('../../helpers/file-stat');
|
const getFileStat = require('../../helpers/file-stat');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -11,27 +11,28 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.get.bind(this.fs), path)
|
return Promise.try(() => this.fs.get(path))
|
||||||
.then(stat => {
|
.then((stat) => {
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return when.try(this.fs.list.bind(this.fs), path)
|
return Promise.try(() => this.fs.list(path))
|
||||||
.then(files => {
|
.then((stats) => [213, stats]);
|
||||||
const fileList = files.map(file => {
|
|
||||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
|
||||||
return {
|
|
||||||
raw: true,
|
|
||||||
message
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')));
|
|
||||||
}
|
}
|
||||||
|
return [212, [stat]];
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.then(([code, fileStats]) => {
|
||||||
|
return Promise.map(fileStats, (file) => {
|
||||||
|
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||||
|
return {
|
||||||
|
raw: true,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.then((messages) => [code, messages]);
|
||||||
|
})
|
||||||
|
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||||
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(450, err.message);
|
return this.reply(450, err.message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'STOR',
|
directive: 'STOR',
|
||||||
@@ -11,29 +11,56 @@ module.exports = {
|
|||||||
|
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append, start: this.restByteCount}))
|
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
|
||||||
.then(stream => {
|
.then((fsResponse) => {
|
||||||
|
let {stream, clientPath} = fsResponse;
|
||||||
|
if (!stream && !clientPath) {
|
||||||
|
stream = fsResponse;
|
||||||
|
clientPath = fileName;
|
||||||
|
}
|
||||||
|
const serverPath = stream.path || fileName;
|
||||||
|
|
||||||
|
const destroyConnection = (connection, reject) => (err) => {
|
||||||
|
try {
|
||||||
|
if (connection) {
|
||||||
|
if (connection.writable) connection.end();
|
||||||
|
connection.destroy(err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamPromise = new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||||
|
stream.once('finish', () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
const socketPromise = new Promise((resolve, reject) => {
|
||||||
|
this.connector.socket.pipe(stream, {end: false});
|
||||||
|
this.connector.socket.once('end', () => {
|
||||||
|
if (stream.listenerCount('close')) stream.emit('close');
|
||||||
|
else stream.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||||
|
});
|
||||||
|
|
||||||
this.restByteCount = 0;
|
this.restByteCount = 0;
|
||||||
return when.promise((resolve, reject) => {
|
|
||||||
stream.once('error', err => this.connector.socket.emit('error', err));
|
|
||||||
stream.once('finish', () => resolve(this.reply(226, fileName)));
|
|
||||||
|
|
||||||
// Emit `close` if stream has a close listener, otherwise emit `finish` with the end() method
|
return this.reply(150).then(() => this.connector.socket && this.connector.socket.resume())
|
||||||
// It is assumed that the `close` handler will call the end() method
|
.then(() => Promise.all([streamPromise, socketPromise]))
|
||||||
this.connector.socket.once('end', () => stream.listenerCount('close') ? stream.emit('close') : stream.end());
|
.tap(() => this.emit('STOR', null, serverPath))
|
||||||
this.connector.socket.once('error', err => reject(err));
|
.then(() => this.reply(226, clientPath))
|
||||||
this.connector.socket.on('data', data => stream.write(data, this.transferType));
|
.finally(() => stream.destroy && stream.destroy());
|
||||||
|
|
||||||
this.reply(150).then(() => this.connector.socket.resume());
|
|
||||||
})
|
|
||||||
.finally(() => stream.destroy ? when.try(stream.destroy.bind(stream)) : null);
|
|
||||||
})
|
})
|
||||||
.catch(when.TimeoutError, err => {
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
this.emit('STOR', err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
const {handler: stor} = require('./stor');
|
||||||
const stor = require('./stor').handler;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'STOU',
|
directive: 'STOU',
|
||||||
@@ -9,12 +8,10 @@ module.exports = {
|
|||||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
const fileName = args.command.arg;
|
const fileName = args.command.arg;
|
||||||
return when.try(() => {
|
return Promise.try(() => this.fs.get(fileName))
|
||||||
return when.try(this.fs.get.bind(this.fs), fileName)
|
.then(() => Promise.try(() => this.fs.getUniqueName(fileName)))
|
||||||
.then(() => when.try(this.fs.getUniqueName.bind(this.fs)))
|
.catch(() => fileName)
|
||||||
.catch(() => when.resolve(fileName));
|
.then((name) => {
|
||||||
})
|
|
||||||
.then(name => {
|
|
||||||
args.command.arg = name;
|
args.command.arg = name;
|
||||||
return stor.call(this, args);
|
return stor.call(this, args);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'TYPE',
|
directive: 'TYPE',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({command} = {}) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module.exports = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(230);
|
return this.reply(230);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(530, err.message || 'Authentication failed');
|
return this.reply(530, err.message || 'Authentication failed');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ const commands = [
|
|||||||
require('./registration/type'),
|
require('./registration/type'),
|
||||||
require('./registration/user'),
|
require('./registration/user'),
|
||||||
require('./registration/pbsz'),
|
require('./registration/pbsz'),
|
||||||
require('./registration/prot')
|
require('./registration/prot'),
|
||||||
|
require('./registration/eprt'),
|
||||||
|
require('./registration/epsv')
|
||||||
];
|
];
|
||||||
|
|
||||||
const registry = commands.reduce((result, cmd) => {
|
const registry = commands.reduce((result, cmd) => {
|
||||||
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
||||||
aliases.forEach(alias => result[alias] = cmd);
|
aliases.forEach((alias) => result[alias] = cmd);
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const sequence = require('when/sequence');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const BaseConnector = require('./connector/base');
|
const BaseConnector = require('./connector/base');
|
||||||
const FileSystem = require('./fs');
|
const FileSystem = require('./fs');
|
||||||
@@ -9,10 +9,12 @@ const Commands = require('./commands');
|
|||||||
const errors = require('./errors');
|
const errors = require('./errors');
|
||||||
const DEFAULT_MESSAGE = require('./messages');
|
const DEFAULT_MESSAGE = require('./messages');
|
||||||
|
|
||||||
class FtpConnection {
|
class FtpConnection extends EventEmitter {
|
||||||
constructor(server, options) {
|
constructor(server, options) {
|
||||||
|
super();
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.id = uuid.v4();
|
this.id = uuid.v4();
|
||||||
|
this.commandSocket = options.socket;
|
||||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||||
this.commands = new Commands(this);
|
this.commands = new Commands(this);
|
||||||
this.transferType = 'binary';
|
this.transferType = 'binary';
|
||||||
@@ -23,28 +25,31 @@ class FtpConnection {
|
|||||||
|
|
||||||
this.connector = new BaseConnector(this);
|
this.connector = new BaseConnector(this);
|
||||||
|
|
||||||
this.commandSocket = options.socket;
|
this.commandSocket.on('error', (err) => {
|
||||||
this.commandSocket.on('error', err => {
|
|
||||||
this.log.error(err, 'Client error');
|
this.log.error(err, 'Client error');
|
||||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
||||||
});
|
});
|
||||||
this.commandSocket.on('data', this._handleData.bind(this));
|
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', () => {
|
this.commandSocket.on('close', () => {
|
||||||
if (this.connector) this.connector.end();
|
if (this.connector) this.connector.end();
|
||||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||||
|
this.removeAllListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleData(data) {
|
_handleData(data) {
|
||||||
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
||||||
this.log.trace(messages);
|
this.log.trace(messages);
|
||||||
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message)));
|
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
get ip() {
|
get ip() {
|
||||||
try {
|
try {
|
||||||
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress;
|
return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -65,14 +70,13 @@ class FtpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close(code = 421, message = 'Closing connection') {
|
close(code = 421, message = 'Closing connection') {
|
||||||
return when
|
return Promise.resolve(code)
|
||||||
.resolve(code)
|
.then((_code) => _code && this.reply(_code, message))
|
||||||
.then(_code => _code && this.reply(_code, message))
|
.finally(() => this.commandSocket && this.commandSocket.destroy());
|
||||||
.then(() => this.commandSocket && this.commandSocket.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
return when.try(() => {
|
return Promise.try(() => {
|
||||||
const loginListeners = this.server.listeners('login');
|
const loginListeners = this.server.listeners('login');
|
||||||
if (!loginListeners || !loginListeners.length) {
|
if (!loginListeners || !loginListeners.length) {
|
||||||
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
||||||
@@ -93,48 +97,58 @@ class FtpConnection {
|
|||||||
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
|
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
|
||||||
if (!Array.isArray(letters)) letters = [letters];
|
if (!Array.isArray(letters)) letters = [letters];
|
||||||
if (!letters.length) letters = [{}];
|
if (!letters.length) letters = [{}];
|
||||||
return when.map(letters, promise => {
|
return Promise.map(letters, (promise, index) => {
|
||||||
return when(promise)
|
return Promise.resolve(promise)
|
||||||
.then(letter => {
|
.then((letter) => {
|
||||||
if (!letter) letter = {};
|
if (!letter) letter = {};
|
||||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||||
|
|
||||||
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
||||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
if (!options.useEmptyMessage) {
|
||||||
if (!letter.encoding) letter.encoding = this.encoding;
|
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||||
return when(letter.message) // allow passing in a promise as a message
|
if (!letter.encoding) letter.encoding = this.encoding;
|
||||||
.then(message => {
|
}
|
||||||
letter.message = message;
|
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
||||||
|
.then((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;
|
return letter;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const processLetter = (letter, index) => {
|
const processLetter = (letter) => {
|
||||||
return when.promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const seperator = !options.hasOwnProperty('eol') ?
|
|
||||||
letters.length - 1 === index ? ' ' : '-' :
|
|
||||||
options.eol ? ' ' : '-';
|
|
||||||
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
|
|
||||||
|
|
||||||
if (letter.socket && letter.socket.writable) {
|
if (letter.socket && letter.socket.writable) {
|
||||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, packet}, 'Reply');
|
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
||||||
letter.socket.write(packet + '\r\n', letter.encoding, err => {
|
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
|
||||||
if (err) {
|
if (error) {
|
||||||
this.log.error(err);
|
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
|
||||||
return reject(err);
|
return reject(error);
|
||||||
}
|
}
|
||||||
resolve();
|
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'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return satisfyParameters()
|
return satisfyParameters()
|
||||||
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index))))
|
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||||
.catch(err => {
|
return processLetter(letter, index);
|
||||||
this.log.error(err);
|
}))
|
||||||
|
.catch((error) => {
|
||||||
|
this.log.error('Satisfy Parameters Error', { error: error.message });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const {Socket} = require('net');
|
const {Socket} = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const when = require('when');
|
const ip = require('ip');
|
||||||
|
const Promise = require('bluebird');
|
||||||
const Connector = require('./base');
|
const Connector = require('./base');
|
||||||
|
const {SocketError} = require('../errors');
|
||||||
|
|
||||||
class Active extends Connector {
|
class Active extends Connector {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
@@ -10,29 +12,34 @@ class Active extends Connector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||||
return when.iterate(
|
const checkSocket = () => {
|
||||||
() => {},
|
if (this.dataSocket && this.dataSocket.connected) {
|
||||||
() => this.dataSocket && this.dataSocket.connected,
|
return Promise.resolve(this.dataSocket);
|
||||||
() => when().delay(delay)
|
}
|
||||||
).timeout(timeout)
|
return Promise.resolve().delay(delay)
|
||||||
.then(() => this.dataSocket);
|
.then(() => checkSocket());
|
||||||
|
};
|
||||||
|
|
||||||
|
return checkSocket().timeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupConnection(host, port, family = 4) {
|
setupConnection(host, port, family = 4) {
|
||||||
const closeExistingServer = () => this.dataSocket ?
|
const closeExistingServer = () => Promise.resolve(
|
||||||
when(this.dataSocket.destroy()) :
|
this.dataSocket ? this.dataSocket.destroy() : undefined);
|
||||||
when.resolve();
|
|
||||||
|
|
||||||
return closeExistingServer()
|
return closeExistingServer()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
|
||||||
|
throw new SocketError('The given address is not yours', 500);
|
||||||
|
}
|
||||||
|
|
||||||
this.dataSocket = new Socket();
|
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.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
this.dataSocket.connect({host, port, family}, () => {
|
||||||
this.dataSocket.connect({ host, port, family }, () => {
|
|
||||||
this.dataSocket.pause();
|
this.dataSocket.pause();
|
||||||
|
|
||||||
if (this.connection.secure) {
|
if (this.connection.secure) {
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
||||||
isServer: true,
|
isServer: true,
|
||||||
secureContext
|
secureContext
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
class Connector {
|
class Connector {
|
||||||
@@ -23,15 +23,31 @@ class Connector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
waitForConnection() {
|
waitForConnection() {
|
||||||
return when.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeSocket() {
|
||||||
|
if (this.dataSocket) {
|
||||||
|
const socket = this.dataSocket;
|
||||||
|
this.dataSocket.end(() => socket && socket.destroy());
|
||||||
|
this.dataSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeServer() {
|
||||||
|
if (this.dataServer) {
|
||||||
|
this.dataServer.close();
|
||||||
|
this.dataServer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
if (this.dataSocket) this.dataSocket.end();
|
this.closeSocket();
|
||||||
if (this.dataServer) this.dataServer.close();
|
this.closeServer();
|
||||||
this.dataSocket = null;
|
|
||||||
this.dataServer = null;
|
|
||||||
this.type = false;
|
this.type = false;
|
||||||
|
this.connection.connector = new Connector(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Connector;
|
module.exports = Connector;
|
||||||
|
|||||||
@@ -1,37 +1,42 @@
|
|||||||
const net = require('net');
|
const net = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const when = require('when');
|
const ip = require('ip');
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
const Connector = require('./base');
|
const Connector = require('./base');
|
||||||
const findPort = require('../helpers/find-port');
|
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
|
const CONNECT_TIMEOUT = 30 * 1000;
|
||||||
|
|
||||||
class Passive extends Connector {
|
class Passive extends Connector {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
this.type = 'passive';
|
this.type = 'passive';
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
waitForConnection({timeout = 5000, delay = 50} = {}) {
|
||||||
if (!this.dataServer) return when.reject(new errors.ConnectorError('Passive server not setup'));
|
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
||||||
return when.iterate(
|
|
||||||
() => {},
|
const checkSocket = () => {
|
||||||
() => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected,
|
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
|
||||||
() => when().delay(delay)
|
return Promise.resolve(this.dataSocket);
|
||||||
).timeout(timeout)
|
}
|
||||||
.then(() => this.dataSocket);
|
return Promise.resolve().delay(delay)
|
||||||
|
.then(() => checkSocket());
|
||||||
|
};
|
||||||
|
|
||||||
|
return checkSocket().timeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupServer() {
|
setupServer() {
|
||||||
const closeExistingServer = () => this.dataServer ?
|
this.closeServer();
|
||||||
when.promise(resolve => this.dataServer.close(() => resolve())) :
|
return this.server.getNextPasvPort()
|
||||||
when.resolve();
|
.then((port) => {
|
||||||
|
this.dataSocket = null;
|
||||||
|
let idleServerTimeout;
|
||||||
|
|
||||||
return closeExistingServer()
|
const connectionHandler = (socket) => {
|
||||||
.then(() => this.getPort())
|
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
||||||
.then(port => {
|
|
||||||
const connectionHandler = socket => {
|
|
||||||
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
|
|
||||||
this.log.error({
|
this.log.error({
|
||||||
pasv_connection: socket.remoteAddress,
|
pasv_connection: socket.remoteAddress,
|
||||||
cmd_connection: this.connection.commandSocket.remoteAddress
|
cmd_connection: this.connection.commandSocket.remoteAddress
|
||||||
@@ -41,57 +46,52 @@ class Passive extends Connector {
|
|||||||
return this.connection.reply(550, 'Remote addresses do not match')
|
return this.connection.reply(550, 'Remote addresses do not match')
|
||||||
.finally(() => this.connection.close());
|
.finally(() => this.connection.close());
|
||||||
}
|
}
|
||||||
|
clearTimeout(idleServerTimeout);
|
||||||
|
|
||||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||||
|
|
||||||
if (this.connection.secure) {
|
this.dataSocket = socket;
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||||
const secureSocket = new tls.TLSSocket(socket, {
|
this.dataSocket.once('close', () => this.closeServer());
|
||||||
isServer: true,
|
|
||||||
secureContext
|
if (!this.connection.secure) {
|
||||||
});
|
this.dataSocket.connected = true;
|
||||||
this.dataSocket = secureSocket;
|
|
||||||
} else {
|
|
||||||
this.dataSocket = socket;
|
|
||||||
}
|
}
|
||||||
this.dataSocket.connected = true;
|
|
||||||
this.dataSocket.setEncoding(this.connection.transferType);
|
|
||||||
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
|
||||||
this.dataSocket.on('close', () => {
|
|
||||||
this.log.trace('Passive connection closed');
|
|
||||||
this.end();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dataSocket = null;
|
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
|
||||||
this.dataServer = net.createServer({ pauseOnConnect: true }, connectionHandler);
|
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
|
||||||
this.dataServer.maxConnections = 1;
|
this.dataServer.maxConnections = 1;
|
||||||
this.dataServer.on('error', err => this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
|
||||||
this.dataServer.on('close', () => {
|
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
||||||
|
this.dataServer.once('close', () => {
|
||||||
this.log.trace('Passive server closed');
|
this.log.trace('Passive server closed');
|
||||||
this.dataServer = null;
|
this.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
return when.promise((resolve, reject) => {
|
if (this.connection.secure) {
|
||||||
this.dataServer.listen(port, err => {
|
this.dataServer.on('secureConnection', (socket) => {
|
||||||
|
socket.connected = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.dataServer.listen(port, this.server.url.hostname, (err) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else {
|
else {
|
||||||
|
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT);
|
||||||
|
|
||||||
this.log.debug({port}, 'Passive connection listening');
|
this.log.debug({port}, 'Passive connection listening');
|
||||||
resolve(this.dataServer);
|
resolve(this.dataServer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.log.trace(error.message);
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPort() {
|
|
||||||
if (this.server.options.pasv_range) {
|
|
||||||
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
|
|
||||||
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
|
|
||||||
[this.server.options.pasv_range];
|
|
||||||
return findPort(min, max);
|
|
||||||
}
|
|
||||||
throw new errors.ConnectorError('Invalid pasv_range');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
module.exports = Passive;
|
module.exports = Passive;
|
||||||
|
|||||||
105
src/fs.js
105
src/fs.js
@@ -1,27 +1,44 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const nodePath = require('path');
|
const nodePath = require('path');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const whenNode = require('when/node');
|
const {createReadStream, createWriteStream, constants} = require('fs');
|
||||||
const syncFs = require('fs');
|
const fsAsync = require('./helpers/fs-async');
|
||||||
const fs = whenNode.liftAll(syncFs);
|
|
||||||
const errors = require('./errors');
|
const errors = require('./errors');
|
||||||
|
|
||||||
|
const UNIX_SEP_REGEX = /\//g;
|
||||||
|
const WIN_SEP_REGEX = /\\/g;
|
||||||
|
|
||||||
class FileSystem {
|
class FileSystem {
|
||||||
constructor(connection, { root, cwd } = {}) {
|
constructor(connection, {root, cwd} = {}) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.cwd = cwd || nodePath.sep;
|
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'));
|
||||||
this.root = root || process.cwd();
|
this._root = nodePath.resolve(root || process.cwd());
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolvePath(path = '') {
|
get root() {
|
||||||
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
|
return this._root;
|
||||||
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
|
}
|
||||||
const serverPath = nodePath.join(nodePath.sep, cwd, path);
|
|
||||||
const fsPath = nodePath.join(this.root, serverPath);
|
_resolvePath(path = '.') {
|
||||||
|
// Unix separators normalize nicer on both unix and win platforms
|
||||||
|
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
|
||||||
|
|
||||||
|
// Join cwd with new path
|
||||||
|
const joinedPath = nodePath.isAbsolute(resolvedPath)
|
||||||
|
? nodePath.normalize(resolvedPath)
|
||||||
|
: nodePath.join('/', this.cwd, resolvedPath);
|
||||||
|
|
||||||
|
// Create local filesystem path using the platform separator
|
||||||
|
const fsPath = nodePath.resolve(nodePath.join(this.root, joinedPath)
|
||||||
|
.replace(UNIX_SEP_REGEX, nodePath.sep)
|
||||||
|
.replace(WIN_SEP_REGEX, nodePath.sep));
|
||||||
|
|
||||||
|
// Create FTP client path using unix separator
|
||||||
|
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverPath,
|
clientPath,
|
||||||
fsPath
|
fsPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -32,20 +49,20 @@ class FileSystem {
|
|||||||
|
|
||||||
get(fileName) {
|
get(fileName) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath} = this._resolvePath(fileName);
|
||||||
return fs.stat(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
.then((stat) => _.set(stat, 'name', fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
list(path = '.') {
|
list(path = '.') {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.readdir(fsPath)
|
return fsAsync.readdir(fsPath)
|
||||||
.then(fileNames => {
|
.then((fileNames) => {
|
||||||
return when.map(fileNames, fileName => {
|
return Promise.map(fileNames, (fileName) => {
|
||||||
const filePath = nodePath.join(fsPath, fileName);
|
const filePath = nodePath.join(fsPath, fileName);
|
||||||
return fs.access(filePath, syncFs.constants.F_OK)
|
return fsAsync.access(filePath, constants.F_OK)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return fs.stat(filePath)
|
return fsAsync.stat(filePath)
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
.then((stat) => _.set(stat, 'name', fileName));
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
});
|
});
|
||||||
@@ -54,61 +71,67 @@ class FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chdir(path = '.') {
|
chdir(path = '.') {
|
||||||
const {fsPath, serverPath} = this._resolvePath(path);
|
const {fsPath, clientPath} = this._resolvePath(path);
|
||||||
return fs.stat(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.tap(stat => {
|
.tap((stat) => {
|
||||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.cwd = serverPath;
|
this.cwd = clientPath;
|
||||||
return this.currentDirectory();
|
return this.currentDirectory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
write(fileName, {append = false, start = undefined} = {}) {
|
write(fileName, {append = false, start = undefined} = {}) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||||
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
const stream = createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
||||||
stream.once('error', () => fs.unlink(fsPath));
|
stream.once('error', () => fsAsync.unlink(fsPath));
|
||||||
stream.once('close', () => stream.end());
|
stream.once('close', () => stream.end());
|
||||||
return stream;
|
return {
|
||||||
|
stream,
|
||||||
|
clientPath
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
read(fileName, {start = undefined} = {}) {
|
read(fileName, {start = undefined} = {}) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||||
return fs.stat(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.tap(stat => {
|
.tap((stat) => {
|
||||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const stream = syncFs.createReadStream(fsPath, {flags: 'r', start});
|
const stream = createReadStream(fsPath, {flags: 'r', start});
|
||||||
return stream;
|
return {
|
||||||
|
stream,
|
||||||
|
clientPath
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(path) {
|
delete(path) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.stat(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.then(stat => {
|
.then((stat) => {
|
||||||
if (stat.isDirectory()) return fs.rmdir(fsPath);
|
if (stat.isDirectory()) return fsAsync.rmdir(fsPath);
|
||||||
else return fs.unlink(fsPath);
|
else return fsAsync.unlink(fsPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir(path) {
|
mkdir(path) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.mkdir(fsPath)
|
return fsAsync.mkdir(fsPath, { recursive: true })
|
||||||
.then(() => fsPath);
|
.then(() => fsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
rename(from, to) {
|
rename(from, to) {
|
||||||
const {fsPath: fromPath} = this._resolvePath(from);
|
const {fsPath: fromPath} = this._resolvePath(from);
|
||||||
const {fsPath: toPath} = this._resolvePath(to);
|
const {fsPath: toPath} = this._resolvePath(to);
|
||||||
return fs.rename(fromPath, toPath);
|
return fsAsync.rename(fromPath, toPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
chmod(path, mode) {
|
chmod(path, mode) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.chmod(fsPath, mode);
|
return fsAsync.chmod(fsPath, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUniqueName() {
|
getUniqueName() {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ module.exports = function (fileStat, format = 'ls') {
|
|||||||
function ls(fileStat) {
|
function ls(fileStat) {
|
||||||
const now = moment.utc();
|
const now = moment.utc();
|
||||||
const mtime = moment.utc(new Date(fileStat.mtime));
|
const mtime = moment.utc(new Date(fileStat.mtime));
|
||||||
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
|
const timeDiff = now.diff(mtime, 'months');
|
||||||
|
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
fileStat.mode ? [
|
fileStat.mode ? [
|
||||||
|
|||||||
@@ -1,27 +1,61 @@
|
|||||||
const net = require('net');
|
const net = require('net');
|
||||||
const when = require('when');
|
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
module.exports = function (min = 1, max = undefined) {
|
const MAX_PORT = 65535;
|
||||||
return when.promise((resolve, reject) => {
|
const MAX_PORT_CHECK_ATTEMPT = 5;
|
||||||
let checkPort = min;
|
|
||||||
let portCheckServer = net.createServer();
|
function* portNumberGenerator(min, max = MAX_PORT) {
|
||||||
|
let current = min;
|
||||||
|
while (true) {
|
||||||
|
if (current > MAX_PORT || current > max) {
|
||||||
|
current = min;
|
||||||
|
}
|
||||||
|
yield current++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
|
||||||
|
const nextPortNumber = portNumberGenerator(portMin, portMax);
|
||||||
|
|
||||||
|
return () => new Promise((resolve, reject) => {
|
||||||
|
const portCheckServer = net.createServer();
|
||||||
portCheckServer.maxConnections = 0;
|
portCheckServer.maxConnections = 0;
|
||||||
portCheckServer.on('error', () => {
|
|
||||||
if (checkPort < 65535 && (!max || checkPort < max)) {
|
let attemptCount = 0;
|
||||||
checkPort = checkPort + 1;
|
const tryGetPort = () => {
|
||||||
portCheckServer.listen(checkPort);
|
attemptCount++;
|
||||||
} else {
|
if (attemptCount > maxAttempts) {
|
||||||
reject(new errors.GeneralError('Unable to find open port', 500));
|
reject(new errors.ConnectorError('Unable to find valid port'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
portCheckServer.on('listening', () => {
|
const {value: port} = nextPortNumber.next();
|
||||||
const {port} = portCheckServer.address();
|
|
||||||
portCheckServer.close(() => {
|
portCheckServer.removeAllListeners();
|
||||||
portCheckServer = null;
|
portCheckServer.once('error', (err) => {
|
||||||
resolve(port);
|
if (['EADDRINUSE'].includes(err.code)) {
|
||||||
|
tryGetPort();
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
portCheckServer.once('listening', () => {
|
||||||
portCheckServer.listen(checkPort);
|
portCheckServer.removeAllListeners();
|
||||||
|
portCheckServer.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
portCheckServer.listen(port, host);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tryGetPort();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getNextPortFactory,
|
||||||
|
portNumberGenerator
|
||||||
};
|
};
|
||||||
|
|||||||
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 when = require('when');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
|
||||||
|
|
||||||
module.exports = function (hostname) {
|
|
||||||
return when.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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
133
src/index.js
133
src/index.js
@@ -1,100 +1,101 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const nodeUrl = require('url');
|
const nodeUrl = require('url');
|
||||||
const buyan = require('bunyan');
|
const buyan = require('bunyan');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const fs = require('fs');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const Connection = require('./connection');
|
const Connection = require('./connection');
|
||||||
const resolveHost = require('./helpers/resolve-host');
|
const {getNextPortFactory} = require('./helpers/find-port');
|
||||||
|
|
||||||
class FtpServer {
|
class FtpServer extends EventEmitter {
|
||||||
constructor(url, options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = _.merge({
|
super();
|
||||||
|
this.options = Object.assign({
|
||||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||||
|
url: 'ftp://127.0.0.1:21',
|
||||||
|
pasv_min: 1024,
|
||||||
|
pasv_max: 65535,
|
||||||
|
pasv_url: null,
|
||||||
anonymous: false,
|
anonymous: false,
|
||||||
pasv_range: 22,
|
|
||||||
file_format: 'ls',
|
file_format: 'ls',
|
||||||
blacklist: [],
|
blacklist: [],
|
||||||
whitelist: [],
|
whitelist: [],
|
||||||
greeting: null,
|
greeting: null,
|
||||||
tls: false
|
tls: false,
|
||||||
|
timeout: 0
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
this._greeting = this.setupGreeting(this.options.greeting);
|
this._greeting = this.setupGreeting(this.options.greeting);
|
||||||
this._features = this.setupFeaturesMessage();
|
this._features = this.setupFeaturesMessage();
|
||||||
this._tls = this.setupTLS(this.options.tls);
|
|
||||||
|
|
||||||
delete this.options.greeting;
|
delete this.options.greeting;
|
||||||
delete this.options.tls;
|
|
||||||
|
|
||||||
this.connections = {};
|
this.connections = {};
|
||||||
this.log = this.options.log;
|
this.log = this.options.log;
|
||||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
this.url = nodeUrl.parse(this.options.url);
|
||||||
|
this.getNextPasvPort = getNextPortFactory(
|
||||||
|
_.get(this, 'url.hostname'),
|
||||||
|
_.get(this, 'options.pasv_min'),
|
||||||
|
_.get(this, 'options.pasv_max'));
|
||||||
|
|
||||||
const serverConnectionHandler = socket => {
|
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});
|
let connection = new Connection(this, {log: this.log, socket});
|
||||||
this.connections[connection.id] = connection;
|
this.connections[connection.id] = connection;
|
||||||
|
|
||||||
socket.on('close', () => this.disconnectClient(connection.id));
|
socket.on('close', () => this.disconnectClient(connection.id));
|
||||||
|
socket.once('close', () => this.emit('disconnect', {connection, id: connection.id}));
|
||||||
|
|
||||||
const greeting = this._greeting || [];
|
const greeting = this._greeting || [];
|
||||||
const features = this._features || 'Ready';
|
const features = this._features || 'Ready';
|
||||||
return connection.reply(220, ...greeting, features)
|
return connection.reply(220, ...greeting, features)
|
||||||
.finally(() => socket.resume());
|
.finally(() => socket.resume());
|
||||||
};
|
};
|
||||||
const serverOptions = _.assign(this.isTLS ? this._tls : {}, { pauseOnConnect: true });
|
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
|
||||||
|
|
||||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
||||||
this.server.on('error', err => this.log.error(err, '[Event] error'));
|
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
|
||||||
this.on = this.server.on.bind(this.server);
|
|
||||||
this.once = this.server.once.bind(this.server);
|
|
||||||
this.listeners = this.server.listeners.bind(this.server);
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => this.quit());
|
const quit = _.debounce(this.quit.bind(this), 100);
|
||||||
process.on('SIGINT', () => this.quit());
|
|
||||||
process.on('SIGQUIT', () => this.quit());
|
process.on('SIGTERM', quit);
|
||||||
|
process.on('SIGINT', quit);
|
||||||
|
process.on('SIGQUIT', quit);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isTLS() {
|
get isTLS() {
|
||||||
return this.url.protocol === 'ftps:' && this._tls;
|
return this.url.protocol === 'ftps:' && this.options.tls;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
return resolveHost(this.url.hostname)
|
if (!this.options.pasv_url) {
|
||||||
.then(hostname => {
|
this.log.warn('Passive URL not set. Passive connections not available.');
|
||||||
this.url.hostname = hostname;
|
}
|
||||||
return when.promise((resolve, reject) => {
|
|
||||||
this.server.listen(this.url.port, err => {
|
return new Promise((resolve, reject) => {
|
||||||
if (err) return reject(err);
|
this.server.once('error', reject);
|
||||||
this.log.info({
|
this.server.listen(this.url.port, this.url.hostname, (err) => {
|
||||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
this.server.removeListener('error', reject);
|
||||||
ip: this.url.hostname,
|
if (err) return reject(err);
|
||||||
port: this.url.port
|
this.log.info({
|
||||||
}, 'Listening');
|
protocol: this.url.protocol.replace(/\W/g, ''),
|
||||||
resolve();
|
ip: this.url.hostname,
|
||||||
});
|
port: this.url.port
|
||||||
|
}, 'Listening');
|
||||||
|
resolve('Listening');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPromise(action, ...data) {
|
emitPromise(action, ...data) {
|
||||||
const defer = when.defer();
|
return new Promise((resolve, reject) => {
|
||||||
const params = _.concat(data, [defer.resolve, defer.reject]);
|
const params = _.concat(data, [resolve, reject]);
|
||||||
this.server.emit(action, ...params);
|
this.emit.call(this, action, ...params);
|
||||||
return defer.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(action, ...data) {
|
|
||||||
this.server.emit(action, ...data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupTLS(_tls) {
|
|
||||||
if (!_tls) return false;
|
|
||||||
return _.assign({}, _tls, {
|
|
||||||
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
|
|
||||||
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
|
|
||||||
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,17 +117,22 @@ class FtpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectClient(id) {
|
disconnectClient(id) {
|
||||||
return when.promise(resolve => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = this.connections[id];
|
const client = this.connections[id];
|
||||||
if (!client) return resolve();
|
if (!client) return resolve();
|
||||||
delete this.connections[id];
|
delete this.connections[id];
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
reject('Timed out disconnecting client')
|
||||||
|
}, this.options.timeout || 1000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.close(0);
|
client.close(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err, 'Error closing connection', {id});
|
this.log.error(err, 'Error closing connection', {id});
|
||||||
} finally {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve('Disconnected');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +142,22 @@ class FtpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.log.info('Server closing...');
|
|
||||||
this.server.maxConnections = 0;
|
this.server.maxConnections = 0;
|
||||||
return when.map(Object.keys(this.connections), id => when.try(this.disconnectClient.bind(this), id))
|
this.log.info('Closing connections:', Object.keys(this.connections).length);
|
||||||
.then(() => when.promise(resolve => {
|
|
||||||
this.server.close(err => {
|
return Promise.all(Object.keys(this.connections).map((id) => this.disconnectClient(id)))
|
||||||
|
.then(() => new Promise((resolve) => {
|
||||||
|
this.server.close((err) => {
|
||||||
|
this.log.info('Server closing...');
|
||||||
if (err) this.log.error(err, 'Error closing server');
|
if (err) this.log.error(err, 'Error closing server');
|
||||||
resolve();
|
resolve('Closed');
|
||||||
});
|
});
|
||||||
}));
|
}))
|
||||||
|
.then(() => {
|
||||||
|
this.log.debug('Removing event listeners...')
|
||||||
|
this.removeAllListeners();
|
||||||
|
return;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ describe('FtpCommands', function () {
|
|||||||
let mockConnection = {
|
let mockConnection = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
log: bunyan.createLogger({name: 'FtpCommands'}),
|
log: bunyan.createLogger({name: 'FtpCommands'}),
|
||||||
reply: () => when.resolve({}),
|
reply: () => Promise.resolve({}),
|
||||||
server: {
|
server: {
|
||||||
options: {
|
options: {
|
||||||
blacklist: ['allo']
|
blacklist: ['allo']
|
||||||
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
commands = new FtpCommands(mockConnection);
|
commands = new FtpCommands(mockConnection);
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
|
|||||||
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
||||||
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
||||||
expect(cmd.directive).to.equal('TEST');
|
expect(cmd.directive).to.equal('TEST');
|
||||||
expect(cmd.arg).to.equal('arg1 arg2');
|
expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
|
||||||
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
|
expect(cmd.flags).to.deep.equal(['-l', '-A']);
|
||||||
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,6 +76,13 @@ describe('FtpCommands', function () {
|
|||||||
expect(cmd.flags).to.deep.equal(['-l']);
|
expect(cmd.flags).to.deep.equal(['-l']);
|
||||||
expect(cmd.raw).to.equal('list -l');
|
expect(cmd.raw).to.equal('list -l');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not check for option flags', () => {
|
||||||
|
const cmd = commands.parse('retr -test');
|
||||||
|
expect(cmd.directive).to.equal('RETR');
|
||||||
|
expect(cmd.arg).to.equal('-test');
|
||||||
|
expect(cmd.flags).to.deep.equal([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handle', function () {
|
describe('handle', function () {
|
||||||
@@ -83,7 +90,7 @@ describe('FtpCommands', function () {
|
|||||||
return commands.handle('bad')
|
return commands.handle('bad')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockConnection.reply.callCount).to.equal(1);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
const CMD = 'ABOR';
|
const CMD = 'ABOR';
|
||||||
describe(CMD, function () {
|
describe.skip(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
connector: {
|
connector: {
|
||||||
waitForConnection: () => when.resolve(),
|
waitForConnection: () => Promise.resolve(),
|
||||||
end: () => when.resolve()
|
end: () => Promise.resolve()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||||
@@ -33,7 +33,7 @@ describe(CMD, function () {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||||
expect(mockClient.connector.end.callCount).to.equal(0);
|
expect(mockClient.connector.end.callCount).to.equal(0);
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(226);
|
expect(mockClient.reply.args[0][0]).to.equal(225);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ const CMD = 'ALLO';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,15 +6,17 @@ const CMD = 'AUTH';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
server: {
|
server: {
|
||||||
_tls: {}
|
options: {
|
||||||
|
tls: {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -23,7 +25,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('TLS // supported', () => {
|
it('TLS // supported', () => {
|
||||||
return cmdFn({command: { arg: 'TLS', directive: CMD}})
|
return cmdFn({command: {arg: 'TLS', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(234);
|
expect(mockClient.reply.args[0][0]).to.equal(234);
|
||||||
expect(mockClient.secure).to.equal(true);
|
expect(mockClient.secure).to.equal(true);
|
||||||
@@ -31,14 +33,14 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('SSL // not supported', () => {
|
it('SSL // not supported', () => {
|
||||||
return cmdFn({command: { arg: 'SSL', directive: CMD}})
|
return cmdFn({command: {arg: 'SSL', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bad // bad', () => {
|
it('bad // bad', () => {
|
||||||
return cmdFn({command: { arg: 'bad', directive: CMD}})
|
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
@@ -8,15 +8,15 @@ describe(CMD, function () {
|
|||||||
let sandbox;
|
let sandbox;
|
||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
fs: {
|
fs: {
|
||||||
chdir: () => when.resolve()
|
chdir: () => Promise.resolve()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.spy(mockClient.fs, 'chdir');
|
sandbox.spy(mockClient.fs, 'chdir');
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ describe(CMD, function () {
|
|||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
fs: { chdir: () => {} }
|
fs: {chdir: () => {}}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||||
@@ -24,7 +24,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs chdir command', () => {
|
it('fails on no fs chdir command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test // successful', () => {
|
it('test // successful', () => {
|
||||||
return cmdFn({log, command: { arg: 'test', directive: CMD}})
|
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||||
@@ -58,7 +58,7 @@ describe(CMD, function () {
|
|||||||
mockClient.fs.chdir.restore();
|
mockClient.fs.chdir.restore();
|
||||||
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
|
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
|
||||||
|
|
||||||
return cmdFn({log, command: { arg: 'test', directive: CMD}})
|
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||||
@@ -69,7 +69,7 @@ describe(CMD, function () {
|
|||||||
mockClient.fs.chdir.restore();
|
mockClient.fs.chdir.restore();
|
||||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
|
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
|
||||||
|
|
||||||
return cmdFn({log, command: { arg: 'bad', directive: CMD}})
|
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
|
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ describe(CMD, function () {
|
|||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
fs: { delete: () => {} }
|
fs: {delete: () => {}}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'delete').resolves();
|
sandbox.stub(mockClient.fs, 'delete').resolves();
|
||||||
@@ -24,7 +24,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs delete command', () => {
|
it('fails on no fs delete command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test // successful', () => {
|
it('test // successful', () => {
|
||||||
return cmdFn({log, command: { arg: 'test', directive: CMD}})
|
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||||
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
|
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
|
||||||
@@ -58,7 +58,7 @@ describe(CMD, function () {
|
|||||||
mockClient.fs.delete.restore();
|
mockClient.fs.delete.restore();
|
||||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
|
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
|
||||||
|
|
||||||
return cmdFn({log, command: { arg: 'bad', directive: CMD}})
|
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||||
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
|
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
|
||||||
|
|||||||
60
test/commands/registration/eprt.spec.js
Normal file
60
test/commands/registration/eprt.spec.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const Promise = require('bluebird');
|
||||||
|
const {expect} = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const ActiveConnector = require('../../../src/connector/active');
|
||||||
|
|
||||||
|
const CMD = 'EPRT';
|
||||||
|
describe(CMD, function () {
|
||||||
|
let sandbox;
|
||||||
|
const mockClient = {
|
||||||
|
reply: () => Promise.resolve()
|
||||||
|
};
|
||||||
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
|
sandbox.spy(mockClient, 'reply');
|
||||||
|
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// unsuccessful | no argument', () => {
|
||||||
|
return cmdFn({})
|
||||||
|
.then(() => {
|
||||||
|
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// unsuccessful | invalid argument', () => {
|
||||||
|
return cmdFn({command: {arg: 'blah'}})
|
||||||
|
.then(() => {
|
||||||
|
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// successful IPv4', () => {
|
||||||
|
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
|
||||||
|
.then(() => {
|
||||||
|
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||||
|
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||||
|
expect(ip).to.equal('192.168.0.100');
|
||||||
|
expect(port).to.equal('35286');
|
||||||
|
expect(family).to.equal(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// successful IPv6', () => {
|
||||||
|
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
|
||||||
|
.then(() => {
|
||||||
|
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||||
|
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||||
|
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23');
|
||||||
|
expect(port).to.equal('35286');
|
||||||
|
expect(family).to.equal(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
test/commands/registration/epsv.spec.js
Normal file
35
test/commands/registration/epsv.spec.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const Promise = require('bluebird');
|
||||||
|
const {expect} = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const PassiveConnector = require('../../../src/connector/passive');
|
||||||
|
|
||||||
|
const CMD = 'EPSV';
|
||||||
|
describe(CMD, function () {
|
||||||
|
let sandbox;
|
||||||
|
const mockClient = {
|
||||||
|
reply: () => Promise.resolve()
|
||||||
|
};
|
||||||
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
|
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
||||||
|
address: () => ({port: 12345})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// successful IPv4', () => {
|
||||||
|
return cmdFn({})
|
||||||
|
.then(() => {
|
||||||
|
const [code, message] = mockClient.reply.args[0];
|
||||||
|
expect(code).to.equal(229);
|
||||||
|
expect(message).to.equal('EPSV OK (|||12345|)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
test/commands/registration/feat.spec.js
Normal file
29
test/commands/registration/feat.spec.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const Promise = require('bluebird');
|
||||||
|
const {expect} = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const CMD = 'FEAT';
|
||||||
|
describe(CMD, function () {
|
||||||
|
let sandbox;
|
||||||
|
const mockClient = {
|
||||||
|
reply: () => Promise.resolve()
|
||||||
|
};
|
||||||
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
|
sandbox.spy(mockClient, 'reply');
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('// successful', () => {
|
||||||
|
return cmdFn({command: {directive: CMD}})
|
||||||
|
.then(() => {
|
||||||
|
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||||
|
expect(mockClient.reply.args[0][2].message).to.equal(' AUTH TLS');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ const CMD = 'HELP';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -20,28 +20,28 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('// successful', () => {
|
it('// successful', () => {
|
||||||
return cmdFn({command: { directive: CMD }})
|
return cmdFn({command: {directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('help // successful', () => {
|
it('help // successful', () => {
|
||||||
return cmdFn({command: { arg: 'help', directive: CMD}})
|
return cmdFn({command: {arg: 'help', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allo // successful', () => {
|
it('allo // successful', () => {
|
||||||
return cmdFn({command: { arg: 'allo', directive: CMD}})
|
return cmdFn({command: {arg: 'allo', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bad // unsuccessful', () => {
|
it('bad // unsuccessful', () => {
|
||||||
return cmdFn({command: { arg: 'bad', directive: CMD}})
|
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
@@ -14,8 +14,8 @@ describe(CMD, function () {
|
|||||||
get: () => {}
|
get: () => {}
|
||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
waitForConnection: () => when({}),
|
waitForConnection: () => Promise.resolve({}),
|
||||||
end: () => {}
|
end: () => Promise.resolve({})
|
||||||
},
|
},
|
||||||
commandSocket: {
|
commandSocket: {
|
||||||
resume: () => {},
|
resume: () => {},
|
||||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||||
@@ -88,7 +88,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs list command', () => {
|
it('fails on no fs list command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('. // unsuccessful (timeout)', () => {
|
it('. // unsuccessful (timeout)', () => {
|
||||||
sandbox.stub(mockClient.connector, 'waitForConnection').returns(when.reject(new when.TimeoutError()));
|
sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
|
||||||
|
|
||||||
return cmdFn({log, command: {directive: CMD}})
|
return cmdFn({log, command: {directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ describe(CMD, function () {
|
|||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
fs: { get: () => {} }
|
fs: {get: () => {}}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
|
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
|
||||||
@@ -24,7 +24,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs get command', () => {
|
it('fails on no fs get command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ describe(CMD, function () {
|
|||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
fs: { mkdir: () => {} }
|
fs: {mkdir: () => {}}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'mkdir').resolves();
|
sandbox.stub(mockClient.fs, 'mkdir').resolves();
|
||||||
@@ -24,7 +24,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs mkdir command', () => {
|
it('fails on no fs mkdir command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ const CMD = 'MODE';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
@@ -14,8 +14,8 @@ describe(CMD, function () {
|
|||||||
list: () => {}
|
list: () => {}
|
||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
waitForConnection: () => when({}),
|
waitForConnection: () => Promise.resolve({}),
|
||||||
end: () => {}
|
end: () => Promise.resolve({})
|
||||||
},
|
},
|
||||||
commandSocket: {
|
commandSocket: {
|
||||||
resume: () => {},
|
resume: () => {},
|
||||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ const CMD = 'NOOP';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ const CMD = 'OPTS';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -29,7 +29,7 @@ describe(CMD, function () {
|
|||||||
it('BAD // unsuccessful', () => {
|
it('BAD // unsuccessful', () => {
|
||||||
return cmdFn({command: {arg: 'BAD', directive: CMD}})
|
return cmdFn({command: {arg: 'BAD', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(500);
|
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ describe(CMD, function () {
|
|||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
login: () => {},
|
login: () => {},
|
||||||
server: { options: { anonymous: false } },
|
server: {options: {anonymous: false}},
|
||||||
username: 'anonymous'
|
username: 'anonymous'
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient, 'login').resolves();
|
sandbox.stub(mockClient, 'login').resolves();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,13 +6,13 @@ const CMD = 'PBSZ';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
server: {}
|
server: {}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -8,12 +8,12 @@ const CMD = 'PORT';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||||
@@ -30,14 +30,14 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('// unsuccessful | invalid argument', () => {
|
it('// unsuccessful | invalid argument', () => {
|
||||||
return cmdFn({ command: { arg: '1,2,3,4,5' } })
|
return cmdFn({command: {arg: '1,2,3,4,5'}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('// successful', () => {
|
it('// successful', () => {
|
||||||
return cmdFn({ command: { arg: '192,168,0,100,137,214' } })
|
return cmdFn({command: {arg: '192,168,0,100,137,214'}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
|
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
@@ -6,13 +6,13 @@ const CMD = 'PROT';
|
|||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
server: {}
|
server: {}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ describe(CMD, function () {
|
|||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => {},
|
reply: () => {},
|
||||||
fs: { currentDirectory: () => {} }
|
fs: {currentDirectory: () => {}}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
|
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
|
||||||
@@ -24,7 +24,7 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
describe('// check', function () {
|
describe('// check', function () {
|
||||||
it('fails on no fs', () => {
|
it('fails on no fs', () => {
|
||||||
const badMockClient = { reply: () => {} };
|
const badMockClient = {reply: () => {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails on no fs currentDirectory command', () => {
|
it('fails on no fs currentDirectory command', () => {
|
||||||
const badMockClient = { reply: () => {}, fs: {} };
|
const badMockClient = {reply: () => {}, fs: {}};
|
||||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||||
sandbox.stub(badMockClient, 'reply').resolves();
|
sandbox.stub(badMockClient, 'reply').resolves();
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('// successful', () => {
|
it('// successful', () => {
|
||||||
return cmdFn({log, command: { arg: 'test', directive: CMD}})
|
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'close').resolves();
|
sandbox.stub(mockClient, 'close').resolves();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
const CMD = 'REST';
|
const CMD = 'REST';
|
||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => when.resolve()
|
reply: () => Promise.resolve()
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -27,21 +27,21 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('-1 // unsuccessful', () => {
|
it('-1 // unsuccessful', () => {
|
||||||
return cmdFn({command: { arg: '-1', directive: CMD } })
|
return cmdFn({command: {arg: '-1', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bad // unsuccessful', () => {
|
it('bad // unsuccessful', () => {
|
||||||
return cmdFn({command: { arg: 'bad', directive: CMD } })
|
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('1 // successful', () => {
|
it('1 // successful', () => {
|
||||||
return cmdFn({command: { arg: '1', directive: CMD } })
|
return cmdFn({command: {arg: '1', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.restByteCount).to.equal(1);
|
expect(mockClient.restByteCount).to.equal(1);
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(350);
|
expect(mockClient.reply.args[0][0]).to.equal(350);
|
||||||
@@ -49,7 +49,7 @@ describe(CMD, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('0 // successful', () => {
|
it('0 // successful', () => {
|
||||||
return cmdFn({command: { arg: '0', directive: CMD } })
|
return cmdFn({command: {arg: '0', directive: CMD}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.restByteCount).to.equal(0);
|
expect(mockClient.restByteCount).to.equal(0);
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(350);
|
expect(mockClient.reply.args[0][0]).to.equal(350);
|
||||||
|
|||||||
@@ -1,34 +1,41 @@
|
|||||||
const when = require('when');
|
const Promise = require('bluebird');
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const CMD = 'RETR';
|
const CMD = 'RETR';
|
||||||
describe(CMD, function () {
|
describe(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
let log = bunyan.createLogger({name: CMD});
|
let log = bunyan.createLogger({name: CMD});
|
||||||
|
let emitter;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
commandSocket: {
|
commandSocket: {
|
||||||
pause: () => {},
|
pause: () => {},
|
||||||
resume: () => {}
|
resume: () => {}
|
||||||
},
|
},
|
||||||
reply: () => when.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
connector: {
|
connector: {
|
||||||
waitForConnection: () => when.resolve({
|
waitForConnection: () => Promise.resolve({
|
||||||
resume: () => {}
|
resume: () => {}
|
||||||
}),
|
}),
|
||||||
end: () => {}
|
end: () => Promise.resolve({})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
mockClient.fs = {
|
mockClient.fs = {
|
||||||
read: () => {}
|
read: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
emitter = new EventEmitter();
|
||||||
|
mockClient.emit = emitter.emit.bind(emitter);
|
||||||
|
mockClient.on = emitter.on.bind(emitter);
|
||||||
|
mockClient.once = emitter.once.bind(emitter);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
afterEach(() => sandbox.restore());
|
afterEach(() => sandbox.restore());
|
||||||
@@ -53,10 +60,11 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
it('// unsuccessful | connector times out', () => {
|
it('// unsuccessful | connector times out', () => {
|
||||||
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
|
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
|
||||||
return when.reject(new when.TimeoutError());
|
return Promise.reject(new Promise.TimeoutError());
|
||||||
});
|
});
|
||||||
|
|
||||||
return cmdFn({log, command: {arg: 'test.txt'} })
|
|
||||||
|
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||||
});
|
});
|
||||||
@@ -64,12 +72,28 @@ describe(CMD, function () {
|
|||||||
|
|
||||||
it('// unsuccessful | connector errors out', () => {
|
it('// unsuccessful | connector errors out', () => {
|
||||||
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
|
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
|
||||||
return when.reject(new Error('test'));
|
return Promise.reject(new Error('test'));
|
||||||
});
|
});
|
||||||
|
|
||||||
return cmdFn({log, command: {arg: 'test.txt'} })
|
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockClient.reply.args[0][0]).to.equal(551);
|
expect(mockClient.reply.args[0][0]).to.equal(551);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('// unsuccessful | emits error event', () => {
|
||||||
|
sandbox.stub(mockClient.connector, 'waitForConnection').callsFake(function () {
|
||||||
|
return Promise.reject(new Error('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
let errorEmitted = false;
|
||||||
|
emitter.once('RETR', (err) => {
|
||||||
|
errorEmitted = !!err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||||
|
.then(() => {
|
||||||
|
expect(errorEmitted).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user