Compare commits

..

3 Commits

Author SHA1 Message Date
Tyler Stewart
90b4b78ddc chore: update confit 2018-01-21 15:01:11 -07:00
Tyler Stewart
5e20556a0e WIP(docs): outline docs in own folder 2018-01-21 14:57:49 -07:00
Tyler Stewart
74760189ed WIP(readme): improve docs in readme 2018-01-21 14:57:49 -07:00
131 changed files with 10061 additions and 20985 deletions

View File

@@ -1,112 +1,140 @@
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 >>
version: 2
jobs:
lint:
executor: node-lts
build_node_8:
docker:
- image: circleci/node:8
steps:
- checkout
- node/install-packages
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Install
command: npm install
- save_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
paths:
- node_modules
build_node_6:
docker:
- image: circleci/node:6
steps:
- checkout
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Install
command: npm install
- save_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
paths:
- node_modules
lint:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Lint
command: npm run verify
command: npm run verify:js
release_dry_run:
executor: node-lts
test_node_8:
docker:
- image: circleci/node:8
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Dry Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
- restore_cache:
key: npm-install-node-8-{{ checksum "package.json" }}
- run:
name: Test Node 8
command: npm run test:coverage
when: always
- store_test_results:
path: reports
- store_artifacts:
path: reports/coverage
prefix: coverage
test_node_6:
docker:
- image: circleci/node:6
steps:
- checkout
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Test Node 6
command: npm run test:coverage
when: always
- store_test_results:
path: reports
- store_artifacts:
path: reports/coverage
prefix: coverage
release:
executor: node-lts
docker:
- image: circleci/node:8
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Release
- restore_cache:
key: npm-install-node-6-{{ checksum "package.json" }}
- run:
name: Update NPM
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
npm install npm@5
npm install semantic-release@11
- deploy:
name: Semantic Release
command: |
npm run semantic-release || true
workflows:
version: 2
release_scheduled:
triggers:
# 6:03 UTC (mornings) 1 monday
- schedule:
cron: "3 6 * * 1"
test_and_tag:
jobs:
- build_node_8:
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:
only: master
- build_node_6:
filters:
branches:
only: main
only: master
- lint:
requires:
- node/test
- lint
- hold_release:
type: approval
- build_node_8
- test_node_6:
requires:
- release_dry_run
- build_node_6
- test_node_8:
requires:
- build_node_8
- release:
context: npm-deploy-av
requires:
- hold_release
- lint
- test_node_6
- test_node_8
build_and_test:
jobs:
- build_node_8:
filters:
branches:
ignore: master
- build_node_6:
filters:
branches:
ignore: master
- lint:
requires:
- build_node_8
- test_node_6:
requires:
- build_node_6
- test_node_8:
requires:
- build_node_8

View File

@@ -1,2 +1,9 @@
# 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

View File

@@ -1,19 +0,0 @@
---
### 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
View File

@@ -1,6 +1,7 @@
test_tmp/
node_modules/
dist/
reports/
npm-debug.log
.nyc_output/
test_tmp/

24
.nycrc Normal file
View File

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

View File

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

View File

@@ -1,25 +1,211 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="400px" />
</a>
</p>
<!--[CN_HEADING]-->
# Contributing
<h1 align="center">
Contributing Guide
</h1>
Welcome! This document explains how you can contribute to making **ftp-srv** even better.
## Welcome
- Thank you for your eagerness to contribute, pull requests are encouraged!
<!--[]-->
## Guidelines
<!--[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-srv/
├──config/ * configuration files live here (e.g. eslint, verify, testUnit)
├──src/ * source code files should be here
├──dist/ * production-build code should live here
├──reports/ * test reports appear here
├──test/ * unit test specifications live here
├──confit.yml * the project config file generated by 'yo confit'
├──CONTRIBUTING.md * how to contribute to the project
├──README.md * this file
└──package.json * NPM package description file
```
<!--[]-->
<!--[CN_GITFLOW_PROCESS]-->
# GitFlow Development Process
This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/index.html) workflow.
## Create a branch
When you're working on a project, you're going to have a bunch of different features or ideas in progress at any given time some of which are ready to go, and others which are not. Branching exists to help you manage this workflow.
When you create a branch in your project, you're creating an environment where you can try out new ideas. Changes you make on a branch don't affect the `master` branch, so you're free to experiment and commit changes, safe in the knowledge that your branch won't be merged until it's ready to be reviewed by someone you're collaborating with.
###ProTip
Branching is a core concept in Git, and the entire GitHub Flow is based upon it. There's only one rule: anything in the `master` branch is always deployable.
Because of this, it's extremely important that your new branch is created off of `master` when working on a feature or a fix. Your branch name should be descriptive (e.g., `refactor-authentication`, `user-content-cache-key`, `make-retina-avatars`), so that others can see what is being worked on.
## Add commits
Once your branch has been created, it's time to start making changes. Whenever you add, edit, or delete a file, you're making a commit, and adding them to your branch. This process of adding commits keeps track of your progress as you work on a feature branch.
Commits also create a transparent history of your work that others can follow to understand what you've done and why. Each commit has an associated commit message, which is a description explaining why a particular change was made. Furthermore, each commit is considered a separate unit of change. This lets you roll back changes if a bug is found, or if you decide to head in a different direction.
###ProTip
Commit messages are important, especially since Git tracks your changes and then displays them as commits once they're pushed to the server. By writing clear commit messages, you can make it easier for other people to follow along and provide feedback.
## Open a pull request
Pull Requests initiate discussion about your commits. Because they're tightly integrated with the underlying Git repository, anyone can see exactly what changes would be merged if they accept your request.
You can open a Pull Request at any point during the development process: when you have little or no code but want to share some screenshots or general ideas, when you're stuck and need help or advice, or when you're ready for someone to review your work. By using GitHub's @mention system in your Pull Request message, you can ask for feedback from specific people or teams, whether they're down the hall or ten time zones away.
###ProTip
Pull Requests are useful for contributing to open source projects and for managing changes to shared repositories. If you're using a Fork & Pull Model, Pull Requests provide a way to notify project maintainers about the changes you'd like them to consider. If you're using a Shared Repository Model, Pull Requests help start code review and conversation about proposed changes before they're merged into the `master` branch.
## Discuss and review your code
Once a Pull Request has been opened, the person or team reviewing your changes may have questions or comments. Perhaps the coding style doesn't match project guidelines, the change is missing unit tests, or maybe everything looks great and props are in order. Pull Requests are designed to encourage and capture this type of conversation.
You can also continue to push to your branch in light of discussion and feedback about your commits. If someone comments that you forgot to do something or if there is a bug in the code, you can fix it in your branch and push up the change. GitHub will show your new commits and any additional feedback you may receive in the unified Pull Request view.
###ProTip
Pull Request comments are written in Markdown, so you can embed images and emoji, use pre-formatted text blocks, and other lightweight formatting.
## Merge to `master`
Once your PR has passed any the integration tests and received approval to merge, it is time to merge your code into the `master` branch.
Once merged, Pull Requests preserve a record of the historical changes to your code. Because they're searchable, they let anyone go back in time to understand why and how a decision was made.
###ProTip
By incorporating certain keywords into the text of your Pull Request, you can associate issues with code. When your Pull Request is merged, the related issues are also closed. For example, entering the phrase Closes #32 would close issue number 32 in the repository. For more information, check out our help article.
<!--[]-->
<!--[CN_BUILD_TASKS]-->
## Build Tasks
Command | Description
:------ | :----------
<pre>npm run build</pre> | Generate production build into [dist/](dist/) folder
<pre>npm run dev</pre> | Run project in development mode (verify code, and re-verify when code is changed)
<pre>npm start</pre> | Alias for `npm run dev` task
<!--[]-->
<!--[CN_TEST_TASKS]-->
## Test Tasks
Command | Description
:------ | :----------
<pre>npm test</pre> | Alias for `npm run test:unit` task
<pre>npm run test:coverage</pre> | Run instrumented unit tests then verify coverage meets defined thresholds<ul><li>Returns non-zero exit code when coverage does not meet thresholds (as defined in istanbul.js)</li></ul>
<pre>npm run test:unit</pre> | Run unit tests whenever JS source or tests change<ul><li>Uses Mocha</li><li>Code coverage</li><li>Runs continuously (best to run in a separate window)</li></ul>
<pre>npm run test:unit:once</pre> | Run unit tests once<ul><li>Uses Mocha</li><li>Code coverage</li></ul>
<!--[]-->
<!--[CN_VERIFY_TASKS]-->
## Verification (Linting) Tasks
Command | Description
:------ | :----------
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
<pre>npm run verify:js</pre> | Verify Javascript code style and syntax
<pre>npm run verify:js:fix</pre> | Verify Javascript code style and syntax and fix any errors that can be fixed automatically
<pre>npm run verify:js:watch</pre> | Verify Javascript code style and syntax and watch files for changes
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed
<!--[]-->
<!--[CN_COMMIT_TASKS]-->
## Commit Tasks
Command | Description
:------ | :----------
<pre>git status</pre> | Lists the current branch and the status of changed files
<pre>git log</pre> | Displays the commit log (press Q to quit viewing)
<pre>git add .</pre> | Stages all modified & untracked files, ready to be committed
<pre>git cz</pre> | Commit changes to local repository using Commitizen.<ul><li>Asks questions about the change to generate a valid conventional commit message</li><li>Can be customised by modifying [config/release/commitMessageConfig.js](config/release/commitMessageConfig.js)</li></ul>
<pre>git push</pre> | Push local repository changes to remote repository
<!--[]-->
<!--[CN_DOCUMENTATION_TASKS]-->
<!--[]-->
<!--[CN_RELEASE_TASKS]-->
## Release Tasks
Command | Description
:------ | :----------
<pre>npm run commitmsg</pre> | Git commit message hook that validates the commit message conforms to your commit message conventions.
<pre>npm run pre-release</pre> | Verify code, run unit tests, check test coverage, build software. This task is designed to be run before
the `semantic-release` task.
<ul><li>Run `semantic-release-cli setup` once you have a remote repository. See https://github.com/semantic-release/cli for details.</li><li>Semantic-release integrates with Travis CI (or similar tools) to generate release notes
for each release (which appears in the "Releases" section in GitHub) and
publishes the package to NPM (when all the tests are successful) with a semantic version number.
</li></ul>
<pre>npm run prepush</pre> | Git pre-push hook that verifies code and checks unit test coverage meet minimum thresholds.
<pre>npm run upload-coverage</pre> | Uploads code-coverage metrics to Coveralls.io<ul><li>Setup - https://coveralls.zendesk.com/hc/en-us/articles/201347419-Coveralls-currently-supports</li><li>Define an environment variable called COVERALLS_REPO_TOKEN in your build environment with the repo token from https://coveralls.io/github/<repo-name>/settings</li><li>In your CI configuration (e.g. `travis.yml`), call `npm run upload-coverage` if the build is successful.</li></ul>
<!--[]-->
<!--[CN_CHANGING_BUILD_TOOL_CONFIG]-->
## Changing build-tool configuration
There are 3 ways you can change the build-tool configuration for this project:
1. BEST: Modify the Confit configuration file ([confit.yml](confit.yml)) by hand, then re-run `yo confit` and tell it to use the existing configuration.
1. OK: Re-run `yo confit` and provide new answers to the questions. **Confit will attempt to overwrite your existing configuration (it will prompt for confirmation), so make sure you have committed your code to a source control (e.g. git) first**.
There are certain configuration settings which can **only** be specified by hand, in which case the first approach is still best.
1. RISKY: Modify the generated build-tool config by hand. Be aware that if you re-run `yo confit` it will attempt to overwrite your changes. So commit your changes to source control first.
Additionally, the **currently-generated** configuration can be extended in the following ways:
- The task configuration is defined in [package.json](package.json). It is possible to change the task definitions to add your own sub-tasks.
You can also use the `pre...` and `post...` script-name prefixes to run commands before (pre) and after (post) the generated commands.
- The `entryPoint.entryPoints` string in [confit.yml](confit.yml) is designed to be edited manually. It represents the starting-point(s) of the application (like a `main()` function). A NodeJS application has one entry point. E.g. `src/index.js`
<!--[]-->
- Any new fixes are features should include new or updated [tests](/test).
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
## Setup
- Clone the repository `git clone`
- Install dependencies `npm install`

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Tyler Stewart
Copyright (c) 2017 Tyler Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

401
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<a href="https://github.com/autovance/ftp-srv">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
@@ -14,394 +14,63 @@
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
<a href="https://circleci.com/gh/trs/ftp-srv">
<img alt="npm" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
<img alt="npm" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
</a>
</p>
---
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Synopsis
`ftp-srv` is an extensible FTP server solution that enables custom file systems per connection allowing the use of virtual file systems. By default, it acts like a regular FTP server. Just include it in your project and start listening.
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- Passive and Active transfer support
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Extensible [file systems](#file-system) per connection
- Promise based API
## Install
`npm install ftp-srv --save`
## Usage
```
$ npm install ftp-srv
```
## Quick Start
```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
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876');
ftpServer.on('login', ({connection, username, password}, resolve, reject) => {
// fetch credentials from database, file, or hard coded
database.users.fetch({username, password})
.then(() => {
connection.on('STOR', (err, file) => console.log(`Uploaded file: ${file}`));
resolve({
root: '/'
});
})
.catch(() => reject);
});
ftpServer.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...')
ftpServer.listen()
.then(() => {
console.log('Waiting for connections!');
});
```
## 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`
Checkout the [Documentation](/docs).
## Contributing

View File

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

View File

@@ -1,144 +0,0 @@
#!/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()
}

View File

@@ -1,44 +0,0 @@
# 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`
----

View File

@@ -0,0 +1,38 @@
'use strict';
module.exports = {
types: [
{value: 'feat', name: 'feat: A new feature'},
{value: 'fix', name: 'fix: A bug fix'},
{value: 'docs', name: 'docs: Documentation only changes'},
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
{value: 'perf', name: 'perf: A code change that improves performance'},
{value: 'test', name: 'test: Adding missing tests'},
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
{value: 'revert', name: 'revert: Revert to a commit'},
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
scopeOverrides: {
fix: [
{name: 'merge'},
{name: 'style'},
{name: 'e2eTest'},
{name: 'unitTest'}
]
},
*/
allowCustomScopes: true,
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: false
};

View File

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

View File

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

162
config/verify/.eslintrc Normal file
View File

@@ -0,0 +1,162 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

49
confit.yml Normal file
View File

@@ -0,0 +1,49 @@
generator-confit:
app:
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
buildProfile: Latest
copyrightOwner: Tyler Stewart
license: MIT
projectType: node
publicRepository: true
repositoryType: GitHub
paths:
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
config:
configDir: config/
input:
srcDir: src/
unitTestDir: test/
output:
prodDir: dist/
reportDir: reports/
buildJS:
_version: ead8ce4280b07d696aff499a5fca1a933727582f
framework: []
frameworkScripts: []
outputFormat: ES6
sourceFormat: ES6
entryPoint:
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
entryPoints:
main:
- ftp-srv.js
testUnit:
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
testDependencies: []
testFramework: mocha
verify:
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
jsCodingStandard: none
documentation:
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
generateDocs: false
release:
_version: 47f220593935b502abf17cb34a396f692e453c49
checkCodeCoverage: true
commitMessageFormat: Conventional
useSemantic: true
sampleApp:
_version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba
createSampleApp: false
zzfinish: {}

2
docs/README.md Normal file
View File

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

229
ftp-srv.d.ts vendored
View File

@@ -1,179 +1,122 @@
import type { Server } from 'node:net'
const EventEmitter = import('events').EventEmitter
import * as tls from 'tls'
import { Stats } from 'fs'
export class FileSystem {
readonly connection: FtpConnection
readonly root: string
readonly cwd: string
constructor(
connection: FtpConnection,
{
root,
cwd
}?: {
root: any
cwd: any
}
)
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
currentDirectory(): string
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
get(fileName: string): Promise<any>
currentDirectory(): string;
list(path?: string): Promise<any>
get(fileName: string): Promise<any>;
chdir(path?: string): Promise<string>
list(path?: string): Promise<any>;
write(
fileName: string,
{
append,
start
}?: {
append?: boolean
start?: any
}
): any
chdir(path?: string): Promise<string>;
read(
fileName: string,
{
start
}?: {
start?: any
}
): Promise<any>
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
delete(path: string): Promise<any>
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
mkdir(path: string): Promise<any>
delete(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>
mkdir(path: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>
rename(from: string, to: string): Promise<any>;
getUniqueName(fileName: string): string
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
export class GeneralError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number)
}
export class FtpConnection {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
export class SocketError extends Error {
/**
* @param message The error message.
* @param code Default value is `500`.
*/
constructor(message: string, code?: number)
}
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>
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
pasv_range?: number | string,
greeting?: string | string[],
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any
}
export class FtpServer extends EventEmitter {
server: Server
constructor(options?: FtpServerOptions)
export class FtpServer {
constructor(url: string, options?: FtpServerOptions);
readonly isTLS: boolean
readonly isTLS: boolean;
listen(): any
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>
emitPromise(action: any, ...data: any[]): Promise<any>;
// emit is exported from super class
emit(action: any, ...data: any[]): void;
setupTLS(_tls: boolean):
| boolean
| {
cert: string
key: string
ca: string
}
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
};
setupGreeting(greet: string): string[]
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>
disconnectClient(id: string): Promise<any>;
close(): any
close(): any;
on(
event: 'login',
listener: (
data: {
connection: FtpConnection
username: string
password: string
},
resolve: (config: {
fs?: FileSystem
root?: string
cwd?: string
blacklist?: Array<string>
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void
): this
on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
},
resolve: (config: {
fs?: FileSystem,
root?: string,
cwd?: string,
blacklist?: Array<string>,
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void)
on(event: 'disconnect', listener: (data: { connection: FtpConnection; id: string }) => void): this
on(
event: 'client-error',
listener: (data: { connection: FtpConnection; context: string; error: Error }) => void
): this
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void)
}
export { FtpServer as FtpSrv }
export default FtpServer
export {FtpServer as FtpSrv};
export default FtpServer;

View File

@@ -1,6 +1,6 @@
import FtpSrv from './src/index.js'
import FileSystem from './src/fs.js'
import ftpErrors from './src/errors.js'
const FtpSrv = require('./src');
const FileSystem = require('./src/fs');
export default FtpSrv
export { FtpSrv, FileSystem, ftpErrors }
module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem;

23
logo/generate.js Normal file
View File

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

View File

@@ -1,30 +0,0 @@
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)
})

View File

@@ -1,25 +0,0 @@
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())
})

21905
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@tabshift/ftp-srv",
"version": "0.0.0",
"name": "ftp-srv",
"version": "0.0.0-development",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
@@ -14,81 +14,82 @@
"license": "MIT",
"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/autovance/ftp-srv"
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm run verify",
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
"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",
"start": "npm run dev",
"test": "npm run test:unit",
"test:check-coverage": "nyc check-coverage",
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts",
"upload-coverage": "cat reports/coverage/lcov.info | coveralls",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
"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"
},
"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
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
"cz-customizable": {
"config": "config/release/commitMessageConfig.js"
}
},
"dependencies": {
"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"
"lodash": "^4.17.4",
"moment": "^2.19.1",
"uuid": "^3.1.0"
},
"devDependencies": {
"@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",
"@icetee/ftp": "^0.3.15",
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"condition-circle": "^1.6.0",
"coveralls": "2.13.1",
"cross-env": "3.1.4",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "3.5.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"mocha-pretty-bunyan-nyan": "^1.0.4",
"npm-run-all": "4.0.2",
"nyc": "11.1.0",
"rimraf": "2.6.1",
"semantic-release": "^11.0.2",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=12"
"node": ">=6.x",
"npm": ">=3.9.5"
},
"release": {
"verifyConditions": "condition-circle"
}
}

View File

@@ -1,76 +1,71 @@
import _ from 'lodash'
import Promise from 'bluebird'
import REGISTRY from './registry.js'
const _ = require('lodash');
const Promise = require('bluebird');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/)
const REGISTRY = require('./registry');
export default class FtpCommands {
class FtpCommands {
constructor(connection) {
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))
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 strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' ');
const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive,
directive: _.chain(directive).trim().toUpper().value(),
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
}
return command
};
return command;
}
handle(command) {
if (typeof command === 'string') command = this.parse(command)
if (typeof command === 'string') command = this.parse(command);
// Obfuscate password from logs
const logCommand = _.clone(command)
if (logCommand.directive === 'PASS') logCommand.arg = '********'
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')
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}`)
return this.connection.reply(402, 'Command not allowed');
}
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, `Command blacklisted: ${command.directive}`)
return this.connection.reply(502, 'Command blacklisted');
}
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, `Command not whitelisted: ${command.directive}`)
return this.connection.reply(502, 'Command not whitelisted');
}
const commandRegister = REGISTRY[command.directive]
const commandFlags = _.get(commandRegister, 'flags', {})
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}`)
return this.connection.reply(530, 'Command requires authentication');
}
if (!commandRegister.handler) {
return this.connection.reply(502, `Handler not set on command: ${command.directive}`)
return this.connection.reply(502, 'Handler not set on command');
}
const handler = commandRegister.handler.bind(this.connection)
return Promise.resolve(handler({ log, command, previous_command: this.previousCommand })).then(() => {
this.previousCommand = _.clone(command)
})
const handler = commandRegister.handler.bind(this.connection);
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => {
this.previousCommand = _.clone(command);
});
}
}
module.exports = FtpCommands;

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
import _ from 'lodash'
import tls from 'node:tls'
const _ = require('lodash');
const tls = require('tls');
export default {
module.exports = {
directive: 'AUTH',
handler: function ({ command } = {}) {
const method = _.upperCase(command.arg)
handler: function ({command} = {}) {
const method = _.upperCase(command.arg);
switch (method) {
case 'TLS':
return handleTLS.call(this)
default:
return this.reply(504)
case 'TLS': return handleTLS.call(this);
default: return this.reply(504);
}
},
syntax: '{{cmd}} <type>',
@@ -19,25 +17,26 @@ export default {
no_auth: true,
feat: 'AUTH TLS'
}
}
};
function handleTLS() {
if (!this.server.options.tls) return this.reply(502)
if (this.secure) return this.reply(202)
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);
return this.reply(234).then(() => {
const secureContext = tls.createSecureContext(this.server.options.tls)
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
})
;['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
function forwardEvent() {
this.emit.apply(this, arguments)
this.emit.apply(this, arguments);
}
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event))
})
this.commandSocket = secureSocket
this.secure = true
})
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
});
this.commandSocket = secureSocket;
this.secure = true;
});
}

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,22 @@
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
const FAMILY = {
1: 4,
2: 6
}
};
export default {
module.exports = {
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')
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this)
return this.connector
.setupConnection(ip, port, family)
.then(() => this.reply(200))
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'
}
};

View File

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

View File

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

View File

@@ -1,30 +1,24 @@
import _ from 'lodash'
const _ = require('lodash');
export default {
module.exports = {
directive: 'HELP',
handler: function ({ command } = {}) {
const registry = import('../registry')
const directive = _.upperCase(command.arg)
handler: function ({command} = {}) {
const registry = require('../registry');
const directive = _.upperCase(command.arg);
if (directive) {
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`)
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)
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.'
)
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',
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
flags: {
no_auth: true
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,25 @@
export default {
module.exports = {
directive: 'PASS',
handler: function ({ log, command } = {}) {
if (!this.username) return this.reply(503)
if (this.authenticated) return this.reply(202)
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')
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')
})
.then(() => {
return this.reply(230);
})
.catch(err => {
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
},
syntax: '{{cmd}} <password>',
description: 'Authentication password',
flags: {
no_auth: true
}
}
};

View File

@@ -1,39 +1,20 @@
import Promise from 'bluebird'
import PassiveConnector from '../../connector/passive.js'
import { isLocalIP } from '../../helpers/is-local.js'
const PassiveConnector = require('../../connector/passive');
export default {
module.exports = {
directive: 'PASV',
handler: function ({ log } = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502)
}
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const address = this.server.url.hostname;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
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)
})
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
});
},
syntax: '{{cmd}}',
description: 'Initiate passive mode'
}
};

View File

@@ -1,9 +1,9 @@
export default {
module.exports = {
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')
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
},
syntax: '{{cmd}}',
description: 'Protection Buffer Size',
@@ -11,4 +11,4 @@ export default {
no_auth: true,
feat: 'PBSZ'
}
}
};

View File

@@ -1,29 +1,21 @@
import _ from 'lodash'
import ActiveConnector from '../../connector/active.js'
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
export default {
module.exports = {
directive: 'PORT',
handler: function ({ log, command } = {}) {
this.connector = new ActiveConnector(this)
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',')
if (rawConnection.length !== 6) return this.reply(425)
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]
const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return this.connector
.setupConnection(ip, port)
.then(() => this.reply(200))
.catch((err) => {
log.error(err)
return this.reply(err.code || 425, err.message)
})
return this.connector.setupConnection(ip, port)
.then(() => this.reply(200));
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'
}
};

View File

@@ -1,20 +1,17 @@
import _ from 'lodash'
const _ = require('lodash');
export default {
module.exports = {
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)
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
switch (_.toUpper(command.arg)) {
case 'P':
return this.reply(200, 'OK')
case 'P': return this.reply(200, 'OK');
case 'C':
case 'S':
case 'E':
return this.reply(536, 'Not supported')
default:
return this.reply(504)
case 'E': return this.reply(536, 'Not supported');
default: return this.reply(504);
}
},
syntax: '{{cmd}}',
@@ -23,4 +20,4 @@ export default {
no_auth: true,
feat: 'PROT'
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,28 @@
import Promise from 'bluebird'
const Promise = require('bluebird');
export default {
module.exports = {
directive: 'RNTO',
handler: function ({ log, command } = {}) {
if (!this.renameFrom) return this.reply(503)
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')
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
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
})
return Promise.resolve(this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})
.catch(err => {
log.error(err);
return this.reply(550, err.message);
})
.finally(() => {
delete this.renameFrom;
});
},
syntax: '{{cmd}} <name>',
description: 'Rename to'
}
};

View File

@@ -1,17 +1,17 @@
import Promise from 'bluebird'
const Promise = require('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')
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, ...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)
})
}
const [mode, ...fileNameParts] = command.arg.split(' ');
const fileName = fileNameParts.join(' ');
return Promise.resolve(this.fs.chmod(fileName, parseInt(mode, 8)))
.then(() => {
return this.reply(200);
})
.catch(err => {
log.error(err);
return this.reply(500);
});
};

View File

@@ -1,20 +1,20 @@
import Promise from 'bluebird'
import _ from 'lodash'
const Promise = require('bluebird');
const _ = require('lodash');
import registry from './registry.js'
const registry = require('./registry');
export default {
module.exports = {
directive: 'SITE',
handler: function ({ log, command } = {}) {
const rawSubCommand = _.get(command, 'arg', '')
const subCommand = this.commands.parse(rawSubCommand)
const subLog = log.child({ subverb: subCommand.directive })
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)
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 }))
const handler = registry[subCommand.directive].handler.bind(this);
return Promise.resolve(handler({log: subLog, command: subCommand}));
},
syntax: '{{cmd}} <subVerb> [...<subParams>]',
description: 'Sends site specific commands to remote server'
}
};

View File

@@ -1,6 +1,5 @@
import chmod from './chmod.js'
export default {
module.exports = {
CHMOD: {
handler: chmod
handler: require('./chmod')
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,28 @@
export default {
module.exports = {
directive: 'USER',
handler: function ({ log, command } = {}) {
if (this.username) return this.reply(530, 'Username already set')
if (this.authenticated) return this.reply(230)
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')
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
) {
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')
})
.then(() => {
return this.reply(230);
})
.catch(err => {
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
}
return this.reply(331)
return this.reply(331);
},
syntax: '{{cmd}} <username>',
description: 'Authentication username',
flags: {
no_auth: true
}
}
};

View File

@@ -1,90 +1,50 @@
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'
/* 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
]
require('./registration/abor'),
require('./registration/allo'),
require('./registration/appe'),
require('./registration/auth'),
require('./registration/cdup'),
require('./registration/cwd'),
require('./registration/dele'),
require('./registration/feat'),
require('./registration/help'),
require('./registration/list'),
require('./registration/mdtm'),
require('./registration/mkd'),
require('./registration/mode'),
require('./registration/nlst'),
require('./registration/noop'),
require('./registration/opts'),
require('./registration/pass'),
require('./registration/pasv'),
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/rest'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),
require('./registration/rnto'),
require('./registration/site'),
require('./registration/size'),
require('./registration/stat'),
require('./registration/stor'),
require('./registration/stou'),
require('./registration/stru'),
require('./registration/syst'),
require('./registration/type'),
require('./registration/user'),
require('./registration/pbsz'),
require('./registration/prot'),
require('./registration/eprt'),
require('./registration/epsv')
];
const registry = commands.reduce((result, cmd) => {
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive]
aliases.forEach((alias) => (result[alias] = cmd))
return result
}, {})
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
aliases.forEach(alias => result[alias] = cmd);
return result;
}, {});
export default registry
module.exports = registry;

View File

@@ -1,162 +1,140 @@
import _ from 'lodash'
import uuid from 'uuid'
import Promise from 'bluebird'
import { EventEmitter } from 'node:events'
import BaseConnector from './connector/base.js'
import { FileSystem } from './fs.js'
import Commands from './commands/index.js'
import errors from './errors.js'
import DEFAULT_MESSAGE from './messages.js'
const _ = require('lodash');
const uuid = require('uuid');
const Promise = require('bluebird');
export default class FtpConnection extends EventEmitter {
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
const Commands = require('./commands');
const errors = require('./errors');
const DEFAULT_MESSAGE = require('./messages');
class FtpConnection {
constructor(server, options) {
super()
this.server = server
this.id = uuid.v4()
this.commandSocket = options.socket
this.log = options.log.child({ id: this.id, ip: this.ip })
this.commands = new Commands(this)
this.transferType = 'binary'
this.encoding = 'utf8'
this.bufferSize = false
this._restByteCount = 0
this._secure = false
this.server = server;
this.id = uuid.v4();
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
this.encoding = 'utf8';
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;
this.connector = new BaseConnector(this)
this.connector = new BaseConnector(this);
this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error')
this.server.emit('client-error', { connection: this, context: 'commandSocket', error: err })
})
this.commandSocket.on('data', this._handleData.bind(this))
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout')
this.close()
})
this.commandSocket = options.socket;
this.commandSocket.on('error', err => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end()
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy()
this.removeAllListeners()
})
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
});
}
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'))
this.log.trace(messages)
return Promise.mapSeries(messages, (message) => this.commands.handle(message))
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, message => this.commands.handle(message));
}
get ip() {
try {
return this.commandSocket ? this.commandSocket.remoteAddress : undefined
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress;
} catch (ex) {
return null
return null;
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc
this._restByteCount = rbc;
}
get secure() {
return this.server.isTLS || this._secure
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec
this._secure = sec;
}
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.destroy())
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
return Promise.try(() => {
const loginListeners = this.server.listeners('login')
const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500)
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emitPromise('login', { connection: this, username, password })
return this.server.emitPromise('login', {connection: this, username, password});
}
}).then(({ root, cwd, fs, blacklist = [], whitelist = [] } = {}) => {
this.authenticated = true
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist)
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist)
this.fs = fs || new FileSystem(this, { root, cwd })
})
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
this.authenticated = true;
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, {root, cwd});
});
}
reply(options = {}, ...letters) {
const satisfyParameters = () => {
if (typeof options === 'number') options = { code: options } // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters]
if (!letters.length) letters = [{}]
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}];
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise).then((letter) => {
if (!letter) letter = {}
else if (typeof letter === 'string') letter = { message: letter } // allow passing in message as first param
return Promise.resolve(promise)
.then(letter => {
if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket
if (!options.useEmptyMessage) {
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information'
if (!letter.encoding) letter.encoding = this.encoding
}
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then((message) => {
if (!options.useEmptyMessage) {
const seperator = !options.hasOwnProperty('eol')
? letters.length - 1 === index
? ' '
: '-'
: options.eol
? ' '
: '-'
message = !letter.raw
? _.compact([letter.code || options.code, message]).join(seperator)
: message
letter.message = message
} else {
letter.message = ''
}
return letter
})
})
})
}
.then(message => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
return letter;
});
});
});
};
const processLetter = (letter) => {
const processLetter = letter => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace(
{ port: letter.socket.address().port, encoding: letter.encoding, message: letter.message },
'Reply'
)
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message })
return reject(error)
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
if (err) {
this.log.error(err);
return reject(err);
}
resolve()
})
} else {
this.log.trace({ message: letter.message }, 'Could not write message')
reject(new errors.SocketError('Socket not writable'))
}
})
}
resolve();
});
} else reject(new errors.SocketError('Socket not writable'));
});
};
return satisfyParameters()
.then((satisfiedLetters) =>
Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index)
})
)
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message })
})
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch(err => {
this.log.error(err);
});
}
}
module.exports = FtpConnection;

View File

@@ -1,57 +1,49 @@
import { Socket } from 'node:net'
import tls from 'node:tls'
import ip from 'ip'
import Promise from 'bluebird'
import Connector from './base.js'
import errors from '../errors.js'
const {Socket} = require('net');
const tls = require('tls');
const Promise = require('bluebird');
const Connector = require('./base');
export default class Active extends Connector {
class Active extends Connector {
constructor(connection) {
super(connection)
this.type = 'active'
super(connection);
this.type = 'active';
}
waitForConnection({ timeout = 5000, delay = 250 } = {}) {
waitForConnection({timeout = 5000, delay = 250} = {}) {
const checkSocket = () => {
if (this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket)
return Promise.resolve(this.dataSocket);
}
return Promise.resolve()
.delay(delay)
.then(() => checkSocket())
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout)
return checkSocket().timeout(timeout);
}
setupConnection(host, port, family = 4) {
const closeExistingServer = () => Promise.resolve(this.dataSocket ? this.dataSocket.destroy() : undefined)
const closeExistingServer = () => Promise.resolve(
this.dataSocket ? this.dataSocket.destroy() : undefined);
return closeExistingServer().then(() => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
throw new errors.SocketError('The given address is not yours', 500)
}
this.dataSocket = new Socket()
this.dataSocket.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', { connection: this.connection, context: 'dataSocket', error: err })
)
this.dataSocket.connect({ host, port, family }, () => {
this.dataSocket.pause()
return closeExistingServer()
.then(() => {
this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {
this.dataSocket.pause();
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server.options.tls)
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext
})
this.dataSocket = secureSocket
});
this.dataSocket = secureSocket;
}
this.dataSocket.connected = true
})
})
this.dataSocket.connected = true;
});
});
}
}
module.exports = Active;

View File

@@ -1,51 +1,47 @@
import Promise from 'bluebird'
import errors from '../errors.js'
const Promise = require('bluebird');
const errors = require('../errors');
export default class Connector {
class Connector {
constructor(connection) {
this.connection = connection
this.connection = connection;
this.dataSocket = null
this.dataServer = null
this.type = false
this.dataSocket = null;
this.dataServer = null;
this.type = false;
}
get log() {
return this.connection.log
return this.connection.log;
}
get socket() {
return this.dataSocket
return this.dataSocket;
}
get server() {
return this.connection.server
return this.connection.server;
}
waitForConnection() {
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'))
}
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket
this.dataSocket.end(() => socket && socket.destroy())
this.dataSocket = null
}
}
closeServer() {
if (this.dataServer) {
this.dataServer.close()
this.dataServer = null
}
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
}
end() {
this.closeSocket()
this.closeServer()
const closeDataSocket = new Promise(resolve => {
if (this.dataSocket) this.dataSocket.end();
else resolve();
});
const closeDataServer = new Promise(resolve => {
if (this.dataServer) this.dataServer.close(() => resolve());
else resolve();
});
this.type = false
this.connection.connector = new Connector(this)
return Promise.all([closeDataSocket, closeDataServer])
.then(() => {
this.dataSocket = null;
this.dataServer = null;
this.type = false;
});
}
}
module.exports = Connector;

View File

@@ -1,121 +1,101 @@
import net from 'node:net'
import tls from 'node:tls'
import ip from 'ip'
import Promise from 'bluebird'
const net = require('net');
const tls = require('tls');
const Promise = require('bluebird');
import Connector from './base.js'
import errors from '../errors.js'
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
const CONNECT_TIMEOUT = 30 * 1000
export default class Passive extends Connector {
class Passive extends Connector {
constructor(connection) {
super(connection)
this.type = 'passive'
super(connection);
this.type = 'passive';
}
waitForConnection({ timeout = 5000, delay = 50 } = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'))
waitForConnection({timeout = 5000, delay = 250} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket)
return Promise.resolve(this.dataSocket);
}
return Promise.resolve()
.delay(delay)
.then(() => checkSocket())
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout)
return checkSocket().timeout(timeout);
}
setupServer() {
this.closeServer()
return this.server
.getNextPasvPort()
.then((port) => {
this.dataSocket = null
let idleServerTimeout
const closeExistingServer = () => this.dataServer ?
new Promise(resolve => this.dataServer.close(() => resolve())) :
Promise.resolve();
const connectionHandler = (socket) => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error(
{
pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress
},
'Connecting addresses do not match'
)
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
this.log.error({
pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress
}, 'Connecting addresses do not match');
socket.destroy()
return this.connection
.reply(550, 'Remote addresses do not match')
.then(() => this.connection.close())
}
clearTimeout(idleServerTimeout)
this.log.trace({ port, remoteAddress: socket.remoteAddress }, 'Passive connection fulfilled.')
this.dataSocket = socket
this.dataSocket.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', {
connection: this.connection,
context: 'dataSocket',
error: err
})
)
this.dataSocket.once('close', () => this.closeServer())
if (!this.connection.secure) {
this.dataSocket.connected = true
}
socket.destroy();
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {
pauseOnConnect: true
})
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler)
this.dataServer.maxConnections = 1
this.dataServer.on(
'error',
(err) =>
this.server &&
this.server.emit('client-error', {
connection: this.connection,
context: 'dataServer',
error: err
})
)
this.dataServer.once('close', () => {
this.log.trace('Passive server closed')
this.end()
})
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true
})
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(socket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
} else {
this.dataSocket = socket;
}
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.trace('Passive connection closed');
this.end();
});
};
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err)
else {
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT)
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.trace('Passive server closed');
this.dataServer = null;
});
this.log.debug({ port }, 'Passive connection listening')
resolve(this.dataServer)
}
})
})
})
.catch((error) => {
this.log.trace(error.message)
throw error
})
return new Promise((resolve, reject) => {
this.dataServer.listen(port, err => {
if (err) reject(err);
else {
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
});
});
}
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

@@ -1,38 +1,44 @@
class GeneralError extends Error {
constructor(message, code = 400) {
super(message)
this.code = code
this.name = 'GeneralError'
super();
this.code = code;
this.name = 'GeneralError';
this.message = message;
}
}
class SocketError extends Error {
constructor(message, code = 500) {
super(message)
this.code = code
this.name = 'SocketError'
super();
this.code = code;
this.name = 'SocketError';
this.message = message;
}
}
class FileSystemError extends Error {
constructor(message, code = 400) {
super(message)
this.code = code
this.name = 'FileSystemError'
super();
this.code = code;
this.name = 'FileSystemError';
this.message = message;
}
}
class ConnectorError extends Error {
constructor(message, code = 400) {
super(message)
this.code = code
this.name = 'ConnectorError'
super();
this.code = code;
this.name = 'ConnectorError';
this.message = message;
}
}
export default {
module.exports = {
SocketError,
FileSystemError,
ConnectorError,
GeneralError
}
};

181
src/fs.js
View File

@@ -1,145 +1,116 @@
import _ from 'lodash'
import nodePath from 'node:path'
import uuid from 'uuid'
import Promise from 'bluebird'
import { createReadStream, createWriteStream, constants } from 'node:fs'
import fsAsync from './helpers/fs-async.js'
import errors from './errors.js'
const _ = require('lodash');
const nodePath = require('path');
const uuid = require('uuid');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const errors = require('./errors');
const UNIX_SEP_REGEX = /\//g
const WIN_SEP_REGEX = /\\/g
export default class FileSystem {
constructor(connection, { root, cwd } = {}) {
this.connection = connection
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'))
this._root = nodePath.resolve(root || process.cwd())
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = cwd || nodePath.sep;
this.root = root || process.cwd();
}
get root() {
return this._root
}
_resolvePath(path = '.') {
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/')
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath)
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(
nodePath
.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep)
)
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/')
_resolvePath(path = '') {
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
const serverPath = nodePath.join(nodePath.sep, cwd, path);
const fsPath = nodePath.join(this.root, serverPath);
return {
clientPath,
serverPath,
fsPath
}
};
}
currentDirectory() {
return this.cwd
return this.cwd;
}
get(fileName) {
const { fsPath } = this._resolvePath(fileName)
return fsAsync.stat(fsPath).then((stat) => _.set(stat, 'name', fileName))
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.then(stat => _.set(stat, 'name', fileName));
}
list(path = '.') {
const { fsPath } = this._resolvePath(path)
return fsAsync
.readdir(fsPath)
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName)
return fsAsync
.access(filePath, constants.F_OK)
.then(() => {
return fsAsync.stat(filePath).then((stat) => _.set(stat, 'name', fileName))
})
.catch(() => null)
const {fsPath} = this._resolvePath(path);
return fs.readdirAsync(fsPath)
.then(fileNames => {
return Promise.map(fileNames, fileName => {
const filePath = nodePath.join(fsPath, fileName);
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fs.statAsync(filePath)
.then(stat => _.set(stat, 'name', fileName));
})
})
.then(_.compact)
.catch(() => null);
});
})
.then(_.compact);
}
chdir(path = '.') {
const { fsPath, clientPath } = this._resolvePath(path)
return fsAsync
.stat(fsPath)
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory')
})
.then(() => {
this.cwd = clientPath
return this.currentDirectory()
})
const {fsPath, serverPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap(stat => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = serverPath;
return this.currentDirectory();
});
}
write(fileName, { append = false, start = undefined } = {}) {
const { fsPath, clientPath } = this._resolvePath(fileName)
const stream = createWriteStream(fsPath, { flags: !append ? 'w+' : 'a+', start })
stream.once('error', () => fsAsync.unlink(fsPath))
stream.once('close', () => stream.end())
return {
stream,
clientPath
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return stream;
}
read(fileName, { start = undefined } = {}) {
const { fsPath, clientPath } = this._resolvePath(fileName)
return fsAsync
.stat(fsPath)
.tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory')
})
.then(() => {
const stream = createReadStream(fsPath, { flags: 'r', start })
return {
stream,
clientPath
}
})
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return stream;
});
}
delete(path) {
const { fsPath } = this._resolvePath(path)
return fsAsync.stat(fsPath).then((stat) => {
if (stat.isDirectory()) return fsAsync.rmdir(fsPath)
else return fsAsync.unlink(fsPath)
})
const {fsPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.then(stat => {
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});
}
mkdir(path) {
const { fsPath } = this._resolvePath(path)
return fsAsync.mkdir(fsPath, { recursive: true }).then(() => fsPath)
const {fsPath} = this._resolvePath(path);
return fs.mkdirAsync(fsPath)
.then(() => fsPath);
}
rename(from, to) {
const { fsPath: fromPath } = this._resolvePath(from)
const { fsPath: toPath } = this._resolvePath(to)
return fsAsync.rename(fromPath, toPath)
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fs.renameAsync(fromPath, toPath);
}
chmod(path, mode) {
const { fsPath } = this._resolvePath(path)
return fsAsync.chmod(fsPath, mode)
const {fsPath} = this._resolvePath(path);
return fs.chmodAsync(fsPath, mode);
}
getUniqueName() {
return uuid.v4().replace(/\W/g, '')
return uuid.v4().replace(/\W/g, '');
}
}
export { FileSystem }
module.exports = FileSystem;

View File

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

View File

@@ -1,50 +1,45 @@
import _ from 'lodash'
import moment from 'moment'
import errors from '../errors.js'
const _ = require('lodash');
const moment = require('moment');
const errors = require('../errors');
const FORMATS = {
ls,
ep
}
};
export default function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat)
module.exports = function (fileStat, format = 'ls') {
if (typeof format === 'function') return format(fileStat);
if (!FORMATS.hasOwnProperty(format)) {
throw new errors.FileSystemError('Bad file stat formatter')
throw new errors.FileSystemError('Bad file stat formatter');
}
return FORMATS[format](fileStat)
}
return FORMATS[format](fileStat);
};
function ls(fileStat) {
const now = moment.utc()
const mtime = moment.utc(new Date(fileStat.mtime))
const timeDiff = now.diff(mtime, 'months')
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY'
const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime));
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
return [
fileStat.mode
? [
fileStat.isDirectory() ? 'd' : '-',
fileStat.mode & 256 ? 'r' : '-',
fileStat.mode & 128 ? 'w' : '-',
fileStat.mode & 64 ? 'x' : '-',
fileStat.mode & 32 ? 'r' : '-',
fileStat.mode & 16 ? 'w' : '-',
fileStat.mode & 8 ? 'x' : '-',
fileStat.mode & 4 ? 'r' : '-',
fileStat.mode & 2 ? 'w' : '-',
fileStat.mode & 1 ? 'x' : '-'
].join('')
: fileStat.isDirectory()
? 'drwxr-xr-x'
: '-rwxr-xr-x',
fileStat.mode ? [
fileStat.isDirectory() ? 'd' : '-',
fileStat.mode & 256 ? 'r' : '-',
fileStat.mode & 128 ? 'w' : '-',
fileStat.mode & 64 ? 'x' : '-',
fileStat.mode & 32 ? 'r' : '-',
fileStat.mode & 16 ? 'w' : '-',
fileStat.mode & 8 ? 'x' : '-',
fileStat.mode & 4 ? 'r' : '-',
fileStat.mode & 2 ? 'w' : '-',
fileStat.mode & 1 ? 'x' : '-'
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
'1',
fileStat.uid || 1,
fileStat.gid || 1,
_.padStart(fileStat.size, 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ')
].join(' ');
}
function ep(fileStat) {
@@ -54,6 +49,6 @@ function ep(fileStat) {
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
fileStat.isDirectory() ? '/' : 'r'
]).join(',')
return `+${facts}\t${fileStat.name}`
]).join(',');
return `+${facts}\t${fileStat.name}`;
}

View File

@@ -1,59 +1,27 @@
import net from 'node:net'
import errors from '../errors.js'
const net = require('net');
const Promise = require('bluebird');
const errors = require('../errors');
const MAX_PORT = 65535
const MAX_PORT_CHECK_ATTEMPT = 5
function* portNumberGenerator(min, max = MAX_PORT) {
let current = min
while (true) {
if (current > MAX_PORT || current > max) {
current = min
}
yield current++
}
}
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
const nextPortNumber = portNumberGenerator(portMin, portMax)
return () =>
new Promise((resolve, reject) => {
const portCheckServer = net.createServer()
portCheckServer.maxConnections = 0
let attemptCount = 0
const tryGetPort = () => {
attemptCount++
if (attemptCount > maxAttempts) {
reject(new errors.ConnectorError('Unable to find valid port'))
return
}
const { value: port } = nextPortNumber.next()
portCheckServer.removeAllListeners()
portCheckServer.once('error', (err) => {
if (['EADDRINUSE'].includes(err.code)) {
tryGetPort()
} else {
reject(err)
}
})
portCheckServer.once('listening', () => {
portCheckServer.removeAllListeners()
portCheckServer.close(() => resolve(port))
})
try {
portCheckServer.listen(port, host)
} catch (err) {
reject(err)
}
module.exports = function (min = 1, max = undefined) {
return new Promise((resolve, reject) => {
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
tryGetPort()
})
}
export { getNextPortFactory, portNumberGenerator }
});
portCheckServer.on('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => {
portCheckServer = null;
resolve(port);
});
});
portCheckServer.listen(checkPort);
});
};

View File

@@ -1,9 +0,0 @@
import fs from 'node:fs'
import Promise from 'bluebird'
const methods = ['stat', 'readdir', 'access', 'unlink', 'rmdir', 'mkdir', 'rename', 'chmod']
export default methods.reduce((obj, method) => {
obj[method] = Promise.promisify(fs[method])
return obj
}, {})

View File

@@ -1,3 +0,0 @@
export const isLocalIP = function (ip) {
return ip === '127.0.0.1' || ip == '::1'
}

View File

@@ -0,0 +1,25 @@
const http = require('http');
const Promise = require('bluebird');
const errors = require('../errors');
const IP_WEBSITE = 'http://api.ipify.org/';
module.exports = function (hostname) {
return new Promise((resolve, reject) => {
if (!hostname || hostname === '0.0.0.0') {
let ip = '';
http.get(IP_WEBSITE, response => {
if (response.statusCode !== 200) {
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
}
response.setEncoding('utf8');
response.on('data', chunk => {
ip += chunk;
});
response.on('end', () => {
resolve(ip);
});
});
} else resolve(hostname);
});
};

View File

@@ -1,194 +1,151 @@
import _ from 'lodash'
import Promise from 'bluebird'
import nodeUrl from 'node:url'
import buyan from 'bunyan'
import net from 'node:net'
import tls from 'node:tls'
import { EventEmitter } from 'node:events'
import Connection from './connection.js'
import { getNextPortFactory } from './helpers/find-port.js'
const _ = require('lodash');
const Promise = require('bluebird');
const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
export default class FtpServer extends EventEmitter {
constructor(options = {}) {
super()
this.options = Object.assign(
{
log: buyan.createLogger({ name: 'ftp-srv' }),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
random_pasv_port: false,
anonymous: false,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false,
timeout: 0
},
options
)
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
this._greeting = this.setupGreeting(this.options.greeting)
this._features = this.setupFeaturesMessage()
class FtpServer {
constructor(url, options = {}) {
this.options = _.merge({
log: buyan.createLogger({name: 'ftp-srv'}),
anonymous: false,
pasv_range: 22,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting
delete this.options.greeting;
delete this.options.tls;
this.connections = {}
this.log = this.options.log
this.url = nodeUrl.parse(this.options.url)
this.getNextPasvPort = this.getNextPortFactory()
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
const timeout = Number(this.options.timeout)
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout)
const serverConnectionHandler = socket => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
const serverConnectionHandler = (socket) => {
this.options.timeout > 0 && socket.setTimeout(this.options.timeout)
let connection = new Connection(this, { log: this.log, socket })
this.connections[connection.id] = connection
socket.on('close', () => this.disconnectClient(connection.id));
socket.on('close', () => this.disconnectClient(connection.id))
socket.once('close', () => {
this.emit('disconnect', {
connection,
id: connection.id,
newConnectionCount: Object.keys(this.connections).length
})
})
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
this.emit('connect', {
connection,
id: connection.id,
newConnectionCount: Object.keys(this.connections).length
})
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error'));
this.on = this.server.on.bind(this.server);
this.once = this.server.once.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);
const greeting = this._greeting || []
const features = this._features || 'Ready'
return connection.reply(220, ...greeting, features).then(() => socket.resume())
}
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, { pauseOnConnect: true })
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler)
this.server.on('error', (err) => {
this.log.error(err, '[Event] error')
this.emit('server-error', { error: err })
})
const quit = _.debounce(this.quit.bind(this), 100)
process.on('SIGTERM', quit)
process.on('SIGINT', quit)
process.on('SIGQUIT', quit)
}
getNextPortFactory() {
const nextPortFactory = getNextPortFactory(
_.get(this, 'url.hostname'),
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max')
)
const randomPortFactory = async () => 0
return _.get(this, 'options.random_pasv_port') ? randomPortFactory : nextPortFactory
process.on('SIGTERM', () => this.quit());
process.on('SIGINT', () => this.quit());
process.on('SIGQUIT', () => this.quit());
}
get isTLS() {
return this.url.protocol === 'ftps:' && this.options.tls
return this.url.protocol === 'ftps:' && this._tls;
}
listen() {
if (!this.options.pasv_url) {
this.log.warn('Passive URL not set. Passive connections not available.')
}
return new Promise((resolve, reject) => {
this.server.once('error', reject)
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject)
if (err) return reject(err)
this.log.info(
{
return resolveHost(this.url.hostname)
.then(hostname => {
this.url.hostname = hostname;
return new Promise((resolve, reject) => {
this.server.listen(this.url.port, err => {
if (err) return reject(err);
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
},
'Listening'
)
resolve('Listening')
})
})
}, 'Listening');
resolve('Listening');
});
});
});
}
emitPromise(action, ...data) {
return new Promise((resolve, reject) => {
const params = _.concat(data, [resolve, reject])
this.emit.call(this, action, ...params)
})
const params = _.concat(data, [resolve, reject]);
this.server.emit(action, ...params);
});
}
emit(action, ...data) {
this.server.emit(action, ...data);
}
setupTLS(_tls) {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
});
}
setupGreeting(greet) {
if (!greet) return []
const greeting = Array.isArray(greet) ? greet : greet.split('\n')
return greeting
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
return greeting;
}
setupFeaturesMessage() {
let features = []
if (this.options.anonymous) features.push('a')
let features = [];
if (this.options.anonymous) features.push('a');
if (features.length) {
features.unshift('Features:')
features.push('.')
features.unshift('Features:');
features.push('.');
}
return features.length ? features.join(' ') : 'Ready'
return features.length ? features.join(' ') : 'Ready';
}
disconnectClient(id) {
return new Promise((resolve, reject) => {
const client = this.connections[id]
if (!client) return resolve()
delete this.connections[id]
setTimeout(() => {
reject(new Error('Timed out disconnecting the client'))
}, this.options.timeout || 1000)
return new Promise(resolve => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
try {
client.close(0)
client.close(0);
} catch (err) {
this.log.error(err, 'Error closing connection', { id })
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve('Disconnected');
}
resolve('Disconnected')
})
});
}
quit() {
return this.close().then(() => process.exit(0))
return this.close()
.finally(() => process.exit(0));
}
close() {
this.server.maxConnections = 0
this.emit('closing')
this.log.info('Closing connections:', Object.keys(this.connections).length)
return Promise.all(Object.keys(this.connections).map((id) => this.disconnectClient(id)))
.then(
() =>
new Promise((resolve) => {
this.server.close((err) => {
this.log.info('Server closing...')
if (err) this.log.error(err, 'Error closing server')
resolve('Closed')
})
})
)
.then(() => {
this.log.debug('Removing event listeners...')
this.emit('closed', {})
this.removeAllListeners()
return
})
this.log.info('Server closing...');
this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise(resolve => {
this.server.close(err => {
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});
}));
}
}
module.exports = FtpServer;

View File

@@ -1,4 +1,4 @@
export default {
module.exports = {
// 100 - 199 :: Remarks
100: 'The requested action is being initiated',
110: 'Restart marker reply',
@@ -23,7 +23,7 @@ export default {
230: 'User logged in, proceed',
234: 'Honored',
250: 'Requested file action okay, completed',
257: "'%s' created",
257: '\'%s\' created',
/// 300 - 399 :: Positive Intermediate Replies
/// These types of replies indicate that the requested action was taken and that the server is awaiting further information to complete the request.
331: 'Username okay, awaiting password',
@@ -53,4 +53,4 @@ export default {
551: 'Requested action aborted. Page type unknown',
552: 'Requested file action aborted. Exceeded storage allocation', // (for current directory or dataset).
553: 'Requested action not taken. File name not allowed'
}
};

View File

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

View File

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

View File

@@ -1,27 +1,28 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'ALLO'
const CMD = 'ALLO';
describe(CMD, function () {
let sandbox
let sandbox;
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply')
})
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
it('// successful', () => {
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202)
})
})
})
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
});

View File

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

View File

@@ -1,34 +1,35 @@
const Promise = require('bluebird')
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CDUP'
const CMD = 'CDUP';
describe(CMD, function () {
let sandbox
let log = bunyan.createLogger({ name: CMD })
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => Promise.resolve(),
fs: {
chdir: () => Promise.resolve()
}
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply')
sandbox.spy(mockClient.fs, 'chdir')
})
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir');
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
it('.. // successful', () => {
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250)
expect(mockClient.fs.chdir.args[0][0]).to.equal('..')
})
})
})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
});
});
});

View File

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

View File

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

View File

@@ -1,56 +1,60 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const ActiveConnector = require('../../../src/connector/active')
const ActiveConnector = require('../../../src/connector/active');
const CMD = 'EPRT'
const CMD = 'EPRT';
describe(CMD, function () {
let sandbox
let sandbox;
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply')
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves()
})
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
it('// unsuccessful | no argument', () => {
return cmdFn({}).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// unsuccessful | invalid argument', () => {
return cmdFn({ command: { arg: 'blah' } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504)
})
})
return cmdFn({command: {arg: 'blah'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// successful IPv4', () => {
return cmdFn({ command: { arg: '|1|192.168.0.100|35286|' } }).then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0]
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(ip).to.equal('192.168.0.100')
expect(port).to.equal('35286')
expect(family).to.equal(4)
})
})
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('192.168.0.100');
expect(port).to.equal('35286');
expect(family).to.equal(4);
});
});
it('// successful IPv6', () => {
return cmdFn({ command: { arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|' } }).then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0]
expect(mockClient.reply.args[0][0]).to.equal(200)
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23')
expect(port).to.equal('35286')
expect(family).to.equal(6)
})
})
})
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23');
expect(port).to.equal('35286');
expect(family).to.equal(6);
});
});
});

View File

@@ -1,34 +1,35 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const PassiveConnector = require('../../../src/connector/passive')
const PassiveConnector = require('../../../src/connector/passive');
const CMD = 'EPSV'
const CMD = 'EPSV';
describe(CMD, function () {
let sandbox
let sandbox;
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
address: () => ({ port: 12345 })
})
})
address: () => ({port: 12345})
});
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
it('// successful IPv4', () => {
return cmdFn({}).then(() => {
const [code, message] = mockClient.reply.args[0]
expect(code).to.equal(229)
expect(message).to.equal('EPSV OK (|||12345|)')
})
})
})
return cmdFn()
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);
expect(message).to.equal('EPSV OK (|||12345|)');
});
});
});

View File

@@ -1,28 +0,0 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const CMD = 'FEAT'
describe(CMD, function () {
let sandbox
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox.spy(mockClient, 'reply')
})
afterEach(() => {
sandbox.restore()
})
it('// successful', () => {
return cmdFn({ command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211)
expect(mockClient.reply.args[0][2].message).to.equal(' AUTH TLS')
})
})
})

View File

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

View File

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

View File

@@ -1,66 +1,66 @@
const bunyan = require('bunyan')
const { expect } = require('chai')
const sinon = require('sinon')
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MDTM'
const CMD = 'MDTM';
describe(CMD, function () {
let sandbox
let log = bunyan.createLogger({ name: CMD })
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: { get: () => {} }
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
fs: {get: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves()
sandbox.stub(mockClient.fs, 'get').resolves({ mtime: 'Mon, 10 Oct 2011 23:24:11 GMT' })
})
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = { reply: () => {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550)
})
})
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
});
it('fails on no fs get command', () => {
const badMockClient = { reply: () => {}, fs: {} }
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(
badMockClient
)
sandbox.stub(badMockClient, 'reply').resolves()
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn().then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402)
})
})
})
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
});
});
it('. // successful', () => {
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213)
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
})
})
});
});
it('. // unsuccessful', () => {
mockClient.fs.get.restore()
sandbox.stub(mockClient.fs, 'get').rejects(new Error())
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
return cmdFn({ log, command: { directive: CMD } }).then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550)
})
})
})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,28 @@
const Promise = require('bluebird')
const { expect } = require('chai')
const sinon = require('sinon')
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'NOOP'
const CMD = 'NOOP';
describe(CMD, function () {
let sandbox
let sandbox;
const mockClient = {
reply: () => Promise.resolve()
}
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient)
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise)
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply')
})
sandbox.spy(mockClient, 'reply');
});
afterEach(() => {
sandbox.restore()
})
sandbox.restore();
});
it('// successful', () => {
return cmdFn().then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200)
})
})
})
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
});

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