feat: remove host resolution (#143)
* fix: correctly try additional ports on EADDRINUSE * feat: remove passive url host resoltion See: https://github.com/trs/ftp-srv/issues/139 * feat: require pasv url to be specified * fix(pasv): check for set pasv url * chore: update scripts * fix(cli): remove undefined values * chore: add cli to eslint verify * chore: run eslint * chore: update scripts * feat: add maximum number of retries to port selection * chore: simplify eslint config * chore: simplify dev dependencies * chore: generate contributors * chore: update readme contributors
This commit is contained in:
@@ -23,10 +23,10 @@ base-build: &base-build
|
|||||||
- node_modules
|
- node_modules
|
||||||
- run:
|
- run:
|
||||||
name: Lint
|
name: Lint
|
||||||
command: npm run verify:js
|
command: npm run verify
|
||||||
- run:
|
- run:
|
||||||
name: Test
|
name: Test
|
||||||
command: npm run test:once
|
command: npm run test
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_node_10:
|
test_node_10:
|
||||||
|
|||||||
@@ -1,9 +1,2 @@
|
|||||||
# START_CONFIT_GENERATED_CONTENT
|
|
||||||
# Common folders to ignore
|
|
||||||
node_modules/*
|
node_modules/*
|
||||||
bower_components/*
|
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,6 @@
|
|||||||
|
test_tmp/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
reports/
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.nyc_output/
|
|
||||||
test_tmp/
|
|
||||||
@@ -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`
|
||||||
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
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -242,7 +242,7 @@ connection.on('RNTO', (error, fileName) => { ... });
|
|||||||
|
|
||||||
Occurs when a file is renamed.
|
Occurs when a file is renamed.
|
||||||
|
|
||||||
`error` if successful, will be `null`
|
`error` if successful, will be `null`
|
||||||
`fileName` name of the file that was renamed
|
`fileName` name of the file that was renamed
|
||||||
|
|
||||||
## Supported Commands
|
## Supported Commands
|
||||||
@@ -321,32 +321,34 @@ __Used in:__ `SITE CHMOD`
|
|||||||
Returns a unique file name to write to
|
Returns a unique file name to write to
|
||||||
__Used in:__ `STOU`
|
__Used in:__ `STOU`
|
||||||
|
|
||||||
<!--[RM_CONTRIBUTING]-->
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
- [OzairP](https://github.com/OzairP)
|
- [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)
|
- [TimLuq](https://github.com/TimLuq)
|
||||||
- [edin-mg](https://github.com/edin-m)
|
- [crabl](https://github.com/crabl)
|
||||||
|
- [hirviid](https://github.com/hirviid)
|
||||||
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
|
- [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)
|
- [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)
|
||||||
|
|
||||||
<!--[RM_LICENSE]-->
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
||||||
|
|||||||
10
bin/index.js
10
bin/index.js
@@ -113,11 +113,15 @@ 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));
|
||||||
|
|||||||
@@ -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, "always"],
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
meta/contributors/contributors.js
Normal file
27
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);
|
||||||
|
});
|
||||||
2896
package-lock.json
generated
2896
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ftp-srv",
|
"name": "ftp-srv",
|
||||||
"version": "0.0.0-development",
|
"version": "0.0.0",
|
||||||
"description": "Modern, extensible FTP Server",
|
"description": "Modern, extensible FTP Server",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ftp",
|
"ftp",
|
||||||
@@ -26,26 +26,40 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pre-release": "npm run verify",
|
"pre-release": "npm run verify",
|
||||||
"commitmsg": "cz-customizable-ghooks",
|
|
||||||
"dev": "cross-env NODE_ENV=development npm run verify:watch",
|
|
||||||
"prepush": "npm run verify && npm run test:once --silent",
|
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"start": "npm run dev",
|
"test": "mocha test/**/*.spec.js --ui bdd --bail",
|
||||||
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
|
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
|
||||||
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
|
|
||||||
"verify": "npm run verify:js --silent",
|
|
||||||
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
|
|
||||||
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success"
|
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"verifyConditions": "condition-circle"
|
"verifyConditions": "condition-circle"
|
||||||
},
|
},
|
||||||
"config": {
|
"husky": {
|
||||||
"commitizen": {
|
"hooks": {
|
||||||
"path": "node_modules/cz-customizable"
|
"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
|
||||||
},
|
},
|
||||||
"cz-customizable": {
|
"parserOptions": {
|
||||||
"config": "config/release/commitMessageConfig.js"
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -58,29 +72,20 @@
|
|||||||
"yargs": "^11.0.0"
|
"yargs": "^11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^7.5.2",
|
||||||
|
"@commitlint/config-conventional": "^7.5.0",
|
||||||
"@icetee/ftp": "^1.0.2",
|
"@icetee/ftp": "^1.0.2",
|
||||||
"chai": "^4.0.2",
|
"chai": "^4.2.0",
|
||||||
"condition-circle": "^1.6.0",
|
"condition-circle": "^2.0.2",
|
||||||
"cross-env": "3.1.4",
|
"eslint": "^5.14.1",
|
||||||
"cz-customizable": "5.2.0",
|
"husky": "^1.3.1",
|
||||||
"cz-customizable-ghooks": "1.5.0",
|
"lint-staged": "^8.1.4",
|
||||||
"dotenv": "^4.0.0",
|
|
||||||
"eslint": "4.5.0",
|
|
||||||
"eslint-config-google": "0.8.0",
|
|
||||||
"eslint-friendly-formatter": "3.0.0",
|
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
|
||||||
"eslint-plugin-node": "5.1.1",
|
|
||||||
"husky": "0.13.3",
|
|
||||||
"istanbul": "0.4.5",
|
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"mocha-junit-reporter": "1.13.0",
|
"rimraf": "^2.6.1",
|
||||||
"mocha-multi-reporters": "1.1.5",
|
|
||||||
"rimraf": "2.6.1",
|
|
||||||
"semantic-release": "^15.10.6",
|
"semantic-release": "^15.10.6",
|
||||||
"sinon": "^2.3.5"
|
"sinon": "^2.3.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.x",
|
"node": ">=6.x"
|
||||||
"npm": ">=5.x"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ const PassiveConnector = require('../../connector/passive');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PASV',
|
directive: 'PASV',
|
||||||
handler: function () {
|
handler: function () {
|
||||||
|
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) => {
|
||||||
|
|||||||
@@ -1,33 +1,56 @@
|
|||||||
const net = require('net');
|
const net = require('net');
|
||||||
const Promise = require('bluebird');
|
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
function* portNumberGenerator(min, max) {
|
const MAX_PORT = 65535;
|
||||||
|
const MAX_PORT_CHECK_ATTEMPT = 5;
|
||||||
|
|
||||||
|
function* portNumberGenerator(min, max = MAX_PORT) {
|
||||||
let current = min;
|
let current = min;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (current > 65535 || current > max) {
|
if (current > MAX_PORT || current > max) {
|
||||||
current = min;
|
current = min;
|
||||||
}
|
}
|
||||||
yield current++;
|
yield current++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextPortFactory(min, max = Infinity) {
|
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
|
||||||
const nextPortNumber = portNumberGenerator(min, max);
|
const nextPortNumber = portNumberGenerator(portMin, portMax);
|
||||||
const portCheckServer = net.createServer();
|
const portCheckServer = net.createServer();
|
||||||
portCheckServer.maxConnections = 0;
|
portCheckServer.maxConnections = 0;
|
||||||
portCheckServer.on('error', () => {
|
|
||||||
portCheckServer.listen(nextPortNumber.next().value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => new Promise((resolve) => {
|
return () => new Promise((resolve, reject) => {
|
||||||
portCheckServer.once('listening', () => {
|
let attemptCount = 0;
|
||||||
const {port} = portCheckServer.address();
|
const tryGetPort = () => {
|
||||||
portCheckServer.close(() => resolve(port));
|
attemptCount++;
|
||||||
});
|
if (attemptCount > maxAttempts) {
|
||||||
portCheckServer.listen(nextPortNumber.next().value);
|
reject(new errors.ConnectorError('Unable to find valid port'));
|
||||||
})
|
return;
|
||||||
.catch(RangeError, (err) => Promise.reject(new errors.ConnectorError(err.message)));
|
}
|
||||||
|
|
||||||
|
const {value: port} = nextPortNumber.next();
|
||||||
|
|
||||||
|
portCheckServer.removeAllListeners();
|
||||||
|
portCheckServer.once('error', (err) => {
|
||||||
|
if (['EADDRINUSE'].includes(err.code)) {
|
||||||
|
tryGetPort();
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
portCheckServer.once('listening', () => {
|
||||||
|
portCheckServer.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
portCheckServer.listen(port, host);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tryGetPort();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
|
||||||
|
|
||||||
module.exports = function (hostname) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!hostname || hostname === '0.0.0.0') {
|
|
||||||
let ip = '';
|
|
||||||
http.get(IP_WEBSITE, (response) => {
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
|
|
||||||
}
|
|
||||||
response.setEncoding('utf8');
|
|
||||||
response.on('data', (chunk) => {
|
|
||||||
ip += chunk;
|
|
||||||
});
|
|
||||||
response.on('end', () => {
|
|
||||||
resolve(ip);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else resolve(hostname);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
31
src/index.js
31
src/index.js
@@ -7,7 +7,6 @@ const tls = require('tls');
|
|||||||
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');
|
const {getNextPortFactory} = require('./helpers/find-port');
|
||||||
|
|
||||||
class FtpServer extends EventEmitter {
|
class FtpServer extends EventEmitter {
|
||||||
@@ -36,6 +35,7 @@ class FtpServer extends EventEmitter {
|
|||||||
this.log = this.options.log;
|
this.log = this.options.log;
|
||||||
this.url = nodeUrl.parse(this.options.url);
|
this.url = nodeUrl.parse(this.options.url);
|
||||||
this.getNextPasvPort = getNextPortFactory(
|
this.getNextPasvPort = getNextPortFactory(
|
||||||
|
_.get(this, 'options.pasv_url'),
|
||||||
_.get(this, 'options.pasv_min'),
|
_.get(this, 'options.pasv_min'),
|
||||||
_.get(this, 'options.pasv_max'));
|
_.get(this, 'options.pasv_max'));
|
||||||
|
|
||||||
@@ -67,22 +67,21 @@ class FtpServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const ActiveConnector = require('../../src/connector/active');
|
|||||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||||
|
|
||||||
describe('Connector - Active //', function () {
|
describe('Connector - Active //', function () {
|
||||||
let getNextPort = getNextPortFactory(1024);
|
let getNextPort = getNextPortFactory('::', 1024);
|
||||||
let PORT;
|
let PORT;
|
||||||
let active;
|
let active;
|
||||||
let mockConnection = {};
|
let mockConnection = {};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('Connector - Passive //', function () {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
url: '',
|
url: '',
|
||||||
getNextPasvPort: getNextPortFactory(1024)
|
getNextPasvPort: getNextPortFactory('::', 1024)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let sandbox;
|
let sandbox;
|
||||||
@@ -48,7 +48,7 @@ describe('Connector - Passive //', function () {
|
|||||||
|
|
||||||
describe('setup', function () {
|
describe('setup', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory());
|
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory('::'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('no pasv range provided', function (done) {
|
it('no pasv range provided', function (done) {
|
||||||
@@ -56,7 +56,7 @@ describe('Connector - Passive //', function () {
|
|||||||
passive.setupServer()
|
passive.setupServer()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
try {
|
try {
|
||||||
expect(err.name).to.equal('ConnectorError');
|
expect(err.name).to.contain('RangeError');
|
||||||
done();
|
done();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
done(ex);
|
done(ex);
|
||||||
@@ -68,7 +68,7 @@ describe('Connector - Passive //', function () {
|
|||||||
describe('setup', function () {
|
describe('setup', function () {
|
||||||
let connection;
|
let connection;
|
||||||
before(function () {
|
before(function () {
|
||||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(-1, -1));
|
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory('::', -1, -1));
|
||||||
|
|
||||||
connection = new PassiveConnector(mockConnection);
|
connection = new PassiveConnector(mockConnection);
|
||||||
});
|
});
|
||||||
@@ -76,7 +76,7 @@ describe('Connector - Passive //', function () {
|
|||||||
it('has invalid pasv range', function (done) {
|
it('has invalid pasv range', function (done) {
|
||||||
connection.setupServer()
|
connection.setupServer()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
expect(err.name).to.equal('ConnectorError');
|
expect(err.name).to.contain('RangeError');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,52 +1,30 @@
|
|||||||
/* eslint no-unused-expressions: 0 */
|
/* eslint no-unused-expressions: 0 */
|
||||||
const {expect} = require('chai');
|
const {expect} = require('chai');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const sinon = require('sinon');
|
|
||||||
|
|
||||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||||
|
|
||||||
describe('helpers // find-port', function () {
|
describe('helpers // find-port', function () {
|
||||||
let sandbox;
|
describe('keeps trying new ports', () => {
|
||||||
let getNextPort;
|
let getNextPort;
|
||||||
|
let serverAlreadyRunning;
|
||||||
|
beforeEach((done) => {
|
||||||
|
const host = '0.0.0.0';
|
||||||
|
getNextPort = getNextPortFactory(host, 8821);
|
||||||
|
|
||||||
beforeEach(() => {
|
serverAlreadyRunning = net.createServer();
|
||||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
serverAlreadyRunning.listen(8821, host, () => done());
|
||||||
|
|
||||||
getNextPort = getNextPortFactory(1, 2);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('finds a port', () => {
|
|
||||||
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) {
|
|
||||||
this.address = () => ({port});
|
|
||||||
setImmediate(() => this.emit('listening'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return getNextPort()
|
afterEach((done) => {
|
||||||
.then((port) => {
|
serverAlreadyRunning.close(() => done());
|
||||||
expect(port).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('restarts count', () => {
|
|
||||||
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) {
|
|
||||||
this.address = () => ({port});
|
|
||||||
setImmediate(() => this.emit('listening'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return getNextPort()
|
it('test', () => {
|
||||||
.then((port) => {
|
return getNextPort()
|
||||||
expect(port).to.equal(1);
|
.then((port) => {
|
||||||
})
|
expect(port).to.equal(8822);
|
||||||
.then(() => getNextPort())
|
});
|
||||||
.then((port) => {
|
|
||||||
expect(port).to.equal(2);
|
|
||||||
})
|
|
||||||
.then(() => getNextPort())
|
|
||||||
.then((port) => {
|
|
||||||
expect(port).to.equal(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
const {expect} = require('chai');
|
|
||||||
const sinon = require('sinon');
|
|
||||||
const resolveHost = require('../../src/helpers/resolve-host');
|
|
||||||
|
|
||||||
describe('helpers //resolve-host', function () {
|
|
||||||
this.timeout(4000);
|
|
||||||
|
|
||||||
let sandbox;
|
|
||||||
beforeEach(() => {
|
|
||||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
|
||||||
});
|
|
||||||
afterEach(() => sandbox.restore());
|
|
||||||
|
|
||||||
it('fetches ip address', () => {
|
|
||||||
const hostname = '0.0.0.0';
|
|
||||||
return resolveHost(hostname)
|
|
||||||
.then((resolvedHostname) => {
|
|
||||||
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fetches ip address', () => {
|
|
||||||
const hostname = null;
|
|
||||||
return resolveHost(hostname)
|
|
||||||
.then((resolvedHostname) => {
|
|
||||||
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing', () => {
|
|
||||||
const hostname = '127.0.0.1';
|
|
||||||
return resolveHost(hostname)
|
|
||||||
.then((resolvedHostname) => {
|
|
||||||
expect(resolvedHostname).to.equal(hostname);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails on getting hostname', () => {
|
|
||||||
sandbox.stub(require('http'), 'get').callsFake(function (url, cb) {
|
|
||||||
cb({
|
|
||||||
statusCode: 420
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return resolveHost(null)
|
|
||||||
.then(() => expect(1).to.equal(2))
|
|
||||||
.catch((err) => {
|
|
||||||
expect(err.code).to.equal(420);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -48,6 +48,7 @@ describe('Integration', function () {
|
|||||||
function startServer(options = {}) {
|
function startServer(options = {}) {
|
||||||
server = new FtpServer(_.assign({
|
server = new FtpServer(_.assign({
|
||||||
log,
|
log,
|
||||||
|
pasv_url: '127.0.0.1',
|
||||||
pasv_min: 8881,
|
pasv_min: 8881,
|
||||||
greeting: ['hello', 'world'],
|
greeting: ['hello', 'world'],
|
||||||
anonymous: true
|
anonymous: true
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
require('dotenv').load();
|
|
||||||
const bunyan = require('bunyan');
|
const bunyan = require('bunyan');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const FtpServer = require('../src');
|
const FtpServer = require('../src');
|
||||||
|
|||||||
Reference in New Issue
Block a user