| @@ -0,0 +1,332 @@ | ||
| /** | ||
| * @fileoverview A rule to choose between single and double quote marks | ||
| * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton | ||
| */ | ||
|
|
||
| "use strict"; | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
| // Requirements | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| const astUtils = require("./utils/ast-utils"); | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
| // Constants | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| const QUOTE_SETTINGS = { | ||
| double: { | ||
| quote: "\"", | ||
| alternateQuote: "'", | ||
| description: "doublequote" | ||
| }, | ||
| single: { | ||
| quote: "'", | ||
| alternateQuote: "\"", | ||
| description: "singlequote" | ||
| }, | ||
| backtick: { | ||
| quote: "`", | ||
| alternateQuote: "\"", | ||
| description: "backtick" | ||
| } | ||
| }; | ||
|
|
||
| // An unescaped newline is a newline preceded by an even number of backslashes. | ||
| const UNESCAPED_LINEBREAK_PATTERN = new RegExp(String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`, "u"); | ||
|
|
||
| /** | ||
| * Switches quoting of javascript string between ' " and ` | ||
| * escaping and unescaping as necessary. | ||
| * Only escaping of the minimal set of characters is changed. | ||
| * Note: escaping of newlines when switching from backtick to other quotes is not handled. | ||
| * @param {string} str A string to convert. | ||
| * @returns {string} The string with changed quotes. | ||
| * @private | ||
| */ | ||
| QUOTE_SETTINGS.double.convert = | ||
| QUOTE_SETTINGS.single.convert = | ||
| QUOTE_SETTINGS.backtick.convert = function(str) { | ||
| const newQuote = this.quote; | ||
| const oldQuote = str[0]; | ||
|
|
||
| if (newQuote === oldQuote) { | ||
| return str; | ||
| } | ||
| return newQuote + str.slice(1, -1).replace(/\\(\$\{|\r\n?|\n|.)|["'`]|\$\{|(\r\n?|\n)/gu, (match, escaped, newline) => { | ||
| if (escaped === oldQuote || oldQuote === "`" && escaped === "${") { | ||
| return escaped; // unescape | ||
| } | ||
| if (match === newQuote || newQuote === "`" && match === "${") { | ||
| return `\\${match}`; // escape | ||
| } | ||
| if (newline && oldQuote === "`") { | ||
| return "\\n"; // escape newlines | ||
| } | ||
| return match; | ||
| }) + newQuote; | ||
| }; | ||
|
|
||
| const AVOID_ESCAPE = "avoid-escape"; | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
| // Rule Definition | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| module.exports = { | ||
| meta: { | ||
| type: "layout", | ||
|
|
||
| docs: { | ||
| description: "enforce the consistent use of either backticks, double, or single quotes", | ||
| category: "Stylistic Issues", | ||
| recommended: false, | ||
| url: "https://eslint.org/docs/rules/quotes" | ||
| }, | ||
|
|
||
| fixable: "code", | ||
|
|
||
| schema: [ | ||
| { | ||
| enum: ["single", "double", "backtick"] | ||
| }, | ||
| { | ||
| anyOf: [ | ||
| { | ||
| enum: ["avoid-escape"] | ||
| }, | ||
| { | ||
| type: "object", | ||
| properties: { | ||
| avoidEscape: { | ||
| type: "boolean" | ||
| }, | ||
| allowTemplateLiterals: { | ||
| type: "boolean" | ||
| } | ||
| }, | ||
| additionalProperties: false | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
|
|
||
| messages: { | ||
| wrongQuotes: "Strings must use {{description}}." | ||
| } | ||
| }, | ||
|
|
||
| create(context) { | ||
|
|
||
| const quoteOption = context.options[0], | ||
| settings = QUOTE_SETTINGS[quoteOption || "double"], | ||
| options = context.options[1], | ||
| allowTemplateLiterals = options && options.allowTemplateLiterals === true, | ||
| sourceCode = context.getSourceCode(); | ||
| let avoidEscape = options && options.avoidEscape === true; | ||
|
|
||
| // deprecated | ||
| if (options === AVOID_ESCAPE) { | ||
| avoidEscape = true; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if a given node is part of JSX syntax. | ||
| * | ||
| * This function returns `true` in the following cases: | ||
| * | ||
| * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`. | ||
| * - `<div>foo</div>` ... If the literal is a text content, the parent of the literal is `JSXElement`. | ||
| * - `<>foo</>` ... If the literal is a text content, the parent of the literal is `JSXFragment`. | ||
| * | ||
| * In particular, this function returns `false` in the following cases: | ||
| * | ||
| * - `<div className={"foo"}></div>` | ||
| * - `<div>{"foo"}</div>` | ||
| * | ||
| * In both cases, inside of the braces is handled as normal JavaScript. | ||
| * The braces are `JSXExpressionContainer` nodes. | ||
| * @param {ASTNode} node The Literal node to check. | ||
| * @returns {boolean} True if the node is a part of JSX, false if not. | ||
| * @private | ||
| */ | ||
| function isJSXLiteral(node) { | ||
| return node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement" || node.parent.type === "JSXFragment"; | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether or not a given node is a directive. | ||
| * The directive is a `ExpressionStatement` which has only a string literal. | ||
| * @param {ASTNode} node A node to check. | ||
| * @returns {boolean} Whether or not the node is a directive. | ||
| * @private | ||
| */ | ||
| function isDirective(node) { | ||
| return ( | ||
| node.type === "ExpressionStatement" && | ||
| node.expression.type === "Literal" && | ||
| typeof node.expression.value === "string" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether or not a given node is a part of directive prologues. | ||
| * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive | ||
| * @param {ASTNode} node A node to check. | ||
| * @returns {boolean} Whether or not the node is a part of directive prologues. | ||
| * @private | ||
| */ | ||
| function isPartOfDirectivePrologue(node) { | ||
| const block = node.parent.parent; | ||
|
|
||
| if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) { | ||
| return false; | ||
| } | ||
|
|
||
| // Check the node is at a prologue. | ||
| for (let i = 0; i < block.body.length; ++i) { | ||
| const statement = block.body[i]; | ||
|
|
||
| if (statement === node.parent) { | ||
| return true; | ||
| } | ||
| if (!isDirective(statement)) { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether or not a given node is allowed as non backtick. | ||
| * @param {ASTNode} node A node to check. | ||
| * @returns {boolean} Whether or not the node is allowed as non backtick. | ||
| * @private | ||
| */ | ||
| function isAllowedAsNonBacktick(node) { | ||
| const parent = node.parent; | ||
|
|
||
| switch (parent.type) { | ||
|
|
||
| // Directive Prologues. | ||
| case "ExpressionStatement": | ||
| return isPartOfDirectivePrologue(node); | ||
|
|
||
| // LiteralPropertyName. | ||
| case "Property": | ||
| case "MethodDefinition": | ||
| return parent.key === node && !parent.computed; | ||
|
|
||
| // ModuleSpecifier. | ||
| case "ImportDeclaration": | ||
| case "ExportNamedDeclaration": | ||
| case "ExportAllDeclaration": | ||
| return parent.source === node; | ||
|
|
||
| // Others don't allow. | ||
| default: | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether or not a given TemplateLiteral node is actually using any of the special features provided by template literal strings. | ||
| * @param {ASTNode} node A TemplateLiteral node to check. | ||
| * @returns {boolean} Whether or not the TemplateLiteral node is using any of the special features provided by template literal strings. | ||
| * @private | ||
| */ | ||
| function isUsingFeatureOfTemplateLiteral(node) { | ||
| const hasTag = node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi; | ||
|
|
||
| if (hasTag) { | ||
| return true; | ||
| } | ||
|
|
||
| const hasStringInterpolation = node.expressions.length > 0; | ||
|
|
||
| if (hasStringInterpolation) { | ||
| return true; | ||
| } | ||
|
|
||
| const isMultilineString = node.quasis.length >= 1 && UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw); | ||
|
|
||
| if (isMultilineString) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| return { | ||
|
|
||
| Literal(node) { | ||
| const val = node.value, | ||
| rawVal = node.raw; | ||
|
|
||
| if (settings && typeof val === "string") { | ||
| let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || | ||
| isJSXLiteral(node) || | ||
| astUtils.isSurroundedBy(rawVal, settings.quote); | ||
|
|
||
| if (!isValid && avoidEscape) { | ||
| isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; | ||
| } | ||
|
|
||
| if (!isValid) { | ||
| context.report({ | ||
| node, | ||
| messageId: "wrongQuotes", | ||
| data: { | ||
| description: settings.description | ||
| }, | ||
| fix(fixer) { | ||
| if (quoteOption === "backtick" && astUtils.hasOctalEscapeSequence(rawVal)) { | ||
|
|
||
| // An octal escape sequence in a template literal would produce syntax error, even in non-strict mode. | ||
| return null; | ||
| } | ||
|
|
||
| return fixer.replaceText(node, settings.convert(node.raw)); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| TemplateLiteral(node) { | ||
| // Don't throw an error if backticks are expected or a template literal feature is in use. | ||
| if ( | ||
| allowTemplateLiterals || | ||
| quoteOption === "backtick" || | ||
| isUsingFeatureOfTemplateLiteral(node) || | ||
| settings && avoidEscape && node.quasis[0].value.raw.indexOf(settings.quote) >= 0 | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| context.report({ | ||
| node, | ||
| messageId: "wrongQuotes", | ||
| data: { | ||
| description: settings.description | ||
| }, | ||
| fix(fixer) { | ||
| if (isPartOfDirectivePrologue(node)) { | ||
|
|
||
| /* | ||
| * TemplateLiterals in a directive prologue aren't actually directives, but if they're | ||
| * in the directive prologue, then fixing them might turn them into directives and change | ||
| * the behavior of the code. | ||
| */ | ||
| return null; | ||
| } | ||
| return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| } | ||
| }; |
| @@ -0,0 +1,4 @@ | ||
| node_modules | ||
| lib/protocol/crypto/poly1305.js | ||
| .eslint-plugins | ||
| !.eslintrc.js |
| @@ -0,0 +1,212 @@ | ||
| 'use strict'; | ||
|
|
||
| /* eslint-env node */ | ||
|
|
||
| const Module = require('module'); | ||
| const path = require('path'); | ||
|
|
||
| const ModuleFindPath = Module._findPath; | ||
| const hacks = [ | ||
| 'eslint-plugin-mscdex', | ||
| ]; | ||
| const eslintRulesPath = | ||
| path.join(path.dirname(process.mainModule.filename), '..', 'lib', 'rules'); | ||
| Module._findPath = (request, paths, isMain) => { | ||
| const r = ModuleFindPath(request, paths.concat(eslintRulesPath), isMain); | ||
| if (!r) { | ||
| if (hacks.includes(request)) | ||
| return path.join(__dirname, '.eslint-plugins', request); | ||
| } | ||
| return r; | ||
| }; | ||
|
|
||
| module.exports = { | ||
| root: true, | ||
| env: { node: true, es6: true }, | ||
| plugins: ['mscdex'], | ||
| parserOptions: { sourceType: 'script', ecmaVersion: '2020' }, | ||
| rules: { | ||
| // ESLint built-in rules | ||
| // https://eslint.org/docs/rules/ | ||
| 'accessor-pairs': 'error', | ||
| 'array-callback-return': 'error', | ||
| 'arrow-parens': ['error', 'always'], | ||
| 'arrow-spacing': ['error', { before: true, after: true }], | ||
| 'block-scoped-var': 'error', | ||
| 'block-spacing': 'error', | ||
| 'brace-style': ['error', '1tbs', { allowSingleLine: true }], | ||
| 'capitalized-comments': ['error', 'always', { | ||
| line: { | ||
| // Ignore all lines that have less characters than 20 and all lines that | ||
| // start with something that looks like a variable name or code. | ||
| // eslint-disable-next-line max-len | ||
| ignorePattern: '.{0,20}$|[a-z]+ ?[0-9A-Z_.(/=:[#-]|std|http|ssh|ftp|(let|var|const) [a-z_A-Z0-9]+ =|[b-z] |[a-z]*[0-9].* ', | ||
| ignoreInlineComments: true, | ||
| ignoreConsecutiveComments: true, | ||
| }, | ||
| block: { | ||
| ignorePattern: '.*', | ||
| }, | ||
| }], | ||
| 'comma-dangle': ['error', 'only-multiline'], | ||
| 'comma-spacing': 'error', | ||
| 'comma-style': 'error', | ||
| 'computed-property-spacing': 'error', | ||
| 'constructor-super': 'error', | ||
| 'default-case-last': 'error', | ||
| 'dot-location': ['error', 'property'], | ||
| 'dot-notation': 'error', | ||
| 'eol-last': 'error', | ||
| 'eqeqeq': ['error', 'smart'], | ||
| 'for-direction': 'error', | ||
| 'func-call-spacing': 'error', | ||
| 'func-name-matching': 'error', | ||
| 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], | ||
| 'getter-return': 'error', | ||
| 'key-spacing': ['error', { mode: 'strict' }], | ||
| 'keyword-spacing': 'error', | ||
| 'linebreak-style': ['error', 'unix'], | ||
| 'max-len': ['error', { | ||
| code: 80, | ||
| ignorePattern: '^// Flags:', | ||
| ignoreRegExpLiterals: true, | ||
| ignoreUrls: true, | ||
| tabWidth: 2, | ||
| }], | ||
| 'new-parens': 'error', | ||
| 'no-async-promise-executor': 'error', | ||
| 'no-class-assign': 'error', | ||
| 'no-confusing-arrow': 'error', | ||
| 'no-const-assign': 'error', | ||
| 'no-constructor-return': 'error', | ||
| 'no-control-regex': 'error', | ||
| 'no-debugger': 'error', | ||
| 'no-delete-var': 'error', | ||
| 'no-dupe-args': 'error', | ||
| 'no-dupe-class-members': 'error', | ||
| 'no-dupe-keys': 'error', | ||
| 'no-dupe-else-if': 'error', | ||
| 'no-duplicate-case': 'error', | ||
| 'no-duplicate-imports': 'error', | ||
| 'no-else-return': ['error', { allowElseIf: true }], | ||
| 'no-empty-character-class': 'error', | ||
| 'no-ex-assign': 'error', | ||
| 'no-extra-boolean-cast': 'error', | ||
| 'no-extra-parens': ['error', 'functions'], | ||
| 'no-extra-semi': 'error', | ||
| 'no-fallthrough': 'error', | ||
| 'no-func-assign': 'error', | ||
| 'no-global-assign': 'error', | ||
| 'no-invalid-regexp': 'error', | ||
| 'no-irregular-whitespace': 'error', | ||
| 'no-lonely-if': 'error', | ||
| 'no-misleading-character-class': 'error', | ||
| 'no-mixed-requires': 'error', | ||
| 'no-mixed-spaces-and-tabs': 'error', | ||
| 'no-multi-spaces': ['error', { ignoreEOLComments: true }], | ||
| 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0, maxBOF: 0 }], | ||
| 'no-new-require': 'error', | ||
| 'no-new-symbol': 'error', | ||
| 'no-obj-calls': 'error', | ||
| 'no-octal': 'error', | ||
| 'no-path-concat': 'error', | ||
| 'no-proto': 'error', | ||
| 'no-redeclare': 'error', | ||
| /* eslint-disable max-len */ | ||
| 'no-restricted-syntax': [ | ||
| 'error', | ||
| { | ||
| selector: "CallExpression[callee.name='setTimeout'][arguments.length<2]", | ||
| message: '`setTimeout()` must be invoked with at least two arguments.', | ||
| }, | ||
| { | ||
| selector: "CallExpression[callee.name='setInterval'][arguments.length<2]", | ||
| message: '`setInterval()` must be invoked with at least two arguments.', | ||
| }, | ||
| { | ||
| selector: 'ThrowStatement > CallExpression[callee.name=/Error$/]', | ||
| message: 'Use `new` keyword when throwing an `Error`.', | ||
| } | ||
| ], | ||
| /* eslint-enable max-len */ | ||
| 'no-return-await': 'error', | ||
| 'no-self-assign': 'error', | ||
| 'no-self-compare': 'error', | ||
| 'no-setter-return': 'error', | ||
| 'no-shadow-restricted-names': 'error', | ||
| 'no-tabs': 'error', | ||
| 'no-template-curly-in-string': 'error', | ||
| 'no-this-before-super': 'error', | ||
| 'no-throw-literal': 'error', | ||
| 'no-trailing-spaces': 'error', | ||
| 'no-undef': ['error', { typeof: true }], | ||
| 'no-undef-init': 'error', | ||
| 'no-unexpected-multiline': 'error', | ||
| 'no-unreachable': 'error', | ||
| 'no-unsafe-finally': 'error', | ||
| 'no-unsafe-negation': 'error', | ||
| 'no-unused-labels': 'error', | ||
| 'no-unused-vars': ['error', { args: 'none', caughtErrors: 'all' }], | ||
| 'no-use-before-define': ['error', { | ||
| classes: true, | ||
| functions: false, | ||
| variables: false, | ||
| }], | ||
| 'no-useless-backreference': 'error', | ||
| 'no-useless-call': 'error', | ||
| 'no-useless-catch': 'error', | ||
| 'no-useless-concat': 'error', | ||
| 'no-useless-constructor': 'error', | ||
| 'no-useless-escape': 'error', | ||
| 'no-useless-return': 'error', | ||
| 'no-var': 'error', | ||
| 'no-void': 'error', | ||
| 'no-whitespace-before-property': 'error', | ||
| 'no-with': 'error', | ||
| 'object-curly-spacing': ['error', 'always'], | ||
| 'one-var': ['error', { initialized: 'never' }], | ||
| 'one-var-declaration-per-line': 'error', | ||
| 'operator-linebreak': ['error', 'before', { overrides: { '=': 'after' } }], | ||
| 'padding-line-between-statements': [ | ||
| 'error', | ||
| { blankLine: 'always', prev: 'function', next: 'function' }, | ||
| ], | ||
| 'prefer-const': ['error', { ignoreReadBeforeAssign: true }], | ||
| 'quote-props': ['error', 'consistent'], | ||
| 'rest-spread-spacing': 'error', | ||
| 'semi': 'error', | ||
| 'semi-spacing': 'error', | ||
| 'space-before-blocks': ['error', 'always'], | ||
| 'space-before-function-paren': ['error', { | ||
| anonymous: 'never', | ||
| named: 'never', | ||
| asyncArrow: 'always', | ||
| }], | ||
| 'space-in-parens': ['error', 'never'], | ||
| 'space-infix-ops': 'error', | ||
| 'space-unary-ops': 'error', | ||
| 'spaced-comment': ['error', 'always', { | ||
| 'block': { 'balanced': true }, | ||
| 'exceptions': ['-'], | ||
| }], | ||
| 'strict': ['error', 'global'], | ||
| 'symbol-description': 'error', | ||
| 'template-curly-spacing': 'error', | ||
| 'unicode-bom': 'error', | ||
| 'use-isnan': 'error', | ||
| 'valid-typeof': 'error', | ||
|
|
||
| // Custom rules | ||
| 'mscdex/curly': ['error', 'multi-or-nest', 'consistent'], | ||
| 'mscdex/quotes': ['error', 'single', { avoidEscape: true }], | ||
| }, | ||
| globals: { | ||
| Atomics: 'readable', | ||
| BigInt: 'readable', | ||
| BigInt64Array: 'readable', | ||
| BigUint64Array: 'readable', | ||
| TextEncoder: 'readable', | ||
| TextDecoder: 'readable', | ||
| globalThis: 'readable', | ||
| }, | ||
| }; |
| @@ -1,113 +1,134 @@ | ||
| 'use strict'; | ||
|
|
||
| const { timingSafeEqual } = require('crypto'); | ||
| const { constants, readFileSync } = require('fs'); | ||
|
|
||
| const { Server, sftp: { OPEN_MODE, STATUS_CODE } } = require('ssh2'); | ||
|
|
||
| const allowedUser = Buffer.from('foo'); | ||
| const allowedPassword = Buffer.from('bar'); | ||
|
|
||
| function checkValue(input, allowed) { | ||
| const autoReject = (input.length !== allowed.length); | ||
| if (autoReject) { | ||
| // Prevent leaking length information by always making a comparison with the | ||
| // same input when lengths don't match what we expect ... | ||
| allowed = input; | ||
| } | ||
| const isMatch = timingSafeEqual(input, allowed); | ||
| return (!autoReject && isMatch); | ||
| } | ||
|
|
||
| new Server({ | ||
| hostKeys: [readFileSync('host.key')] | ||
| }, (client) => { | ||
| console.log('Client connected!'); | ||
|
|
||
| client.on('authentication', (ctx) => { | ||
| let allowed = true; | ||
| if (!checkValue(Buffer.from(ctx.username), allowedUser)) | ||
| allowed = false; | ||
|
|
||
| switch (ctx.method) { | ||
| case 'password': | ||
| if (!checkValue(Buffer.from(ctx.password), allowedPassword)) | ||
| return ctx.reject(); | ||
| break; | ||
| default: | ||
| return ctx.reject(); | ||
| } | ||
|
|
||
| if (allowed) | ||
| ctx.accept(); | ||
| else | ||
| ctx.reject(); | ||
| }).on('ready', () => { | ||
| console.log('Client authenticated!'); | ||
|
|
||
| client.on('session', (accept, reject) => { | ||
| const session = accept(); | ||
| session.on('sftp', (accept, reject) => { | ||
| console.log('Client SFTP session'); | ||
|
|
||
| const openFiles = new Map(); | ||
| let handleCount = 0; | ||
| const sftp = accept(); | ||
| sftp.on('OPEN', (reqid, filename, flags, attrs) => { | ||
| // Only allow opening /tmp/foo.txt for writing | ||
| if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.READ)) | ||
| return sftp.status(reqid, STATUS_CODE.FAILURE); | ||
|
|
||
| // Create a fake handle to return to the client, this could easily | ||
| // be a real file descriptor number for example if actually opening | ||
| // the file on the disk | ||
| const handle = Buffer.alloc(4); | ||
| openFiles.set(handleCount, { read: false }); | ||
| handle.writeUInt32BE(handleCount++, 0, true); | ||
|
|
||
| console.log('Opening file for read'); | ||
| sftp.handle(reqid, handle); | ||
| }).on('READ', (reqid, handle, offset, length) => { | ||
| let fnum; | ||
| if (handle.length !== 4 | ||
| || !openFiles.has(fnum = handle.readUInt32BE(0, true))) { | ||
| return sftp.status(reqid, STATUS_CODE.FAILURE); | ||
| } | ||
|
|
||
| // Fake the read | ||
| const state = openFiles.get(fnum); | ||
| if (state.read) { | ||
| sftp.status(reqid, STATUS_CODE.EOF); | ||
| } else { | ||
| state.read = true; | ||
|
|
||
| console.log( | ||
| 'Read from file at offset %d, length %d', offset, length | ||
| ); | ||
| sftp.data(reqid, 'bar'); | ||
| } | ||
| }).on('CLOSE', (reqid, handle) => { | ||
| let fnum; | ||
| if (handle.length !== 4 | ||
| || !openFiles.has(fnum = handle.readUInt32BE(0))) { | ||
| return sftp.status(reqid, STATUS_CODE.FAILURE); | ||
| } | ||
|
|
||
| openFiles.delete(fnum); | ||
|
|
||
| console.log('Closing file'); | ||
| sftp.status(reqid, STATUS_CODE.OK); | ||
| }).on('REALPATH', function(reqid, path) { | ||
| const name = [{ | ||
| filename: '/tmp/foo.txt', | ||
| longname: '-rwxrwxrwx 1 foo foo 3 Dec 8 2009 foo.txt', | ||
| attrs: {} | ||
| }]; | ||
| sftp.name(reqid, name); | ||
| }).on('STAT', onSTAT) | ||
| .on('LSTAT', onSTAT); | ||
|
|
||
| function onSTAT(reqid, path) { | ||
| if (path !== '/tmp/foo.txt') | ||
| return sftp.status(reqid, STATUS_CODE.FAILURE); | ||
|
|
||
| let mode = constants.S_IFREG; // Regular file | ||
| mode |= constants.S_IRWXU; // Read, write, execute for user | ||
| mode |= constants.S_IRWXG; // Read, write, execute for group | ||
| mode |= constants.S_IRWXO; // Read, write, execute for other | ||
| sftp.attrs(reqid, { | ||
| mode: mode, | ||
| uid: 0, | ||
| gid: 0, | ||
| size: 3, | ||
| atime: Date.now(), | ||
| mtime: Date.now(), | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| }).on('close', () => { | ||
| console.log('Client disconnected'); | ||
| }); | ||
| }).listen(0, '127.0.0.1', function() { | ||
| console.log(`Listening on port ${this.address().port}`); | ||
| }); |
| @@ -0,0 +1,17 @@ | ||
| 'use strict'; | ||
|
|
||
| const { spawnSync } = require('child_process'); | ||
|
|
||
| // Attempt to build the bundled optional binding | ||
| const result = spawnSync('node-gyp', ['rebuild'], { | ||
| cwd: 'lib/protocol/crypto', | ||
| encoding: 'utf8', | ||
| shell: true, | ||
| stdio: 'inherit', | ||
| windowsHide: true, | ||
| }); | ||
| if (result.error || result.status !== 0) | ||
| console.log('Failed to build optional crypto binding'); | ||
| else | ||
| console.log('Succeeded in building optional crypto binding'); | ||
| process.exit(0); |
| @@ -1,63 +1,66 @@ | ||
| 'use strict'; | ||
|
|
||
| const { Agent: HttpAgent } = require('http'); | ||
| const { Agent: HttpsAgent } = require('https'); | ||
| const { connect: tlsConnect } = require('tls'); | ||
|
|
||
| let Client; | ||
|
|
||
| for (const ctor of [HttpAgent, HttpsAgent]) { | ||
| class SSHAgent extends ctor { | ||
| constructor(connectCfg, agentOptions) { | ||
| super(agentOptions); | ||
|
|
||
| this._connectCfg = connectCfg; | ||
| this._defaultSrcIP = (agentOptions && agentOptions.srcIP) || 'localhost'; | ||
| } | ||
|
|
||
| createConnection(options, cb) { | ||
| const srcIP = (options && options.localAddress) || this._defaultSrcIP; | ||
| const srcPort = (options && options.localPort) || 0; | ||
| const dstIP = options.host; | ||
| const dstPort = options.port; | ||
|
|
||
| if (Client === undefined) | ||
| ({ Client } = require('./client.js')); | ||
|
|
||
| const client = new Client(); | ||
| let triedForward = false; | ||
| client.on('ready', () => { | ||
| client.forwardOut(srcIP, srcPort, dstIP, dstPort, (err, stream) => { | ||
| triedForward = true; | ||
| if (err) { | ||
| client.end(); | ||
| return cb(err); | ||
| } | ||
| stream.once('close', () => client.end()); | ||
| cb(null, decorateStream(stream, ctor, options)); | ||
| }); | ||
| }).on('error', cb).on('close', () => { | ||
| if (!triedForward) | ||
| cb(new Error('Unexpected connection close')); | ||
| }).connect(this._connectCfg); | ||
| } | ||
| } | ||
|
|
||
| exports[ctor === HttpAgent ? 'SSHTTPAgent' : 'SSHTTPSAgent'] = SSHAgent; | ||
| } | ||
|
|
||
| function noop() {} | ||
|
|
||
| function decorateStream(stream, ctor, options) { | ||
| if (ctor === HttpAgent) { | ||
| // HTTP | ||
| stream.setKeepAlive = noop; | ||
| stream.setNoDelay = noop; | ||
| stream.setTimeout = noop; | ||
| stream.ref = noop; | ||
| stream.unref = noop; | ||
| stream.destroySoon = stream.destroy; | ||
| return stream; | ||
| } | ||
|
|
||
| // HTTPS | ||
| options.socket = stream; | ||
| return tlsConnect(options); | ||
| } |
| @@ -0,0 +1,26 @@ | ||
| 'use strict'; | ||
|
|
||
| const HTTPAgents = require('./http-agents.js'); | ||
| const { parseKey } = require('./protocol/keyParser.js'); | ||
| const { | ||
| flagsToString, | ||
| OPEN_MODE, | ||
| STATUS_CODE, | ||
| stringToFlags, | ||
| } = require('./protocol/SFTP.js'); | ||
|
|
||
| module.exports = { | ||
| Client: require('./client.js'), | ||
| HTTPAgent: HTTPAgents.SSHTTPAgent, | ||
| HTTPSAgent: HTTPAgents.SSHTTPSAgent, | ||
| Server: require('./server.js'), | ||
| utils: { | ||
| parseKey, | ||
| sftp: { | ||
| flagsToString, | ||
| OPEN_MODE, | ||
| STATUS_CODE, | ||
| stringToFlags, | ||
| }, | ||
| }, | ||
| }; |
| @@ -0,0 +1,345 @@ | ||
| // TODO: support server host key format: rsa-sha2-256/512 | ||
| 'use strict'; | ||
|
|
||
| const crypto = require('crypto'); | ||
|
|
||
| let cpuInfo; | ||
| try { | ||
| cpuInfo = require('cpu-features')(); | ||
| } catch {} | ||
|
|
||
| const { bindingAvailable } = require('./crypto.js'); | ||
|
|
||
| const eddsaSupported = (() => { | ||
| if (typeof crypto.sign === 'function' | ||
| && typeof crypto.verify === 'function') { | ||
| const key = | ||
| '-----BEGIN PRIVATE KEY-----\r\nMC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD' | ||
| + '/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r\n-----END PRIVATE KEY-----'; | ||
| const data = Buffer.from('a'); | ||
| let sig; | ||
| let verified; | ||
| try { | ||
| sig = crypto.sign(null, data, key); | ||
| verified = crypto.verify(null, data, key, sig); | ||
| } catch {} | ||
| return (Buffer.isBuffer(sig) && sig.length === 64 && verified === true); | ||
| } | ||
|
|
||
| return false; | ||
| })(); | ||
|
|
||
| const curve25519Supported = (typeof crypto.diffieHellman === 'function' | ||
| && typeof crypto.generateKeyPairSync === 'function' | ||
| && typeof crypto.createPublicKey === 'function'); | ||
|
|
||
| const DEFAULT_KEX = [ | ||
| // https://tools.ietf.org/html/rfc5656#section-10.1 | ||
| 'ecdh-sha2-nistp256', | ||
| 'ecdh-sha2-nistp384', | ||
| 'ecdh-sha2-nistp521', | ||
|
|
||
| // https://tools.ietf.org/html/rfc4419#section-4 | ||
| 'diffie-hellman-group-exchange-sha256', | ||
|
|
||
| // https://tools.ietf.org/html/rfc8268 | ||
| 'diffie-hellman-group14-sha256', | ||
| 'diffie-hellman-group15-sha512', | ||
| 'diffie-hellman-group16-sha512', | ||
| 'diffie-hellman-group17-sha512', | ||
| 'diffie-hellman-group18-sha512', | ||
| ]; | ||
| if (curve25519Supported) { | ||
| DEFAULT_KEX.unshift('curve25519-sha256'); | ||
| DEFAULT_KEX.unshift('curve25519-sha256@libssh.org'); | ||
| } | ||
| const SUPPORTED_KEX = DEFAULT_KEX.concat([ | ||
| // https://tools.ietf.org/html/rfc4419#section-4 | ||
| 'diffie-hellman-group-exchange-sha1', | ||
|
|
||
| 'diffie-hellman-group14-sha1', // REQUIRED | ||
| 'diffie-hellman-group1-sha1', // REQUIRED | ||
| ]); | ||
|
|
||
|
|
||
| const DEFAULT_SERVER_HOST_KEY = [ | ||
| 'ecdsa-sha2-nistp256', | ||
| 'ecdsa-sha2-nistp384', | ||
| 'ecdsa-sha2-nistp521', | ||
| 'rsa-sha2-512', // RFC 8332 | ||
| 'rsa-sha2-256', // RFC 8332 | ||
| 'ssh-rsa', | ||
| ]; | ||
| if (eddsaSupported) | ||
| DEFAULT_SERVER_HOST_KEY.unshift('ssh-ed25519'); | ||
| const SUPPORTED_SERVER_HOST_KEY = DEFAULT_SERVER_HOST_KEY.concat([ | ||
| 'ssh-dss', | ||
| ]); | ||
|
|
||
|
|
||
| const DEFAULT_CIPHER = [ | ||
| // http://tools.ietf.org/html/rfc5647 | ||
| 'aes128-gcm', | ||
| 'aes128-gcm@openssh.com', | ||
| 'aes256-gcm', | ||
| 'aes256-gcm@openssh.com', | ||
|
|
||
| // http://tools.ietf.org/html/rfc4344#section-4 | ||
| 'aes128-ctr', | ||
| 'aes192-ctr', | ||
| 'aes256-ctr', | ||
| ]; | ||
| if (cpuInfo && cpuInfo.flags && !cpuInfo.flags.aes) { | ||
| // We know for sure the CPU does not support AES acceleration | ||
| if (bindingAvailable) | ||
| DEFAULT_CIPHER.unshift('chacha20-poly1305@openssh.com'); | ||
| else | ||
| DEFAULT_CIPHER.push('chacha20-poly1305@openssh.com'); | ||
| } else if (bindingAvailable && cpuInfo && cpuInfo.arch === 'x86') { | ||
| // Places chacha20-poly1305 immediately after GCM ciphers since GCM ciphers | ||
| // seem to outperform it on x86, but it seems to be faster than CTR ciphers | ||
| DEFAULT_CIPHER.splice(4, 0, 'chacha20-poly1305@openssh.com'); | ||
| } else { | ||
| DEFAULT_CIPHER.push('chacha20-poly1305@openssh.com'); | ||
| } | ||
| const SUPPORTED_CIPHER = DEFAULT_CIPHER.concat([ | ||
| 'aes256-cbc', | ||
| 'aes192-cbc', | ||
| 'aes128-cbc', | ||
| 'blowfish-cbc', | ||
| '3des-cbc', | ||
|
|
||
| // http://tools.ietf.org/html/rfc4345#section-4: | ||
| 'arcfour256', | ||
| 'arcfour128', | ||
|
|
||
| 'cast128-cbc', | ||
| 'arcfour', | ||
| ]); | ||
|
|
||
|
|
||
| const DEFAULT_MAC = [ | ||
| 'hmac-sha2-256-etm@openssh.com', | ||
| 'hmac-sha2-512-etm@openssh.com', | ||
| 'hmac-sha1-etm@openssh.com', | ||
| 'hmac-sha2-256', | ||
| 'hmac-sha2-512', | ||
| 'hmac-sha1', | ||
| ]; | ||
| const SUPPORTED_MAC = DEFAULT_MAC.concat([ | ||
| 'hmac-md5', | ||
| 'hmac-sha2-256-96', // first 96 bits of HMAC-SHA256 | ||
| 'hmac-sha2-512-96', // first 96 bits of HMAC-SHA512 | ||
| 'hmac-ripemd160', | ||
| 'hmac-sha1-96', // first 96 bits of HMAC-SHA1 | ||
| 'hmac-md5-96', // first 96 bits of HMAC-MD5 | ||
| ]); | ||
|
|
||
| const DEFAULT_COMPRESSION = [ | ||
| 'none', | ||
| 'zlib@openssh.com', // ZLIB (LZ77) compression, except | ||
| // compression/decompression does not start until after | ||
| // successful user authentication | ||
| 'zlib', // ZLIB (LZ77) compression | ||
| ]; | ||
| const SUPPORTED_COMPRESSION = DEFAULT_COMPRESSION.concat([ | ||
| ]); | ||
|
|
||
|
|
||
| const COMPAT = { | ||
| BAD_DHGEX: 1 << 0, | ||
| OLD_EXIT: 1 << 1, | ||
| DYN_RPORT_BUG: 1 << 2, | ||
| BUG_DHGEX_LARGE: 1 << 3, | ||
| }; | ||
|
|
||
| module.exports = { | ||
| MESSAGE: { | ||
| // Transport layer protocol -- generic (1-19) | ||
| DISCONNECT: 1, | ||
| IGNORE: 2, | ||
| UNIMPLEMENTED: 3, | ||
| DEBUG: 4, | ||
| SERVICE_REQUEST: 5, | ||
| SERVICE_ACCEPT: 6, | ||
|
|
||
| // Transport layer protocol -- algorithm negotiation (20-29) | ||
| KEXINIT: 20, | ||
| NEWKEYS: 21, | ||
|
|
||
| // Transport layer protocol -- key exchange method-specific (30-49) | ||
| KEXDH_INIT: 30, | ||
| KEXDH_REPLY: 31, | ||
|
|
||
| KEXDH_GEX_GROUP: 31, | ||
| KEXDH_GEX_INIT: 32, | ||
| KEXDH_GEX_REPLY: 33, | ||
| KEXDH_GEX_REQUEST: 34, | ||
|
|
||
| KEXECDH_INIT: 30, | ||
| KEXECDH_REPLY: 31, | ||
|
|
||
| // User auth protocol -- generic (50-59) | ||
| USERAUTH_REQUEST: 50, | ||
| USERAUTH_FAILURE: 51, | ||
| USERAUTH_SUCCESS: 52, | ||
| USERAUTH_BANNER: 53, | ||
|
|
||
| // User auth protocol -- user auth method-specific (60-79) | ||
| USERAUTH_PASSWD_CHANGEREQ: 60, | ||
|
|
||
| USERAUTH_PK_OK: 60, | ||
|
|
||
| USERAUTH_INFO_REQUEST: 60, | ||
| USERAUTH_INFO_RESPONSE: 61, | ||
|
|
||
| // Connection protocol -- generic (80-89) | ||
| GLOBAL_REQUEST: 80, | ||
| REQUEST_SUCCESS: 81, | ||
| REQUEST_FAILURE: 82, | ||
|
|
||
| // Connection protocol -- channel-related (90-127) | ||
| CHANNEL_OPEN: 90, | ||
| CHANNEL_OPEN_CONFIRMATION: 91, | ||
| CHANNEL_OPEN_FAILURE: 92, | ||
| CHANNEL_WINDOW_ADJUST: 93, | ||
| CHANNEL_DATA: 94, | ||
| CHANNEL_EXTENDED_DATA: 95, | ||
| CHANNEL_EOF: 96, | ||
| CHANNEL_CLOSE: 97, | ||
| CHANNEL_REQUEST: 98, | ||
| CHANNEL_SUCCESS: 99, | ||
| CHANNEL_FAILURE: 100 | ||
|
|
||
| // Reserved for client protocols (128-191) | ||
|
|
||
| // Local extensions (192-155) | ||
| }, | ||
| DISCONNECT_REASON: { | ||
| HOST_NOT_ALLOWED_TO_CONNECT: 1, | ||
| PROTOCOL_ERROR: 2, | ||
| KEY_EXCHANGE_FAILED: 3, | ||
| RESERVED: 4, | ||
| MAC_ERROR: 5, | ||
| COMPRESSION_ERROR: 6, | ||
| SERVICE_NOT_AVAILABLE: 7, | ||
| PROTOCOL_VERSION_NOT_SUPPORTED: 8, | ||
| HOST_KEY_NOT_VERIFIABLE: 9, | ||
| CONNECTION_LOST: 10, | ||
| BY_APPLICATION: 11, | ||
| TOO_MANY_CONNECTIONS: 12, | ||
| AUTH_CANCELED_BY_USER: 13, | ||
| NO_MORE_AUTH_METHODS_AVAILABLE: 14, | ||
| ILLEGAL_USER_NAME: 15, | ||
| }, | ||
| DISCONNECT_REASON_STR: undefined, | ||
| CHANNEL_OPEN_FAILURE: { | ||
| ADMINISTRATIVELY_PROHIBITED: 1, | ||
| CONNECT_FAILED: 2, | ||
| UNKNOWN_CHANNEL_TYPE: 3, | ||
| RESOURCE_SHORTAGE: 4 | ||
| }, | ||
| TERMINAL_MODE: { | ||
| TTY_OP_END: 0, // Indicates end of options. | ||
| VINTR: 1, // Interrupt character; 255 if none. Similarly for the | ||
| // other characters. Not all of these characters are | ||
| // supported on all systems. | ||
| VQUIT: 2, // The quit character (sends SIGQUIT signal on POSIX | ||
| // systems). | ||
| VERASE: 3, // Erase the character to left of the cursor. | ||
| VKILL: 4, // Kill the current input line. | ||
| VEOF: 5, // End-of-file character (sends EOF from the | ||
| // terminal). | ||
| VEOL: 6, // End-of-line character in addition to carriage | ||
| // return and/or linefeed. | ||
| VEOL2: 7, // Additional end-of-line character. | ||
| VSTART: 8, // Continues paused output (normally control-Q). | ||
| VSTOP: 9, // Pauses output (normally control-S). | ||
| VSUSP: 10, // Suspends the current program. | ||
| VDSUSP: 11, // Another suspend character. | ||
| VREPRINT: 12, // Reprints the current input line. | ||
| VWERASE: 13, // Erases a word left of cursor. | ||
| VLNEXT: 14, // Enter the next character typed literally, even if | ||
| // it is a special character | ||
| VFLUSH: 15, // Character to flush output. | ||
| VSWTCH: 16, // Switch to a different shell layer. | ||
| VSTATUS: 17, // Prints system status line (load, command, pid, | ||
| // etc). | ||
| VDISCARD: 18, // Toggles the flushing of terminal output. | ||
| IGNPAR: 30, // The ignore parity flag. The parameter SHOULD be 0 | ||
| // if this flag is FALSE, and 1 if it is TRUE. | ||
| PARMRK: 31, // Mark parity and framing errors. | ||
| INPCK: 32, // Enable checking of parity errors. | ||
| ISTRIP: 33, // Strip 8th bit off characters. | ||
| INLCR: 34, // Map NL into CR on input. | ||
| IGNCR: 35, // Ignore CR on input. | ||
| ICRNL: 36, // Map CR to NL on input. | ||
| IUCLC: 37, // Translate uppercase characters to lowercase. | ||
| IXON: 38, // Enable output flow control. | ||
| IXANY: 39, // Any char will restart after stop. | ||
| IXOFF: 40, // Enable input flow control. | ||
| IMAXBEL: 41, // Ring bell on input queue full. | ||
| ISIG: 50, // Enable signals INTR, QUIT, [D]SUSP. | ||
| ICANON: 51, // Canonicalize input lines. | ||
| XCASE: 52, // Enable input and output of uppercase characters by | ||
| // preceding their lowercase equivalents with "\". | ||
| ECHO: 53, // Enable echoing. | ||
| ECHOE: 54, // Visually erase chars. | ||
| ECHOK: 55, // Kill character discards current line. | ||
| ECHONL: 56, // Echo NL even if ECHO is off. | ||
| NOFLSH: 57, // Don't flush after interrupt. | ||
| TOSTOP: 58, // Stop background jobs from output. | ||
| IEXTEN: 59, // Enable extensions. | ||
| ECHOCTL: 60, // Echo control characters as ^(Char). | ||
| ECHOKE: 61, // Visual erase for line kill. | ||
| PENDIN: 62, // Retype pending input. | ||
| OPOST: 70, // Enable output processing. | ||
| OLCUC: 71, // Convert lowercase to uppercase. | ||
| ONLCR: 72, // Map NL to CR-NL. | ||
| OCRNL: 73, // Translate carriage return to newline (output). | ||
| ONOCR: 74, // Translate newline to carriage return-newline | ||
| // (output). | ||
| ONLRET: 75, // Newline performs a carriage return (output). | ||
| CS7: 90, // 7 bit mode. | ||
| CS8: 91, // 8 bit mode. | ||
| PARENB: 92, // Parity enable. | ||
| PARODD: 93, // Odd parity, else even. | ||
| TTY_OP_ISPEED: 128, // Specifies the input baud rate in bits per second. | ||
| TTY_OP_OSPEED: 129, // Specifies the output baud rate in bits per second. | ||
| }, | ||
| CHANNEL_EXTENDED_DATATYPE: { | ||
| STDERR: 1, | ||
| }, | ||
|
|
||
| SIGNALS: [ | ||
| 'ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'QUIT', 'SEGV', 'TERM', 'USR1', | ||
| 'USR2', 'KILL', 'PIPE' | ||
| ].reduce((cur, val) => ({ ...cur, [val]: 1 }), {}), | ||
|
|
||
| COMPAT, | ||
| COMPAT_CHECKS: [ | ||
| [ 'Cisco-1.25', COMPAT.BAD_DHGEX ], | ||
| [ /^Cisco-1\./, COMPAT.BUG_DHGEX_LARGE ], | ||
| [ /^[0-9.]+$/, COMPAT.OLD_EXIT ], // old SSH.com implementations | ||
| [ /^OpenSSH_5\.\d+/, COMPAT.DYN_RPORT_BUG ], | ||
| ], | ||
|
|
||
| // KEX proposal-related | ||
| DEFAULT_KEX, | ||
| SUPPORTED_KEX, | ||
| DEFAULT_SERVER_HOST_KEY, | ||
| SUPPORTED_SERVER_HOST_KEY, | ||
| DEFAULT_CIPHER, | ||
| SUPPORTED_CIPHER, | ||
| DEFAULT_MAC, | ||
| SUPPORTED_MAC, | ||
| DEFAULT_COMPRESSION, | ||
| SUPPORTED_COMPRESSION, | ||
|
|
||
| curve25519Supported, | ||
| eddsaSupported, | ||
| }; | ||
|
|
||
| module.exports.DISCONNECT_REASON_BY_VALUE = | ||
| Array.from(Object.entries(module.exports.DISCONNECT_REASON)) | ||
| .reduce((obj, [key, value]) => ({ ...obj, [value]: key }), {}); |
| @@ -0,0 +1,14 @@ | ||
| { | ||
| 'targets': [ | ||
| { | ||
| 'target_name': 'sshcrypto', | ||
| 'include_dirs': [ | ||
| "<!(node -e \"require('nan')\")", | ||
| ], | ||
| 'sources': [ | ||
| 'src/binding.cc' | ||
| ], | ||
| 'cflags': [ '-O3' ], | ||
| }, | ||
| ], | ||
| } |
| @@ -0,0 +1,16 @@ | ||
| 'use strict'; | ||
|
|
||
| const MESSAGE_HANDLERS = new Array(256); | ||
| [ | ||
| require('./kex.js').HANDLERS, | ||
| require('./handlers.misc.js'), | ||
| ].forEach((handlers) => { | ||
| // eslint-disable-next-line prefer-const | ||
| for (let [type, handler] of Object.entries(handlers)) { | ||
| type = +type; | ||
| if (isFinite(type) && type >= 0 && type < MESSAGE_HANDLERS.length) | ||
| MESSAGE_HANDLERS[type] = handler; | ||
| } | ||
| }); | ||
|
|
||
| module.exports = MESSAGE_HANDLERS; |
| @@ -0,0 +1,115 @@ | ||
| 'use strict'; | ||
|
|
||
| const assert = require('assert'); | ||
| const { inspect } = require('util'); | ||
|
|
||
| // Only use this for integers! Decimal numbers do not work with this function. | ||
| function addNumericalSeparator(val) { | ||
| let res = ''; | ||
| let i = val.length; | ||
| const start = val[0] === '-' ? 1 : 0; | ||
| for (; i >= start + 4; i -= 3) | ||
| res = `_${val.slice(i - 3, i)}${res}`; | ||
| return `${val.slice(0, i)}${res}`; | ||
| } | ||
|
|
||
| function oneOf(expected, thing) { | ||
| assert(typeof thing === 'string', '`thing` has to be of type string'); | ||
| if (Array.isArray(expected)) { | ||
| const len = expected.length; | ||
| assert(len > 0, 'At least one expected value needs to be specified'); | ||
| expected = expected.map((i) => String(i)); | ||
| if (len > 2) { | ||
| return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` | ||
| + expected[len - 1]; | ||
| } else if (len === 2) { | ||
| return `one of ${thing} ${expected[0]} or ${expected[1]}`; | ||
| } | ||
| return `of ${thing} ${expected[0]}`; | ||
| } | ||
| return `of ${thing} ${String(expected)}`; | ||
| } | ||
|
|
||
|
|
||
| exports.ERR_INTERNAL_ASSERTION = class ERR_INTERNAL_ASSERTION extends Error { | ||
| constructor(message) { | ||
| super(); | ||
| Error.captureStackTrace(this, ERR_INTERNAL_ASSERTION); | ||
|
|
||
| const suffix = 'This is caused by either a bug in ssh2 ' | ||
| + 'or incorrect usage of ssh2 internals.\n' | ||
| + 'Please open an issue with this stack trace at ' | ||
| + 'https://github.com/mscdex/ssh2/issues\n'; | ||
|
|
||
| this.message = (message === undefined ? suffix : `${message}\n${suffix}`); | ||
| } | ||
| }; | ||
|
|
||
| const MAX_32BIT_INT = 2 ** 32; | ||
| const MAX_32BIT_BIGINT = (() => { | ||
| try { | ||
| return new Function('return 2n ** 32n')(); | ||
| } catch {} | ||
| })(); | ||
| exports.ERR_OUT_OF_RANGE = class ERR_OUT_OF_RANGE extends RangeError { | ||
| constructor(str, range, input, replaceDefaultBoolean) { | ||
| super(); | ||
| Error.captureStackTrace(this, ERR_OUT_OF_RANGE); | ||
|
|
||
| assert(range, 'Missing "range" argument'); | ||
| let msg = (replaceDefaultBoolean | ||
| ? str | ||
| : `The value of "${str}" is out of range.`); | ||
| let received; | ||
| if (Number.isInteger(input) && Math.abs(input) > MAX_32BIT_INT) { | ||
| received = addNumericalSeparator(String(input)); | ||
| } else if (typeof input === 'bigint') { | ||
| received = String(input); | ||
| if (input > MAX_32BIT_BIGINT || input < -MAX_32BIT_BIGINT) | ||
| received = addNumericalSeparator(received); | ||
| received += 'n'; | ||
| } else { | ||
| received = inspect(input); | ||
| } | ||
| msg += ` It must be ${range}. Received ${received}`; | ||
|
|
||
| this.message = msg; | ||
| } | ||
| }; | ||
|
|
||
| class ERR_INVALID_ARG_TYPE extends TypeError { | ||
| constructor(name, expected, actual) { | ||
| super(); | ||
| Error.captureStackTrace(this, ERR_INVALID_ARG_TYPE); | ||
|
|
||
| assert(typeof name === 'string', `'name' must be a string`); | ||
|
|
||
| // determiner: 'must be' or 'must not be' | ||
| let determiner; | ||
| if (typeof expected === 'string' && expected.startsWith('not ')) { | ||
| determiner = 'must not be'; | ||
| expected = expected.replace(/^not /, ''); | ||
| } else { | ||
| determiner = 'must be'; | ||
| } | ||
|
|
||
| let msg; | ||
| if (name.endsWith(' argument')) { | ||
| // For cases like 'first argument' | ||
| msg = `The ${name} ${determiner} ${oneOf(expected, 'type')}`; | ||
| } else { | ||
| const type = (name.includes('.') ? 'property' : 'argument'); | ||
| msg = `The "${name}" ${type} ${determiner} ${oneOf(expected, 'type')}`; | ||
| } | ||
|
|
||
| msg += `. Received type ${typeof actual}`; | ||
|
|
||
| this.message = msg; | ||
| } | ||
| } | ||
| exports.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE; | ||
|
|
||
| exports.validateNumber = function validateNumber(value, name) { | ||
| if (typeof value !== 'number') | ||
| throw new ERR_INVALID_ARG_TYPE(name, 'number', value); | ||
| }; |
| @@ -0,0 +1,376 @@ | ||
| 'use strict'; | ||
|
|
||
| const { timingSafeEqual: timingSafeEqual_ } = require('crypto'); | ||
|
|
||
| const Ber = require('asn1').Ber; | ||
|
|
||
| let DISCONNECT_REASON; | ||
|
|
||
| const FastBuffer = Buffer[Symbol.species]; | ||
| const TypedArrayFill = Object.getPrototypeOf(Uint8Array.prototype).fill; | ||
|
|
||
| const EMPTY_BUFFER = Buffer.alloc(0); | ||
|
|
||
| function readUInt32BE(buf, offset) { | ||
| return (buf[offset++] * 16777216) | ||
| + (buf[offset++] * 65536) | ||
| + (buf[offset++] * 256) | ||
| + buf[offset]; | ||
| } | ||
|
|
||
| function bufferCopy(src, dest, srcStart, srcEnd, destStart) { | ||
| if (!destStart) | ||
| destStart = 0; | ||
| if (srcEnd > src.length) | ||
| srcEnd = src.length; | ||
| let nb = srcEnd - srcStart; | ||
| const destLeft = (dest.length - destStart); | ||
| if (nb > destLeft) | ||
| nb = destLeft; | ||
| dest.set(new Uint8Array(src.buffer, src.byteOffset + srcStart, nb), | ||
| destStart); | ||
| return nb; | ||
| } | ||
|
|
||
| function bufferSlice(buf, start, end) { | ||
| if (end === undefined) | ||
| end = buf.length; | ||
| return new FastBuffer(buf.buffer, buf.byteOffset + start, end - start); | ||
| } | ||
|
|
||
| function makeBufferParser() { | ||
| let pos = 0; | ||
| let buffer; | ||
|
|
||
| const self = { | ||
| init: (buf, start) => { | ||
| buffer = buf; | ||
| pos = (typeof start === 'number' ? start : 0); | ||
| }, | ||
| pos: () => pos, | ||
| length: () => (buffer ? buffer.length : 0), | ||
| avail: () => (buffer && pos < buffer.length ? buffer.length - pos : 0), | ||
| clear: () => { | ||
| buffer = undefined; | ||
| }, | ||
| readUInt32BE: () => { | ||
| if (!buffer || pos + 3 >= buffer.length) | ||
| return; | ||
| return (buffer[pos++] * 16777216) | ||
| + (buffer[pos++] * 65536) | ||
| + (buffer[pos++] * 256) | ||
| + buffer[pos++]; | ||
| }, | ||
| readUInt64BE: (behavior) => { | ||
| if (!buffer || pos + 7 >= buffer.length) | ||
| return; | ||
| switch (behavior) { | ||
| case 'always': | ||
| return BigInt(`0x${buffer.hexSlice(pos, pos += 8)}`); | ||
| case 'maybe': | ||
| if (buffer[pos] > 0x1F) | ||
| return BigInt(`0x${buffer.hexSlice(pos, pos += 8)}`); | ||
| // FALLTHROUGH | ||
| default: | ||
| return (buffer[pos++] * 72057594037927940) | ||
| + (buffer[pos++] * 281474976710656) | ||
| + (buffer[pos++] * 1099511627776) | ||
| + (buffer[pos++] * 4294967296) | ||
| + (buffer[pos++] * 16777216) | ||
| + (buffer[pos++] * 65536) | ||
| + (buffer[pos++] * 256) | ||
| + buffer[pos++]; | ||
| } | ||
| }, | ||
| skip: (n) => { | ||
| if (buffer && n > 0) | ||
| pos += n; | ||
| }, | ||
| skipString: () => { | ||
| const len = self.readUInt32BE(); | ||
| if (len === undefined) | ||
| return; | ||
| pos += len; | ||
| return (pos <= buffer.length ? len : undefined); | ||
| }, | ||
| readByte: () => { | ||
| if (buffer && pos < buffer.length) | ||
| return buffer[pos++]; | ||
| }, | ||
| readBool: () => { | ||
| if (buffer && pos < buffer.length) | ||
| return !!buffer[pos++]; | ||
| }, | ||
| readList: () => { | ||
| const list = self.readString(true); | ||
| if (list === undefined) | ||
| return; | ||
| return (list ? list.split(',') : []); | ||
| }, | ||
| readString: (dest, maxLen) => { | ||
| if (typeof dest === 'number') { | ||
| maxLen = dest; | ||
| dest = undefined; | ||
| } | ||
|
|
||
| const len = self.readUInt32BE(); | ||
| if (len === undefined) | ||
| return; | ||
|
|
||
| if ((buffer.length - pos) < len | ||
| || (typeof maxLen === 'number' && len > maxLen)) { | ||
| return; | ||
| } | ||
|
|
||
| if (dest) { | ||
| if (Buffer.isBuffer(dest)) | ||
| return bufferCopy(buffer, dest, pos, pos += len); | ||
| return buffer.utf8Slice(pos, pos += len); | ||
| } | ||
| return bufferSlice(buffer, pos, pos += len); | ||
| }, | ||
| readRaw: (len) => { | ||
| if (!buffer) | ||
| return; | ||
| if (typeof len !== 'number') | ||
| return bufferSlice(buffer, pos, pos += (buffer.length - pos)); | ||
| if ((buffer.length - pos) >= len) | ||
| return bufferSlice(buffer, pos, pos += len); | ||
| }, | ||
| }; | ||
|
|
||
| return self; | ||
| } | ||
|
|
||
| function makeError(msg, level, fatal) { | ||
| const err = new Error(msg); | ||
| if (typeof level === 'boolean') { | ||
| fatal = level; | ||
| err.level = 'protocol'; | ||
| } else { | ||
| err.level = level || 'protocol'; | ||
| } | ||
| err.fatal = !!fatal; | ||
| return err; | ||
| } | ||
|
|
||
| function writeUInt32BE(buf, value, offset) { | ||
| buf[offset++] = (value >>> 24); | ||
| buf[offset++] = (value >>> 16); | ||
| buf[offset++] = (value >>> 8); | ||
| buf[offset++] = value; | ||
| return offset; | ||
| } | ||
|
|
||
| const utilBufferParser = makeBufferParser(); | ||
|
|
||
| module.exports = { | ||
| bufferCopy, | ||
| bufferSlice, | ||
| FastBuffer, | ||
| bufferFill: (buf, value, start, end) => { | ||
| return TypedArrayFill.call(buf, value, start, end); | ||
| }, | ||
| makeError, | ||
| doFatalError: (protocol, msg, level, reason) => { | ||
| let err; | ||
| if (DISCONNECT_REASON === undefined) | ||
| ({ DISCONNECT_REASON } = require('./utils.js')); | ||
| if (msg instanceof Error) { | ||
| // doFatalError(protocol, err[, reason]) | ||
| err = msg; | ||
| if (typeof level !== 'number') | ||
| reason = DISCONNECT_REASON.PROTOCOL_ERROR; | ||
| else | ||
| reason = level; | ||
| } else { | ||
| // doFatalError(protocol, msg[, level[, reason]]) | ||
| err = makeError(msg, level, true); | ||
| } | ||
| if (typeof reason !== 'number') | ||
| reason = DISCONNECT_REASON.PROTOCOL_ERROR; | ||
| protocol.disconnect(reason); | ||
| protocol._destruct(); | ||
| protocol._onError(err); | ||
| return Infinity; | ||
| }, | ||
| getBinaryList: (list) => { | ||
| if (Buffer.isBuffer(list)) | ||
| return list; | ||
| if (typeof list === 'string') | ||
| return (list.length === 0 ? EMPTY_BUFFER : Buffer.from(list)); | ||
| if (Array.isArray(list)) | ||
| return (list.length === 0 ? EMPTY_BUFFER : Buffer.from(list.join(','))); | ||
| throw new Error(`Invalid list type: ${typeof list}`); | ||
| }, | ||
| timingSafeEquals: (a, b) => { | ||
| if (a.length !== b.length) { | ||
| timingSafeEqual_(a, a); | ||
| return false; | ||
| } | ||
| return timingSafeEqual_(a, b); | ||
| }, | ||
| readUInt32BE, | ||
| writeUInt32BE, | ||
| writeUInt32LE: (buf, value, offset) => { | ||
| buf[offset++] = value; | ||
| buf[offset++] = (value >>> 8); | ||
| buf[offset++] = (value >>> 16); | ||
| buf[offset++] = (value >>> 24); | ||
| return offset; | ||
| }, | ||
| makeBufferParser, | ||
| bufferParser: makeBufferParser(), | ||
| readString: (buffer, start, dest, maxLen) => { | ||
| if (typeof dest === 'number') { | ||
| maxLen = dest; | ||
| dest = undefined; | ||
| } | ||
|
|
||
| if (start === undefined) | ||
| start = 0; | ||
|
|
||
| const left = (buffer.length - start); | ||
| if (start < 0 || start >= buffer.length || left < 4) | ||
| return; | ||
|
|
||
| const len = readUInt32BE(buffer, start); | ||
| if (left < (4 + len) || (typeof maxLen === 'number' && len > maxLen)) | ||
| return; | ||
|
|
||
| start += 4; | ||
| const end = start + len; | ||
| buffer._pos = end; | ||
|
|
||
| if (dest) { | ||
| if (Buffer.isBuffer(dest)) | ||
| return bufferCopy(buffer, dest, start, end); | ||
| return buffer.utf8Slice(start, end); | ||
| } | ||
| return bufferSlice(buffer, start, end); | ||
| }, | ||
| sigSSHToASN1: (sig, type) => { | ||
| switch (type) { | ||
| case 'ssh-dss': { | ||
| if (sig.length > 40) | ||
| return sig; | ||
| // Change bare signature r and s values to ASN.1 BER values for OpenSSL | ||
| const asnWriter = new Ber.Writer(); | ||
| asnWriter.startSequence(); | ||
| let r = sig.slice(0, 20); | ||
| let s = sig.slice(20); | ||
| if (r[0] & 0x80) { | ||
| const rNew = Buffer.allocUnsafe(21); | ||
| rNew[0] = 0x00; | ||
| r.copy(rNew, 1); | ||
| r = rNew; | ||
| } else if (r[0] === 0x00 && !(r[1] & 0x80)) { | ||
| r = r.slice(1); | ||
| } | ||
| if (s[0] & 0x80) { | ||
| const sNew = Buffer.allocUnsafe(21); | ||
| sNew[0] = 0x00; | ||
| s.copy(sNew, 1); | ||
| s = sNew; | ||
| } else if (s[0] === 0x00 && !(s[1] & 0x80)) { | ||
| s = s.slice(1); | ||
| } | ||
| asnWriter.writeBuffer(r, Ber.Integer); | ||
| asnWriter.writeBuffer(s, Ber.Integer); | ||
| asnWriter.endSequence(); | ||
| return asnWriter.buffer; | ||
| } | ||
| case 'ecdsa-sha2-nistp256': | ||
| case 'ecdsa-sha2-nistp384': | ||
| case 'ecdsa-sha2-nistp521': { | ||
| utilBufferParser.init(sig, 0); | ||
| const r = utilBufferParser.readString(); | ||
| const s = utilBufferParser.readString(); | ||
| utilBufferParser.clear(); | ||
| if (r === undefined || s === undefined) | ||
| return; | ||
|
|
||
| const asnWriter = new Ber.Writer(); | ||
| asnWriter.startSequence(); | ||
| asnWriter.writeBuffer(r, Ber.Integer); | ||
| asnWriter.writeBuffer(s, Ber.Integer); | ||
| asnWriter.endSequence(); | ||
| return asnWriter.buffer; | ||
| } | ||
| default: | ||
| return sig; | ||
| } | ||
| }, | ||
| convertSignature: (signature, keyType) => { | ||
| switch (keyType) { | ||
| case 'ssh-dss': { | ||
| if (signature.length <= 40) | ||
| return signature; | ||
| // This is a quick and dirty way to get from BER encoded r and s that | ||
| // OpenSSL gives us, to just the bare values back to back (40 bytes | ||
| // total) like OpenSSH (and possibly others) are expecting | ||
| const asnReader = new Ber.Reader(signature); | ||
| asnReader.readSequence(); | ||
| let r = asnReader.readString(Ber.Integer, true); | ||
| let s = asnReader.readString(Ber.Integer, true); | ||
| let rOffset = 0; | ||
| let sOffset = 0; | ||
| if (r.length < 20) { | ||
| const rNew = Buffer.allocUnsafe(20); | ||
| rNew.set(r, 1); | ||
| r = rNew; | ||
| r[0] = 0; | ||
| } | ||
| if (s.length < 20) { | ||
| const sNew = Buffer.allocUnsafe(20); | ||
| sNew.set(s, 1); | ||
| s = sNew; | ||
| s[0] = 0; | ||
| } | ||
| if (r.length > 20 && r[0] === 0) | ||
| rOffset = 1; | ||
| if (s.length > 20 && s[0] === 0) | ||
| sOffset = 1; | ||
| const newSig = | ||
| Buffer.allocUnsafe((r.length - rOffset) + (s.length - sOffset)); | ||
| bufferCopy(r, newSig, rOffset, r.length, 0); | ||
| bufferCopy(s, newSig, sOffset, s.length, r.length - rOffset); | ||
| return newSig; | ||
| } | ||
| case 'ecdsa-sha2-nistp256': | ||
| case 'ecdsa-sha2-nistp384': | ||
| case 'ecdsa-sha2-nistp521': { | ||
| if (signature[0] === 0) | ||
| return signature; | ||
| // Convert SSH signature parameters to ASN.1 BER values for OpenSSL | ||
| const asnReader = new Ber.Reader(signature); | ||
| asnReader.readSequence(); | ||
| const r = asnReader.readString(Ber.Integer, true); | ||
| const s = asnReader.readString(Ber.Integer, true); | ||
| if (r === null || s === null) | ||
| return; | ||
| const newSig = Buffer.allocUnsafe(4 + r.length + 4 + s.length); | ||
| writeUInt32BE(newSig, r.length, 0); | ||
| newSig.set(r, 4); | ||
| writeUInt32BE(newSig, s.length, 4 + r.length); | ||
| newSig.set(s, 4 + 4 + r.length); | ||
| return newSig; | ||
| } | ||
| } | ||
|
|
||
| return signature; | ||
| }, | ||
| sendPacket: (proto, packet, bypass) => { | ||
| if (!bypass && proto._kexinit !== undefined) { | ||
| // We're currently in the middle of a handshake | ||
|
|
||
| if (proto._queue === undefined) | ||
| proto._queue = []; | ||
| proto._queue.push(packet); | ||
| proto._debug && proto._debug('Outbound: ... packet queued'); | ||
| return false; | ||
| } | ||
| proto._cipher.encrypt(packet); | ||
| return true; | ||
| }, | ||
| }; |
| @@ -0,0 +1,255 @@ | ||
| 'use strict'; | ||
|
|
||
| const { kMaxLength } = require('buffer'); | ||
| const { | ||
| createInflate, | ||
| constants: { | ||
| DEFLATE, | ||
| INFLATE, | ||
| Z_DEFAULT_CHUNK, | ||
| Z_DEFAULT_COMPRESSION, | ||
| Z_DEFAULT_MEMLEVEL, | ||
| Z_DEFAULT_STRATEGY, | ||
| Z_DEFAULT_WINDOWBITS, | ||
| Z_PARTIAL_FLUSH, | ||
| } | ||
| } = require('zlib'); | ||
| const ZlibHandle = createInflate()._handle.constructor; | ||
|
|
||
| function processCallback() { | ||
| throw new Error('Should not get here'); | ||
| } | ||
|
|
||
| function zlibOnError(message, errno, code) { | ||
| const self = this._owner; | ||
| // There is no way to cleanly recover. | ||
| // Continuing only obscures problems. | ||
|
|
||
| const error = new Error(message); | ||
| error.errno = errno; | ||
| error.code = code; | ||
| self._err = error; | ||
| } | ||
|
|
||
| function _close(engine) { | ||
| // Caller may invoke .close after a zlib error (which will null _handle). | ||
| if (!engine._handle) | ||
| return; | ||
|
|
||
| engine._handle.close(); | ||
| engine._handle = null; | ||
| } | ||
|
|
||
| class Zlib { | ||
| constructor(mode) { | ||
| const windowBits = Z_DEFAULT_WINDOWBITS; | ||
| const level = Z_DEFAULT_COMPRESSION; | ||
| const memLevel = Z_DEFAULT_MEMLEVEL; | ||
| const strategy = Z_DEFAULT_STRATEGY; | ||
| const dictionary = undefined; | ||
|
|
||
| this._err = undefined; | ||
| this._writeState = new Uint32Array(2); | ||
| this._chunkSize = Z_DEFAULT_CHUNK; | ||
| this._maxOutputLength = kMaxLength; | ||
| this._outBuffer = Buffer.allocUnsafe(this._chunkSize); | ||
| this._outOffset = 0; | ||
|
|
||
| this._handle = new ZlibHandle(mode); | ||
| this._handle._owner = this; | ||
| this._handle.onerror = zlibOnError; | ||
| this._handle.init(windowBits, | ||
| level, | ||
| memLevel, | ||
| strategy, | ||
| this._writeState, | ||
| processCallback, | ||
| dictionary); | ||
| } | ||
|
|
||
| writeSync(chunk, retChunks) { | ||
| const handle = this._handle; | ||
| if (!handle) | ||
| throw new Error('Invalid Zlib instance'); | ||
|
|
||
| let availInBefore = chunk.length; | ||
| let availOutBefore = this._chunkSize - this._outOffset; | ||
| let inOff = 0; | ||
| let availOutAfter; | ||
| let availInAfter; | ||
|
|
||
| let buffers; | ||
| let nread = 0; | ||
| const state = this._writeState; | ||
| let buffer = this._outBuffer; | ||
| let offset = this._outOffset; | ||
| const chunkSize = this._chunkSize; | ||
|
|
||
| while (true) { | ||
| handle.writeSync(Z_PARTIAL_FLUSH, | ||
| chunk, // in | ||
| inOff, // in_off | ||
| availInBefore, // in_len | ||
| buffer, // out | ||
| offset, // out_off | ||
| availOutBefore); // out_len | ||
| if (this._err) | ||
| throw this._err; | ||
|
|
||
| availOutAfter = state[0]; | ||
| availInAfter = state[1]; | ||
|
|
||
| const inDelta = availInBefore - availInAfter; | ||
| const have = availOutBefore - availOutAfter; | ||
|
|
||
| if (have > 0) { | ||
| const out = (offset === 0 && have === buffer.length | ||
| ? buffer | ||
| : buffer.slice(offset, offset + have)); | ||
| offset += have; | ||
| if (!buffers) | ||
| buffers = out; | ||
| else if (buffers.push === undefined) | ||
| buffers = [buffers, out]; | ||
| else | ||
| buffers.push(out); | ||
| nread += out.byteLength; | ||
|
|
||
| if (nread > this._maxOutputLength) { | ||
| _close(this); | ||
| throw new Error( | ||
| `Output length exceeded maximum of ${this._maxOutputLength}` | ||
| ); | ||
| } | ||
| } else if (have !== 0) { | ||
| throw new Error('have should not go down'); | ||
| } | ||
|
|
||
| // Exhausted the output buffer, or used all the input create a new one. | ||
| if (availOutAfter === 0 || offset >= chunkSize) { | ||
| availOutBefore = chunkSize; | ||
| offset = 0; | ||
| buffer = Buffer.allocUnsafe(chunkSize); | ||
| } | ||
|
|
||
| if (availOutAfter === 0) { | ||
| // Not actually done. Need to reprocess. | ||
| // Also, update the availInBefore to the availInAfter value, | ||
| // so that if we have to hit it a third (fourth, etc.) time, | ||
| // it'll have the correct byte counts. | ||
| inOff += inDelta; | ||
| availInBefore = availInAfter; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| this._outBuffer = buffer; | ||
| this._outOffset = offset; | ||
|
|
||
| if (nread === 0) | ||
| buffers = Buffer.alloc(0); | ||
|
|
||
| if (retChunks) { | ||
| buffers.totalLen = nread; | ||
| return buffers; | ||
| } | ||
|
|
||
| if (buffers.push === undefined) | ||
| return buffers; | ||
|
|
||
| const output = Buffer.allocUnsafe(nread); | ||
| for (let i = 0, p = 0; i < buffers.length; ++i) { | ||
| const buf = buffers[i]; | ||
| output.set(buf, p); | ||
| p += buf.length; | ||
| } | ||
| return output; | ||
| } | ||
| } | ||
|
|
||
| class ZlibPacketWriter { | ||
| constructor(protocol) { | ||
| this.allocStart = 0; | ||
| this.allocStartKEX = 0; | ||
| this._protocol = protocol; | ||
| this._zlib = new Zlib(DEFLATE); | ||
| } | ||
|
|
||
| cleanup() { | ||
| if (this._zlib) | ||
| _close(this._zlib); | ||
| } | ||
|
|
||
| alloc(payloadSize, force) { | ||
| return Buffer.allocUnsafe(payloadSize); | ||
| } | ||
|
|
||
| finalize(payload, force) { | ||
| if (this._protocol._kexinit === undefined || force) { | ||
| const output = this._zlib.writeSync(payload, true); | ||
| const packet = this._protocol._cipher.allocPacket(output.totalLen); | ||
| if (output.push === undefined) { | ||
| packet.set(output, 5); | ||
| } else { | ||
| for (let i = 0, p = 5; i < output.length; ++i) { | ||
| const chunk = output[i]; | ||
| packet.set(chunk, p); | ||
| p += chunk.length; | ||
| } | ||
| } | ||
| return packet; | ||
| } | ||
| return payload; | ||
| } | ||
| } | ||
|
|
||
| class PacketWriter { | ||
| constructor(protocol) { | ||
| this.allocStart = 5; | ||
| this.allocStartKEX = 5; | ||
| this._protocol = protocol; | ||
| } | ||
|
|
||
| cleanup() {} | ||
|
|
||
| alloc(payloadSize, force) { | ||
| if (this._protocol._kexinit === undefined || force) | ||
| return this._protocol._cipher.allocPacket(payloadSize); | ||
| return Buffer.allocUnsafe(payloadSize); | ||
| } | ||
|
|
||
| finalize(packet, force) { | ||
| return packet; | ||
| } | ||
| } | ||
|
|
||
| class ZlibPacketReader { | ||
| constructor() { | ||
| this._zlib = new Zlib(INFLATE); | ||
| } | ||
|
|
||
| cleanup() { | ||
| if (this._zlib) | ||
| _close(this._zlib); | ||
| } | ||
|
|
||
| read(data) { | ||
| return this._zlib.writeSync(data, false); | ||
| } | ||
| } | ||
|
|
||
| class PacketReader { | ||
| cleanup() {} | ||
|
|
||
| read(data) { | ||
| return data; | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
| PacketReader, | ||
| PacketWriter, | ||
| ZlibPacketReader, | ||
| ZlibPacketWriter, | ||
| }; |
| @@ -0,0 +1,316 @@ | ||
| 'use strict'; | ||
|
|
||
| const { SFTP } = require('./protocol/SFTP.js'); | ||
|
|
||
| const MAX_CHANNEL = 2 ** 32 - 1; | ||
|
|
||
| function onChannelOpenFailure(self, recipient, info, cb) { | ||
| self._chanMgr.remove(recipient); | ||
| if (typeof cb !== 'function') | ||
| return; | ||
|
|
||
| let err; | ||
| if (info instanceof Error) { | ||
| err = info; | ||
| } else if (typeof info === 'object' && info !== null) { | ||
| err = new Error(`(SSH) Channel open failure: ${info.description}`); | ||
| err.reason = info.reason; | ||
| } else { | ||
| err = new Error( | ||
| '(SSH) Channel open failure: server closed channel unexpectedly' | ||
| ); | ||
| err.reason = ''; | ||
| } | ||
|
|
||
| cb(err); | ||
| } | ||
|
|
||
| function onCHANNEL_CLOSE(self, recipient, channel, err, dead) { | ||
| if (typeof channel === 'function') { | ||
| // We got CHANNEL_CLOSE instead of CHANNEL_OPEN_FAILURE when | ||
| // requesting to open a channel | ||
| onChannelOpenFailure(self, recipient, err, channel); | ||
| return; | ||
| } | ||
| if (typeof channel !== 'object' | ||
| || channel === null | ||
| || channel.incoming.state === 'closed') { | ||
| return; | ||
| } | ||
|
|
||
| channel.incoming.state = 'closed'; | ||
|
|
||
| if (channel.readable) | ||
| channel.push(null); | ||
| if (channel.server) { | ||
| if (channel.stderr.writable) | ||
| channel.stderr.end(); | ||
| } else if (channel.stderr.readable) { | ||
| channel.stderr.push(null); | ||
| } | ||
|
|
||
| if (channel.constructor !== SFTP | ||
| && (channel.outgoing.state === 'open' | ||
| || channel.outgoing.state === 'eof') | ||
| && !dead) { | ||
| channel.close(); | ||
| } | ||
| if (channel.outgoing.state === 'closing') | ||
| channel.outgoing.state = 'closed'; | ||
|
|
||
| self._chanMgr.remove(recipient); | ||
|
|
||
| const state = channel._writableState; | ||
| if (state && !state.ending && !state.finished && !dead) | ||
| channel.end(); | ||
|
|
||
| // Take care of any outstanding channel requests | ||
| const chanCallbacks = channel._callbacks; | ||
| channel._callbacks = []; | ||
| for (let i = 0; i < chanCallbacks.length; ++i) | ||
| chanCallbacks[i](true); | ||
|
|
||
| if (channel.server) { | ||
| if (!channel.readable) { | ||
| channel.emit('close'); | ||
| } else { | ||
| channel.once('end', () => { | ||
| channel.emit('close'); | ||
| }); | ||
| } | ||
| } else { | ||
| const exit = channel._exit; | ||
| // Align more with node child processes, where the close event gets | ||
| // the same arguments as the exit event | ||
| if (!channel.readable) { | ||
| if (exit.code === null) { | ||
| channel.emit('close', exit.code, exit.signal, exit.dump, | ||
| exit.desc, exit.lang); | ||
| } else { | ||
| channel.emit('close', exit.code); | ||
| } | ||
| } else { | ||
| channel.once('end', () => { | ||
| if (exit.code === null) { | ||
| channel.emit('close', exit.code, exit.signal, exit.dump, | ||
| exit.desc, exit.lang); | ||
| } else { | ||
| channel.emit('close', exit.code); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (!channel.stderr.readable) { | ||
| channel.stderr.emit('close'); | ||
| } else { | ||
| channel.stderr.once('end', () => { | ||
| channel.stderr.emit('close'); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class ChannelManager { | ||
| constructor(client) { | ||
| this._client = client; | ||
| this._channels = {}; | ||
| this._cur = -1; | ||
| this._count = 0; | ||
| } | ||
| add(val) { | ||
| // Attempt to reserve an id | ||
|
|
||
| let id; | ||
| // Optimized paths | ||
| if (this._cur < MAX_CHANNEL) { | ||
| id = ++this._cur; | ||
| } else if (this._count === 0) { | ||
| // Revert and reset back to fast path once we no longer have any channels | ||
| // open | ||
| this._cur = 0; | ||
| id = 0; | ||
| } else { | ||
| // Slower lookup path | ||
|
|
||
| // This path is triggered we have opened at least MAX_CHANNEL channels | ||
| // while having at least one channel open at any given time, so we have | ||
| // to search for a free id. | ||
| const channels = this._channels; | ||
| for (let i = 0; i < MAX_CHANNEL; ++i) { | ||
| if (channels[i] === undefined) { | ||
| id = i; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (id === undefined) | ||
| return -1; | ||
|
|
||
| this._channels[id] = (val || true); | ||
| ++this._count; | ||
|
|
||
| return id; | ||
| } | ||
| update(id, val) { | ||
| if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) | ||
| throw new Error(`Invalid channel id: ${id}`); | ||
|
|
||
| if (val && this._channels[id]) | ||
| this._channels[id] = val; | ||
| } | ||
| get(id) { | ||
| if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) | ||
| throw new Error(`Invalid channel id: ${id}`); | ||
|
|
||
| return this._channels[id]; | ||
| } | ||
| remove(id) { | ||
| if (typeof id !== 'number' || id < 0 || id >= MAX_CHANNEL || !isFinite(id)) | ||
| throw new Error(`Invalid channel id: ${id}`); | ||
|
|
||
| if (this._channels[id]) { | ||
| delete this._channels[id]; | ||
| if (this._count) | ||
| --this._count; | ||
| } | ||
| } | ||
| cleanup(err) { | ||
| const channels = this._channels; | ||
| this._channels = {}; | ||
| this._cur = -1; | ||
| this._count = 0; | ||
|
|
||
| const chanIDs = Object.keys(channels); | ||
| const client = this._client; | ||
| for (let i = 0; i < chanIDs.length; ++i) { | ||
| const id = +chanIDs[i]; | ||
| onCHANNEL_CLOSE(client, id, channels[id], err, true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const isRegExp = (() => { | ||
| const toString = Object.prototype.toString; | ||
| return (val) => toString.call(val) === '[object RegExp]'; | ||
| })(); | ||
|
|
||
| function generateAlgorithmList(algoList, defaultList, supportedList) { | ||
| if (Array.isArray(algoList) && algoList.length > 0) { | ||
| // Exact list | ||
| for (let i = 0; i < algoList.length; ++i) { | ||
| if (supportedList.indexOf(algoList[i]) === -1) | ||
| throw new Error(`Unsupported algorithm: ${algoList[i]}`); | ||
| } | ||
| return algoList; | ||
| } | ||
|
|
||
| if (typeof algoList === 'object' && algoList !== null) { | ||
| // Operations based on the default list | ||
| const keys = Object.keys(algoList); | ||
| let list = defaultList; | ||
| for (let i = 0; i < keys.length; ++i) { | ||
| const key = keys[i]; | ||
| let val = algoList[key]; | ||
| switch (key) { | ||
| case 'append': | ||
| if (!Array.isArray(val)) | ||
| val = [val]; | ||
| if (Array.isArray(val)) { | ||
| for (let j = 0; j < val.length; ++j) { | ||
| const append = val[j]; | ||
| if (typeof append === 'string') { | ||
| if (!append || list.indexOf(append) !== -1) | ||
| continue; | ||
| if (supportedList.indexOf(append) === -1) | ||
| throw new Error(`Unsupported algorithm: ${append}`); | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.push(append); | ||
| } else if (isRegExp(append)) { | ||
| for (let k = 0; k < supportedList.length; ++k) { | ||
| const algo = list[k]; | ||
| if (append.test(algo)) { | ||
| if (list.indexOf(algo) !== -1) | ||
| continue; | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.push(algo); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| break; | ||
| case 'prepend': | ||
| if (!Array.isArray(val)) | ||
| val = [val]; | ||
| if (Array.isArray(val)) { | ||
| for (let j = val.length; j >= 0; --j) { | ||
| const prepend = val[j]; | ||
| if (typeof prepend === 'string') { | ||
| if (!prepend || list.indexOf(prepend) !== -1) | ||
| continue; | ||
| if (supportedList.indexOf(prepend) === -1) | ||
| throw new Error(`Unsupported algorithm: ${prepend}`); | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.unshift(prepend); | ||
| } else if (isRegExp(prepend)) { | ||
| for (let k = supportedList.length; k >= 0; --k) { | ||
| const algo = list[k]; | ||
| if (prepend.test(algo)) { | ||
| if (list.indexOf(algo) !== -1) | ||
| continue; | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.unshift(algo); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| break; | ||
| case 'remove': | ||
| if (!Array.isArray(val)) | ||
| val = [val]; | ||
| if (Array.isArray(val)) { | ||
| for (let j = 0; j < val.length; ++j) { | ||
| const search = val[j]; | ||
| if (typeof search === 'string') { | ||
| if (!search) | ||
| continue; | ||
| const idx = list.indexOf(search); | ||
| if (idx === -1) | ||
| continue; | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.splice(idx, 1); | ||
| } else if (isRegExp(search)) { | ||
| for (let k = 0; k < list.length; ++k) { | ||
| if (search.test(list[k])) { | ||
| if (list === defaultList) | ||
| list = list.slice(); | ||
| list.splice(k, 1); | ||
| --k; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return list; | ||
| } | ||
|
|
||
| return defaultList; | ||
| } | ||
|
|
||
| module.exports = { | ||
| ChannelManager, | ||
| generateAlgorithmList, | ||
| onChannelOpenFailure, | ||
| onCHANNEL_CLOSE, | ||
| }; |
| @@ -1,16 +1,43 @@ | ||
| { | ||
| "name": "ssh2", | ||
| "version": "1.0.0-beta.0", | ||
| "author": "Brian White <mscdex@mscdex.net>", | ||
| "description": "SSH2 client and server modules written in pure JavaScript for node.js", | ||
| "main": "./lib/client", | ||
| "engines": { | ||
| "node": ">=10.16.0" | ||
| }, | ||
| "dependencies": { | ||
| "asn1": "^0.2.4", | ||
| "bcrypt-pbkdf": "^1.0.2" | ||
| }, | ||
| "optionalDependencies": { | ||
| "cpu-features": "0.0.2", | ||
| "nan": "^2.14.1" | ||
| }, | ||
| "scripts": { | ||
| "install": "node install.js", | ||
| "rebuild": "node install.js", | ||
| "test": "node test/test.js" | ||
| }, | ||
| "keywords": [ | ||
| "ssh", | ||
| "ssh2", | ||
| "sftp", | ||
| "secure", | ||
| "shell", | ||
| "exec", | ||
| "remote", | ||
| "client" | ||
| ], | ||
| "licenses": [ | ||
| { | ||
| "type": "MIT", | ||
| "url": "http://github.com/mscdex/ssh2/raw/master/LICENSE" | ||
| } | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "http://github.com/mscdex/ssh2.git" | ||
| } | ||
| } |