ssh-config
Advanced tools
Comparing version 4.1.6 to 4.2.0
499
index.js
@@ -1,478 +0,23 @@ | ||
'use strict' | ||
const glob = require('./src/glob') | ||
const RE_SPACE = /\s/ | ||
const RE_LINE_BREAK = /\r|\n/ | ||
const RE_SECTION_DIRECTIVE = /^(Host|Match)$/i | ||
const RE_MULTI_VALUE_DIRECTIVE = /^(GlobalKnownHostsFile|Host|IPQoS|SendEnv|UserKnownHostsFile|ProxyCommand)$/i | ||
const RE_QUOTE_DIRECTIVE = /^(?:CertificateFile|IdentityFile|IdentityAgent|User)$/i | ||
const RE_SINGLE_LINE_DIRECTIVE = /^(Include|IdentityFile)$/i | ||
const DIRECTIVE = 1 | ||
const COMMENT = 2 | ||
const MULTIPLE_VALUE_PROPS = [ | ||
'IdentityFile', | ||
'LocalForward', | ||
'RemoteForward', | ||
'DynamicForward', | ||
'CertificateFile' | ||
] | ||
function compare(line, opts) { | ||
return opts.hasOwnProperty(line.param) && opts[line.param] === line.value | ||
} | ||
function getIndent(config) { | ||
for (const line of config) { | ||
if (RE_SECTION_DIRECTIVE.test(line.param)) { | ||
for (const subline of line.config) { | ||
if (subline.before) { | ||
return subline.before | ||
} | ||
} | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
} | ||
return ' ' | ||
} | ||
class SSHConfig extends Array { | ||
/** | ||
* Query ssh config by host. | ||
* | ||
* @return {Object} The applied options of current Host | ||
*/ | ||
compute(host) { | ||
const obj = {} | ||
const setProperty = (name, value) => { | ||
if (MULTIPLE_VALUE_PROPS.includes(name)) { | ||
const list = obj[name] || (obj[name] = []) | ||
list.push(value) | ||
} | ||
else if (obj[name] == null) { | ||
obj[name] = value | ||
} | ||
} | ||
for (const line of this) { | ||
if (line.type !== DIRECTIVE) continue | ||
if (line.param === 'Host') { | ||
if (glob(line.value, host)) { | ||
setProperty(line.param, line.value) | ||
line.config | ||
.filter(line => line.type === DIRECTIVE) | ||
.forEach(line => setProperty(line.param, line.value)) | ||
} | ||
} | ||
else if (line.param === 'Match') { | ||
// TODO | ||
} | ||
else { | ||
setProperty(line.param, line.value) | ||
} | ||
} | ||
return obj | ||
} | ||
/** | ||
* find section by Host / Match or function | ||
*/ | ||
find(opts = {}) { | ||
if (typeof opts === 'function') return super.find(opts) | ||
if (!(opts && ('Host' in opts || 'Match' in opts))) { | ||
throw new Error('Can only find by Host or Match') | ||
} | ||
return super.find(line => compare(line, opts)) | ||
} | ||
/** | ||
* Remove section by Host / Match or function | ||
*/ | ||
remove(opts = {}) { | ||
let index; | ||
if (typeof opts === 'function') { | ||
index = super.findIndex(opts); | ||
} else if (!(opts && ('Host' in opts || 'Match' in opts))) { | ||
throw new Error('Can only remove by Host or Match'); | ||
} else { | ||
index = super.findIndex(line => compare(line, opts)); | ||
} | ||
if (index >= 0) return this.splice(index, 1) | ||
} | ||
/** | ||
* toString() | ||
* @returns {string} | ||
*/ | ||
toString() { | ||
return this.constructor.stringify(this) | ||
} | ||
/** | ||
* Append new section to existing ssh config. | ||
* @param {Object} opts | ||
*/ | ||
append(opts) { | ||
const indent = getIndent(this) | ||
const lastEntry = this.length > 0 ? this[this.length - 1] : null | ||
let config = lastEntry && lastEntry.config || this | ||
let configWas = this | ||
let lastLine = config.length > 0 ? config[config.length - 1] : lastEntry | ||
if (lastLine && !lastLine.after) lastLine.after = '\n' | ||
for (const param in opts) { | ||
const line = { | ||
type: DIRECTIVE, | ||
param, | ||
separator: ' ', | ||
value: opts[param], | ||
before: '', | ||
after: '\n' | ||
} | ||
if (RE_SECTION_DIRECTIVE.test(param)) { | ||
config = configWas | ||
// separate sections with an extra newline | ||
// https://github.com/cyjake/ssh-config/issues/23#issuecomment-564768248 | ||
if (lastLine && lastLine.after === '\n') lastLine.after += '\n' | ||
config.push(line) | ||
config = line.config = new SSHConfig() | ||
} else { | ||
line.before = config === configWas ? '' : indent | ||
config.push(line) | ||
} | ||
lastLine = line | ||
} | ||
return configWas | ||
} | ||
/** | ||
* Prepend new section to existing ssh config. | ||
* @param {Object} opts | ||
*/ | ||
prepend(opts, beforeFirstSection = false) { | ||
const indent = getIndent(this) | ||
let config = this | ||
let i = 0 | ||
// insert above known sections | ||
if (beforeFirstSection) { | ||
while (i < this.length && !RE_SECTION_DIRECTIVE.test(this[i].param)) { | ||
i += 1 | ||
} | ||
if (i >= this.length) { // No sections in original config | ||
return this.append(opts) | ||
} | ||
} | ||
// Prepend new section above the first section | ||
let sectionLineFound = false | ||
let processedLines = 0 | ||
for (const param in opts) { | ||
processedLines += 1 | ||
const line = { | ||
type: DIRECTIVE, | ||
param, | ||
separator: ' ', | ||
value: opts[param], | ||
before: '', | ||
after: '\n' | ||
} | ||
if (RE_SECTION_DIRECTIVE.test(param)) { | ||
config.splice(i, 0, line) | ||
config = line.config = new SSHConfig() | ||
sectionLineFound = true | ||
continue | ||
} | ||
// separate from previous sections with an extra newline | ||
if (processedLines === Object.keys(opts).length) { | ||
line.after += '\n' | ||
} | ||
if (!sectionLineFound) { | ||
config.splice(i, 0, line) | ||
i += 1 | ||
// Add an extra newline if a single line directive like Include | ||
if (RE_SINGLE_LINE_DIRECTIVE.test(param)) { | ||
line.after += '\n' | ||
} | ||
continue | ||
} | ||
line.before = indent | ||
config.push(line) | ||
} | ||
return config | ||
} | ||
/** | ||
* Stringify structured object into ssh config text | ||
* @param {SSHConfig} config | ||
* @returns {string} | ||
*/ | ||
static stringify(config) { | ||
let str = '' | ||
function formatValue(value, quoted) { | ||
if (Array.isArray(value)) { | ||
return value.map(chunk => formatValue(chunk, RE_SPACE.test(chunk))).join(' ') | ||
} | ||
return quoted ? `"${value}"` : value | ||
} | ||
function formatDirective(line) { | ||
const quoted = line.quoted | ||
|| (RE_QUOTE_DIRECTIVE.test(line.param) && RE_SPACE.test(line.value)) | ||
const value = formatValue(line.value, quoted) | ||
return `${line.param}${line.separator}${value}` | ||
} | ||
const format = line => { | ||
str += line.before | ||
if (line.type === COMMENT) { | ||
str += line.content | ||
} | ||
else if (line.type === DIRECTIVE && MULTIPLE_VALUE_PROPS.includes(line.param)) { | ||
[].concat(line.value).forEach(function (value, i, values) { | ||
str += formatDirective({ ...line, value }) | ||
if (i < values.length - 1) str += `\n${line.before}` | ||
}) | ||
} | ||
else if (line.type === DIRECTIVE) { | ||
str += formatDirective(line) | ||
} | ||
str += line.after | ||
if (line.config) { | ||
line.config.forEach(format) | ||
} | ||
} | ||
config.forEach(format) | ||
return str | ||
} | ||
static get DIRECTIVE() { | ||
return DIRECTIVE | ||
} | ||
static get COMMENT() { | ||
return COMMENT | ||
} | ||
/** | ||
* Parse ssh config text into structured object. | ||
*/ | ||
static parse(str) { | ||
let i = 0 | ||
let chr = next() | ||
let config = new SSHConfig() | ||
let configWas = config | ||
function next() { | ||
return str[i++] | ||
} | ||
function space() { | ||
let spaces = '' | ||
while (RE_SPACE.test(chr)) { | ||
spaces += chr | ||
chr = next() | ||
} | ||
return spaces | ||
} | ||
function linebreak() { | ||
let breaks = '' | ||
while (RE_LINE_BREAK.test(chr)) { | ||
breaks += chr | ||
chr = next() | ||
} | ||
return breaks | ||
} | ||
function parameter() { | ||
let param = '' | ||
while (chr && /[^ \t=]/.test(chr)) { | ||
param += chr | ||
chr = next() | ||
} | ||
return param | ||
} | ||
function separator() { | ||
let sep = space() | ||
if (chr === '=') { | ||
sep += chr | ||
chr = next() | ||
} | ||
return sep + space() | ||
} | ||
function value() { | ||
let val = '' | ||
let quoted = false | ||
let escaped = false | ||
while (chr && !RE_LINE_BREAK.test(chr)) { | ||
// backslash escapes only double quotes | ||
if (escaped) { | ||
val += chr === '"' ? chr : `\\${chr}` | ||
escaped = false | ||
} | ||
// ProxyCommand ssh -W "%h:%p" firewall.example.org | ||
else if (chr === '"' && (!val || quoted)) { | ||
quoted = !quoted | ||
} | ||
else if (chr === '\\') { | ||
escaped = true | ||
} | ||
else { | ||
val += chr | ||
} | ||
chr = next() | ||
} | ||
if (quoted || escaped) { | ||
throw new Error(`Unexpected line break at ${val}`) | ||
} | ||
return val.trim() | ||
} | ||
function comment() { | ||
const type = COMMENT | ||
let content = '' | ||
while (chr && !RE_LINE_BREAK.test(chr)) { | ||
content += chr | ||
chr = next() | ||
} | ||
return { type, content } | ||
} | ||
// Host *.co.uk | ||
// Host * !local.dev | ||
// Host "foo bar" | ||
function values() { | ||
const results = [] | ||
let val = '' | ||
let quoted = false | ||
let escaped = false | ||
while (chr && !RE_LINE_BREAK.test(chr)) { | ||
if (escaped) { | ||
val += chr === '"' ? chr : `\\${chr}` | ||
escaped = false | ||
} | ||
else if (chr === '"') { | ||
quoted = !quoted | ||
} | ||
else if (chr === '\\') { | ||
escaped = true | ||
} | ||
else if (quoted) { | ||
val += chr | ||
} | ||
else if (/[ \t]/.test(chr)) { | ||
if (val) { | ||
results.push(val) | ||
val = '' | ||
} | ||
// otherwise ignore the space | ||
} | ||
else { | ||
val += chr | ||
} | ||
chr = next() | ||
} | ||
if (quoted || escaped) { | ||
throw new Error(`Unexpected line break at ${results.concat(val).join(' ')}`) | ||
} | ||
if (val) results.push(val) | ||
return results.length > 1 ? results : results[0] | ||
} | ||
function directive() { | ||
const type = DIRECTIVE | ||
const param = parameter() | ||
// Host "foo bar" baz | ||
const multiple = RE_MULTI_VALUE_DIRECTIVE.test(param) | ||
const result = { | ||
type, | ||
param, | ||
separator: separator(), | ||
quoted: !multiple && chr === '"', | ||
value: multiple ? values() : value() | ||
} | ||
if (!result.quoted) delete result.quoted | ||
return result | ||
} | ||
function line() { | ||
const before = space() | ||
const node = chr === '#' ? comment() : directive() | ||
const after = linebreak() | ||
node.before = before | ||
node.after = after | ||
return node | ||
} | ||
while (chr) { | ||
let node = line() | ||
if (node.type === DIRECTIVE && RE_SECTION_DIRECTIVE.test(node.param)) { | ||
config = configWas | ||
config.push(node) | ||
config = node.config = new SSHConfig() | ||
} | ||
else if (node.type === DIRECTIVE && !node.param) { | ||
// blank lines at file end | ||
config[config.length - 1].after += node.before | ||
} | ||
else { | ||
config.push(node) | ||
} | ||
} | ||
return configWas | ||
} | ||
} | ||
module.exports = SSHConfig | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const ssh_config_1 = __importDefault(require("./src/ssh-config")); | ||
__exportStar(require("./src/ssh-config"), exports); | ||
exports.default = ssh_config_1.default; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "ssh-config", | ||
"description": "SSH config parser and stringifier", | ||
"version": "4.1.6", | ||
"version": "4.2.0", | ||
"author": "Chen Yangjian (https://www.cyj.me)", | ||
@@ -12,9 +12,10 @@ "repository": { | ||
"index.js", | ||
"src", | ||
"types" | ||
"src" | ||
], | ||
"devDependencies": { | ||
"@types/mocha": "^9.1.0", | ||
"@types/node": "^17.0.23", | ||
"eslint": "^7.17.0", | ||
"@types/node": "^17.0.45", | ||
"@typescript-eslint/eslint-plugin": "^5.48.0", | ||
"@typescript-eslint/parser": "^5.48.0", | ||
"eslint": "^8.31.0", | ||
"heredoc": "^1.3.1", | ||
@@ -26,3 +27,5 @@ "mocha": "^8.2.1", | ||
"scripts": { | ||
"lint": "eslint .", | ||
"lint": "eslint --ext ts .", | ||
"lint:fix": "eslint --ext ts --fix .", | ||
"prepack": "tsc", | ||
"pretest": "tsc", | ||
@@ -35,4 +38,3 @@ "test": "NODE_OPTIONS=--enable-source-maps mocha --exit --recursive", | ||
}, | ||
"types": "types/index.d.ts", | ||
"license": "MIT" | ||
} |
@@ -1,20 +0,16 @@ | ||
'use strict' | ||
function escapeChars(str, chars) { | ||
for (let char of chars) { | ||
str = str.replace(new RegExp('\\' + char, 'g'), '\\' + char) | ||
} | ||
return str | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
function escapeChars(text, chars) { | ||
for (let char of chars) { | ||
text = text.replace(new RegExp('\\' + char, 'g'), '\\' + char); | ||
} | ||
return text; | ||
} | ||
function match(pattern, str) { | ||
pattern = escapeChars(pattern, '\\()[]{}.+^$|') | ||
pattern = pattern | ||
.replace(/\*/g, '.*') | ||
.replace(/\?/g, '.?') | ||
return new RegExp('^(?:' + pattern + ')$').test(str) | ||
function match(pattern, text) { | ||
pattern = escapeChars(pattern, '\\()[]{}.+^$|'); | ||
pattern = pattern | ||
.replace(/\*/g, '.*') | ||
.replace(/\?/g, '.?'); | ||
return new RegExp('^(?:' + pattern + ')$').test(text); | ||
} | ||
/** | ||
@@ -27,21 +23,19 @@ * A helper function to match input against [pattern-list](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5#PATTERNS). | ||
*/ | ||
function glob(patternList, str) { | ||
const patterns = Array.isArray(patternList) ? patternList : patternList.split(/,/) | ||
// > If a negated entry is matched, then the Host entry is ignored, regardless of whether any other patterns on the line match. | ||
let result = false | ||
for (const pattern of patterns) { | ||
const negate = pattern[0] == '!' | ||
if (negate && match(pattern.slice(1), str)) { | ||
return false | ||
} else if (match(pattern, str)) { | ||
// wait until all of the pattern match results because there might be a negated pattern | ||
result = true | ||
function glob(patternList, text) { | ||
const patterns = Array.isArray(patternList) ? patternList : patternList.split(/,/); | ||
// > If a negated entry is matched, then the Host entry is ignored, regardless of whether any other patterns on the line match. | ||
let result = false; | ||
for (const pattern of patterns) { | ||
const negate = pattern[0] == '!'; | ||
if (negate && match(pattern.slice(1), text)) { | ||
return false; | ||
} | ||
else if (match(pattern, text)) { | ||
// wait until all of the pattern match results because there might be a negated pattern | ||
result = true; | ||
} | ||
} | ||
} | ||
return result | ||
return result; | ||
} | ||
module.exports = glob | ||
exports.default = glob; | ||
//# sourceMappingURL=glob.js.map |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
54416
10
1012
9
2