Compare commits

..

13 Commits

Author SHA1 Message Date
Tyler Stewart
aa1e543c24 WIP: wip 2018-05-25 09:55:50 -06:00
Tyler Stewart
57f7fa23cc WIP: wip 2018-05-21 21:38:11 -06:00
Tyler Stewart
e0dbbfce2d WIP(example): basic example 2018-03-28 20:33:13 -06:00
Tyler Stewart
fc8981021c WIP: message constants 2018-03-28 20:33:04 -06:00
Tyler Stewart
cf9852465a WIP: command processing and handling 2018-03-28 20:32:43 -06:00
Tyler Stewart
1f3f26706e WIP: queue system 2018-03-28 20:32:32 -06:00
Tyler Stewart
54cb2a2fe4 chore: meta files 2018-03-28 20:31:58 -06:00
Tyler Stewart
5fd6414e60 chore: update circle ci config 2018-03-11 17:38:12 -06:00
Tyler Stewart
ef7750def0 chore(examples): add basic example of usage 2018-03-07 21:07:37 -07:00
Tyler Stewart
427275a0b8 test: remove old test folder
Tests will now live beside src files
2018-03-07 21:07:09 -07:00
Tyler Stewart
c428787ade refactor: rewrite for v3 basis
BREAKING CHANGE: rewrite with new api
2018-03-07 21:06:09 -07:00
Tyler Stewart
8566df451e refactor(logo): async/await logo generation 2018-03-07 21:05:41 -07:00
Tyler Stewart
35789e430a chore: initiate new config files
Begining of v3
2018-03-07 21:05:13 -07:00
170 changed files with 5392 additions and 26848 deletions

View File

@@ -1,78 +1,53 @@
version: 2
jobs:
build:
docker:
- image: &node-image circleci/node:lts
- image: circleci/node:8
steps:
- checkout
- run:
name: Building...
command: npm install
- save_cache:
paths:
- node_modules
key: node_modules_{{ checksum package.json }}
lint:
docker:
- image: circleci/node:8
steps:
- checkout
- restore_cache:
keys:
- &npm-cache-key npm-cache-{{ .Branch }}-{{ .Revision }}
- npm-cache-{{ .Branch }}
- npm-cache
key: node_modules_{{ checksum package.json }}
- run:
name: Install
command: npm ci
- persist_to_workspace:
root: .
paths:
- node_modules
- save_cache:
key: *npm-cache-key
paths:
- ~/.npm/_cacache
lint:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Lint
command: npm run verify
name: Linting...
command: npm run lint
test:
docker:
- image: *node-image
- image: circleci/node:8
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Test
command: |
npm run test
release_dry_run:
- restore_cache:
key: node_modules_{{ checksum package.json }}
- run:
name: Testing...
command: npm run test
publish:
branches:
only: master
docker:
- image: *node-image
- image: circleci/node:8
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Dry Release
command: |
npm run semantic-release -- --dry-run
release:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Release
command: |
npm run semantic-release
- restore_cache:
key: node_modules_{{ checksum package.json }}
- run:
name: Publishing...
command: npx semantic-release
workflows:
version: 2
publish:
main:
jobs:
- build
- lint:
@@ -80,18 +55,7 @@ workflows:
- build
- test:
requires:
- build
- release_dry_run:
filters:
branches:
only: master
requires:
- test
- lint
- hold_release:
type: approval
- publish:
requires:
- release_dry_run
- release:
requires:
- hold_release
- test

2
.config/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/*
config/*

15
.config/.eslintrc.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true,
"jest": true
},
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true
}
}
}

View File

@@ -1,6 +1,3 @@
# Editor Config - generated by Confit. This file will NOT be re-overwritten by Confit
# Feel free to customise it further.
# http://editorconfig.org
root = true
[*]

1
.gitattributes vendored
View File

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

1
.github/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
yarn.lock binary

2
.github/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.vscode

211
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,211 @@
<!--[CN_HEADING]-->
# Contributing
Welcome! This document explains how you can contribute to making **ftp-srv** even better.
<!--[]-->
<!--[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`
<!--[]-->

6
.gitignore vendored
View File

@@ -1,6 +0,0 @@
test_tmp/
node_modules/
dist/
npm-debug.log

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Tyler Stewart
Copyright (c) 2018 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

View File

@@ -0,0 +1,35 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
<p align="center">
Modern, extensible FTP Server
</p>
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/trs/ftp-srv">
<img alt="circleci" 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="coveralls" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
</a>
</p>
---
> Looking for v2? Check the [v2](#v2) branch.
# Installation
```
$ yarn install
```

View File

@@ -1,15 +0,0 @@
import FTPServer from '../src/server';
const server = new FTPServer();
server.registerPlugin({
command: 'PASS',
handler: async ({connection, reply}) => {
const username = connection.getContext('username');
const password = connection.getContext('password');
// AUTHENTICATE
reply.set([230]);
}
});
server.listen();

11
examples/basic.js Normal file
View File

@@ -0,0 +1,11 @@
/* eslint no-console: 0 */
const FtpSrv = require('../src');
const server = new FtpSrv();
server.listen(8880)
.then(() => {
console.log('listening');
})
.catch(err => {
console.log('err', err)
})

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

19
logo/generate.js Normal file
View File

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

View File

@@ -1,2 +0,0 @@
node_modules/*
bower_components/*

View File

@@ -1,25 +0,0 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="400px" />
</a>
</p>
<h1 align="center">
Contributing Guide
</h1>
## Welcome
- Thank you for your eagerness to contribute, pull requests are encouraged!
## Guidelines
- Any new fixes are features should include new or updated [tests](/test).
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
## Setup
- Clone the repository `git clone`
- Install dependencies `npm install`

View File

@@ -1,358 +0,0 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
<p align="center">
Modern, extensible FTP Server
</p>
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
---
- [Overview](#overview)
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [CLI](#cli)
- [Events](#events)
- [Supported Commands](#supported-commands)
- [File System](#file-system)
- [Contributing](#contributing)
- [License](#license)
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Promise based API
## Install
`npm install ftp-srv --save`
## Usage
```js
// Quick start
const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv({ options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
ftpServer.listen()
.then(() => { ... });
```
## 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`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"`
#### `pasv_min`
Tne 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`
#### `--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`.
## 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`.
### `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
### `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
### `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()`](src/fs.js#L131)
Returns a unique file name to write to
__Used in:__ `STOU`
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Contributors
- [OzairP](https://github.com/OzairP)
- [TimLuq](https://github.com/TimLuq)
- [crabl](https://github.com/crabl)
- [hirviid](https://github.com/hirviid)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [edin-m](https://github.com/edin-m)
- [voxsoftware](https://github.com/voxsoftware)
- [jorinvo](https://github.com/jorinvo)
- [Johnnyrook777](https://github.com/Johnnyrook777)
- [qchar](https://github.com/qchar)
- [mikejestes](https://github.com/mikejestes)
- [pkeuter](https://github.com/pkeuter)
- [qiansc](https://github.com/qiansc)
- [broofa](https://github.com/broofa)
- [lafin](https://github.com/lafin)
- [alancnet](https://github.com/alancnet)
- [zgwit](https://github.com/zgwit)
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

View File

@@ -1,141 +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'
})
.option('pasv_min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
})
.option('pasv_max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
})
.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`
----

127
old/ftp-srv.d.ts vendored
View File

@@ -1,127 +0,0 @@
import * as tls from 'tls'
import { Stats } from 'fs'
import { EventEmitter } from 'events';
export class FileSystem {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
export class FtpConnection 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 | Record<string, any>, ...letters: any[]): Promise<any>
}
export interface FtpServerOptions {
url?: string;
pasv_min?: number;
pasv_max?: number;
pasv_url?: string;
greeting?: string | string[];
tls?: tls.SecureContextOptions | false;
anonymous?: boolean;
blacklist?: string[];
whitelist?: string[];
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep";
log?: any;
timeout?: number;
}
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
readonly isTLS: boolean;
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>;
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string;
};
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>;
close(): any;
on(event: "login", listener: (
data: {
connection: FtpConnection;
username: string;
password: string;
},
resolve: (config: {
fs?: FileSystem;
root?: string;
cwd?: string;
blacklist?: string[];
whitelist?: string[];
}) => void,
reject: (err?: Error) => void
) => void): this;
on(event: "client-error", listener: (
data: {
connection: FtpConnection;
context: string;
error: Error;
}
) => void): this;
}
export {FtpServer as FtpSrv};
export default FtpServer;

View File

@@ -1,8 +0,0 @@
const FtpSrv = require('./src');
const FileSystem = require('./src/fs');
const errors = require('./src/errors');
module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem;
module.exports.ftpErrors = errors;

View File

@@ -1,27 +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,23 +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());
});

9976
old/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
{
"name": "ftp-srv",
"version": "0.0.0",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
"ftp-server",
"ftp-srv",
"ftp-svr",
"ftpd",
"ftpserver",
"server"
],
"license": "MIT",
"files": [
"src",
"bin",
"ftp-srv.d.ts"
],
"main": "ftp-srv.js",
"bin": "./bin/index.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm run verify",
"semantic-release": "semantic-release",
"test": "mocha **/*.spec.js --ui bdd --bail",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
},
"release": {
"verifyConditions": "condition-circle"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
},
"dependencies": {
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.10",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
},
"devDependencies": {
"@commitlint/cli": "^7.5.2",
"@commitlint/config-conventional": "^7.5.0",
"@icetee/ftp": "^1.0.2",
"chai": "^4.2.0",
"condition-circle": "^2.0.2",
"eslint": "^5.14.1",
"husky": "^1.3.1",
"lint-staged": "^8.1.4",
"mocha": "^5.2.0",
"rimraf": "^2.6.1",
"semantic-release": "^15.10.6",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x"
}
}

View File

@@ -1,76 +0,0 @@
const _ = require('lodash');
const Promise = require('bluebird');
const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
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));
}
parse(message) {
const strippedMessage = message.replace(/"/g, '');
let [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive,
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
};
return command;
}
handle(command) {
if (typeof command === 'string') command = this.parse(command);
// Obfuscate password from logs
const logCommand = _.clone(command);
if (logCommand.directive === 'PASS') logCommand.arg = '********';
const log = this.connection.log.child({directive: command.directive});
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');
}
if (_.includes(this.blacklist, 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');
}
const commandRegister = REGISTRY[command.directive];
const commandFlags = _.get(commandRegister, 'flags', {});
if (!commandFlags.no_auth && !this.connection.authenticated) {
return this.connection.reply(530, 'Command requires authentication');
}
if (!commandRegister.handler) {
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}))
.finally(() => {
this.previousCommand = _.clone(command);
});
}
}
module.exports = FtpCommands;

View File

@@ -1,14 +0,0 @@
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))
.finally(() => this.connector.end());
},
syntax: '{{cmd}}',
description: 'Abort an active file transfer'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
const FAMILY = {
1: 4,
2: 6
};
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
const Promise = require('bluebird');
const escapePath = require('../../helpers/escape-path');
module.exports = {
directive: ['MKD', 'XMKD'],
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return Promise.try(() => 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 +0,0 @@
module.exports = {
directive: 'MODE',
handler: function ({command} = {}) {
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
},
syntax: '{{cmd}} <mode>',
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
flags: {
obsolete: true
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'PASV',
handler: function ({log} = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502);
}
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
const address = this.server.options.pasv_url;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
})
.catch((err) => {
log.error(err);
return this.reply(425);
});
},
syntax: '{{cmd}}',
description: 'Initiate passive mode'
};

View File

@@ -1,14 +0,0 @@
module.exports = {
directive: 'PBSZ',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
},
syntax: '{{cmd}}',
description: 'Protection Buffer Size',
flags: {
no_auth: true,
feat: 'PBSZ'
}
};

View File

@@ -1,25 +0,0 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({log, command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).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(425);
});
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'
};

View File

@@ -1,23 +0,0 @@
const _ = require('lodash');
module.exports = {
directive: 'PROT',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
switch (_.toUpper(command.arg)) {
case 'P': return this.reply(200, 'OK');
case 'C':
case 'S':
case 'E': return this.reply(536, 'Not supported');
default: return this.reply(504);
}
},
syntax: '{{cmd}}',
description: 'Data Channel Protection Level',
flags: {
no_auth: true,
feat: 'PROT'
}
};

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
const _ = require('lodash');
module.exports = {
directive: 'REST',
handler: function ({command} = {}) {
const arg = _.get(command, 'arg');
const byteCount = parseInt(arg, 10);
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
this.restByteCount = byteCount;
return this.reply(350, `Restarting next transfer at ${byteCount}`);
},
syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
};

View File

@@ -1,64 +0,0 @@
const Promise = require('bluebird');
module.exports = {
directive: 'RETR',
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
const filePath = command.arg;
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
const destroyConnection = (connection, reject) => (err) => {
if (connection) connection.destroy(err);
reject(err);
};
const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', (data) => {
if (stream) stream.pause();
if (this.connector.socket) {
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
}
});
stream.once('end', () => resolve());
stream.once('error', destroyConnection(this.connector.socket, reject));
this.connector.socket.once('error', destroyConnection(stream, reject));
});
this.restByteCount = 0;
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise)
.tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => 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);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} <path>',
description: 'Retrieve a copy of the file'
};

View File

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

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
const Promise = require('bluebird');
module.exports = function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
const [mode, ...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);
});
};

View File

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

View File

@@ -1,5 +0,0 @@
module.exports = {
CHMOD: {
handler: require('./chmod')
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,146 +0,0 @@
const _ = require('lodash');
const uuid = require('uuid');
const Promise = require('bluebird');
const EventEmitter = require('events');
const BaseConnector = require('./connector/base');
const FileSystem = require('./fs');
const Commands = require('./commands');
const errors = require('./errors');
const DEFAULT_MESSAGE = require('./messages');
class FtpConnection extends EventEmitter {
constructor(server, options) {
super();
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.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.log.trace('Client timeout');
this.close().catch((e) => this.log.trace(e, 'Client close error'));
});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
this.removeAllListeners();
});
}
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
}
get ip() {
try {
return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
} catch (ex) {
return null;
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc;
}
get secure() {
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec;
}
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.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');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
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});
});
}
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 = [{}];
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
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) => {
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) => {
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, (err) => {
if (err) {
this.log.error(err);
return reject(err);
}
resolve();
});
} else reject(new errors.SocketError('Socket not writable'));
});
};
return satisfyParameters()
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((err) => {
this.log.error(err);
});
}
}
module.exports = FtpConnection;

View File

@@ -1,49 +0,0 @@
const {Socket} = require('net');
const tls = require('tls');
const Promise = require('bluebird');
const Connector = require('./base');
class Active extends Connector {
constructor(connection) {
super(connection);
this.type = 'active';
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
const checkSocket = () => {
if (this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout);
}
setupConnection(host, port, family = 4) {
const closeExistingServer = () => Promise.resolve(
this.dataSocket ? this.dataSocket.destroy() : undefined);
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 secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
}
this.dataSocket.connected = true;
});
});
}
}
module.exports = Active;

View File

@@ -1,53 +0,0 @@
const Promise = require('bluebird');
const errors = require('../errors');
class Connector {
constructor(connection) {
this.connection = connection;
this.dataSocket = null;
this.dataServer = null;
this.type = false;
}
get log() {
return this.connection.log;
}
get socket() {
return this.dataSocket;
}
get server() {
return this.connection.server;
}
waitForConnection() {
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
}
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket = null;
}
}
closeServer() {
if (this.dataServer) {
this.dataServer.close();
this.dataServer = null;
}
}
end() {
this.closeSocket();
this.closeServer();
this.type = false;
this.connection.connector = new Connector(this);
}
}
module.exports = Connector;

View File

@@ -1,86 +0,0 @@
const net = require('net');
const tls = require('tls');
const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const errors = require('../errors');
class Passive extends Connector {
constructor(connection) {
super(connection);
this.type = 'passive';
}
waitForConnection({timeout = 5000, delay = 50} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
return Promise.resolve(this.dataSocket);
}
return Promise.resolve().delay(delay)
.then(() => checkSocket());
};
return checkSocket().timeout(timeout);
}
setupServer() {
this.closeServer();
return this.server.getNextPasvPort()
.then((port) => {
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');
socket.destroy();
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
};
this.dataSocket = null;
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
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();
});
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err);
else {
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
});
});
}
}
module.exports = Passive;

View File

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

View File

@@ -1,135 +0,0 @@
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');
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this._root = nodePath.resolve(root || process.cwd());
}
get root() {
return this._root;
}
_resolvePath(path = '.') {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
return nodePath.join(resolvedPath);
})();
return {
clientPath,
fsPath
};
}
currentDirectory() {
return this.cwd;
}
get(fileName) {
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.then((stat) => _.set(stat, 'name', fileName));
}
list(path = '.') {
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));
})
.catch(() => null);
});
})
.then(_.compact);
}
chdir(path = '.') {
const {fsPath, clientPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = clientPath;
return this.currentDirectory();
});
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return {
stream,
clientPath
};
}
read(fileName, {start = undefined} = {}) {
const {fsPath, clientPath} = 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,
clientPath
};
});
}
delete(path) {
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 fs.mkdirAsync(fsPath)
.then(() => fsPath);
}
rename(from, to) {
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 fs.chmodAsync(fsPath, mode);
}
getUniqueName() {
return uuid.v4().replace(/\W/g, '');
}
}
module.exports = FileSystem;

View File

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

View File

@@ -1,55 +0,0 @@
const _ = require('lodash');
const moment = require('moment');
const errors = require('../errors');
const FORMATS = {
ls,
ep
};
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');
}
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';
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',
'1',
fileStat.uid || 1,
fileStat.gid || 1,
_.padStart(fileStat.size, 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ');
}
function ep(fileStat) {
const facts = _.compact([
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
fileStat.size ? `s${fileStat.size}` : null,
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}`;
}

View File

@@ -1,59 +0,0 @@
const net = require('net');
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);
const portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
return () => new Promise((resolve, reject) => {
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.close(() => resolve(port));
});
try {
portCheckServer.listen(port, host);
} catch (err) {
reject(err);
}
};
tryGetPort();
});
}
module.exports = {
getNextPortFactory,
portNumberGenerator
};

View File

@@ -1,152 +0,0 @@
const _ = require('lodash');
const Promise = require('bluebird');
const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const EventEmitter = require('events');
const Connection = require('./connection');
const {getNextPortFactory} = require('./helpers/find-port');
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,
anonymous: false,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false,
timeout: 0
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
delete this.options.greeting;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'url.hostname'),
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const timeout = Number(this.options.timeout);
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
const serverConnectionHandler = (socket) => {
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));
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => 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'));
const quit = _.debounce(this.quit.bind(this), 100);
process.on('SIGTERM', quit);
process.on('SIGINT', quit);
process.on('SIGQUIT', quit);
}
get isTLS() {
return this.url.protocol === 'ftps:' && this.options.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({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
});
});
}
emitPromise(action, ...data) {
return new Promise((resolve, reject) => {
const params = _.concat(data, [resolve, reject]);
this.emit.call(this, action, ...params);
});
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
return greeting;
}
setupFeaturesMessage() {
let features = [];
if (this.options.anonymous) features.push('a');
if (features.length) {
features.unshift('Features:');
features.push('.');
}
return features.length ? features.join(' ') : 'Ready';
}
disconnectClient(id) {
return new Promise((resolve) => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
try {
client.close(0);
} catch (err) {
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve('Disconnected');
}
});
}
quit() {
return this.close()
.finally(() => process.exit(0));
}
close() {
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');
});
}))
.then(() => this.removeAllListeners());
}
}
module.exports = FtpServer;

View File

@@ -1,56 +0,0 @@
module.exports = {
| 100
| 110
| 120
| 125
| 150
| 200
| 202
| 211
| 212
| 213
| 214
| 215
| 220
| 221
| 225
| 226
| 227
| 230
| 234
| 250
| 257
| 331
| 332
| 350
| 421
| 425
| 426
| 450
| 451
| 452
| 500
| 501
| 502
| 503
| 504
| 530
| 532
| 550
| 551
| 552
| 553
};

View File

@@ -1,12 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBtTCCAR4CCQDsFyLCxvy4qzANBgkqhkiG9w0BAQUFADAfMQswCQYDVQQGEwJD
QTEQMA4GA1UECBMHQWxiZXJ0YTAeFw0xNzA1MDgyMzQzMjNaFw0xODA1MDgyMzQz
MjNaMB8xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdBbGJlcnRhMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOia
C4xbwpF6w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/
agkklBnRtdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7
rQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAOkX32keFpo0kKQKpeZgxVYjvn4/Voy6
6oLsj7jJYq3oZts1dX6kHVpLEbF9sWKB2iz7nqz7pSN1ATq0IL/5rcxvNwiL4Idv
F8CCBvsBui+0gwX755NJK/L57a5i8yQ5HC65NujGAA4I5+2x8HlefMVuBpEYjzQ2
6lW2OJJ8xtP/
-----END CERTIFICATE-----

View File

@@ -1,10 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBXjCByAIBADAfMQswCQYDVQQGEwJDQTEQMA4GA1UECBMHQWxiZXJ0YTCBnzAN
BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA9rjsI1ezW+7mwbCv7R2UOYVKWkPUA/Sa
PKo+wwTomguMW8KResPmQ3+Tp5Eex5d0JXKH25FypC1phtDJucLxQOpGFug/LzE+
Q4u/npLH/2oJJJQZ0bXaAW95bGzG6N/xF4o8B2wim5vrE03u3cBiv8Oqe/Niv5IB
1VpPi+fDu60CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBACzo+Wecs3CTbItrugdL
pP4crsRs+HJljWA0e+WEGKhcd1FjrcLBr4WqzHFQJWHOTz2vM5PiKXPZk9crLxWa
Y8kMhU6eQPnCM6+7Gffm32+VS1ipNlhzHyYsjYpgC3ROElqo0J5M5sas4lbaamr+
FnlyRjrPSUcFdcbPL6ozND3e
-----END CERTIFICATE REQUEST-----

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOiaC4xbwpF6
w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/agkklBnR
tdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7rQIDAQAB
AoGANRPVYUkVwfpkVFkBj/5kC/fb5g1fiDZQFCr/846Tx8giOv9hssqAOBczGcKD
n6a6iu/XwGnLAvzuDd3O+BKzObrKV36u9HfvCxohKaKvhPg3lBlJ5fFq/UNBoLv8
eHz0GUGGoCxwJBAV43ojV1GdyRZ7vdmYw2hzltsHIp7UDqECQQD7yltCCJm3+gcw
p+Sde0+M9CubkTETPwpd3XPu6Bs5f2nxNVj6RAInPEBr5//5UY7Q9BBEHrDPsq0j
/+gsSlWZAkEA+tjf9qRXk9JHoN3PD0xLNEgUZAsQwDic29jxb3xrGkuUjCebKRuC
FU2sAfNgDp+MyG1iyAoZcySzH2Dp3+v7NQJAaBwBo8oelT2in3GsS5ljCSskpMxh
+E1Gog0hFJWQPDP8wCmIwuI/6a02Def9pT8dyDRCTYhLH3YHtSzo+Pc7cQJBAJ4G
XiD8qwc+o00eLsEOaRoIhn/30JenknmVE5QOJ1KrZmtc0Ax3fd15zvBzp4HO1Vu2
PVKTujClYApWfT9JZDkCQQC3Ne79bb5WSsGNbg4eT+FWde4hkdpheBsWraEDN1Pp
NanupXMPNP0EduAQ1O+oPRiZ5pG38MQYcPZHTtlULoiO
-----END RSA PRIVATE KEY-----

View File

@@ -1,18 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,A253DC8068106194
jrYyhujaDsASrOfb2kn5Tvb0mRyIRsz7gVdjwlUdF2+lPdA/w6Os/NQBo1BIUAJp
lfS5KDTwiE3QPgrBXUNgpy71Yr+MSzmsYWdonGlXGtchohQKXpxtL3qOpczX3ERR
0AZninOKOYLw9+pe/tLNZI78DHxN1X0qTXS56RFydlW3XZbnl2Ux9CGuaCVq6vwh
yzr9H+XTqeh95wTfdXkRdFRSTyUuJ72cvMsBFRDhz60epDmUDo1XDf844BpXXfcU
kQoXHEtNkWZqzsc4ClOopp3Bgtd7eYoOLQluyovHgXzjtsur0xeMkHn9uTfkJ+IM
cYMS71ZKbMePS7XBt3YPLBvVXNcyYhWUP7VdGYxXTPqd1AVWciDB9S5EvfMxnnZz
O4M4ejxV8S7fF/cGju+sRzXx9oHPfo091Q3XKV1hrsUcF+ULrA8A8rHr64bDJ3wp
luhekzwb+5yFfZDj9XUuGMD6pSWYoWB8Jmk8cxVsdZPtGXbTQHFL9/+UZ65wSGpj
CjTLuFyVhY8pliynZH80vsNeRycdfmx93XoLqfS4xwEmI5v/MGUF24eTpF1/VIa5
oKDrVuERdXAn4JBKeaMratrl6p1BhkPe7VNnMUFw3U+C4x+QHISxbboUJiTcCe1C
pT6+YYkxQJ88rKunSEXkQYt6LeYSDg8Dw5y5Oq68DmW2Rp1m4ptTbqk3+uh83vzV
Ff0JnfNfT80GHD3T5hMgizZal2vV8DeH1WAwPzpNaeV6wTy6MSgRLgQ89cCQ0TXV
0GYnCZCaoXA2ldvbB3vW3fweOTr8Mp7aSl4s8K0R8sT3eief9/SyWA==
-----END RSA PRIVATE KEY-----

View File

@@ -1,125 +0,0 @@
const {expect} = require('chai');
const Promise = require('bluebird');
const bunyan = require('bunyan');
const sinon = require('sinon');
const FtpCommands = require('../../src/commands');
describe('FtpCommands', function () {
let sandbox;
let commands;
let mockConnection = {
authenticated: false,
log: bunyan.createLogger({name: 'FtpCommands'}),
reply: () => Promise.resolve({}),
server: {
options: {
blacklist: ['allo']
}
}
};
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
commands = new FtpCommands(mockConnection);
sandbox.spy(mockConnection, 'reply');
sandbox.spy(commands, 'handle');
sandbox.spy(commands, 'parse');
});
afterEach(() => {
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');
});
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');
});
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');
});
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"');
});
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');
});
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([]);
});
});
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(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/);
});
});
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/);
});
});
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/);
});
});
});
});

View File

@@ -1,49 +0,0 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'ABOR';
describe.skip(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);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection');
sandbox.spy(mockClient.connector, 'end');
});
afterEach(() => {
sandbox.restore();
});
it('// successful | no active connection', () => {
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);
});
});
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);
});
});
});

View File

@@ -1,28 +0,0 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'ALLO';
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()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
});
});

View File

@@ -1,48 +0,0 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'AUTH';
describe(CMD, function () {
let sandbox;
const mockClient = {
reply: () => Promise.resolve(),
server: {
options: {
tls: {}
}
}
};
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('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);
});
});
it('SSL // not supported', () => {
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);
});
});
});

View File

@@ -1,35 +0,0 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CDUP';
describe(CMD, function () {
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);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir');
});
afterEach(() => {
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('..');
});
});
});

View File

@@ -1,78 +0,0 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CWD';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {chdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves();
});
afterEach(() => {
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();
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();
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');
});
});
it('test // successful', () => {
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');
});
});
it('bad // unsuccessful', () => {
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');
});
});
});

View File

@@ -1,67 +0,0 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'DELE';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {delete: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'delete').resolves();
});
afterEach(() => {
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();
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();
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');
});
});
it('bad // unsuccessful', () => {
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');
});
});
});

View File

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

View File

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

View File

@@ -1,49 +0,0 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'HELP';
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);
});
});
it('help // successful', () => {
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);
});
});
it('bad // unsuccessful', () => {
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
});
});

View File

@@ -1,175 +0,0 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'LIST';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {
list: () => {},
get: () => {}
},
connector: {
waitForConnection: () => Promise.resolve({}),
end: () => Promise.resolve({})
},
commandSocket: {
resume: () => {},
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testdir',
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();
});
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();
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();
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);
});
});
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
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
});
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());
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()));
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
});

View File

@@ -1,66 +0,0 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MDTM';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {get: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
});
afterEach(() => {
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();
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();
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);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
});
});
it('. // unsuccessful', () => {
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);
});
});
});

View File

@@ -1,78 +0,0 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MKD';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {mkdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'mkdir').resolves();
});
afterEach(() => {
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();
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();
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');
});
});
it('test // successful', () => {
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');
});
});
it('bad // unsuccessful', () => {
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');
});
});
});

View File

@@ -1,35 +0,0 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MODE';
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('S // successful', () => {
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);
});
});
});

View File

@@ -1,132 +0,0 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'NLST';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {
get: () => {},
list: () => {}
},
connector: {
waitForConnection: () => Promise.resolve({}),
end: () => Promise.resolve({})
},
commandSocket: {
resume: () => {},
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testdir',
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();
});
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);
});
});
it('testfile.txt // successful', () => {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').resolves({
name: 'testfile.txt',
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
});
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);
});
});
});

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