Compare commits

...

20 Commits

Author SHA1 Message Date
Matt Forster
78de22f518 chore: fix semantic release dry run 2020-12-16 15:36:11 -07:00
Matt Forster
2b140ecb0d chore: condition-circle using 'branch' 2020-12-16 15:27:40 -07:00
Matt Forster
57ddfb5e08 chore: set master as release branch 2020-12-16 15:22:56 -07:00
Matt Forster
beef19af30 chore: set release branch 2020-12-16 15:15:55 -07:00
Matt Forster
e7c5f83311 chore: setup release branch 2020-12-16 15:07:06 -07:00
Matt Forster
1ab793c04e chore: add install to release steps
requires some dev deps
2020-12-16 15:00:53 -07:00
Matt Forster
24f7126acf chore: improve ci flows (#227)
current release flow doesn't work
improve supported version testing
2020-12-16 14:58:20 -07:00
Matt Forster
457b859450 fix(fs): check resolved path against root (#224)
* fix(fs): check resolved path against root

This should prevent paths from being resolved above the root.

Should affect all commands that utilize the FS functions.

Fixes #167

* test: use __dirname for relative certs

* fix: improve path resolution

* chore: remove unused package

* fix: normalize resolve path if absolute

Otherwise join will normalize

Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-12-16 10:19:28 -07:00
Matt Forster
722da60a82 chore: set deploy context 2020-12-15 12:04:35 -07:00
dependabot[bot]
9f95d60916 chore(deps): bump ini from 1.3.5 to 1.3.7 (#223)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-10 10:46:24 -07:00
techmunk
4cd88b129c fix: ensure commandSocket is set before retrieving ip address of connection (#222) 2020-12-08 11:05:50 -07:00
dependabot[bot]
db49063b0d chore(deps-dev): bump semantic-release from 15.14.0 to 17.2.3 (#220)
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 15.14.0 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v15.14.0...v17.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-11-25 09:03:08 -07:00
dependabot[bot]
b55557292e chore(deps): bump node-fetch from 2.6.0 to 2.6.1 (#218)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-11 08:28:39 -06:00
Devonte
c87ce2fef6 fix: log errors, improved messages (#156)
Spelling errors, improved error logs.
2020-09-08 08:38:14 -06:00
Yaser
05a68cfb08 feat: pipe stream on fs write (#206)
* pipe allloewd on write stream

* code readable

* fix: 'if' statment missed curly braces

* Update src/commands/registration/stor.js

Replacing write data with pipe on STOR command

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>

* fix: socket on data replaced with socket on pipe

* fix: extra space

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>
Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-08-25 15:44:37 -06:00
Matt Forster
31290fc964 chore: security.md (#215)
* chore: security.md

* Update SECURITY.md
2020-08-18 11:44:53 -06:00
Matt Forster
a598fab03c fix: packages updates - npm advisories (#214)
Correct NPM advisories by updating package-lock.json and package
dependencies.

Major upgrade for yargs, but did not affect our code directly.
2020-08-17 15:44:23 -06:00
Matt Forster
87e8ac6ca8 chore: fix maintenance version regex 2020-08-17 13:43:27 -06:00
Matt Forster
75e34988f4 chore: add release support for backport branches (#213) 2020-08-17 13:37:49 -06:00
Tyler Stewart
e449e75219 fix: disallow PORT connections to alternate hosts
Ensure the data socket that the server connects to from the PORT command is the same IP as the current command socket.

* fix: add error handling to additional connection commands
2020-08-17 13:15:06 -06:00
22 changed files with 3449 additions and 2406 deletions

View File

@@ -1,97 +1,86 @@
version: 2
version: 2.1
orbs:
node: circleci/node@4.1.0
commands:
setup_git_bot:
description: set up the bot git user to make changes
steps:
- run:
name: "Git: Botovance"
command: |
git config --global user.name "Bot Vance"
git config --global user.email bot@autovance.com
executors:
node-lts:
parameters:
node-version:
type: string
default: lts
docker:
- image: cimg/node:<< parameters.node-version >>
jobs:
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
executor: node-lts
steps:
- checkout
- attach_workspace:
at: .
- node/install-packages
- 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
executor: node-lts
steps:
- checkout
- attach_workspace:
at: .
- node/install-packages
- setup_git_bot
- deploy:
name: Dry Release
command: |
npm run semantic-release -- --dry-run
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
release:
docker:
- image: *node-image
executor: node-lts
steps:
- checkout
- attach_workspace:
at: .
- node/install-packages
- setup_git_bot
- deploy:
name: Release
command: |
npm run semantic-release
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
workflows:
version: 2
publish:
test:
jobs:
- build
- lint:
requires:
- build
- test:
requires:
- build
- lint
- node/test:
matrix:
parameters:
version:
- '10.23'
- '12.20'
- '14.15'
- 'current'
- release_dry_run:
filters:
branches:
only: master
requires:
- test
- node/test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
context: deploy-npm
requires:
- hold_release

16
SECURITY.md Normal file
View File

@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 4.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| < 3.0 | :x: |
__Critical vulnerabilities will be ported as far back as possible.__
## Reporting a Vulnerability
Report a security vulnerability directly to the maintainers by sending an email to security@autovance.com
or by reporting a vulnerability to the [NPM and Github security teams](https://docs.npmjs.com/reporting-a-vulnerability-in-an-npm-package).

5547
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,12 +26,13 @@
},
"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"
"verifyConditions": "condition-circle",
"branch": "master",
"branches": ["master"]
},
"husky": {
"hooks": {
@@ -69,20 +70,20 @@
"lodash": "^4.17.15",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
"yargs": "^15.4.1"
},
"devDependencies": {
"@commitlint/cli": "^8.1.0",
"@commitlint/cli": "^10.0.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",
"lint-staged": "^8.2.1",
"mocha": "^8.1.1",
"rimraf": "^2.6.1",
"semantic-release": "^15.13.24",
"semantic-release": "^17.2.3",
"sinon": "^2.3.5"
},
"engines": {

View File

@@ -8,14 +8,18 @@ const FAMILY = {
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
handler: function ({log, command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'

View File

@@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'EPSV',
handler: function () {
handler: function ({log}) {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
const {port} = server.address();
return this.reply(229, `EPSV OK (|||${port}|)`);
})
.catch((err) => {
log.error(err);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} [<protocol>]',

View File

@@ -25,7 +25,7 @@ module.exports = {
})
.catch((err) => {
log.error(err);
return this.reply(425);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}}',

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'PBSZ',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.secure) return this.reply(202, 'Not supported');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
},

View File

@@ -17,7 +17,7 @@ module.exports = {
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(425);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',

View File

@@ -3,7 +3,7 @@ const _ = require('lodash');
module.exports = {
directive: 'PROT',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.secure) return this.reply(202, 'Not supported');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
switch (_.toUpper(command.arg)) {

View File

@@ -37,12 +37,7 @@ module.exports = {
});
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', (data) => {
if (this.connector.socket) this.connector.socket.pause();
if (stream && stream.writable) {
stream.write(data, () => this.connector.socket && this.connector.socket.resume());
}
});
this.connector.socket.pipe(stream, {end: false});
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
@@ -53,7 +48,7 @@ module.exports = {
this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket.resume())
return this.reply(150).then(() => this.connector.socket && this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))

View File

@@ -14,6 +14,7 @@ class FtpConnection extends EventEmitter {
super();
this.server = server;
this.id = uuid.v4();
this.commandSocket = options.socket;
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
@@ -24,7 +25,6 @@ class FtpConnection extends EventEmitter {
this.connector = new BaseConnector(this);
this.commandSocket = options.socket;
this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
@@ -129,10 +129,10 @@ class FtpConnection extends EventEmitter {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
if (err) {
this.log.error(err);
return reject(err);
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
return reject(error);
}
resolve();
});
@@ -147,8 +147,8 @@ class FtpConnection extends EventEmitter {
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((err) => {
this.log.error(err);
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message });
});
}
}

View File

@@ -1,7 +1,9 @@
const {Socket} = require('net');
const tls = require('tls');
const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const {SocketError} = require('../errors');
class Active extends Connector {
constructor(connection) {
@@ -27,6 +29,10 @@ class Active extends Connector {
return closeExistingServer()
.then(() => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
throw new SocketError('The given address is not yours', 500);
}
this.dataSocket = new Socket();
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {

View File

@@ -29,7 +29,7 @@ class Connector {
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket.end(() => socket && socket.destroy());
this.dataSocket = null;
}
}

View File

@@ -86,6 +86,10 @@ class Passive extends Connector {
}
});
});
})
.catch((error) => {
this.log.trace(error.message);
throw error;
});
}

View File

@@ -6,10 +6,13 @@ const {createReadStream, createWriteStream, constants} = require('fs');
const fsAsync = require('./helpers/fs-async');
const errors = require('./errors');
const UNIX_SEP_REGEX = /\//g;
const WIN_SEP_REGEX = /\\/g;
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = nodePath.normalize(cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep);
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'));
this._root = nodePath.resolve(root || process.cwd());
}
@@ -18,19 +21,21 @@ class FileSystem {
}
_resolvePath(path = '.') {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
const fsPath = (() => {
const resolvedPath = nodePath.join(this.root, clientPath);
return nodePath.resolve(nodePath.normalize(nodePath.join(resolvedPath)));
})();
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath);
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(nodePath.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep));
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/');
return {
clientPath,

View File

@@ -23,7 +23,7 @@ describe(CMD, function () {
});
it('// unsuccessful | no argument', () => {
return cmdFn()
return cmdFn({})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -25,7 +25,7 @@ describe(CMD, function () {
});
it('// successful IPv4', () => {
return cmdFn()
return cmdFn({})
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);

View File

@@ -29,7 +29,7 @@ describe(CMD, function () {
it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});

View File

@@ -13,14 +13,16 @@ describe('Connector - Active //', function () {
let getNextPort = getNextPortFactory(host, 1024);
let PORT;
let active;
let mockConnection = {};
let mockConnection = {
commandSocket: {
remoteAddress: '::ffff:127.0.0.1'
}
};
let sandbox;
let server;
before(() => {
active = new ActiveConnector(mockConnection);
});
beforeEach((done) => {
active = new ActiveConnector(mockConnection);
sandbox = sinon.sandbox.create().usingPromise(Promise);
getNextPort()
@@ -31,9 +33,12 @@ describe('Connector - Active //', function () {
.listen(PORT, () => done());
});
});
afterEach((done) => {
afterEach(() => {
sandbox.restore();
server.close(done);
server.close();
active.end();
});
it('sets up a connection', function () {
@@ -43,13 +48,27 @@ describe('Connector - Active //', function () {
});
});
it('destroys existing connection, then sets up a connection', function () {
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
it('rejects alternative host', function () {
return active.setupConnection('123.45.67.89', PORT)
.catch((err) => {
expect(err.code).to.equal(500);
expect(err.message).to.equal('The given address is not yours');
})
.finally(() => {
expect(active.dataSocket).not.to.exist;
});
});
it('destroys existing connection, then sets up a connection', function () {
return active.setupConnection(host, PORT)
.then(() => {
expect(destroyFnSpy.callCount).to.equal(1);
expect(active.dataSocket).to.exist;
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
return active.setupConnection(host, PORT)
.then(() => {
expect(destroyFnSpy.callCount).to.equal(1);
expect(active.dataSocket).to.exist;
});
});
});

View File

@@ -36,7 +36,7 @@ describe('FileSystem', function () {
describe('#_resolvePath', function () {
it('gets correct relative path', function () {
const result = fs._resolvePath();
const result = fs._resolvePath('.');
expect(result).to.be.an('object');
expect(result.clientPath).to.equal(
nodePath.normalize('/file/1/2/3'));
@@ -53,6 +53,15 @@ describe('FileSystem', function () {
nodePath.resolve('/tmp/ftp-srv/file/1/2'));
});
it('gets correct relative path', function () {
const result = fs._resolvePath('other');
expect(result).to.be.an('object');
expect(result.clientPath).to.equal(
nodePath.normalize('/file/1/2/3/other'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/file/1/2/3/other'));
});
it('gets correct absolute path', function () {
const result = fs._resolvePath('/other');
expect(result).to.be.an('object');
@@ -62,7 +71,7 @@ describe('FileSystem', function () {
nodePath.resolve('/tmp/ftp-srv/other'));
});
it('cannot escape root', function () {
it('cannot escape root - unix', function () {
const result = fs._resolvePath('../../../../../../../../../../..');
expect(result).to.be.an('object');
expect(result.clientPath).to.equal(
@@ -71,6 +80,24 @@ describe('FileSystem', function () {
nodePath.resolve('/tmp/ftp-srv'));
});
it('cannot escape root - win', function () {
const result = fs._resolvePath('.\\..\\..\\..\\..\\..\\..\\');
expect(result).to.be.an('object');
expect(result.clientPath).to.equal(
nodePath.normalize('/'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv'));
});
it('cannot escape root - backslash prefix', function () {
const result = fs._resolvePath('\\/../../../../../../');
expect(result).to.be.an('object');
expect(result.clientPath).to.equal(
nodePath.normalize('/'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv'));
});
it('resolves to file', function () {
const result = fs._resolvePath('/cool/file.txt');
expect(result).to.be.an('object');

View File

@@ -9,16 +9,16 @@ const server = new FtpServer({
pasv_min: 8881,
greeting: ['Welcome', 'to', 'the', 'jungle!'],
tls: {
key: fs.readFileSync(`${process.cwd()}/test/cert/server.key`),
cert: fs.readFileSync(`${process.cwd()}/test/cert/server.crt`),
ca: fs.readFileSync(`${process.cwd()}/test/cert/server.csr`)
key: fs.readFileSync(`${__dirname}/cert/server.key`),
cert: fs.readFileSync(`${__dirname}/cert/server.crt`),
ca: fs.readFileSync(`${__dirname}/cert/server.csr`)
},
file_format: 'ep',
anonymous: 'sillyrabbit'
});
server.on('login', ({username, password}, resolve, reject) => {
if (username === 'test' && password === 'test' || username === 'anonymous') {
resolve({root: require('os').homedir()});
resolve({root: __dirname});
} else reject('Bad username or password');
});
server.listen();