Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cfb94829e | ||
|
|
7a51ed9d07 | ||
|
|
cf7c678136 | ||
|
|
c716470b5b | ||
|
|
f443df52a7 | ||
|
|
08ab571f4c | ||
|
|
9035aefa4d | ||
|
|
89bd13dcb1 | ||
|
|
e8efe32126 | ||
|
|
18277e93ea | ||
|
|
a8cdcb0eb3 | ||
|
|
4ba583420a | ||
|
|
51049f631a | ||
|
|
50e8c455d6 | ||
|
|
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 | ||
|
|
ed086e576a | ||
|
|
31f0f3b0dc | ||
|
|
d763820c86 | ||
|
|
f3183314cc | ||
|
|
dde7b36c46 | ||
|
|
00af9e7e61 | ||
|
|
99a885cd44 | ||
|
|
443051d753 | ||
|
|
27ecc4d835 | ||
|
|
c8526be1f4 | ||
|
|
e0b11ff480 | ||
|
|
58b9d8db9d | ||
|
|
fa121ba0fd | ||
|
|
2e02dc20ad | ||
|
|
8aeb6976d2 | ||
|
|
84a68ae03c | ||
|
|
9dfc80b99d | ||
|
|
090e3d8105 | ||
|
|
c3b0dbf5b0 | ||
|
|
69a5133936 | ||
|
|
5394908a6b | ||
|
|
3e7bd5bcf9 | ||
|
|
175b422c5f | ||
|
|
b2a9851204 | ||
|
|
977dd1579a | ||
|
|
176b2b7ca8 | ||
|
|
63777c0d74 | ||
|
|
9be8ffa60d | ||
|
|
b8cd6022e1 | ||
|
|
0618a3c675 | ||
|
|
3c533a5fbc | ||
|
|
3d0a58ca15 | ||
|
|
4b4c809af8 | ||
|
|
a234534de0 | ||
|
|
635fb35341 | ||
|
|
51a6448ac2 | ||
|
|
4d8a69615c | ||
|
|
ab5a2e9641 | ||
|
|
a7f25accd2 | ||
|
|
c49a361c36 | ||
|
|
d3d65aa5cf | ||
|
|
e53848f881 | ||
|
|
b5cf75b09f | ||
|
|
dd0a790519 | ||
|
|
e25ee55865 | ||
|
|
0433bb48cd | ||
|
|
350c2b3e81 | ||
|
|
887bf1fa58 | ||
|
|
70a62f1da1 | ||
|
|
3a1afdb694 | ||
|
|
10127b32e5 | ||
|
|
e1aad3e021 | ||
|
|
a254e6c5f3 | ||
|
|
c46d6086ea | ||
|
|
88f02cd498 | ||
|
|
2cc5d54d7f | ||
|
|
f127d0e7b6 | ||
|
|
13048a96bd | ||
|
|
f6355e66c3 | ||
|
|
6e79e958cc | ||
|
|
db7d88f411 | ||
|
|
323ee62110 | ||
|
|
1e446a7801 | ||
|
|
977fbd4190 | ||
|
|
d5d1b98b04 | ||
|
|
df0a4d640c | ||
|
|
73274191fe | ||
|
|
37c3da3a62 | ||
|
|
9bece5f946 | ||
|
|
83947142df | ||
|
|
c54045e0b9 | ||
|
|
cf71243729 | ||
|
|
7fb43a5790 | ||
|
|
e99059125e | ||
|
|
954e9a1252 | ||
|
|
2b9e163958 | ||
|
|
c6a49d2191 | ||
|
|
14e5f87cc3 | ||
|
|
580b8d6eae | ||
|
|
a75d63df92 | ||
|
|
301ae110e8 | ||
|
|
4d69b48466 | ||
|
|
ec010697bb | ||
|
|
cf3d543f1a | ||
|
|
69bec2b01c | ||
|
|
2eac41d127 | ||
|
|
eb32f93fc6 | ||
|
|
095423606e | ||
|
|
61cf1bda39 | ||
|
|
75f847ed5d | ||
|
|
ad4b32fc13 | ||
|
|
be3c57bed0 | ||
|
|
dc7dd1075c | ||
|
|
543e6cc1cc | ||
|
|
5c1f8f7a65 | ||
|
|
557995a1a9 | ||
|
|
45eca5afe0 | ||
|
|
695e594d97 | ||
|
|
97b55fc92c | ||
|
|
577066850b | ||
|
|
0ec989cf1e | ||
|
|
568833e216 | ||
|
|
6b0c06e588 | ||
|
|
acd485a571 | ||
|
|
2b2ca45673 | ||
|
|
a62b6f9559 | ||
|
|
84d54cbc2b | ||
|
|
ef6134d91b | ||
|
|
043d9369cc | ||
|
|
6b81748fd7 | ||
|
|
0f4f5cdbd7 | ||
|
|
0293752635 | ||
|
|
aa278105f9 | ||
|
|
bbe0bf2942 | ||
|
|
846df72e24 | ||
|
|
8227c512dd | ||
|
|
b7e17af99e | ||
|
|
9276f7f448 | ||
|
|
99a0ebd536 | ||
|
|
0c5f8562d5 | ||
|
|
5154743a3a | ||
|
|
6654f2c25c | ||
|
|
83540d268a | ||
|
|
795c3d7c65 | ||
|
|
f6d1a3828a | ||
|
|
e5b10c5858 | ||
|
|
4a6ab71731 | ||
|
|
ccc053ac8d | ||
|
|
43cb87a660 | ||
|
|
1d07ff86d5 |
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/*
|
||||
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
|
||||
19
.github/pull_request_template.md
vendored
Normal file
19
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
---
|
||||
|
||||
### Acceptance Checklist
|
||||
|
||||
- [ ] **Story**: Code is focused on the linked stories and solves a problem
|
||||
- One of:
|
||||
- [ ] **For Bugs**: A unit test is added or an existing one modified
|
||||
- [ ] **For Features**: New unit tests are added covering the new functions or modifications
|
||||
- [ ] Code Documentation changes are included for public interfaces and important / complex additions
|
||||
- [ ] External Documentation is included for API changes, or other external facing interfaces
|
||||
|
||||
### Review Checklist
|
||||
|
||||
- [ ] The code does not duplicate existing functionality that exists elsewhere
|
||||
- [ ] The code has been linted and follows team practices and style guidelines
|
||||
- [ ] The changes in the PR are relevant to the title
|
||||
- changes not related should be moved to a different PR
|
||||
- [ ] All errors or error handling is actionable, and informs the viewer on how to correct it
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
test_tmp/
|
||||
|
||||
node_modules/
|
||||
|
||||
dist/
|
||||
reports/
|
||||
.env
|
||||
npm-debug.log
|
||||
|
||||
12
.travis.yml
12
.travis.yml
@@ -1,12 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
install: npm install
|
||||
|
||||
script:
|
||||
- npm run verify:js
|
||||
- npm run test:coverage
|
||||
|
||||
after_success:
|
||||
- if [ $TRAVIS_BRANCH = 'master' ]; then npm run semantic-release; fi
|
||||
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]-->
|
||||
# Contributing
|
||||
<p align="center">
|
||||
<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-svr** even better.
|
||||
<h1 align="center">
|
||||
Contributing Guide
|
||||
</h1>
|
||||
|
||||
## Welcome
|
||||
|
||||
<!--[]-->
|
||||
- Thank you for your eagerness to contribute, pull requests are encouraged!
|
||||
|
||||
<!--[CN_GETTING_STARTED]-->
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
git clone <this repo>
|
||||
npm install -g commitizen
|
||||
npm install -g semantic-release-cli
|
||||
npm install
|
||||
```
|
||||
|
||||
|
||||
<!--[]-->
|
||||
|
||||
<!--[RM_DIR_STRUCTURE]-->
|
||||
## Directory Structure
|
||||
|
||||
Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks:
|
||||
```
|
||||
ftp-svr/
|
||||
├──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`
|
||||
|
||||
|
||||
|
||||
|
||||
<!--[]-->
|
||||
## Guidelines
|
||||
|
||||
- Any new fixes are features should include new or updated [tests](/test).
|
||||
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
|
||||
- Submit your pull requests to the `master` branch, these will normally be merged into a 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`
|
||||
22
LICENSE
22
LICENSE
@@ -1,9 +1,21 @@
|
||||
ftp-svr Copyright (c) 2017 Tyler Stewart
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Copyright (c) 2019 Tyler Stewart
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
415
README.md
415
README.md
@@ -1,39 +1,416 @@
|
||||
# ftp-svr [](https://github.com/semantic-release/semantic-release) [](http://commitizen.github.io/cz-cli/)
|
||||
<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>
|
||||
|
||||
<!--[]-->
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/ftp-srv">
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
<a href="https://circleci.com/gh/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
|
||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
||||
|
||||
## Features
|
||||
- Extensible [file systems](#file-system) per connection
|
||||
- Passive and active transfers
|
||||
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
|
||||
- Promise based API
|
||||
|
||||
## Install
|
||||
|
||||
`npm install ftp-svr --save`
|
||||
|
||||
`yarn add ftp-svr`
|
||||
`npm install ftp-srv --save`
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
// Quick start, create an active ftp server.
|
||||
const FtpSrv = require('ftp-srv');
|
||||
|
||||
const port=21;
|
||||
const ftpServer = new FtpSrv({
|
||||
url: "ftp://0.0.0.0:" + port,
|
||||
anonymous: true
|
||||
});
|
||||
|
||||
ftpServer.on('login', ({ connection, username, password }, resolve, reject) => {
|
||||
if(username === 'anonymous' && password === 'anonymous'){
|
||||
return resolve({ root:"/" });
|
||||
}
|
||||
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||
});
|
||||
|
||||
ftpServer.listen().then(() => {
|
||||
console.log('Ftp server is starting...')
|
||||
});
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `new FtpSrv({options})`
|
||||
#### url
|
||||
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
|
||||
Supported protocols:
|
||||
- `ftp` Plain FTP
|
||||
- `ftps` Implicit FTP over TLS
|
||||
|
||||
_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"`
|
||||
|
||||
#### `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');
|
||||
|
||||
const nets = networkInterfaces();
|
||||
function getNetworks() {
|
||||
let networks = {};
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
__Default:__ `null`
|
||||
|
||||
#### `tls`
|
||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
||||
__Default:__ `false`
|
||||
|
||||
#### `anonymous`
|
||||
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
||||
Can also set as a string which allows users to authenticate using the username provided.
|
||||
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
||||
__Default:__ `false`
|
||||
|
||||
#### `blacklist`
|
||||
Array of commands that are not allowed.
|
||||
Response code `502` is sent to clients sending one of these commands.
|
||||
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
||||
__Default:__ `[]`
|
||||
|
||||
#### `whitelist`
|
||||
Array of commands that are only allowed.
|
||||
Response code `502` is sent to clients sending any other command.
|
||||
__Default:__ `[]`
|
||||
|
||||
#### `file_format`
|
||||
Sets the format to use for file stat queries such as `LIST`.
|
||||
__Default:__ `"ls"`
|
||||
__Allowable values:__
|
||||
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
|
||||
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
|
||||
- `function () {}` A custom function returning a format or promise for one.
|
||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
||||
|
||||
#### `log`
|
||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||
|
||||
#### `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
|
||||
|
||||
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`.
|
||||
|
||||
### `client-error`
|
||||
```js
|
||||
ftpServer.on('client-error', ({connection, context, error}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when an error arises in the client connection.
|
||||
|
||||
`connection` [client class object](src/connection.js)
|
||||
`context` string of where the error occurred
|
||||
`error` error object
|
||||
|
||||
### `disconnect`
|
||||
```js
|
||||
ftpServer.on('disconnect', ({connection, id, newConnectionCount}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when a client has disconnected.
|
||||
|
||||
`connection` [client class object](src/connection.js)
|
||||
`id` string of the disconnected connection id
|
||||
`id` number of the new connection count (exclusive the disconnected client connection)
|
||||
|
||||
### `closed`
|
||||
```js
|
||||
ftpServer.on('closed', ({}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when the FTP server has been closed.
|
||||
|
||||
### `closing`
|
||||
```js
|
||||
ftpServer.on('closing', ({}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when the FTP server has started closing.
|
||||
|
||||
### `login`
|
||||
```js
|
||||
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.
|
||||
|
||||
`connection` [client class object](src/connection.js)
|
||||
`username` string of username from `USER` command
|
||||
`password` string of password from `PASS` command
|
||||
`resolve` takes an object of arguments:
|
||||
- `fs`
|
||||
- Set a custom file system class for this connection to use.
|
||||
- See [File System](#file-system) for implementation details.
|
||||
- `root`
|
||||
- If `fs` is not provided, this will set the root directory for the connection.
|
||||
- The user cannot traverse lower than this directory.
|
||||
- `cwd`
|
||||
- If `fs` is not provided, will set the starting directory for the connection
|
||||
- This is relative to the `root` directory.
|
||||
- `blacklist`
|
||||
- Commands that are forbidden for only this connection
|
||||
- `whitelist`
|
||||
- If set, this connection will only be able to use the provided commands
|
||||
|
||||
`reject` takes an error object
|
||||
|
||||
### `server-error`
|
||||
```js
|
||||
ftpServer.on('server-error', ({error}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when an error arises in the FTP server.
|
||||
|
||||
`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
|
||||
|
||||
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
||||
|
||||
## File System
|
||||
The default [file system](src/fs.js) can be overwritten to use your own implementation.
|
||||
This can allow for virtual file systems, and more.
|
||||
Each connection can set it's own file system based on the user.
|
||||
|
||||
The default file system is exported and can be extended as needed:
|
||||
```js
|
||||
const {FtpSrv, FileSystem} = require('ftp-srv');
|
||||
|
||||
class MyFileSystem extends FileSystem {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
}
|
||||
|
||||
get(fileName) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Custom file systems can implement the following variables depending on the developers needs:
|
||||
|
||||
### Methods
|
||||
#### [`currentDirectory()`](src/fs.js#L40)
|
||||
Returns a string of the current working directory
|
||||
__Used in:__ `PWD`
|
||||
|
||||
#### [`get(fileName)`](src/fs.js#L44)
|
||||
Returns a file stat object of file or directory
|
||||
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||
|
||||
#### [`list(path)`](src/fs.js#L50)
|
||||
Returns array of file and directory stat objects
|
||||
__Used in:__ `LIST`, `NLST`, `STAT`
|
||||
|
||||
#### [`chdir(path)`](src/fs.js#L67)
|
||||
Returns new directory relative to current directory
|
||||
__Used in:__ `CWD`, `CDUP`
|
||||
|
||||
#### [`mkdir(path)`](src/fs.js#L114)
|
||||
Returns a path to a newly created directory
|
||||
__Used in:__ `MKD`
|
||||
|
||||
#### [`write(fileName, {append, start})`](src/fs.js#L79)
|
||||
Returns a writable stream
|
||||
Options:
|
||||
`append` if true, append to existing file
|
||||
`start` if set, specifies the byte offset to write to
|
||||
__Used in:__ `STOR`, `APPE`
|
||||
|
||||
#### [`read(fileName, {start})`](src/fs.js#L90)
|
||||
Returns a readable stream
|
||||
Options:
|
||||
`start` if set, specifies the byte offset to read from
|
||||
__Used in:__ `RETR`
|
||||
|
||||
#### [`delete(path)`](src/fs.js#L105)
|
||||
Delete a file or directory
|
||||
__Used in:__ `DELE`
|
||||
|
||||
#### [`rename(from, to)`](src/fs.js#L120)
|
||||
Renames a file or directory
|
||||
__Used in:__ `RNFR`, `RNTO`
|
||||
|
||||
#### [`chmod(path)`](src/fs.js#L126)
|
||||
Modifies a file or directory's permissions
|
||||
__Used in:__ `SITE CHMOD`
|
||||
|
||||
#### [`getUniqueName(fileName)`](src/fs.js#L131)
|
||||
Returns a unique file name to write to. Client requested filename available if you want to base your function on it.
|
||||
__Used in:__ `STOU`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
|
||||
<!--[]-->
|
||||
|
||||
<!--[RM_LICENSE]-->
|
||||
## License
|
||||
|
||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
||||
|
||||
<!--[]-->
|
||||
## References
|
||||
|
||||
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
||||
|
||||
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,43 +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: [
|
||||
{name: 'accounts'},
|
||||
{name: 'admin'},
|
||||
{name: 'exampleScope'},
|
||||
{name: 'changeMe'}
|
||||
],
|
||||
|
||||
// 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: true
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
// Use JS to support loading of threshold data from external file
|
||||
var coverageConfig = {
|
||||
instrumentation: {
|
||||
root: 'src/'
|
||||
},
|
||||
check: require('./thresholds.json'),
|
||||
reporting: {
|
||||
print: 'both',
|
||||
dir: 'reports/coverage/',
|
||||
reports: [
|
||||
'cobertura',
|
||||
'html',
|
||||
'lcovonly',
|
||||
'html',
|
||||
'json'
|
||||
],
|
||||
'report-config': {
|
||||
cobertura: {
|
||||
file: 'cobertura/coverage.xml'
|
||||
},
|
||||
json: {
|
||||
file: 'json/coverage.json'
|
||||
},
|
||||
lcovonly: {
|
||||
file: 'lcov/lcov.info'
|
||||
},
|
||||
text: {
|
||||
file: null
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = coverageConfig;
|
||||
@@ -1,4 +0,0 @@
|
||||
test/**/*.spec.js
|
||||
--reporter list
|
||||
--no-timeouts
|
||||
--ui bdd
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"global": {
|
||||
"statements": 70,
|
||||
"branches": 60,
|
||||
"functions": 80,
|
||||
"lines": 80
|
||||
},
|
||||
"each": {
|
||||
"statements": 0,
|
||||
"branches": 0,
|
||||
"functions": 0,
|
||||
"lines": 0
|
||||
}
|
||||
}
|
||||
@@ -1,60 +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:
|
||||
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: {}
|
||||
179
ftp-srv.d.ts
vendored
Normal file
179
ftp-srv.d.ts
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { Server } from 'node:net'
|
||||
const EventEmitter = import('events').EventEmitter
|
||||
|
||||
export class FileSystem {
|
||||
readonly connection: FtpConnection
|
||||
readonly root: string
|
||||
readonly cwd: string
|
||||
|
||||
constructor(
|
||||
connection: FtpConnection,
|
||||
{
|
||||
root,
|
||||
cwd
|
||||
}?: {
|
||||
root: any
|
||||
cwd: any
|
||||
}
|
||||
)
|
||||
|
||||
currentDirectory(): string
|
||||
|
||||
get(fileName: string): Promise<any>
|
||||
|
||||
list(path?: string): Promise<any>
|
||||
|
||||
chdir(path?: string): Promise<string>
|
||||
|
||||
write(
|
||||
fileName: string,
|
||||
{
|
||||
append,
|
||||
start
|
||||
}?: {
|
||||
append?: boolean
|
||||
start?: any
|
||||
}
|
||||
): any
|
||||
|
||||
read(
|
||||
fileName: string,
|
||||
{
|
||||
start
|
||||
}?: {
|
||||
start?: any
|
||||
}
|
||||
): Promise<any>
|
||||
|
||||
delete(path: string): Promise<any>
|
||||
|
||||
mkdir(path: string): Promise<any>
|
||||
|
||||
rename(from: string, to: string): Promise<any>
|
||||
|
||||
chmod(path: string, mode: string): Promise<any>
|
||||
|
||||
getUniqueName(fileName: string): string
|
||||
}
|
||||
|
||||
export class GeneralError extends Error {
|
||||
/**
|
||||
* @param message The error message.
|
||||
* @param code Default value is `400`.
|
||||
*/
|
||||
constructor(message: string, code?: number)
|
||||
}
|
||||
|
||||
export class SocketError extends Error {
|
||||
/**
|
||||
* @param message The error message.
|
||||
* @param code Default value is `500`.
|
||||
*/
|
||||
constructor(message: string, code?: number)
|
||||
}
|
||||
|
||||
export class FileSystemError extends Error {
|
||||
/**
|
||||
* @param message The error message.
|
||||
* @param code Default value is `400`.
|
||||
*/
|
||||
constructor(message: string, code?: number)
|
||||
}
|
||||
|
||||
export class ConnectorError extends Error {
|
||||
/**
|
||||
* @param message The error message.
|
||||
* @param code Default value is `400`.
|
||||
*/
|
||||
constructor(message: string, code?: number)
|
||||
}
|
||||
|
||||
export class FtpConnection extends EventEmitter {
|
||||
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
|
||||
random_pasv_port?: boolean
|
||||
greeting?: string | string[]
|
||||
tls?: import('tls').SecureContextOptions | false
|
||||
anonymous?: boolean
|
||||
blacklist?: Array<string>
|
||||
whitelist?: Array<string>
|
||||
file_format?: ((stat: import('fs').Stats) => string) | 'ls' | 'ep'
|
||||
log?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export class FtpServer extends EventEmitter {
|
||||
server: Server
|
||||
constructor(options?: FtpServerOptions)
|
||||
|
||||
readonly isTLS: boolean
|
||||
|
||||
listen(): any
|
||||
|
||||
emitPromise(action: any, ...data: any[]): Promise<any>
|
||||
|
||||
// emit is exported from super class
|
||||
|
||||
setupTLS(_tls: boolean):
|
||||
| boolean
|
||||
| {
|
||||
cert: string
|
||||
key: string
|
||||
ca: string
|
||||
}
|
||||
|
||||
setupGreeting(greet: string): string[]
|
||||
|
||||
setupFeaturesMessage(): string
|
||||
|
||||
disconnectClient(id: string): Promise<any>
|
||||
|
||||
close(): any
|
||||
|
||||
on(
|
||||
event: 'login',
|
||||
listener: (
|
||||
data: {
|
||||
connection: FtpConnection
|
||||
username: string
|
||||
password: string
|
||||
},
|
||||
resolve: (config: {
|
||||
fs?: FileSystem
|
||||
root?: string
|
||||
cwd?: string
|
||||
blacklist?: Array<string>
|
||||
whitelist?: Array<string>
|
||||
}) => void,
|
||||
reject: (err?: Error) => void
|
||||
) => void
|
||||
): 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
|
||||
}
|
||||
|
||||
export { FtpServer as FtpSrv }
|
||||
export default FtpServer
|
||||
6
ftp-srv.js
Normal file
6
ftp-srv.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import FtpSrv from './src/index.js'
|
||||
import FileSystem from './src/fs.js'
|
||||
import ftpErrors from './src/errors.js'
|
||||
|
||||
export default FtpSrv
|
||||
export { FtpSrv, FileSystem, ftpErrors }
|
||||
30
meta/contributors/contributors.js
Normal file
30
meta/contributors/contributors.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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)
|
||||
})
|
||||
25
meta/logo/generate.js
Normal file
25
meta/logo/generate.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
17768
package-lock.json
generated
Normal file
17768
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
122
package.json
122
package.json
@@ -1,80 +1,94 @@
|
||||
{
|
||||
"name": "ftp-svr",
|
||||
"name": "@tabshift/ftp-srv",
|
||||
"version": "0.0.0",
|
||||
"description": "Modern, extensible FTP Server",
|
||||
"keywords": [
|
||||
"ftp",
|
||||
"ftp-server",
|
||||
"ftp-srv",
|
||||
"ftp-svr",
|
||||
"ftpd",
|
||||
"ftpserver",
|
||||
"server"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"bin",
|
||||
"ftp-srv.d.ts"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "ftp-srv.js",
|
||||
"bin": "./bin/index.js",
|
||||
"types": "./ftp-srv.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stewarttylerr/ftp-svr"
|
||||
"url": "https://github.com/autovance/ftp-srv"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-release": "npm-run-all verify test:coverage build ",
|
||||
"build": "cross-env NODE_ENV=production npm run clean:prod",
|
||||
"clean:prod": "rimraf dist/",
|
||||
"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"
|
||||
"pre-release": "npm run verify",
|
||||
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
|
||||
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
"release": {
|
||||
"verifyConditions": "condition-circle",
|
||||
"branch": "main",
|
||||
"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": {
|
||||
"config": "config/release/commitMessageConfig.js"
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.5",
|
||||
"date-fns": "^1.28.0",
|
||||
"lodash": "^4.17.4",
|
||||
"minimist-string": "^1.0.2",
|
||||
"uuid": "^3.0.1",
|
||||
"when": "^3.7.8"
|
||||
"bluebird": "^3.5.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.22.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"chokidar-cli": "1.2.0",
|
||||
"coveralls": "2.11.15",
|
||||
"cross-env": "3.1.4",
|
||||
"cz-customizable": "4.0.0",
|
||||
"cz-customizable-ghooks": "1.5.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"eslint": "3.14.1",
|
||||
"eslint-config-google": "0.7.1",
|
||||
"eslint-plugin-node": "3.0.5",
|
||||
"ftp": "^0.3.10",
|
||||
"husky": "0.13.1",
|
||||
"istanbul": "0.4.5",
|
||||
"mocha": "3.2.0",
|
||||
"npm-run-all": "4.0.1",
|
||||
"rimraf": "2.5.4",
|
||||
"semantic-release": "^6.3.2",
|
||||
"sinon": "^1.17.7",
|
||||
"sinon-as-promised": "^4.0.2"
|
||||
"@commitlint/cli": "^10.0.0",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@icetee/ftp": "^1.0.2",
|
||||
"chai": "^4.2.0",
|
||||
"condition-circle": "^2.0.2",
|
||||
"eslint": "^5.14.1",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^12.3.7",
|
||||
"mocha": "^9.2.2",
|
||||
"rimraf": "^2.6.1",
|
||||
"semantic-release": "^19.0.2",
|
||||
"sinon": "^2.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
"npm": ">=3.9.5"
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(202);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const method = _.upperCase(command._[1]);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
case 'SSL': return handleSSL.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTLS() {
|
||||
return this.reply(504);
|
||||
}
|
||||
|
||||
function handleSSL() {
|
||||
return this.reply(504);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const cwd = require('./cwd');
|
||||
|
||||
module.exports = function(args) {
|
||||
args.command._ = [args.command._[0], '..'];
|
||||
return cwd.call(this, args);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.chdir(command._[1]))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.delete(command._[1]))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function () {
|
||||
const registry = require('./registry');
|
||||
const features = Object.keys(registry)
|
||||
.filter(cmd => registry[cmd].hasOwnProperty('feat'))
|
||||
.reduce((feats, cmd) => _.concat(feats, registry[cmd].feat), [])
|
||||
.map(feat => ` ${feat}`);
|
||||
return this.reply(211, 'Extensions supported', ...features, 'END');
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const registry = require('./registry');
|
||||
const directive = _.upperCase(command._[1]);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
const {syntax, help, obsolete} = registry[directive];
|
||||
const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]);
|
||||
return this.reply(214, ...reply);
|
||||
} else {
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
}
|
||||
};
|
||||
@@ -1,35 +1,76 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
import _ from 'lodash'
|
||||
import Promise from 'bluebird'
|
||||
import REGISTRY from './registry.js'
|
||||
|
||||
class FtpCommands {
|
||||
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/)
|
||||
|
||||
export default class FtpCommands {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.registry = require('./registry');
|
||||
this.previousCommand = {};
|
||||
this.connection = connection
|
||||
this.previousCommand = {}
|
||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd))
|
||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd))
|
||||
}
|
||||
|
||||
parse(message) {
|
||||
const strippedMessage = message.replace(/"/g, '')
|
||||
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) => {
|
||||
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param)
|
||||
else arg.push(param)
|
||||
return { arg, flags }
|
||||
},
|
||||
{ arg: [], flags: [] }
|
||||
)
|
||||
|
||||
const command = {
|
||||
directive,
|
||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||
flags: params.flags,
|
||||
raw: message
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
handle(command) {
|
||||
const log = this.connection.log.child({command});
|
||||
log.trace('Handle command');
|
||||
if (typeof command === 'string') command = this.parse(command)
|
||||
|
||||
if (!this.registry.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
// Obfuscate password from logs
|
||||
const logCommand = _.clone(command)
|
||||
if (logCommand.directive === 'PASS') logCommand.arg = '********'
|
||||
|
||||
const log = this.connection.log.child({ directive: command.directive })
|
||||
log.trace({ command: logCommand }, 'Handle command')
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(502, `Command not allowed: ${command.directive}`)
|
||||
}
|
||||
|
||||
const commandRegister = this.registry[command.directive];
|
||||
if (!commandRegister.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530);
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, `Command blacklisted: ${command.directive}`)
|
||||
}
|
||||
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, `Command not whitelisted: ${command.directive}`)
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive]
|
||||
const commandFlags = _.get(commandRegister, 'flags', {})
|
||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530, `Command requires authentication: ${command.directive}`)
|
||||
}
|
||||
|
||||
if (!commandRegister.handler) {
|
||||
return this.connection.reply(502, 'Handler not set on command');
|
||||
return this.connection.reply(502, `Handler not set on command: ${command.directive}`)
|
||||
}
|
||||
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
return when.try(handler, { log, command, previous_command: this.previousCommand })
|
||||
.finally(() => {
|
||||
this.previousCommand = _.clone(command);
|
||||
});
|
||||
const handler = commandRegister.handler.bind(this.connection)
|
||||
return Promise.resolve(handler({ log, command, previous_command: this.previousCommand })).then(() => {
|
||||
this.previousCommand = _.clone(command)
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = FtpCommands;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../helpers/file-stat');
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = function ({log, command, previous_command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
let dataSocket;
|
||||
const directory = command._[1] || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.list(directory)))
|
||||
.then(files => {
|
||||
const getFileMessage = (file) => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileMessage(file);
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: dataSocket
|
||||
};
|
||||
})
|
||||
return this.reply(150)
|
||||
.then(() => this.reply(...fileList));
|
||||
})
|
||||
.then(() => {
|
||||
return this.reply(226, 'Transfer OK');
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const format = require('date-fns/format');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.get(command._[1]))
|
||||
.then(fileStat => {
|
||||
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime)
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.mkdir(command._[1]))
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'S' ? 200 : 504);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(200);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(501);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.username && this.authenticated &&
|
||||
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command._[1];
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
const PassiveConnector = require('../connector/passive');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = this.server.url.hostname;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const ActiveConnector = require('../connector/active');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
const rawConnection = command._[1].split(',');
|
||||
if (rawConnection.length !== 6) return this.reply(425);
|
||||
|
||||
const ip = rawConnection.slice(0, 4).join('.');
|
||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(socket => {
|
||||
return this.reply(200);
|
||||
})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.currentDirectory())
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.close(221);
|
||||
}
|
||||
14
src/commands/registration/abor.js
Normal file
14
src/commands/registration/abor.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
directive: 'ABOR',
|
||||
handler: function () {
|
||||
return this.connector
|
||||
.waitForConnection()
|
||||
.then((socket) => {
|
||||
return this.reply(426, { socket }).then(() => this.reply(226))
|
||||
})
|
||||
.catch(() => this.reply(225))
|
||||
.then(() => this.connector.end())
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
}
|
||||
11
src/commands/registration/allo.js
Normal file
11
src/commands/registration/allo.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'ALLO',
|
||||
handler: function () {
|
||||
return this.reply(202)
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Allocate sufficient disk space to receive a file',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
}
|
||||
10
src/commands/registration/appe.js
Normal file
10
src/commands/registration/appe.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import stor from './stor.js'
|
||||
|
||||
export default {
|
||||
directive: 'APPE',
|
||||
handler: function (args) {
|
||||
return stor.handler.call(this, args)
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Append to a file'
|
||||
}
|
||||
43
src/commands/registration/auth.js
Normal file
43
src/commands/registration/auth.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import _ from 'lodash'
|
||||
import tls from 'node:tls'
|
||||
|
||||
export default {
|
||||
directive: 'AUTH',
|
||||
handler: function ({ command } = {}) {
|
||||
const method = _.upperCase(command.arg)
|
||||
|
||||
switch (method) {
|
||||
case 'TLS':
|
||||
return handleTLS.call(this)
|
||||
default:
|
||||
return this.reply(504)
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} <type>',
|
||||
description: 'Set authentication mechanism',
|
||||
flags: {
|
||||
no_auth: true,
|
||||
feat: 'AUTH TLS'
|
||||
}
|
||||
}
|
||||
|
||||
function handleTLS() {
|
||||
if (!this.server.options.tls) return this.reply(502)
|
||||
if (this.secure) return this.reply(202)
|
||||
|
||||
return this.reply(234).then(() => {
|
||||
const secureContext = tls.createSecureContext(this.server.options.tls)
|
||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
})
|
||||
;['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
|
||||
function forwardEvent() {
|
||||
this.emit.apply(this, arguments)
|
||||
}
|
||||
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event))
|
||||
})
|
||||
this.commandSocket = secureSocket
|
||||
this.secure = true
|
||||
})
|
||||
}
|
||||
11
src/commands/registration/cdup.js
Normal file
11
src/commands/registration/cdup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import cwd from './cwd.js'
|
||||
|
||||
export default {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function (args) {
|
||||
args.command.arg = '..'
|
||||
return cwd.handler.call(this, args)
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
}
|
||||
22
src/commands/registration/cwd.js
Normal file
22
src/commands/registration/cwd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Promise from 'bluebird'
|
||||
import escapePath from '../../helpers/escape-path.js'
|
||||
|
||||
export default {
|
||||
directive: ['CWD', 'XCWD'],
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.chdir(command.arg))
|
||||
.then((cwd) => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined
|
||||
return this.reply(250, path)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Change working directory'
|
||||
}
|
||||
20
src/commands/registration/dele.js
Normal file
20
src/commands/registration/dele.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'DELE',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.delete(command.arg))
|
||||
.then(() => {
|
||||
return this.reply(250)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Delete file'
|
||||
}
|
||||
27
src/commands/registration/eprt.js
Normal file
27
src/commands/registration/eprt.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import _ from 'lodash'
|
||||
import ActiveConnector from '../../connector/active.js'
|
||||
|
||||
const FAMILY = {
|
||||
1: 4,
|
||||
2: 6
|
||||
}
|
||||
|
||||
export default {
|
||||
directive: 'EPRT',
|
||||
handler: function ({ log, command } = {}) {
|
||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value()
|
||||
const family = FAMILY[protocol]
|
||||
if (!family) return this.reply(504, 'Unknown network protocol')
|
||||
|
||||
this.connector = new ActiveConnector(this)
|
||||
return this.connector
|
||||
.setupConnection(ip, port, family)
|
||||
.then(() => this.reply(200))
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(err.code || 425, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
}
|
||||
21
src/commands/registration/epsv.js
Normal file
21
src/commands/registration/epsv.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import PassiveConnector from '../../connector/passive.js'
|
||||
|
||||
export default {
|
||||
directive: 'EPSV',
|
||||
handler: function ({ log }) {
|
||||
this.connector = new PassiveConnector(this)
|
||||
return this.connector
|
||||
.setupServer()
|
||||
.then((server) => {
|
||||
const { port } = server.address()
|
||||
|
||||
return this.reply(229, `EPSV OK (|||${port}|)`)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(err.code || 425, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} [<protocol>]',
|
||||
description: 'Initiate passive mode'
|
||||
}
|
||||
30
src/commands/registration/feat.js
Normal file
30
src/commands/registration/feat.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
directive: 'FEAT',
|
||||
handler: function () {
|
||||
const registry = import('../registry')
|
||||
const features = Object.keys(registry)
|
||||
.reduce(
|
||||
(feats, cmd) => {
|
||||
const feat = _.get(registry[cmd], 'flags.feat', null)
|
||||
if (feat) return _.concat(feats, feat)
|
||||
return feats
|
||||
},
|
||||
['UTF8']
|
||||
)
|
||||
.sort()
|
||||
.map((feat) => ({
|
||||
message: ` ${feat}`,
|
||||
raw: true
|
||||
}))
|
||||
return features.length
|
||||
? this.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: this.reply(211, 'No features')
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
30
src/commands/registration/help.js
Normal file
30
src/commands/registration/help.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
directive: 'HELP',
|
||||
handler: function ({ command } = {}) {
|
||||
const registry = import('../registry')
|
||||
const directive = _.upperCase(command.arg)
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`)
|
||||
|
||||
const { syntax, description } = registry[directive]
|
||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description])
|
||||
return this.reply(214, ...reply)
|
||||
} else {
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'))
|
||||
return this.reply(
|
||||
211,
|
||||
'Supported commands:',
|
||||
...supportedCommands,
|
||||
'Use "HELP [command]" for syntax help.'
|
||||
)
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<command>]',
|
||||
description:
|
||||
'Returns usage documentation on a command if specified, else a general help document is returned',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
61
src/commands/registration/list.js
Normal file
61
src/commands/registration/list.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import _ from 'lodash'
|
||||
import Promise from 'bluebird'
|
||||
import getFileStat from '../../helpers/file-stat.js'
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
export default {
|
||||
directive: 'LIST',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const simple = command.directive === 'NLST'
|
||||
|
||||
const path = command.arg || '.'
|
||||
return this.connector
|
||||
.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.get(path)))
|
||||
.then((stat) => (stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat]))
|
||||
.then((files) => {
|
||||
const getFileMessage = (file) => {
|
||||
if (simple) return file.name
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'))
|
||||
}
|
||||
|
||||
return Promise.try(() =>
|
||||
files.map((file) => {
|
||||
const message = getFileMessage(file)
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: this.connector.socket
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
.tap(() => this.reply(150))
|
||||
.then((fileList) => {
|
||||
if (fileList.length) return this.reply({}, ...fileList)
|
||||
return this.reply({ socket: this.connector.socket, useEmptyMessage: true })
|
||||
})
|
||||
.tap(() => this.reply(226))
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err)
|
||||
return this.reply(425, 'No connection established')
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(451, err.message || 'No directory')
|
||||
})
|
||||
.then(() => {
|
||||
this.connector.end()
|
||||
this.commandSocket.resume()
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description:
|
||||
'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
}
|
||||
25
src/commands/registration/mdtm.js
Normal file
25
src/commands/registration/mdtm.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Promise from 'bluebird'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
directive: 'MDTM',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.get(command.arg))
|
||||
.then((fileStat) => {
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS')
|
||||
return this.reply(213, modificationTime)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Return the last-modified time of a specified file',
|
||||
flags: {
|
||||
feat: 'MDTM'
|
||||
}
|
||||
}
|
||||
22
src/commands/registration/mkd.js
Normal file
22
src/commands/registration/mkd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Promise from 'bluebird'
|
||||
import escapePath from '../../helpers/escape-path.js'
|
||||
|
||||
export default {
|
||||
directive: ['MKD', 'XMKD'],
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.mkdir(command.arg, { recursive: true }))
|
||||
.then((dir) => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined
|
||||
return this.reply(257, path)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Make directory'
|
||||
}
|
||||
11
src/commands/registration/mode.js
Normal file
11
src/commands/registration/mode.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'MODE',
|
||||
handler: function ({ command } = {}) {
|
||||
return this.reply(/^S$/i.test(command.arg) ? 200 : 504)
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
}
|
||||
10
src/commands/registration/nlst.js
Normal file
10
src/commands/registration/nlst.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import list from './list.js'
|
||||
|
||||
export default {
|
||||
directive: 'NLST',
|
||||
handler: function (args) {
|
||||
return list.handler.call(this, args)
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
}
|
||||
11
src/commands/registration/noop.js
Normal file
11
src/commands/registration/noop.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'NOOP',
|
||||
handler: function () {
|
||||
return this.reply(200)
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'No operation',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
41
src/commands/registration/opts.js
Normal file
41
src/commands/registration/opts.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
const OPTIONS = {
|
||||
UTF8: utf8,
|
||||
'UTF-8': utf8
|
||||
}
|
||||
|
||||
export default {
|
||||
directive: 'OPTS',
|
||||
handler: function ({ command } = {}) {
|
||||
if (!_.has(command, 'arg')) return this.reply(501)
|
||||
|
||||
const [_option, ...args] = command.arg.split(' ')
|
||||
const option = _.toUpper(_option)
|
||||
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command')
|
||||
return OPTIONS[option].call(this, args)
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
}
|
||||
|
||||
function utf8([setting] = []) {
|
||||
const getEncoding = () => {
|
||||
switch (_.toUpper(setting)) {
|
||||
case 'ON':
|
||||
return 'utf8'
|
||||
case 'OFF':
|
||||
return 'ascii'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const encoding = getEncoding()
|
||||
if (!encoding) return this.reply(501, 'Unknown setting for option')
|
||||
|
||||
this.encoding = encoding
|
||||
|
||||
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`)
|
||||
}
|
||||
25
src/commands/registration/pass.js
Normal file
25
src/commands/registration/pass.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
directive: 'PASS',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.username) return this.reply(503)
|
||||
if (this.authenticated) return this.reply(202)
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command.arg
|
||||
if (!password) return this.reply(501, 'Must provide password')
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(530, err.message || 'Authentication failed')
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <password>',
|
||||
description: 'Authentication password',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
39
src/commands/registration/pasv.js
Normal file
39
src/commands/registration/pasv.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Promise from 'bluebird'
|
||||
import PassiveConnector from '../../connector/passive.js'
|
||||
import { isLocalIP } from '../../helpers/is-local.js'
|
||||
|
||||
export default {
|
||||
directive: 'PASV',
|
||||
handler: function ({ log } = {}) {
|
||||
if (!this.server.options.pasv_url) {
|
||||
return this.reply(502)
|
||||
}
|
||||
|
||||
this.connector = new PassiveConnector(this)
|
||||
return this.connector
|
||||
.setupServer()
|
||||
.then((server) => {
|
||||
const { 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 portByte1 = (port / 256) | 0
|
||||
const portByte2 = port % 256
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(err.code || 425, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Initiate passive mode'
|
||||
}
|
||||
14
src/commands/registration/pbsz.js
Normal file
14
src/commands/registration/pbsz.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
directive: 'PBSZ',
|
||||
handler: function ({ command } = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not supported')
|
||||
this.bufferSize = parseInt(command.arg, 10)
|
||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0')
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Protection Buffer Size',
|
||||
flags: {
|
||||
no_auth: true,
|
||||
feat: 'PBSZ'
|
||||
}
|
||||
}
|
||||
29
src/commands/registration/port.js
Normal file
29
src/commands/registration/port.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import _ from 'lodash'
|
||||
import ActiveConnector from '../../connector/active.js'
|
||||
|
||||
export default {
|
||||
directive: 'PORT',
|
||||
handler: function ({ log, command } = {}) {
|
||||
this.connector = new ActiveConnector(this)
|
||||
|
||||
const rawConnection = _.get(command, 'arg', '').split(',')
|
||||
if (rawConnection.length !== 6) return this.reply(425)
|
||||
|
||||
const ip = rawConnection
|
||||
.slice(0, 4)
|
||||
.map((b) => parseInt(b))
|
||||
.join('.')
|
||||
const portBytes = rawConnection.slice(4).map((p) => parseInt(p))
|
||||
const port = portBytes[0] * 256 + portBytes[1]
|
||||
|
||||
return this.connector
|
||||
.setupConnection(ip, port)
|
||||
.then(() => this.reply(200))
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(err.code || 425, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
}
|
||||
26
src/commands/registration/prot.js
Normal file
26
src/commands/registration/prot.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
directive: 'PROT',
|
||||
handler: function ({ command } = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not supported')
|
||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503)
|
||||
|
||||
switch (_.toUpper(command.arg)) {
|
||||
case 'P':
|
||||
return this.reply(200, 'OK')
|
||||
case 'C':
|
||||
case 'S':
|
||||
case 'E':
|
||||
return this.reply(536, 'Not supported')
|
||||
default:
|
||||
return this.reply(504)
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Data Channel Protection Level',
|
||||
flags: {
|
||||
no_auth: true,
|
||||
feat: 'PROT'
|
||||
}
|
||||
}
|
||||
22
src/commands/registration/pwd.js
Normal file
22
src/commands/registration/pwd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Promise from 'bluebird'
|
||||
import escapePath from '../../helpers/escape-path.js'
|
||||
|
||||
export default {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
handler: function ({ log } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.currentDirectory())
|
||||
.then((cwd) => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined
|
||||
return this.reply(257, path)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Print current working directory'
|
||||
}
|
||||
11
src/commands/registration/quit.js
Normal file
11
src/commands/registration/quit.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'QUIT',
|
||||
handler: function () {
|
||||
return this.close(221, 'Client called QUIT')
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
16
src/commands/registration/rest.js
Normal file
16
src/commands/registration/rest.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
directive: 'REST',
|
||||
handler: function ({ command } = {}) {
|
||||
const arg = _.get(command, 'arg')
|
||||
const byteCount = parseInt(arg, 10)
|
||||
|
||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater')
|
||||
|
||||
this.restByteCount = byteCount
|
||||
return this.reply(350, `Restarting next transfer at ${byteCount}`)
|
||||
},
|
||||
syntax: '{{cmd}} <byte-count>',
|
||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
||||
}
|
||||
66
src/commands/registration/retr.js
Normal file
66
src/commands/registration/retr.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'RETR',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const filePath = command.arg
|
||||
|
||||
return this.connector
|
||||
.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.read(filePath, { start: this.restByteCount })))
|
||||
.then((fsResponse) => {
|
||||
let { stream, clientPath } = fsResponse
|
||||
if (!stream && !clientPath) {
|
||||
stream = fsResponse
|
||||
clientPath = filePath
|
||||
}
|
||||
const serverPath = stream.path || filePath
|
||||
|
||||
const destroyConnection = (connection, reject) => (err) => {
|
||||
if (connection) connection.destroy(err)
|
||||
reject(err)
|
||||
}
|
||||
|
||||
const eventsPromise = new Promise((resolve, reject) => {
|
||||
stream.on('data', (data) => {
|
||||
if (stream) stream.pause()
|
||||
if (this.connector.socket) {
|
||||
this.connector.socket.write(data, () => 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))
|
||||
.then(() => stream.destroy && stream.destroy())
|
||||
})
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err)
|
||||
return this.reply(425, 'No connection established')
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
this.emit('RETR', err)
|
||||
return this.reply(551, err.message)
|
||||
})
|
||||
.then(() => {
|
||||
this.connector.end()
|
||||
this.commandSocket.resume()
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Retrieve a copy of the file'
|
||||
}
|
||||
10
src/commands/registration/rmd.js
Normal file
10
src/commands/registration/rmd.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import dele from './dele.js'
|
||||
|
||||
export default {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (args) {
|
||||
return dele.handler.call(this, args)
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Remove a directory'
|
||||
}
|
||||
22
src/commands/registration/rnfr.js
Normal file
22
src/commands/registration/rnfr.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'RNFR',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const fileName = command.arg
|
||||
return Promise.try(() => this.fs.get(fileName))
|
||||
.then(() => {
|
||||
this.renameFrom = fileName
|
||||
return this.reply(350)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
description: 'Rename from'
|
||||
}
|
||||
30
src/commands/registration/rnto.js
Normal file
30
src/commands/registration/rnto.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'RNTO',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.renameFrom) return this.reply(503)
|
||||
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const from = this.renameFrom
|
||||
const to = command.arg
|
||||
|
||||
return Promise.try(() => this.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return this.reply(250)
|
||||
})
|
||||
.tap(() => this.emit('RNTO', null, to))
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
this.emit('RNTO', err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
.then(() => {
|
||||
delete this.renameFrom
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
description: 'Rename to'
|
||||
}
|
||||
17
src/commands/registration/site/chmod.js
Normal file
17
src/commands/registration/site/chmod.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const [mode, ...fileNameParts] = command.arg.split(' ')
|
||||
const fileName = fileNameParts.join(' ')
|
||||
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
|
||||
.then(() => {
|
||||
return this.reply(200)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(500)
|
||||
})
|
||||
}
|
||||
20
src/commands/registration/site/index.js
Normal file
20
src/commands/registration/site/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Promise from 'bluebird'
|
||||
import _ from 'lodash'
|
||||
|
||||
import registry from './registry.js'
|
||||
|
||||
export default {
|
||||
directive: 'SITE',
|
||||
handler: function ({ log, command } = {}) {
|
||||
const rawSubCommand = _.get(command, 'arg', '')
|
||||
const subCommand = this.commands.parse(rawSubCommand)
|
||||
const subLog = log.child({ subverb: subCommand.directive })
|
||||
|
||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502)
|
||||
|
||||
const handler = registry[subCommand.directive].handler.bind(this)
|
||||
return Promise.resolve(handler({ log: subLog, command: subCommand }))
|
||||
},
|
||||
syntax: '{{cmd}} <subVerb> [...<subParams>]',
|
||||
description: 'Sends site specific commands to remote server'
|
||||
}
|
||||
6
src/commands/registration/site/registry.js
Normal file
6
src/commands/registration/site/registry.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import chmod from './chmod.js'
|
||||
export default {
|
||||
CHMOD: {
|
||||
handler: chmod
|
||||
}
|
||||
}
|
||||
23
src/commands/registration/size.js
Normal file
23
src/commands/registration/size.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'SIZE',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.get(command.arg))
|
||||
.then((fileStat) => {
|
||||
return this.reply(213, { message: fileStat.size })
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Return the size of a file',
|
||||
flags: {
|
||||
feat: 'SIZE'
|
||||
}
|
||||
}
|
||||
43
src/commands/registration/stat.js
Normal file
43
src/commands/registration/stat.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import _ from 'lodash'
|
||||
import Promise from 'bluebird'
|
||||
import getFileStat from '../../helpers/file-stat.js'
|
||||
|
||||
export default {
|
||||
directive: 'STAT',
|
||||
handler: function (args = {}) {
|
||||
const { log, command } = args
|
||||
const path = _.get(command, 'arg')
|
||||
if (path) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.get(path))
|
||||
.then((stat) => {
|
||||
if (stat.isDirectory()) {
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
return Promise.try(() => this.fs.list(path)).then((stats) => [213, stats])
|
||||
}
|
||||
return [212, [stat]]
|
||||
})
|
||||
.then(([code, fileStats]) => {
|
||||
return Promise.map(fileStats, (file) => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'))
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
}
|
||||
}).then((messages) => [code, messages])
|
||||
})
|
||||
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(450, err.message)
|
||||
})
|
||||
} else {
|
||||
return this.reply(211, 'Status OK')
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns the current status'
|
||||
}
|
||||
75
src/commands/registration/stor.js
Normal file
75
src/commands/registration/stor.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export default {
|
||||
directive: 'STOR',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const append = command.directive === 'APPE'
|
||||
const fileName = command.arg
|
||||
|
||||
return this.connector
|
||||
.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.write(fileName, { append, start: this.restByteCount })))
|
||||
.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
|
||||
|
||||
return this.reply(150)
|
||||
.then(() => this.connector.socket && this.connector.socket.resume())
|
||||
.then(() => Promise.all([streamPromise, socketPromise]))
|
||||
.tap(() => this.emit('STOR', null, serverPath))
|
||||
.then(() => this.reply(226, clientPath))
|
||||
.then(() => stream.destroy && stream.destroy())
|
||||
})
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err)
|
||||
return this.reply(425, 'No connection established')
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
this.emit('STOR', err)
|
||||
return this.reply(550, err.message)
|
||||
})
|
||||
.then(() => {
|
||||
this.connector.end()
|
||||
this.commandSocket.resume()
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Store data as a file at the server site'
|
||||
}
|
||||
21
src/commands/registration/stou.js
Normal file
21
src/commands/registration/stou.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Promise from 'bluebird'
|
||||
import stor from './stor.js'
|
||||
|
||||
export default {
|
||||
directive: 'STOU',
|
||||
handler: function (args) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated')
|
||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system')
|
||||
|
||||
const fileName = args.command.arg
|
||||
return Promise.try(() => this.fs.get(fileName))
|
||||
.then(() => Promise.try(() => this.fs.getUniqueName(fileName)))
|
||||
.catch(() => fileName)
|
||||
.then((name) => {
|
||||
args.command.arg = name
|
||||
return stor.handler.call(this, args)
|
||||
})
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Store file uniquely'
|
||||
}
|
||||
11
src/commands/registration/stru.js
Normal file
11
src/commands/registration/stru.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'STRU',
|
||||
handler: function ({ command } = {}) {
|
||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504)
|
||||
},
|
||||
syntax: '{{cmd}} <structure>',
|
||||
description: 'Set file transfer structure',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
}
|
||||
11
src/commands/registration/syst.js
Normal file
11
src/commands/registration/syst.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
directive: 'SYST',
|
||||
handler: function () {
|
||||
return this.reply(215)
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Return system type',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
18
src/commands/registration/type.js
Normal file
18
src/commands/registration/type.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
directive: 'TYPE',
|
||||
handler: function ({ command } = {}) {
|
||||
if (/^A[0-9]?$/i.test(command.arg)) {
|
||||
this.transferType = 'ascii'
|
||||
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
|
||||
this.transferType = 'binary'
|
||||
} else {
|
||||
return this.reply(501)
|
||||
}
|
||||
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`)
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Set the transfer mode, binary (I) or ascii (A)',
|
||||
flags: {
|
||||
feat: 'TYPE A,I,L'
|
||||
}
|
||||
}
|
||||
30
src/commands/registration/user.js
Normal file
30
src/commands/registration/user.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
directive: 'USER',
|
||||
handler: function ({ log, command } = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set')
|
||||
if (this.authenticated) return this.reply(230)
|
||||
|
||||
this.username = command.arg
|
||||
if (!this.username) return this.reply(501, 'Must provide username')
|
||||
|
||||
if (
|
||||
(this.server.options.anonymous === true && this.username === 'anonymous') ||
|
||||
this.username === this.server.options.anonymous
|
||||
) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230)
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err)
|
||||
return this.reply(530, err.message || 'Authentication failed')
|
||||
})
|
||||
}
|
||||
return this.reply(331)
|
||||
},
|
||||
syntax: '{{cmd}} <username>',
|
||||
description: 'Authentication username',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
@@ -1,200 +1,90 @@
|
||||
module.exports = {
|
||||
AUTH: {
|
||||
handler: require('./auth'),
|
||||
syntax: 'AUTH [type]',
|
||||
help: 'Not supported',
|
||||
no_auth: true
|
||||
},
|
||||
USER: {
|
||||
handler: require('./user'),
|
||||
syntax: 'USER [username]',
|
||||
help: 'Authentication username',
|
||||
no_auth: true
|
||||
},
|
||||
PASS: {
|
||||
handler: require('./pass'),
|
||||
syntax: 'PASS [password]',
|
||||
help: 'Authentication password',
|
||||
no_auth: true
|
||||
},
|
||||
SYST: {
|
||||
handler: require('./syst'),
|
||||
syntax: 'SYST',
|
||||
help: 'Return system type',
|
||||
no_auth: true
|
||||
},
|
||||
FEAT: {
|
||||
handler: require('./feat'),
|
||||
syntax: 'FEAT',
|
||||
help: 'Get the feature list implemented by the server',
|
||||
no_auth: true
|
||||
},
|
||||
PWD: {
|
||||
handler: require('./pwd'),
|
||||
syntax: 'PWD',
|
||||
help: 'Print current working directory'
|
||||
},
|
||||
XPWD: {
|
||||
handler: require('./pwd'),
|
||||
syntax: 'XPWD',
|
||||
help: 'Print current working directory'
|
||||
},
|
||||
TYPE: {
|
||||
handler: require('./type'),
|
||||
syntax: 'TYPE',
|
||||
help: 'Set the transfer mode'
|
||||
},
|
||||
PASV: {
|
||||
handler: require('./pasv'),
|
||||
syntax: 'PASV',
|
||||
help: 'Initiate passive mode'
|
||||
},
|
||||
PORT: {
|
||||
handler: require('./port'),
|
||||
syntax: 'PORT [x,x,x,x,y,y]',
|
||||
help: 'Specifies an address and port to which the server should connect'
|
||||
},
|
||||
LIST: {
|
||||
handler: require('./list'),
|
||||
syntax: 'LIST [path(optional)]',
|
||||
help: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
},
|
||||
NLST: {
|
||||
handler: require('./list'),
|
||||
syntax: 'NLST [path(optional)]',
|
||||
help: 'Returns a list of file names in a specified directory'
|
||||
},
|
||||
CWD: {
|
||||
handler: require('./cwd'),
|
||||
syntax: 'CWD [path]',
|
||||
help: 'Change working directory'
|
||||
},
|
||||
XCWD: {
|
||||
handler: require('./cwd'),
|
||||
syntax: 'XCWD [path]',
|
||||
help: 'Change working directory'
|
||||
},
|
||||
CDUP: {
|
||||
handler: require('./cdup'),
|
||||
syntax: 'CDUP',
|
||||
help: 'Change to Parent Directory'
|
||||
},
|
||||
XCUP: {
|
||||
handler: require('./cdup'),
|
||||
syntax: 'XCUP',
|
||||
help: 'Change to Parent Directory'
|
||||
},
|
||||
STOR: {
|
||||
handler: require('./stor'),
|
||||
syntax: 'STOR [path]',
|
||||
help: 'Accept the data and to store the data as a file at the server site'
|
||||
},
|
||||
APPE: {
|
||||
handler: require('./stor'),
|
||||
syntax: 'APPE [path]',
|
||||
help: 'Append to file'
|
||||
},
|
||||
RETR: {
|
||||
handler: require('./retr'),
|
||||
syntax: 'RETR [path]',
|
||||
help: 'Retrieve a copy of the file'
|
||||
},
|
||||
DELE: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'DELE [path]',
|
||||
help: 'Delete file'
|
||||
},
|
||||
RMD: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'RMD [path]',
|
||||
help: 'Remove a directory'
|
||||
},
|
||||
XRMD: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'XRMD [path]',
|
||||
help: 'Remove a directory'
|
||||
},
|
||||
HELP: {
|
||||
handler: require('./help'),
|
||||
syntax: 'HELP [command(optional)]',
|
||||
help: 'Returns usage documentation on a command if specified, else a general help document is returned'
|
||||
},
|
||||
MDTM: {
|
||||
handler: require('./mdtm'),
|
||||
syntax: 'MDTM [path]',
|
||||
help: 'Return the last-modified time of a specified file',
|
||||
feat: 'MDTM'
|
||||
},
|
||||
MKD: {
|
||||
handler: require('./mkd'),
|
||||
syntax: 'MKD [path]',
|
||||
help: 'Make directory'
|
||||
},
|
||||
XMKD: {
|
||||
handler: require('./mkd'),
|
||||
syntax: 'XMKD [path]',
|
||||
help: 'Make directory'
|
||||
},
|
||||
NOOP: {
|
||||
handler: require('./noop'),
|
||||
syntax: 'NOOP',
|
||||
help: 'No operation',
|
||||
no_auth: true
|
||||
},
|
||||
QUIT: {
|
||||
handler: require('./quit'),
|
||||
syntax: 'QUIT',
|
||||
help: 'Disconnect',
|
||||
no_auth: true
|
||||
},
|
||||
RNFR: {
|
||||
handler: require('./rnfr'),
|
||||
syntax: 'RNFR [name]',
|
||||
help: 'Rename from'
|
||||
},
|
||||
RNTO: {
|
||||
handler: require('./rnto'),
|
||||
syntax: 'RNTO [name]',
|
||||
help: 'Rename to'
|
||||
},
|
||||
SIZE: {
|
||||
handler: require('./size'),
|
||||
syntax: 'SIZE [path]',
|
||||
help: 'Return the size of a file',
|
||||
feat: 'SIZE'
|
||||
},
|
||||
STAT: {
|
||||
handler: require('./stat'),
|
||||
syntax: 'SIZE [path(optional)]',
|
||||
help: 'Returns the current status'
|
||||
},
|
||||
SITE: {
|
||||
handler: require('./site'),
|
||||
syntax: 'SITE [subVerb] [subParams]',
|
||||
help: 'Sends site specific commands to remote server'
|
||||
},
|
||||
OPTS: {
|
||||
handler: require('./opts'),
|
||||
syntax: 'OPTS',
|
||||
help: 'Select options for a feature'
|
||||
},
|
||||
import abor from './registration/abor.js'
|
||||
import allo from './registration/allo.js'
|
||||
import appe from './registration/appe.js'
|
||||
import auth from './registration/auth.js'
|
||||
import cdup from './registration/cdup.js'
|
||||
import cwd from './registration/cwd.js'
|
||||
import dele from './registration/dele.js'
|
||||
import feat from './registration/feat.js'
|
||||
import help from './registration/help.js'
|
||||
import list from './registration/list.js'
|
||||
import mdtm from './registration/mdtm.js'
|
||||
import mkd from './registration/mkd.js'
|
||||
import mode from './registration/mode.js'
|
||||
import nlst from './registration/nlst.js'
|
||||
import noop from './registration/noop.js'
|
||||
import opts from './registration/opts.js'
|
||||
import pass from './registration/pass.js'
|
||||
import pasv from './registration/pasv.js'
|
||||
import port from './registration/port.js'
|
||||
import pwd from './registration/pwd.js'
|
||||
import quit from './registration/quit.js'
|
||||
import rest from './registration/rest.js'
|
||||
import retr from './registration/retr.js'
|
||||
import rmd from './registration/rmd.js'
|
||||
import rnfr from './registration/rnfr.js'
|
||||
import rnto from './registration/rnto.js'
|
||||
import site from './registration/site/index.js'
|
||||
import size from './registration/size.js'
|
||||
import stat from './registration/stat.js'
|
||||
import stor from './registration/stor.js'
|
||||
import stou from './registration/stou.js'
|
||||
import stru from './registration/stru.js'
|
||||
import syst from './registration/syst.js'
|
||||
import type from './registration/type.js'
|
||||
import user from './registration/user.js'
|
||||
import pbsz from './registration/pbsz.js'
|
||||
import prot from './registration/prot.js'
|
||||
import eprt from './registration/eprt.js'
|
||||
import epsv from './registration/epsv.js'
|
||||
|
||||
STRU: {
|
||||
handler: require('./stru'),
|
||||
syntax: 'STRU [structure]',
|
||||
help: 'Set file transfer structure',
|
||||
obsolete: true
|
||||
},
|
||||
ALLO: {
|
||||
handler: require('./allo'),
|
||||
syntax: 'ALLO',
|
||||
help: 'Allocate sufficient disk space to receive a file',
|
||||
obsolete: true
|
||||
},
|
||||
MODE: {
|
||||
handler: require('./mode'),
|
||||
syntax: 'MODE [mode]',
|
||||
help: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
/* eslint no-return-assign: 0 */
|
||||
const commands = [
|
||||
abor,
|
||||
allo,
|
||||
appe,
|
||||
auth,
|
||||
cdup,
|
||||
cwd,
|
||||
dele,
|
||||
feat,
|
||||
help,
|
||||
list,
|
||||
mdtm,
|
||||
mkd,
|
||||
mode,
|
||||
nlst,
|
||||
noop,
|
||||
opts,
|
||||
pass,
|
||||
pasv,
|
||||
port,
|
||||
pwd,
|
||||
quit,
|
||||
rest,
|
||||
retr,
|
||||
rmd,
|
||||
rnfr,
|
||||
rnto,
|
||||
site,
|
||||
size,
|
||||
stat,
|
||||
stor,
|
||||
stou,
|
||||
stru,
|
||||
syst,
|
||||
type,
|
||||
user,
|
||||
pbsz,
|
||||
prot,
|
||||
eprt,
|
||||
epsv
|
||||
]
|
||||
|
||||
const registry = commands.reduce((result, cmd) => {
|
||||
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive]
|
||||
aliases.forEach((alias) => (result[alias] = cmd))
|
||||
return result
|
||||
}, {})
|
||||
|
||||
export default registry
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.read(command._[1])))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
dataSocket.on('error', err => stream.emit('error', err));
|
||||
|
||||
stream.on('data', data => dataSocket.write(data, this.encoding));
|
||||
stream.on('end', () => resolve(this.reply(226)));
|
||||
stream.on('error', err => reject(err));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(551);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = command._[1];
|
||||
return when(this.fs.get(fileName))
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const from = this.renameFrom;
|
||||
const to = command._[1];
|
||||
|
||||
return when(this.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const [, mode, fileName] = command._;
|
||||
return this.fs.chmod(fileName, parseInt(mode, 8))
|
||||
.then(() => {
|
||||
return this.reply(200);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(500);
|
||||
})
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const registry = require('./registry');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
let [, subverb, ...subparameters] = command._;
|
||||
subverb = _.upperCase(subverb);
|
||||
const subLog = log.child({subverb});
|
||||
|
||||
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
|
||||
|
||||
const subCommand = {
|
||||
_: [subverb, ...subparameters],
|
||||
directive: subverb
|
||||
}
|
||||
const handler = registry[subverb].handler.bind(this);
|
||||
return when.try(handler, { log: subLog, command: subCommand });
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
CHMOD: {
|
||||
handler: require('./chmod')
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.get(command._[1]))
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../helpers/file-stat');
|
||||
|
||||
module.exports = function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = command._[1];
|
||||
if (path) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.get(path))
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
return when(this.fs.list(path))
|
||||
.then(files => {
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
})
|
||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
||||
})
|
||||
} else {
|
||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450);
|
||||
})
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const append = command.directive === 'APPE';
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.write(command._[1], {append})))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
stream.on('error', err => dataSocket.emit('error', err));
|
||||
|
||||
dataSocket.on('end', () => resolve(this.reply(226)));
|
||||
dataSocket.on('error', err => reject(err));
|
||||
dataSocket.on('data', data => stream.write(data, this.encoding));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(553);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'F' ? 200 : 504);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(215);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const encoding = _.upperCase(command._[1]);
|
||||
switch (encoding) {
|
||||
case 'A':
|
||||
this.encoding = 'utf-8';
|
||||
case 'I':
|
||||
case 'L':
|
||||
this.encoding = 'binary';
|
||||
return this.reply(200);
|
||||
default:
|
||||
return this.reply(501);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user