Compare commits
60 Commits
v2.19.5
...
observable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30956e1d64 | ||
|
|
c59e191a39 | ||
|
|
b2b1b2a0d3 | ||
|
|
81fa7fcb89 | ||
|
|
a18841d770 | ||
|
|
0dbb7f9070 | ||
|
|
0b9167e1e4 | ||
|
|
484409d2eb | ||
|
|
5ffcef3312 | ||
|
|
290769a042 | ||
|
|
a1c7f2ffda | ||
|
|
7153ffab4d | ||
|
|
c0e132b70e | ||
|
|
e661bd10e2 | ||
|
|
bece42a0c9 | ||
|
|
b1fe56826c | ||
|
|
16dbc7895c | ||
|
|
94f0b893e4 | ||
|
|
79d7bd9062 | ||
|
|
44999c714d | ||
|
|
0d9131c370 | ||
|
|
eafaf6f642 | ||
|
|
ea99e6ebbd | ||
|
|
342911eb36 | ||
|
|
0094614ee3 | ||
|
|
0773967ba5 | ||
|
|
e5820688c4 | ||
|
|
a3fa1a2f71 | ||
|
|
e540822d5b | ||
|
|
36f331d15d | ||
|
|
03ff982959 | ||
|
|
8c8f3922a3 | ||
|
|
eca26ee86a | ||
|
|
811db7b1a7 | ||
|
|
ab085a1bca | ||
|
|
a5f26480e5 | ||
|
|
e41b04be46 | ||
|
|
7acf861a4d | ||
|
|
4801ecc0cc | ||
|
|
8e34e4c71a | ||
|
|
0afd578683 | ||
|
|
46b0d52ff2 | ||
|
|
185e473edc | ||
|
|
92a323f3dd | ||
|
|
f67e487306 | ||
|
|
2716123da7 | ||
|
|
ef207f60c1 | ||
|
|
4d8cf42ad0 | ||
|
|
50c6b92d12 | ||
|
|
a2103e5a3c | ||
|
|
2302b749fa | ||
|
|
27b43d702b | ||
|
|
fae003e644 | ||
|
|
a51678ae70 | ||
|
|
bc26886a0d | ||
|
|
c9b4371579 | ||
|
|
95471bdd15 | ||
|
|
5a36a6685d | ||
|
|
90a7419661 | ||
|
|
29cb035f66 |
@@ -1,108 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
create-cache-file: &create-cache-file
|
|
||||||
run:
|
|
||||||
name: Setup cache
|
|
||||||
command: echo "$NODE_VERSION" > _cache_node_version
|
|
||||||
|
|
||||||
package-json-cache: &package-json-cache
|
|
||||||
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
|
|
||||||
|
|
||||||
base-build: &base-build
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- <<: *create-cache-file
|
|
||||||
- restore_cache:
|
|
||||||
<<: *package-json-cache
|
|
||||||
- run:
|
|
||||||
name: Install
|
|
||||||
command: npm install
|
|
||||||
- save_cache:
|
|
||||||
<<: *package-json-cache
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
- run:
|
|
||||||
name: Lint
|
|
||||||
command: npm run verify:js
|
|
||||||
- run:
|
|
||||||
name: Test
|
|
||||||
command: npm run test:unit:once
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test_node_10:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:10
|
|
||||||
environment:
|
|
||||||
- NODE_VERSION: 10
|
|
||||||
<<: *base-build
|
|
||||||
|
|
||||||
test_node_8:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:8
|
|
||||||
environment:
|
|
||||||
- NODE_VERSION: 8
|
|
||||||
<<: *base-build
|
|
||||||
|
|
||||||
test_node_6:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:6
|
|
||||||
environment:
|
|
||||||
- NODE_VERSION: 6
|
|
||||||
<<: *base-build
|
|
||||||
|
|
||||||
release:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:8
|
|
||||||
environment:
|
|
||||||
- NODE_VERSION: 8
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- <<: *create-cache-file
|
|
||||||
- restore_cache:
|
|
||||||
<<: *package-json-cache
|
|
||||||
- run:
|
|
||||||
name: Update NPM
|
|
||||||
command: |
|
|
||||||
npm install npm@5
|
|
||||||
npm install semantic-release@11
|
|
||||||
- deploy:
|
|
||||||
name: Semantic Release
|
|
||||||
command: |
|
|
||||||
npm run semantic-release || true
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
test_and_tag:
|
|
||||||
jobs:
|
|
||||||
- test_node_10:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
- test_node_8:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
- test_node_6:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
- release:
|
|
||||||
requires:
|
|
||||||
- test_node_6
|
|
||||||
- test_node_8
|
|
||||||
- test_node_10
|
|
||||||
|
|
||||||
build_and_test:
|
|
||||||
jobs:
|
|
||||||
- test_node_10:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: master
|
|
||||||
- test_node_8:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: master
|
|
||||||
- test_node_6:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: master
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# START_CONFIT_GENERATED_CONTENT
|
|
||||||
# Common folders to ignore
|
|
||||||
node_modules/*
|
|
||||||
bower_components/*
|
|
||||||
|
|
||||||
# Config folder (optional - you might want to lint this...)
|
|
||||||
config/*
|
|
||||||
|
|
||||||
# END_CONFIT_GENERATED_CONTENT
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
reports/
|
|
||||||
npm-debug.log
|
|
||||||
.nyc_output/
|
|
||||||
test_tmp/
|
|
||||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}\\dist\\index.js",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/dist/**/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
.vscode/tasks.json
vendored
Normal file
12
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm run build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 Tyler Stewart
|
Copyright (c) 2019 Tyler Stewart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
371
README.md
371
README.md
@@ -1,339 +1,48 @@
|
|||||||
<p align="center">
|
```ts
|
||||||
<a href="https://github.com/trs/ftp-srv">
|
const loginMiddleware = () => (client) => {
|
||||||
<img alt="ftp-srv" src="logo.png" width="600px" />
|
let username;
|
||||||
</a>
|
let password;
|
||||||
</p>
|
|
||||||
|
return {
|
||||||
|
USER(client, command) => {
|
||||||
<p align="center">
|
username = command.arg;
|
||||||
Modern, extensible FTP Server
|
},
|
||||||
</p>
|
PASS(client, command) => {
|
||||||
|
password = command.arg;
|
||||||
<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>
|
|
||||||
|
const fileSystemMiddleware = () => (client) => {
|
||||||
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
|
return {
|
||||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
|
CWD(client, command) => {},
|
||||||
</a>
|
CDUP(client) => {},
|
||||||
</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('ftp://0.0.0.0:9876', { options ... });
|
|
||||||
|
|
||||||
ftpServer.on('login', (data, resolve, reject) => { ... });
|
|
||||||
...
|
|
||||||
|
|
||||||
ftpServer.listen()
|
|
||||||
.then(() => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### `new FtpSrv(url, [{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"`
|
|
||||||
|
|
||||||
#### options
|
|
||||||
|
|
||||||
##### `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_range`
|
|
||||||
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
|
|
||||||
This range is then queried for an available port to use when required.
|
|
||||||
__Default:__ `22`
|
|
||||||
|
|
||||||
##### `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.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## 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) {
|
|
||||||
...
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transferMiddleware = () => (client) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
client.use(loginMiddleware());
|
||||||
```
|
```
|
||||||
|
|
||||||
Custom file systems can implement the following variables depending on the developers needs:
|
5.1. MINIMUM IMPLEMENTATION
|
||||||
|
|
||||||
### Methods
|
In order to make FTP workable without needless error messages, the
|
||||||
#### [`currentDirectory()`](src/fs.js#L29)
|
following minimum implementation is required for all servers:
|
||||||
Returns a string of the current working directory
|
|
||||||
__Used in:__ `PWD`
|
|
||||||
|
|
||||||
#### [`get(fileName)`](src/fs.js#L33)
|
TYPE - ASCII Non-print
|
||||||
Returns a file stat object of file or directory
|
MODE - Stream
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
STRUCTURE - File, Record
|
||||||
|
COMMANDS - USER, QUIT, PORT,
|
||||||
|
TYPE, MODE, STRU,
|
||||||
|
for the default values
|
||||||
|
RETR, STOR,
|
||||||
|
NOOP.
|
||||||
|
|
||||||
#### [`list(path)`](src/fs.js#L39)
|
The default values for transfer parameters are:
|
||||||
Returns array of file and directory stat objects
|
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`
|
|
||||||
|
|
||||||
#### [`chdir(path)`](src/fs.js#L56)
|
TYPE - ASCII Non-print
|
||||||
Returns new directory relative to current directory
|
MODE - Stream
|
||||||
__Used in:__ `CWD`, `CDUP`
|
STRU - File
|
||||||
|
|
||||||
#### [`mkdir(path)`](src/fs.js#L96)
|
|
||||||
Returns a path to a newly created directory
|
|
||||||
__Used in:__ `MKD`
|
|
||||||
|
|
||||||
#### [`write(fileName, {append, start})`](src/fs.js#L68)
|
|
||||||
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#L75)
|
|
||||||
Returns a readable stream
|
|
||||||
Options:
|
|
||||||
`start` if set, specifies the byte offset to read from
|
|
||||||
__Used in:__ `RETR`
|
|
||||||
|
|
||||||
#### [`delete(path)`](src/fs.js#L87)
|
|
||||||
Delete a file or directory
|
|
||||||
__Used in:__ `DELE`
|
|
||||||
|
|
||||||
#### [`rename(from, to)`](src/fs.js#L102)
|
|
||||||
Renames a file or directory
|
|
||||||
__Used in:__ `RNFR`, `RNTO`
|
|
||||||
|
|
||||||
#### [`chmod(path)`](src/fs.js#L108)
|
|
||||||
Modifies a file or directory's permissions
|
|
||||||
__Used in:__ `SITE CHMOD`
|
|
||||||
|
|
||||||
#### [`getUniqueName()`](src/fs.js#L113)
|
|
||||||
Returns a unique file name to write to
|
|
||||||
__Used in:__ `STOU`
|
|
||||||
|
|
||||||
<!--[RM_CONTRIBUTING]-->
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
- [OzairP](https://github.com/OzairP)
|
|
||||||
- [qchar](https://github.com/qchar)
|
|
||||||
- [jorinvo](https://github.com/jorinvo)
|
|
||||||
- [voxsoftware](https://github.com/voxsoftware)
|
|
||||||
- [pkeuter](https://github.com/pkeuter)
|
|
||||||
- [TimLuq](https://github.com/TimLuq)
|
|
||||||
- [edin-mg](https://github.com/edin-m)
|
|
||||||
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
|
|
||||||
|
|
||||||
<!--[RM_LICENSE]-->
|
|
||||||
## License
|
|
||||||
|
|
||||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
types: [
|
|
||||||
{value: 'feat', name: 'feat: A new feature'},
|
|
||||||
{value: 'fix', name: 'fix: A bug fix'},
|
|
||||||
{value: 'docs', name: 'docs: Documentation only changes'},
|
|
||||||
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
|
|
||||||
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
|
|
||||||
{value: 'perf', name: 'perf: A code change that improves performance'},
|
|
||||||
{value: 'test', name: 'test: Adding missing tests'},
|
|
||||||
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
|
|
||||||
{value: 'revert', name: 'revert: Revert to a commit'},
|
|
||||||
{value: 'WIP', name: 'WIP: Work in progress'}
|
|
||||||
],
|
|
||||||
|
|
||||||
scopes: [],
|
|
||||||
|
|
||||||
// it needs to match the value for field type. Eg.: 'fix'
|
|
||||||
/*
|
|
||||||
scopeOverrides: {
|
|
||||||
fix: [
|
|
||||||
|
|
||||||
{name: 'merge'},
|
|
||||||
{name: 'style'},
|
|
||||||
{name: 'e2eTest'},
|
|
||||||
{name: 'unitTest'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
allowCustomScopes: true,
|
|
||||||
allowBreakingChanges: ['feat', 'fix'],
|
|
||||||
|
|
||||||
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
|
|
||||||
appendBranchNameToCommitMessage: false
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
test/**/*.spec.js
|
|
||||||
--reporter mocha-multi-reporters
|
|
||||||
--reporter-options configFile=config/testUnit/reporters.json
|
|
||||||
--ui bdd
|
|
||||||
--bail
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"reporterEnabled": "spec",
|
|
||||||
"mochaJunitReporterReporterOptions": {
|
|
||||||
"mochaFile": "reports/junit.xml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"mocha": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"mocha",
|
|
||||||
"node"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"mocha/no-exclusive-tests": 2,
|
|
||||||
"no-warning-comments": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"terms": ["todo", "fixme", "xxx"],
|
|
||||||
"location": "start"
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"object-curly-spacing": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"array-bracket-spacing": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"brace-style": [
|
|
||||||
2,
|
|
||||||
"1tbs"
|
|
||||||
],
|
|
||||||
"consistent-return": 0,
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1,
|
|
||||||
"MemberExpression": "off"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-multiple-empty-lines": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"max": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-use-before-define": [
|
|
||||||
2,
|
|
||||||
"nofunc"
|
|
||||||
],
|
|
||||||
"one-var": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"quote-props": [
|
|
||||||
2,
|
|
||||||
"as-needed"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
2,
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"keyword-spacing": 2,
|
|
||||||
"space-before-function-paren": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"anonymous": "always",
|
|
||||||
"named": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"space-in-parens": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"strict": [
|
|
||||||
2,
|
|
||||||
"global"
|
|
||||||
],
|
|
||||||
"curly": [
|
|
||||||
2,
|
|
||||||
"multi-line"
|
|
||||||
],
|
|
||||||
"eol-last": 2,
|
|
||||||
"key-spacing": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"beforeColon": false,
|
|
||||||
"afterColon": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-eval": 2,
|
|
||||||
"no-with": 2,
|
|
||||||
"space-infix-ops": 2,
|
|
||||||
"dot-notation": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"allowKeywords": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"eqeqeq": 2,
|
|
||||||
"no-alert": 2,
|
|
||||||
"no-caller": 2,
|
|
||||||
"no-extend-native": 2,
|
|
||||||
"no-extra-bind": 2,
|
|
||||||
"no-implied-eval": 2,
|
|
||||||
"no-iterator": 2,
|
|
||||||
"no-label-var": 2,
|
|
||||||
"no-labels": 2,
|
|
||||||
"no-lone-blocks": 2,
|
|
||||||
"no-loop-func": 2,
|
|
||||||
"no-multi-spaces": 2,
|
|
||||||
"no-multi-str": 2,
|
|
||||||
"no-native-reassign": 2,
|
|
||||||
"no-new": 2,
|
|
||||||
"no-new-func": 2,
|
|
||||||
"no-new-wrappers": 2,
|
|
||||||
"no-octal-escape": 2,
|
|
||||||
"no-proto": 2,
|
|
||||||
"no-return-assign": 2,
|
|
||||||
"no-script-url": 2,
|
|
||||||
"no-sequences": 2,
|
|
||||||
"no-unused-expressions": 2,
|
|
||||||
"yoda": 2,
|
|
||||||
"no-shadow": 2,
|
|
||||||
"no-shadow-restricted-names": 2,
|
|
||||||
"no-undef-init": 2,
|
|
||||||
"no-console": 1,
|
|
||||||
"camelcase": [
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"properties": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"comma-spacing": 2,
|
|
||||||
"comma-dangle": 1,
|
|
||||||
"new-cap": 2,
|
|
||||||
"new-parens": 2,
|
|
||||||
"arrow-parens": [2, "as-needed"],
|
|
||||||
"no-array-constructor": 2,
|
|
||||||
"array-callback-return": 1,
|
|
||||||
"no-extra-parens": 2,
|
|
||||||
"no-new-object": 2,
|
|
||||||
"no-spaced-func": 2,
|
|
||||||
"no-trailing-spaces": 2,
|
|
||||||
"no-underscore-dangle": 0,
|
|
||||||
"no-fallthrough": 0,
|
|
||||||
"semi": 2,
|
|
||||||
"semi-spacing": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"before": false,
|
|
||||||
"after": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"emcaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"impliedStrict": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
97
old/.circleci/config.yml
Normal file
97
old/.circleci/config.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: &node-image circleci/node:lts
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- &npm-cache-key npm-cache-{{ .Branch }}-{{ .Revision }}
|
||||||
|
- npm-cache-{{ .Branch }}
|
||||||
|
- npm-cache
|
||||||
|
- run:
|
||||||
|
name: Install
|
||||||
|
command: npm ci
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- node_modules
|
||||||
|
- save_cache:
|
||||||
|
key: *npm-cache-key
|
||||||
|
paths:
|
||||||
|
- ~/.npm/_cacache
|
||||||
|
|
||||||
|
lint:
|
||||||
|
docker:
|
||||||
|
- image: *node-image
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run:
|
||||||
|
name: Lint
|
||||||
|
command: npm run verify
|
||||||
|
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: *node-image
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- deploy:
|
||||||
|
name: Test
|
||||||
|
command: |
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
release_dry_run:
|
||||||
|
docker:
|
||||||
|
- image: *node-image
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- deploy:
|
||||||
|
name: Dry Release
|
||||||
|
command: |
|
||||||
|
npm run semantic-release -- --dry-run
|
||||||
|
|
||||||
|
release:
|
||||||
|
docker:
|
||||||
|
- image: *node-image
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- deploy:
|
||||||
|
name: Release
|
||||||
|
command: |
|
||||||
|
npm run semantic-release
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
publish:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- lint:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- test:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- release_dry_run:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
|
requires:
|
||||||
|
- test
|
||||||
|
- lint
|
||||||
|
- hold_release:
|
||||||
|
type: approval
|
||||||
|
requires:
|
||||||
|
- release_dry_run
|
||||||
|
- release:
|
||||||
|
requires:
|
||||||
|
- hold_release
|
||||||
2
old/.eslintignore
Normal file
2
old/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/*
|
||||||
|
bower_components/*
|
||||||
0
.gitattributes → old/.gitattributes
vendored
0
.gitattributes → old/.gitattributes
vendored
6
old/.gitignore
vendored
Normal file
6
old/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
test_tmp/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
dist/
|
||||||
|
npm-debug.log
|
||||||
@@ -16,5 +16,10 @@
|
|||||||
|
|
||||||
- Any new fixes are features should include new or updated [tests](/test).
|
- 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
|
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
|
||||||
- Submit your pull requests to the `master` branch, these will normally be merged into a seperate branch for any finally changes before being merged into `master`.
|
- Submit 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.
|
- Submit any bugs or requests to the issues page in Github.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- Clone the repository `git clone`
|
||||||
|
- Install dependencies `npm install`
|
||||||
21
old/LICENSE
Normal file
21
old/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Tyler Stewart
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
357
old/README.md
Normal file
357
old/README.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<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.
|
||||||
|
|
||||||
|
__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)
|
||||||
40
bin/index.js → old/bin/index.js
Executable file → Normal file
40
bin/index.js → old/bin/index.js
Executable file → Normal file
@@ -19,7 +19,8 @@ function setupYargs() {
|
|||||||
})
|
})
|
||||||
.option('username', {
|
.option('username', {
|
||||||
describe: 'Blank for anonymous',
|
describe: 'Blank for anonymous',
|
||||||
type: 'string'
|
type: 'string',
|
||||||
|
default: ''
|
||||||
})
|
})
|
||||||
.option('password', {
|
.option('password', {
|
||||||
describe: 'Password for given username',
|
describe: 'Password for given username',
|
||||||
@@ -36,6 +37,20 @@ function setupYargs() {
|
|||||||
boolean: true,
|
boolean: true,
|
||||||
default: false
|
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();
|
.parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +61,18 @@ function setupState(_args) {
|
|||||||
if (_args._ && _args._.length > 0) {
|
if (_args._ && _args._.length > 0) {
|
||||||
_state.url = _args._[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 === '';
|
_state.anonymous = _args.username === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRoot() {
|
function setupRoot() {
|
||||||
const dirPath = _args.root;
|
const dirPath = _args.root;
|
||||||
if (dirPath) {
|
if (dirPath) {
|
||||||
_state.root = process.cwd();
|
|
||||||
} else {
|
|
||||||
_state.root = dirPath;
|
_state.root = dirPath;
|
||||||
|
} else {
|
||||||
|
_state.root = process.cwd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,17 +113,25 @@ function setupState(_args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startFtpServer(_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) {
|
function checkLogin(data, resolve, reject) {
|
||||||
const user = _state.credentials[data.username]
|
const user = _state.credentials[data.username];
|
||||||
if (_state.anonymous || (user && user.password === data.password)) {
|
if (_state.anonymous || user && user.password === data.password) {
|
||||||
return resolve({root: (user && user.root) || _state.root});
|
return resolve({root: user && user.root || _state.root});
|
||||||
}
|
}
|
||||||
|
|
||||||
return reject(new errors.GeneralError('Invalid username or password', 401));
|
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ftpServer = new FtpSrv(_state.url, {
|
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,
|
anonymous: _state.anonymous,
|
||||||
blacklist: _state.blacklist
|
blacklist: _state.blacklist
|
||||||
});
|
});
|
||||||
44
old/changelog/v2_to_v3_migation.md
Normal file
44
old/changelog/v2_to_v3_migation.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Migration Guide - v2 to v3
|
||||||
|
|
||||||
|
The `FtpServer` constructor has been changed to only take one object option. Combining the two just made sense.
|
||||||
|
|
||||||
|
### From:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer('ftp://0.0.0.0:21');
|
||||||
|
```
|
||||||
|
|
||||||
|
### To:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer({
|
||||||
|
url: 'ftp://0.0.0.0:21'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
The `pasv_range` option has been changed to separate integer variables: `pasv_min`, `pasv_max`.
|
||||||
|
|
||||||
|
### From:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer(..., {
|
||||||
|
pasv_range: '1000-2000'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### To:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const server = new FtpServer({
|
||||||
|
pasv_min: 1000,
|
||||||
|
pasv_max: 2000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
The default passive port range has been changed to `1024` - `65535`
|
||||||
|
|
||||||
|
----
|
||||||
19
ftp-srv.d.ts → old/ftp-srv.d.ts
vendored
19
ftp-srv.d.ts → old/ftp-srv.d.ts
vendored
@@ -59,18 +59,22 @@ export class FtpConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FtpServerOptions {
|
export interface FtpServerOptions {
|
||||||
pasv_range?: number | string,
|
url?: string,
|
||||||
|
pasv_min?: number,
|
||||||
|
pasv_max?: number,
|
||||||
|
pasv_url?: string,
|
||||||
greeting?: string | string[],
|
greeting?: string | string[],
|
||||||
tls?: tls.SecureContext | false,
|
tls?: tls.SecureContextOptions | false,
|
||||||
anonymous?: boolean,
|
anonymous?: boolean,
|
||||||
blacklist?: Array<string>,
|
blacklist?: Array<string>,
|
||||||
whitelist?: Array<string>,
|
whitelist?: Array<string>,
|
||||||
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
||||||
log?: any
|
log?: any,
|
||||||
|
timeout?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FtpServer extends EventEmitter {
|
export class FtpServer extends EventEmitter {
|
||||||
constructor(url: string, options?: FtpServerOptions);
|
constructor(options?: FtpServerOptions);
|
||||||
|
|
||||||
readonly isTLS: boolean;
|
readonly isTLS: boolean;
|
||||||
|
|
||||||
@@ -108,6 +112,13 @@ export class FtpServer extends EventEmitter {
|
|||||||
whitelist?: Array<string>
|
whitelist?: Array<string>
|
||||||
}) => void,
|
}) => void,
|
||||||
reject: (err?: Error) => void
|
reject: (err?: Error) => void
|
||||||
|
) => void): this;
|
||||||
|
|
||||||
|
on(event: "disconnect", listener: (
|
||||||
|
data: {
|
||||||
|
connection: FtpConnection,
|
||||||
|
id: string
|
||||||
|
}
|
||||||
) => void): this;
|
) => void): this;
|
||||||
|
|
||||||
on(event: "client-error", listener: (
|
on(event: "client-error", listener: (
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
27
old/meta/contributors/contributors.js
Normal file
27
old/meta/contributors/contributors.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const {get} = require('https');
|
||||||
|
|
||||||
|
get('https://api.github.com/repos/trs/ftp-srv/contributors', {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Chrome'
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
let response = '';
|
||||||
|
res.on('data', (data) => {
|
||||||
|
response += data;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
const contributors = JSON.parse(response)
|
||||||
|
.filter((contributor) => contributor.type === 'User');
|
||||||
|
|
||||||
|
for (const contributor of contributors) {
|
||||||
|
const url = contributor.html_url;
|
||||||
|
const username = contributor.login;
|
||||||
|
|
||||||
|
const markdown = `- [${username}](${url})\n`;
|
||||||
|
|
||||||
|
process.stdout.write(markdown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
process.stderr.write(err);
|
||||||
|
});
|
||||||
10366
old/package-lock.json
generated
Normal file
10366
old/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
91
old/package.json
Normal file
91
old/package.json
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"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 test/**/*.spec.js test/*.spec.js --ui bdd",
|
||||||
|
"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.15",
|
||||||
|
"moment": "^2.22.1",
|
||||||
|
"uuid": "^3.2.1",
|
||||||
|
"yargs": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^8.1.0",
|
||||||
|
"@commitlint/config-conventional": "^8.1.0",
|
||||||
|
"@icetee/ftp": "^1.0.2",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"condition-circle": "^2.0.2",
|
||||||
|
"eslint": "^5.14.1",
|
||||||
|
"husky": "^1.3.1",
|
||||||
|
"lint-staged": "^8.1.4",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"rimraf": "^2.6.1",
|
||||||
|
"semantic-release": "^15.13.24",
|
||||||
|
"sinon": "^2.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.x"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,25 +3,30 @@ const Promise = require('bluebird');
|
|||||||
|
|
||||||
const REGISTRY = require('./registry');
|
const REGISTRY = require('./registry');
|
||||||
|
|
||||||
|
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
|
||||||
|
|
||||||
class FtpCommands {
|
class FtpCommands {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.previousCommand = {};
|
this.previousCommand = {};
|
||||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
|
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
|
||||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
|
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(message) {
|
parse(message) {
|
||||||
const strippedMessage = message.replace(/"/g, '');
|
const strippedMessage = message.replace(/"/g, '');
|
||||||
const [directive, ...args] = strippedMessage.split(' ');
|
let [directive, ...args] = strippedMessage.split(' ');
|
||||||
|
directive = _.chain(directive).trim().toUpper().value();
|
||||||
|
|
||||||
|
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
|
||||||
const params = args.reduce(({arg, flags}, param) => {
|
const params = args.reduce(({arg, flags}, param) => {
|
||||||
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
|
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
|
||||||
else arg.push(param);
|
else arg.push(param);
|
||||||
return {arg, flags};
|
return {arg, flags};
|
||||||
}, {arg: [], flags: []});
|
}, {arg: [], flags: []});
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
directive: _.chain(directive).trim().toUpper().value(),
|
directive,
|
||||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||||
flags: params.flags,
|
flags: params.flags,
|
||||||
raw: message
|
raw: message
|
||||||
@@ -40,7 +45,7 @@ class FtpCommands {
|
|||||||
log.trace({command: logCommand}, 'Handle command');
|
log.trace({command: logCommand}, 'Handle command');
|
||||||
|
|
||||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||||
return this.connection.reply(402, 'Command not allowed');
|
return this.connection.reply(502, 'Command not allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.includes(this.blacklist, command.directive)) {
|
if (_.includes(this.blacklist, command.directive)) {
|
||||||
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
directive: 'ABOR',
|
directive: 'ABOR',
|
||||||
handler: function () {
|
handler: function () {
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.then(socket => {
|
.then((socket) => {
|
||||||
return this.reply(426, {socket})
|
return this.reply(426, {socket})
|
||||||
.then(() => this.connector.end())
|
|
||||||
.then(() => this.reply(226));
|
.then(() => this.reply(226));
|
||||||
})
|
})
|
||||||
.catch(() => this.reply(225));
|
.catch(() => this.reply(225))
|
||||||
|
.finally(() => this.connector.end());
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
description: 'Abort an active file transfer'
|
description: 'Abort an active file transfer'
|
||||||
@@ -20,17 +20,17 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function handleTLS() {
|
function handleTLS() {
|
||||||
if (!this.server._tls) return this.reply(502);
|
if (!this.server.options.tls) return this.reply(502);
|
||||||
if (this.secure) return this.reply(202);
|
if (this.secure) return this.reply(202);
|
||||||
|
|
||||||
return this.reply(234)
|
return this.reply(234)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
||||||
isServer: true,
|
isServer: true,
|
||||||
secureContext
|
secureContext
|
||||||
});
|
});
|
||||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
|
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
|
||||||
function forwardEvent() {
|
function forwardEvent() {
|
||||||
this.emit.apply(this, arguments);
|
this.emit.apply(this, arguments);
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.chdir(command.arg))
|
return Promise.try(() => this.fs.chdir(command.arg))
|
||||||
.then(cwd => {
|
.then((cwd) => {
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||||
return this.reply(250, path);
|
return this.reply(250, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -6,11 +6,11 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.delete(command.arg))
|
return Promise.try(() => this.fs.delete(command.arg))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(250);
|
return this.reply(250);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -5,7 +5,7 @@ module.exports = {
|
|||||||
handler: function () {
|
handler: function () {
|
||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then(server => {
|
.then((server) => {
|
||||||
const {port} = server.address();
|
const {port} = server.address();
|
||||||
|
|
||||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
return feats;
|
return feats;
|
||||||
}, ['UTF8'])
|
}, ['UTF8'])
|
||||||
.sort()
|
.sort()
|
||||||
.map(feat => ({
|
.map((feat) => ({
|
||||||
message: ` ${feat}`,
|
message: ` ${feat}`,
|
||||||
raw: true
|
raw: true
|
||||||
}));
|
}));
|
||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
||||||
return this.reply(214, ...reply);
|
return this.reply(214, ...reply);
|
||||||
} else {
|
} else {
|
||||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'));
|
||||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -16,33 +16,34 @@ module.exports = {
|
|||||||
const path = command.arg || '.';
|
const path = command.arg || '.';
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => Promise.resolve(this.fs.get(path)))
|
.then(() => Promise.try(() => this.fs.get(path)))
|
||||||
.then(stat => stat.isDirectory() ? Promise.resolve(this.fs.list(path)) : [stat])
|
.then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
|
||||||
.then(files => {
|
.then((files) => {
|
||||||
const getFileMessage = file => {
|
const getFileMessage = (file) => {
|
||||||
if (simple) return file.name;
|
if (simple) return file.name;
|
||||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileList = files.map(file => {
|
return Promise.try(() => files.map((file) => {
|
||||||
const message = getFileMessage(file);
|
const message = getFileMessage(file);
|
||||||
return {
|
return {
|
||||||
raw: true,
|
raw: true,
|
||||||
message,
|
message,
|
||||||
socket: this.connector.socket
|
socket: this.connector.socket
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
return this.reply(150)
|
|
||||||
.then(() => {
|
|
||||||
if (fileList.length) return this.reply({}, ...fileList);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => this.reply(226))
|
.tap(() => this.reply(150))
|
||||||
.catch(Promise.TimeoutError, err => {
|
.then((fileList) => {
|
||||||
|
if (fileList.length) return this.reply({}, ...fileList);
|
||||||
|
return this.reply({socket: this.connector.socket, useEmptyMessage: true});
|
||||||
|
})
|
||||||
|
.tap(() => this.reply(226))
|
||||||
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(451, err.message || 'No directory');
|
return this.reply(451, err.message || 'No directory');
|
||||||
})
|
})
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.get(command.arg))
|
return Promise.try(() => this.fs.get(command.arg))
|
||||||
.then(fileStat => {
|
.then((fileStat) => {
|
||||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||||
return this.reply(213, modificationTime);
|
return this.reply(213, modificationTime);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.mkdir(command.arg))
|
return Promise.try(() => this.fs.mkdir(command.arg))
|
||||||
.then(dir => {
|
.then((dir) => {
|
||||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||||
return this.reply(257, path);
|
return this.reply(257, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -13,7 +13,7 @@ module.exports = {
|
|||||||
const [_option, ...args] = command.arg.split(' ');
|
const [_option, ...args] = command.arg.split(' ');
|
||||||
const option = _.toUpper(_option);
|
const option = _.toUpper(_option);
|
||||||
|
|
||||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command');
|
||||||
return OPTIONS[option].call(this, args);
|
return OPTIONS[option].call(this, args);
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
@@ -33,7 +33,6 @@ function utf8([setting] = []) {
|
|||||||
if (!encoding) return this.reply(501, 'Unknown setting for option');
|
if (!encoding) return this.reply(501, 'Unknown setting for option');
|
||||||
|
|
||||||
this.encoding = encoding;
|
this.encoding = encoding;
|
||||||
if (this.transferType !== 'binary') this.transferType = this.encoding;
|
|
||||||
|
|
||||||
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
|
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(230);
|
return this.reply(230);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(530, err.message || 'Authentication failed');
|
return this.reply(530, err.message || 'Authentication failed');
|
||||||
});
|
});
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
const PassiveConnector = require('../../connector/passive');
|
const PassiveConnector = require('../../connector/passive');
|
||||||
|
const {isLocalIP} = require('../../helpers/is-local');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PASV',
|
directive: 'PASV',
|
||||||
handler: function () {
|
handler: function ({log} = {}) {
|
||||||
|
if (!this.server.options.pasv_url) {
|
||||||
|
return this.reply(502);
|
||||||
|
}
|
||||||
|
|
||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then(server => {
|
.then((server) => {
|
||||||
const address = this.server.options.pasv_url;
|
let address = this.server.options.pasv_url;
|
||||||
|
// Allow connecting from local
|
||||||
|
if (isLocalIP(this.ip)) {
|
||||||
|
address = this.ip;
|
||||||
|
}
|
||||||
const {port} = server.address();
|
const {port} = server.address();
|
||||||
const host = address.replace(/\./g, ',');
|
const host = address.replace(/\./g, ',');
|
||||||
const portByte1 = port / 256 | 0;
|
const portByte1 = port / 256 | 0;
|
||||||
const portByte2 = port % 256;
|
const portByte2 = port % 256;
|
||||||
|
|
||||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.error(err);
|
||||||
|
return this.reply(425);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
@@ -3,18 +3,22 @@ const ActiveConnector = require('../../connector/active');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PORT',
|
directive: 'PORT',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({log, command} = {}) {
|
||||||
this.connector = new ActiveConnector(this);
|
this.connector = new ActiveConnector(this);
|
||||||
|
|
||||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||||
if (rawConnection.length !== 6) return this.reply(425);
|
if (rawConnection.length !== 6) return this.reply(425);
|
||||||
|
|
||||||
const ip = rawConnection.slice(0, 4).join('.');
|
const ip = rawConnection.slice(0, 4).join('.');
|
||||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
|
||||||
const port = portBytes[0] * 256 + portBytes[1];
|
const port = portBytes[0] * 256 + portBytes[1];
|
||||||
|
|
||||||
return this.connector.setupConnection(ip, port)
|
return this.connector.setupConnection(ip, port)
|
||||||
.then(() => this.reply(200));
|
.then(() => this.reply(200))
|
||||||
|
.catch((err) => {
|
||||||
|
log.error(err);
|
||||||
|
return this.reply(425);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
description: 'Specifies an address and port to which the server should connect'
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.currentDirectory())
|
return Promise.try(() => this.fs.currentDirectory())
|
||||||
.then(cwd => {
|
.then((cwd) => {
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||||
return this.reply(257, path);
|
return this.reply(257, path);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -10,18 +10,25 @@ module.exports = {
|
|||||||
|
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => Promise.resolve(this.fs.read(filePath, {start: this.restByteCount})))
|
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
|
||||||
.then(stream => {
|
.then((fsResponse) => {
|
||||||
const destroyConnection = (connection, reject) => err => {
|
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);
|
if (connection) connection.destroy(err);
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventsPromise = new Promise((resolve, reject) => {
|
const eventsPromise = new Promise((resolve, reject) => {
|
||||||
stream.on('data', data => {
|
stream.on('data', (data) => {
|
||||||
if (stream) stream.pause();
|
if (stream) stream.pause();
|
||||||
if (this.connector.socket) {
|
if (this.connector.socket) {
|
||||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
this.connector.socket.write(data, () => stream && stream.resume());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream.once('end', () => resolve());
|
stream.once('end', () => resolve());
|
||||||
@@ -34,15 +41,15 @@ module.exports = {
|
|||||||
|
|
||||||
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
||||||
.then(() => eventsPromise)
|
.then(() => eventsPromise)
|
||||||
.tap(() => this.emit('RETR', null, filePath))
|
.tap(() => this.emit('RETR', null, serverPath))
|
||||||
|
.then(() => this.reply(226, clientPath))
|
||||||
.finally(() => stream.destroy && stream.destroy());
|
.finally(() => stream.destroy && stream.destroy());
|
||||||
})
|
})
|
||||||
.then(() => this.reply(226))
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
.catch(Promise.TimeoutError, err => {
|
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
this.emit('RETR', err);
|
this.emit('RETR', err);
|
||||||
return this.reply(551, err.message);
|
return this.reply(551, err.message);
|
||||||
@@ -7,12 +7,12 @@ module.exports = {
|
|||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
const fileName = command.arg;
|
const fileName = command.arg;
|
||||||
return Promise.resolve(this.fs.get(fileName))
|
return Promise.try(() => this.fs.get(fileName))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.renameFrom = fileName;
|
this.renameFrom = fileName;
|
||||||
return this.reply(350);
|
return this.reply(350);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -11,12 +11,14 @@ module.exports = {
|
|||||||
const from = this.renameFrom;
|
const from = this.renameFrom;
|
||||||
const to = command.arg;
|
const to = command.arg;
|
||||||
|
|
||||||
return Promise.resolve(this.fs.rename(from, to))
|
return Promise.try(() => this.fs.rename(from, to))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(250);
|
return this.reply(250);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.tap(() => this.emit('RNTO', null, to))
|
||||||
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
this.emit('RNTO', err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -6,11 +6,11 @@ module.exports = function ({log, command} = {}) {
|
|||||||
|
|
||||||
const [mode, ...fileNameParts] = command.arg.split(' ');
|
const [mode, ...fileNameParts] = command.arg.split(' ');
|
||||||
const fileName = fileNameParts.join(' ');
|
const fileName = fileNameParts.join(' ');
|
||||||
return Promise.resolve(this.fs.chmod(fileName, parseInt(mode, 8)))
|
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(200);
|
return this.reply(200);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(500);
|
return this.reply(500);
|
||||||
});
|
});
|
||||||
@@ -6,11 +6,11 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.get(command.arg))
|
return Promise.try(() => this.fs.get(command.arg))
|
||||||
.then(fileStat => {
|
.then((fileStat) => {
|
||||||
return this.reply(213, {message: fileStat.size});
|
return this.reply(213, {message: fileStat.size});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
});
|
});
|
||||||
@@ -11,28 +11,28 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.get(path))
|
return Promise.try(() => this.fs.get(path))
|
||||||
.then(stat => {
|
.then((stat) => {
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
return Promise.resolve(this.fs.list(path))
|
return Promise.try(() => this.fs.list(path))
|
||||||
.then(stats => [213, stats]);
|
.then((stats) => [213, stats]);
|
||||||
}
|
}
|
||||||
return [212, [stat]];
|
return [212, [stat]];
|
||||||
})
|
})
|
||||||
.then(([code, fileStats]) => {
|
.then(([code, fileStats]) => {
|
||||||
return Promise.map(fileStats, file => {
|
return Promise.map(fileStats, (file) => {
|
||||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||||
return {
|
return {
|
||||||
raw: true,
|
raw: true,
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.then(messages => [code, messages]);
|
.then((messages) => [code, messages]);
|
||||||
})
|
})
|
||||||
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(450, err.message);
|
return this.reply(450, err.message);
|
||||||
});
|
});
|
||||||
@@ -11,11 +11,24 @@ module.exports = {
|
|||||||
|
|
||||||
return this.connector.waitForConnection()
|
return this.connector.waitForConnection()
|
||||||
.tap(() => this.commandSocket.pause())
|
.tap(() => this.commandSocket.pause())
|
||||||
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
|
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
|
||||||
.then(stream => {
|
.then((fsResponse) => {
|
||||||
const destroyConnection = (connection, reject) => err => {
|
let {stream, clientPath} = fsResponse;
|
||||||
if (connection) connection.destroy(err);
|
if (!stream && !clientPath) {
|
||||||
reject(err);
|
stream = fsResponse;
|
||||||
|
clientPath = fileName;
|
||||||
|
}
|
||||||
|
const serverPath = stream.path || fileName;
|
||||||
|
|
||||||
|
const destroyConnection = (connection, reject) => (err) => {
|
||||||
|
try {
|
||||||
|
if (connection) {
|
||||||
|
if (connection.writable) connection.end();
|
||||||
|
connection.destroy(err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const streamPromise = new Promise((resolve, reject) => {
|
const streamPromise = new Promise((resolve, reject) => {
|
||||||
@@ -24,10 +37,10 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const socketPromise = new Promise((resolve, reject) => {
|
const socketPromise = new Promise((resolve, reject) => {
|
||||||
this.connector.socket.on('data', data => {
|
this.connector.socket.on('data', (data) => {
|
||||||
if (this.connector.socket) this.connector.socket.pause();
|
if (this.connector.socket) this.connector.socket.pause();
|
||||||
if (stream) {
|
if (stream && stream.writable) {
|
||||||
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
|
stream.write(data, () => this.connector.socket && this.connector.socket.resume());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.connector.socket.once('end', () => {
|
this.connector.socket.once('end', () => {
|
||||||
@@ -41,16 +54,16 @@ module.exports = {
|
|||||||
this.restByteCount = 0;
|
this.restByteCount = 0;
|
||||||
|
|
||||||
return this.reply(150).then(() => this.connector.socket.resume())
|
return this.reply(150).then(() => this.connector.socket.resume())
|
||||||
.then(() => Promise.join(streamPromise, socketPromise))
|
.then(() => Promise.all([streamPromise, socketPromise]))
|
||||||
.tap(() => this.emit('STOR', null, fileName))
|
.tap(() => this.emit('STOR', null, serverPath))
|
||||||
|
.then(() => this.reply(226, clientPath))
|
||||||
.finally(() => stream.destroy && stream.destroy());
|
.finally(() => stream.destroy && stream.destroy());
|
||||||
})
|
})
|
||||||
.then(() => this.reply(226, fileName))
|
.catch(Promise.TimeoutError, (err) => {
|
||||||
.catch(Promise.TimeoutError, err => {
|
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425, 'No connection established');
|
return this.reply(425, 'No connection established');
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
this.emit('STOR', err);
|
this.emit('STOR', err);
|
||||||
return this.reply(550, err.message);
|
return this.reply(550, err.message);
|
||||||
@@ -8,12 +8,10 @@ module.exports = {
|
|||||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||||
|
|
||||||
const fileName = args.command.arg;
|
const fileName = args.command.arg;
|
||||||
return Promise.try(() => {
|
return Promise.try(() => this.fs.get(fileName))
|
||||||
return Promise.resolve(this.fs.get(fileName))
|
.then(() => Promise.try(() => this.fs.getUniqueName()))
|
||||||
.then(() => Promise.resolve(this.fs.getUniqueName()))
|
.catch(() => fileName)
|
||||||
.catch(() => Promise.resolve(fileName));
|
.then((name) => {
|
||||||
})
|
|
||||||
.then(name => {
|
|
||||||
args.command.arg = name;
|
args.command.arg = name;
|
||||||
return stor.call(this, args);
|
return stor.call(this, args);
|
||||||
});
|
});
|
||||||
@@ -13,7 +13,7 @@ module.exports = {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return this.reply(230);
|
return this.reply(230);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(530, err.message || 'Authentication failed');
|
return this.reply(530, err.message || 'Authentication failed');
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ const commands = [
|
|||||||
|
|
||||||
const registry = commands.reduce((result, cmd) => {
|
const registry = commands.reduce((result, cmd) => {
|
||||||
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
||||||
aliases.forEach(alias => result[alias] = cmd);
|
aliases.forEach((alias) => result[alias] = cmd);
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -25,12 +25,15 @@ class FtpConnection extends EventEmitter {
|
|||||||
this.connector = new BaseConnector(this);
|
this.connector = new BaseConnector(this);
|
||||||
|
|
||||||
this.commandSocket = options.socket;
|
this.commandSocket = options.socket;
|
||||||
this.commandSocket.on('error', err => {
|
this.commandSocket.on('error', (err) => {
|
||||||
this.log.error(err, 'Client error');
|
this.log.error(err, 'Client error');
|
||||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
||||||
});
|
});
|
||||||
this.commandSocket.on('data', this._handleData.bind(this));
|
this.commandSocket.on('data', this._handleData.bind(this));
|
||||||
this.commandSocket.on('timeout', () => {});
|
this.commandSocket.on('timeout', () => {
|
||||||
|
this.log.trace('Client timeout');
|
||||||
|
this.close().catch((e) => this.log.trace(e, 'Client close error'));
|
||||||
|
});
|
||||||
this.commandSocket.on('close', () => {
|
this.commandSocket.on('close', () => {
|
||||||
if (this.connector) this.connector.end();
|
if (this.connector) this.connector.end();
|
||||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||||
@@ -41,7 +44,7 @@ class FtpConnection extends EventEmitter {
|
|||||||
_handleData(data) {
|
_handleData(data) {
|
||||||
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
||||||
this.log.trace(messages);
|
this.log.trace(messages);
|
||||||
return Promise.mapSeries(messages, message => this.commands.handle(message));
|
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
get ip() {
|
get ip() {
|
||||||
@@ -68,7 +71,7 @@ class FtpConnection extends EventEmitter {
|
|||||||
|
|
||||||
close(code = 421, message = 'Closing connection') {
|
close(code = 421, message = 'Closing connection') {
|
||||||
return Promise.resolve(code)
|
return Promise.resolve(code)
|
||||||
.then(_code => _code && this.reply(_code, message))
|
.then((_code) => _code && this.reply(_code, message))
|
||||||
.then(() => this.commandSocket && this.commandSocket.end());
|
.then(() => this.commandSocket && this.commandSocket.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,31 +99,37 @@ class FtpConnection extends EventEmitter {
|
|||||||
if (!letters.length) letters = [{}];
|
if (!letters.length) letters = [{}];
|
||||||
return Promise.map(letters, (promise, index) => {
|
return Promise.map(letters, (promise, index) => {
|
||||||
return Promise.resolve(promise)
|
return Promise.resolve(promise)
|
||||||
.then(letter => {
|
.then((letter) => {
|
||||||
if (!letter) letter = {};
|
if (!letter) letter = {};
|
||||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||||
|
|
||||||
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
||||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
if (!options.useEmptyMessage) {
|
||||||
if (!letter.encoding) letter.encoding = this.encoding;
|
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||||
|
if (!letter.encoding) letter.encoding = this.encoding;
|
||||||
|
}
|
||||||
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
||||||
.then(message => {
|
.then((message) => {
|
||||||
const seperator = !options.hasOwnProperty('eol') ?
|
if (!options.useEmptyMessage) {
|
||||||
letters.length - 1 === index ? ' ' : '-' :
|
const seperator = !options.hasOwnProperty('eol') ?
|
||||||
options.eol ? ' ' : '-';
|
letters.length - 1 === index ? ' ' : '-' :
|
||||||
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
|
options.eol ? ' ' : '-';
|
||||||
letter.message = message;
|
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
|
||||||
|
letter.message = message;
|
||||||
|
} else {
|
||||||
|
letter.message = '';
|
||||||
|
}
|
||||||
return letter;
|
return letter;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const processLetter = letter => {
|
const processLetter = (letter) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (letter.socket && letter.socket.writable) {
|
if (letter.socket && letter.socket.writable) {
|
||||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
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 => {
|
letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.log.error(err);
|
this.log.error(err);
|
||||||
return reject(err);
|
return reject(err);
|
||||||
@@ -132,10 +141,10 @@ class FtpConnection extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return satisfyParameters()
|
return satisfyParameters()
|
||||||
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||||
return processLetter(letter, index);
|
return processLetter(letter, index);
|
||||||
}))
|
}))
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
this.log.error(err);
|
this.log.error(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -28,13 +28,12 @@ class Active extends Connector {
|
|||||||
return closeExistingServer()
|
return closeExistingServer()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.dataSocket = new Socket();
|
this.dataSocket = new Socket();
|
||||||
this.dataSocket.setEncoding(this.connection.transferType);
|
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
|
||||||
this.dataSocket.connect({host, port, family}, () => {
|
this.dataSocket.connect({host, port, family}, () => {
|
||||||
this.dataSocket.pause();
|
this.dataSocket.pause();
|
||||||
|
|
||||||
if (this.connection.secure) {
|
if (this.connection.secure) {
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
||||||
isServer: true,
|
isServer: true,
|
||||||
secureContext
|
secureContext
|
||||||
@@ -26,22 +26,28 @@ class Connector {
|
|||||||
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
closeSocket() {
|
||||||
const closeDataSocket = new Promise(resolve => {
|
if (this.dataSocket) {
|
||||||
if (this.dataSocket) this.dataSocket.end();
|
const socket = this.dataSocket;
|
||||||
else resolve();
|
this.dataSocket.end(() => socket.destroy());
|
||||||
});
|
|
||||||
const closeDataServer = new Promise(resolve => {
|
|
||||||
if (this.dataServer) this.dataServer.close(() => resolve());
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([closeDataSocket, closeDataServer])
|
|
||||||
.then(() => {
|
|
||||||
this.dataSocket = null;
|
this.dataSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeServer() {
|
||||||
|
if (this.dataServer) {
|
||||||
|
this.dataServer.close();
|
||||||
this.dataServer = null;
|
this.dataServer = null;
|
||||||
this.type = false;
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
|
end() {
|
||||||
|
this.closeSocket();
|
||||||
|
this.closeServer();
|
||||||
|
|
||||||
|
this.type = false;
|
||||||
|
this.connection.connector = new Connector(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Connector;
|
module.exports = Connector;
|
||||||
@@ -4,16 +4,17 @@ const ip = require('ip');
|
|||||||
const Promise = require('bluebird');
|
const Promise = require('bluebird');
|
||||||
|
|
||||||
const Connector = require('./base');
|
const Connector = require('./base');
|
||||||
const findPort = require('../helpers/find-port');
|
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
|
const CONNECT_TIMEOUT = 30 * 1000;
|
||||||
|
|
||||||
class Passive extends Connector {
|
class Passive extends Connector {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
super(connection);
|
super(connection);
|
||||||
this.type = 'passive';
|
this.type = 'passive';
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
waitForConnection({timeout = 5000, delay = 50} = {}) {
|
||||||
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
||||||
|
|
||||||
const checkSocket = () => {
|
const checkSocket = () => {
|
||||||
@@ -28,14 +29,13 @@ class Passive extends Connector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupServer() {
|
setupServer() {
|
||||||
const closeExistingServer = () => this.dataServer ?
|
this.closeServer();
|
||||||
new Promise(resolve => this.dataServer.close(() => resolve())) :
|
return this.server.getNextPasvPort()
|
||||||
Promise.resolve();
|
.then((port) => {
|
||||||
|
this.dataSocket = null;
|
||||||
|
let idleServerTimeout;
|
||||||
|
|
||||||
return closeExistingServer()
|
const connectionHandler = (socket) => {
|
||||||
.then(() => this.getPort())
|
|
||||||
.then(port => {
|
|
||||||
const connectionHandler = socket => {
|
|
||||||
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
||||||
this.log.error({
|
this.log.error({
|
||||||
pasv_connection: socket.remoteAddress,
|
pasv_connection: socket.remoteAddress,
|
||||||
@@ -46,34 +46,41 @@ class Passive extends Connector {
|
|||||||
return this.connection.reply(550, 'Remote addresses do not match')
|
return this.connection.reply(550, 'Remote addresses do not match')
|
||||||
.finally(() => this.connection.close());
|
.finally(() => this.connection.close());
|
||||||
}
|
}
|
||||||
|
clearTimeout(idleServerTimeout);
|
||||||
|
|
||||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||||
|
|
||||||
this.dataSocket = socket;
|
this.dataSocket = socket;
|
||||||
this.dataSocket.connected = true;
|
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||||
this.dataSocket.setEncoding(this.connection.transferType);
|
this.dataSocket.once('close', () => this.closeServer());
|
||||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
|
||||||
this.dataSocket.on('close', () => {
|
if (!this.connection.secure) {
|
||||||
this.log.trace('Passive connection closed');
|
this.dataSocket.connected = true;
|
||||||
this.end();
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dataSocket = null;
|
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
|
||||||
|
|
||||||
const serverOptions = Object.assign({}, this.connection.secure ? this.server._tls : {}, {pauseOnConnect: true});
|
|
||||||
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
|
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
|
||||||
this.dataServer.maxConnections = 1;
|
this.dataServer.maxConnections = 1;
|
||||||
|
|
||||||
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
||||||
this.dataServer.on('close', () => {
|
this.dataServer.once('close', () => {
|
||||||
this.log.trace('Passive server closed');
|
this.log.trace('Passive server closed');
|
||||||
this.dataServer = null;
|
this.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.connection.secure) {
|
||||||
|
this.dataServer.on('secureConnection', (socket) => {
|
||||||
|
socket.connected = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.dataServer.listen(port, this.server.url.hostname, err => {
|
this.dataServer.listen(port, this.server.url.hostname, (err) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else {
|
else {
|
||||||
|
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT);
|
||||||
|
|
||||||
this.log.debug({port}, 'Passive connection listening');
|
this.log.debug({port}, 'Passive connection listening');
|
||||||
resolve(this.dataServer);
|
resolve(this.dataServer);
|
||||||
}
|
}
|
||||||
@@ -82,15 +89,5 @@ class Passive extends Connector {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPort() {
|
|
||||||
if (this.server.options.pasv_range) {
|
|
||||||
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
|
|
||||||
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
|
|
||||||
[this.server.options.pasv_range];
|
|
||||||
return findPort(min, max);
|
|
||||||
}
|
|
||||||
throw new errors.ConnectorError('Invalid pasv_range');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
module.exports = Passive;
|
module.exports = Passive;
|
||||||
@@ -2,13 +2,14 @@ const _ = require('lodash');
|
|||||||
const nodePath = require('path');
|
const nodePath = require('path');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
const Promise = require('bluebird');
|
const Promise = require('bluebird');
|
||||||
const fs = Promise.promisifyAll(require('fs'));
|
const {createReadStream, createWriteStream, constants} = require('fs');
|
||||||
|
const fsAsync = require('./helpers/fs-async');
|
||||||
const errors = require('./errors');
|
const errors = require('./errors');
|
||||||
|
|
||||||
class FileSystem {
|
class FileSystem {
|
||||||
constructor(connection, {root, cwd} = {}) {
|
constructor(connection, {root, cwd} = {}) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
|
this.cwd = nodePath.normalize(cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep);
|
||||||
this._root = nodePath.resolve(root || process.cwd());
|
this._root = nodePath.resolve(root || process.cwd());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_resolvePath(path = '.') {
|
_resolvePath(path = '.') {
|
||||||
const serverPath = (() => {
|
const clientPath = (() => {
|
||||||
path = nodePath.normalize(path);
|
path = nodePath.normalize(path);
|
||||||
if (nodePath.isAbsolute(path)) {
|
if (nodePath.isAbsolute(path)) {
|
||||||
return nodePath.join(path);
|
return nodePath.join(path);
|
||||||
@@ -27,12 +28,12 @@ class FileSystem {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const fsPath = (() => {
|
const fsPath = (() => {
|
||||||
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${serverPath}`);
|
const resolvedPath = nodePath.join(this.root, clientPath);
|
||||||
return nodePath.join(resolvedPath);
|
return nodePath.resolve(nodePath.normalize(nodePath.join(resolvedPath)));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverPath,
|
clientPath,
|
||||||
fsPath
|
fsPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -43,20 +44,20 @@ class FileSystem {
|
|||||||
|
|
||||||
get(fileName) {
|
get(fileName) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath} = this._resolvePath(fileName);
|
||||||
return fs.statAsync(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
.then((stat) => _.set(stat, 'name', fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
list(path = '.') {
|
list(path = '.') {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.readdirAsync(fsPath)
|
return fsAsync.readdir(fsPath)
|
||||||
.then(fileNames => {
|
.then((fileNames) => {
|
||||||
return Promise.map(fileNames, fileName => {
|
return Promise.map(fileNames, (fileName) => {
|
||||||
const filePath = nodePath.join(fsPath, fileName);
|
const filePath = nodePath.join(fsPath, fileName);
|
||||||
return fs.accessAsync(filePath, fs.constants.F_OK)
|
return fsAsync.access(filePath, constants.F_OK)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return fs.statAsync(filePath)
|
return fsAsync.stat(filePath)
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
.then((stat) => _.set(stat, 'name', fileName));
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
});
|
});
|
||||||
@@ -65,61 +66,67 @@ class FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chdir(path = '.') {
|
chdir(path = '.') {
|
||||||
const {fsPath, serverPath} = this._resolvePath(path);
|
const {fsPath, clientPath} = this._resolvePath(path);
|
||||||
return fs.statAsync(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.tap(stat => {
|
.tap((stat) => {
|
||||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.cwd = serverPath;
|
this.cwd = clientPath;
|
||||||
return this.currentDirectory();
|
return this.currentDirectory();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
write(fileName, {append = false, start = undefined} = {}) {
|
write(fileName, {append = false, start = undefined} = {}) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||||
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
const stream = createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
||||||
stream.once('error', () => fs.unlinkAsync(fsPath));
|
stream.once('error', () => fsAsync.unlink(fsPath));
|
||||||
stream.once('close', () => stream.end());
|
stream.once('close', () => stream.end());
|
||||||
return stream;
|
return {
|
||||||
|
stream,
|
||||||
|
clientPath
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
read(fileName, {start = undefined} = {}) {
|
read(fileName, {start = undefined} = {}) {
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
const {fsPath, clientPath} = this._resolvePath(fileName);
|
||||||
return fs.statAsync(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.tap(stat => {
|
.tap((stat) => {
|
||||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
|
const stream = createReadStream(fsPath, {flags: 'r', start});
|
||||||
return stream;
|
return {
|
||||||
|
stream,
|
||||||
|
clientPath
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(path) {
|
delete(path) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.statAsync(fsPath)
|
return fsAsync.stat(fsPath)
|
||||||
.then(stat => {
|
.then((stat) => {
|
||||||
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
|
if (stat.isDirectory()) return fsAsync.rmdir(fsPath);
|
||||||
else return fs.unlinkAsync(fsPath);
|
else return fsAsync.unlink(fsPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir(path) {
|
mkdir(path) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.mkdirAsync(fsPath)
|
return fsAsync.mkdir(fsPath)
|
||||||
.then(() => fsPath);
|
.then(() => fsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
rename(from, to) {
|
rename(from, to) {
|
||||||
const {fsPath: fromPath} = this._resolvePath(from);
|
const {fsPath: fromPath} = this._resolvePath(from);
|
||||||
const {fsPath: toPath} = this._resolvePath(to);
|
const {fsPath: toPath} = this._resolvePath(to);
|
||||||
return fs.renameAsync(fromPath, toPath);
|
return fsAsync.rename(fromPath, toPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
chmod(path, mode) {
|
chmod(path, mode) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fs.chmodAsync(fsPath, mode);
|
return fsAsync.chmod(fsPath, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUniqueName() {
|
getUniqueName() {
|
||||||
61
old/src/helpers/find-port.js
Normal file
61
old/src/helpers/find-port.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
return () => new Promise((resolve, reject) => {
|
||||||
|
const portCheckServer = net.createServer();
|
||||||
|
portCheckServer.maxConnections = 0;
|
||||||
|
|
||||||
|
let attemptCount = 0;
|
||||||
|
const tryGetPort = () => {
|
||||||
|
attemptCount++;
|
||||||
|
if (attemptCount > maxAttempts) {
|
||||||
|
reject(new errors.ConnectorError('Unable to find valid port'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {value: port} = nextPortNumber.next();
|
||||||
|
|
||||||
|
portCheckServer.removeAllListeners();
|
||||||
|
portCheckServer.once('error', (err) => {
|
||||||
|
if (['EADDRINUSE'].includes(err.code)) {
|
||||||
|
tryGetPort();
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
portCheckServer.once('listening', () => {
|
||||||
|
portCheckServer.removeAllListeners();
|
||||||
|
portCheckServer.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
portCheckServer.listen(port, host);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tryGetPort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getNextPortFactory,
|
||||||
|
portNumberGenerator
|
||||||
|
};
|
||||||
18
old/src/helpers/fs-async.js
Normal file
18
old/src/helpers/fs-async.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const {promisify} = require('bluebird');
|
||||||
|
|
||||||
|
const methods = [
|
||||||
|
'stat',
|
||||||
|
'readdir',
|
||||||
|
'access',
|
||||||
|
'unlink',
|
||||||
|
'rmdir',
|
||||||
|
'mkdir',
|
||||||
|
'rename',
|
||||||
|
'chmod'
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = methods.reduce((obj, method) => {
|
||||||
|
obj[method] = promisify(fs[method]);
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
3
old/src/helpers/is-local.js
Normal file
3
old/src/helpers/is-local.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports.isLocalIP = function(ip) {
|
||||||
|
return ip === '127.0.0.1' || ip == '::1';
|
||||||
|
}
|
||||||
@@ -4,38 +4,47 @@ const nodeUrl = require('url');
|
|||||||
const buyan = require('bunyan');
|
const buyan = require('bunyan');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const fs = require('fs');
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const Connection = require('./connection');
|
const Connection = require('./connection');
|
||||||
const resolveHost = require('./helpers/resolve-host');
|
const {getNextPortFactory} = require('./helpers/find-port');
|
||||||
|
|
||||||
class FtpServer extends EventEmitter {
|
class FtpServer extends EventEmitter {
|
||||||
constructor(url, options = {}) {
|
constructor(options = {}) {
|
||||||
super();
|
super();
|
||||||
this.options = _.merge({
|
this.options = Object.assign({
|
||||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||||
anonymous: false,
|
url: 'ftp://127.0.0.1:21',
|
||||||
pasv_range: 22,
|
pasv_min: 1024,
|
||||||
|
pasv_max: 65535,
|
||||||
pasv_url: null,
|
pasv_url: null,
|
||||||
|
anonymous: false,
|
||||||
file_format: 'ls',
|
file_format: 'ls',
|
||||||
blacklist: [],
|
blacklist: [],
|
||||||
whitelist: [],
|
whitelist: [],
|
||||||
greeting: null,
|
greeting: null,
|
||||||
tls: false
|
tls: false,
|
||||||
|
timeout: 0
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
this._greeting = this.setupGreeting(this.options.greeting);
|
this._greeting = this.setupGreeting(this.options.greeting);
|
||||||
this._features = this.setupFeaturesMessage();
|
this._features = this.setupFeaturesMessage();
|
||||||
this._tls = this.setupTLS(this.options.tls);
|
|
||||||
|
|
||||||
delete this.options.greeting;
|
delete this.options.greeting;
|
||||||
delete this.options.tls;
|
|
||||||
|
|
||||||
this.connections = {};
|
this.connections = {};
|
||||||
this.log = this.options.log;
|
this.log = this.options.log;
|
||||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
this.url = nodeUrl.parse(this.options.url);
|
||||||
|
this.getNextPasvPort = getNextPortFactory(
|
||||||
|
_.get(this, 'url.hostname'),
|
||||||
|
_.get(this, 'options.pasv_min'),
|
||||||
|
_.get(this, 'options.pasv_max'));
|
||||||
|
|
||||||
const serverConnectionHandler = socket => {
|
const timeout = Number(this.options.timeout);
|
||||||
|
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
|
||||||
|
|
||||||
|
const serverConnectionHandler = (socket) => {
|
||||||
|
socket.setTimeout(this.options.timeout);
|
||||||
let connection = new Connection(this, {log: this.log, socket});
|
let connection = new Connection(this, {log: this.log, socket});
|
||||||
this.connections[connection.id] = connection;
|
this.connections[connection.id] = connection;
|
||||||
|
|
||||||
@@ -44,12 +53,12 @@ class FtpServer extends EventEmitter {
|
|||||||
const greeting = this._greeting || [];
|
const greeting = this._greeting || [];
|
||||||
const features = this._features || 'Ready';
|
const features = this._features || 'Ready';
|
||||||
return connection.reply(220, ...greeting, features)
|
return connection.reply(220, ...greeting, features)
|
||||||
.finally(() => socket.resume());
|
.finally(() => socket.resume());
|
||||||
};
|
};
|
||||||
const serverOptions = Object.assign({}, this.isTLS ? this._tls : {}, {pauseOnConnect: true});
|
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
|
||||||
|
|
||||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
||||||
this.server.on('error', err => this.log.error(err, '[Event] error'));
|
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
|
||||||
|
|
||||||
const quit = _.debounce(this.quit.bind(this), 100);
|
const quit = _.debounce(this.quit.bind(this), 100);
|
||||||
|
|
||||||
@@ -59,26 +68,25 @@ class FtpServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isTLS() {
|
get isTLS() {
|
||||||
return this.url.protocol === 'ftps:' && this._tls;
|
return this.url.protocol === 'ftps:' && this.options.tls;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
return resolveHost(this.options.pasv_url || this.url.hostname)
|
if (!this.options.pasv_url) {
|
||||||
.then(pasvUrl => {
|
this.log.warn('Passive URL not set. Passive connections not available.');
|
||||||
this.options.pasv_url = pasvUrl;
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.server.once('error', reject);
|
this.server.once('error', reject);
|
||||||
this.server.listen(this.url.port, this.url.hostname, err => {
|
this.server.listen(this.url.port, this.url.hostname, (err) => {
|
||||||
this.server.removeListener('error', reject);
|
this.server.removeListener('error', reject);
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
this.log.info({
|
this.log.info({
|
||||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
protocol: this.url.protocol.replace(/\W/g, ''),
|
||||||
ip: this.url.hostname,
|
ip: this.url.hostname,
|
||||||
port: this.url.port
|
port: this.url.port
|
||||||
}, 'Listening');
|
}, 'Listening');
|
||||||
resolve('Listening');
|
resolve('Listening');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,15 +98,6 @@ class FtpServer extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTLS(_tls) {
|
|
||||||
if (!_tls) return false;
|
|
||||||
return _.assign({}, _tls, {
|
|
||||||
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
|
|
||||||
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
|
|
||||||
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupGreeting(greet) {
|
setupGreeting(greet) {
|
||||||
if (!greet) return [];
|
if (!greet) return [];
|
||||||
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
|
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
|
||||||
@@ -117,9 +116,10 @@ class FtpServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectClient(id) {
|
disconnectClient(id) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const client = this.connections[id];
|
const client = this.connections[id];
|
||||||
if (!client) return resolve();
|
if (!client) return resolve();
|
||||||
|
this.emit('disconnect', {connection: client, id});
|
||||||
delete this.connections[id];
|
delete this.connections[id];
|
||||||
try {
|
try {
|
||||||
client.close(0);
|
client.close(0);
|
||||||
@@ -139,9 +139,9 @@ class FtpServer extends EventEmitter {
|
|||||||
close() {
|
close() {
|
||||||
this.log.info('Server closing...');
|
this.log.info('Server closing...');
|
||||||
this.server.maxConnections = 0;
|
this.server.maxConnections = 0;
|
||||||
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
|
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
|
||||||
.then(() => new Promise(resolve => {
|
.then(() => new Promise((resolve) => {
|
||||||
this.server.close(err => {
|
this.server.close((err) => {
|
||||||
if (err) this.log.error(err, 'Error closing server');
|
if (err) this.log.error(err, 'Error closing server');
|
||||||
resolve('Closed');
|
resolve('Closed');
|
||||||
});
|
});
|
||||||
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
commands = new FtpCommands(mockConnection);
|
commands = new FtpCommands(mockConnection);
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
|
|||||||
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
||||||
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
||||||
expect(cmd.directive).to.equal('TEST');
|
expect(cmd.directive).to.equal('TEST');
|
||||||
expect(cmd.arg).to.equal('arg1 arg2');
|
expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
|
||||||
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
|
expect(cmd.flags).to.deep.equal(['-l', '-A']);
|
||||||
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,6 +76,13 @@ describe('FtpCommands', function () {
|
|||||||
expect(cmd.flags).to.deep.equal(['-l']);
|
expect(cmd.flags).to.deep.equal(['-l']);
|
||||||
expect(cmd.raw).to.equal('list -l');
|
expect(cmd.raw).to.equal('list -l');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not check for option flags', () => {
|
||||||
|
const cmd = commands.parse('retr -test');
|
||||||
|
expect(cmd.directive).to.equal('RETR');
|
||||||
|
expect(cmd.arg).to.equal('-test');
|
||||||
|
expect(cmd.flags).to.deep.equal([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handle', function () {
|
describe('handle', function () {
|
||||||
@@ -83,7 +90,7 @@ describe('FtpCommands', function () {
|
|||||||
return commands.handle('bad')
|
return commands.handle('bad')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(mockConnection.reply.callCount).to.equal(1);
|
expect(mockConnection.reply.callCount).to.equal(1);
|
||||||
expect(mockConnection.reply.args[0][0]).to.equal(402);
|
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ const {expect} = require('chai');
|
|||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
const CMD = 'ABOR';
|
const CMD = 'ABOR';
|
||||||
describe(CMD, function () {
|
describe.skip(CMD, function () {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => Promise.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
@@ -15,7 +15,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -8,13 +8,15 @@ describe(CMD, function () {
|
|||||||
const mockClient = {
|
const mockClient = {
|
||||||
reply: () => Promise.resolve(),
|
reply: () => Promise.resolve(),
|
||||||
server: {
|
server: {
|
||||||
_tls: {}
|
options: {
|
||||||
|
tls: {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -16,7 +16,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.spy(mockClient.fs, 'chdir');
|
sandbox.spy(mockClient.fs, 'chdir');
|
||||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'delete').resolves();
|
sandbox.stub(mockClient.fs, 'delete').resolves();
|
||||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||||
@@ -13,7 +13,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
||||||
@@ -11,7 +11,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.spy(mockClient, 'reply');
|
sandbox.spy(mockClient, 'reply');
|
||||||
});
|
});
|
||||||
@@ -15,7 +15,7 @@ describe(CMD, function () {
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
waitForConnection: () => Promise.resolve({}),
|
waitForConnection: () => Promise.resolve({}),
|
||||||
end: () => {}
|
end: () => Promise.resolve({})
|
||||||
},
|
},
|
||||||
commandSocket: {
|
commandSocket: {
|
||||||
resume: () => {},
|
resume: () => {},
|
||||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
|||||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||||
|
|
||||||
sandbox.stub(mockClient, 'reply').resolves();
|
sandbox.stub(mockClient, 'reply').resolves();
|
||||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user