ssh-config 4.1.6 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,544 @@
1
+
2
+ import glob from './glob'
3
+ import { spawnSync } from 'child_process'
4
+
5
+ const RE_SPACE = /\s/
6
+ const RE_LINE_BREAK = /\r|\n/
7
+ const RE_SECTION_DIRECTIVE = /^(Host|Match)$/i
8
+ const RE_MULTI_VALUE_DIRECTIVE = /^(GlobalKnownHostsFile|Host|IPQoS|SendEnv|UserKnownHostsFile|ProxyCommand|Match)$/i
9
+ const RE_QUOTE_DIRECTIVE = /^(?:CertificateFile|IdentityFile|IdentityAgent|User)$/i
10
+ const RE_SINGLE_LINE_DIRECTIVE = /^(Include|IdentityFile)$/i
11
+
12
+ enum LineType {
13
+ DIRECTIVE = 1,
14
+ COMMENT = 2,
15
+ }
16
+
17
+ type Separator = ' ' | '=' | '\t';
18
+
19
+ type Space = ' ' | '\t' | '\n';
20
+
21
+ interface Directive {
22
+ type: LineType.DIRECTIVE;
23
+ before: string;
24
+ after: string;
25
+ param: string;
26
+ separator: Separator;
27
+ value: string | string[];
28
+ quoted?: boolean;
29
+ }
30
+
31
+ interface Section extends Directive {
32
+ config: SSHConfig;
33
+ }
34
+
35
+ interface Match extends Section {
36
+ criteria: Record<string, string | string[]>
37
+ }
38
+
39
+ interface Comment {
40
+ type: LineType.COMMENT;
41
+ before: string;
42
+ after: string;
43
+ content: string;
44
+ }
45
+
46
+ type Line = Match | Section | Directive | Comment;
47
+
48
+ interface FindOptions {
49
+ Host?: string;
50
+ }
51
+
52
+ const MULTIPLE_VALUE_PROPS = [
53
+ 'IdentityFile',
54
+ 'LocalForward',
55
+ 'RemoteForward',
56
+ 'DynamicForward',
57
+ 'CertificateFile',
58
+ ]
59
+
60
+ function compare(line, opts) {
61
+ return opts.hasOwnProperty(line.param) && opts[line.param] === line.value
62
+ }
63
+
64
+ function getIndent(config: SSHConfig) {
65
+ for (const line of config) {
66
+ if (line.type === LineType.DIRECTIVE && 'config' in line) {
67
+ for (const subline of line.config) {
68
+ if (subline.before) {
69
+ return subline.before
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ return ' '
76
+ }
77
+
78
+ function capitalize(str) {
79
+ if (typeof str !== 'string') return str
80
+ return str[0].toUpperCase() + str.slice(1)
81
+ }
82
+
83
+ function match(criteria, params) {
84
+ for (const key in criteria) {
85
+ const criterion = criteria[key]
86
+ const keyword = key.toLowerCase()
87
+ if (keyword === 'exec') {
88
+ const command = `function main {
89
+ ${criterion}
90
+ }
91
+ main`
92
+ const { status } = spawnSync(command, { shell: true })
93
+ if (status != 0) return false
94
+ } else if (!glob(criterion, params[capitalize(keyword)])) {
95
+ return false
96
+ }
97
+ }
98
+ return true
99
+ }
100
+
101
+ class SSHConfig extends Array<Line> {
102
+ static DIRECTIVE: LineType.DIRECTIVE = LineType.DIRECTIVE
103
+ static COMMENT: LineType.COMMENT = LineType.COMMENT
104
+
105
+ /**
106
+ * Query ssh config by host.
107
+ */
108
+ compute(params): Record<string, string | string[]> {
109
+ if (typeof params === 'string') params = { Host: params }
110
+ const obj = {}
111
+ const setProperty = (name, value) => {
112
+ if (MULTIPLE_VALUE_PROPS.includes(name)) {
113
+ const list = obj[name] || (obj[name] = [])
114
+ list.push(value)
115
+ } else if (obj[name] == null) {
116
+ obj[name] = value
117
+ }
118
+ }
119
+
120
+ for (const line of this) {
121
+ if (line.type !== LineType.DIRECTIVE) continue
122
+ if (line.param === 'Host' && glob(line.value, params.Host)) {
123
+ setProperty(line.param, line.value)
124
+ for (const subline of (line as Section).config) {
125
+ if (subline.type === LineType.DIRECTIVE) {
126
+ setProperty(subline.param, subline.value)
127
+ }
128
+ }
129
+ } else if (line.param === 'Match' && 'criteria' in line && match(line.criteria, params)) {
130
+ for (const subline of (line as Section).config) {
131
+ if (subline.type === LineType.DIRECTIVE) {
132
+ setProperty(subline.param, subline.value)
133
+ }
134
+ }
135
+ } else {
136
+ setProperty(line.param, line.value)
137
+ }
138
+ }
139
+
140
+ return obj
141
+ }
142
+
143
+ /**
144
+ * find section by Host / Match or function
145
+ */
146
+ find(opts: ((line: Line, index: number, config: Line[]) => unknown) | FindOptions) {
147
+ if (typeof opts === 'function') return super.find(opts)
148
+
149
+ if (!(opts && ('Host' in opts || 'Match' in opts))) {
150
+ throw new Error('Can only find by Host or Match')
151
+ }
152
+
153
+ return super.find(line => compare(line, opts))
154
+ }
155
+
156
+ /**
157
+ * Remove section by Host / Match or function
158
+ */
159
+ remove(opts: ((line: Line, index: number, config: Line[]) => unknown) | FindOptions) {
160
+ let index: number
161
+
162
+ if (typeof opts === 'function') {
163
+ index = super.findIndex(opts)
164
+ } else if (!(opts && ('Host' in opts || 'Match' in opts))) {
165
+ throw new Error('Can only remove by Host or Match')
166
+ } else {
167
+ index = super.findIndex(line => compare(line, opts))
168
+ }
169
+
170
+ if (index >= 0) return this.splice(index, 1)
171
+ }
172
+
173
+ toString(): string {
174
+ return stringify(this)
175
+ }
176
+
177
+ /**
178
+ * Append new section to existing ssh config.
179
+ * @param {Object} opts
180
+ */
181
+ append(opts: Record<string, string | string[]>) {
182
+ const indent = getIndent(this)
183
+ const lastEntry = this.length > 0 ? this[this.length - 1] : null
184
+ let config = lastEntry && (lastEntry as Section).config || this
185
+ let configWas = this
186
+
187
+ let lastLine = config.length > 0 ? config[config.length - 1] : lastEntry
188
+ if (lastLine && !lastLine.after) lastLine.after = '\n'
189
+
190
+ let sectionLineFound = config !== configWas
191
+
192
+ for (const param in opts) {
193
+ const value = opts[param]
194
+ const line: Directive = {
195
+ type: LineType.DIRECTIVE,
196
+ param,
197
+ separator: ' ',
198
+ value,
199
+ before: sectionLineFound ? indent : indent.replace(/ |\t/, ''),
200
+ after: '\n',
201
+ }
202
+
203
+ if (RE_SECTION_DIRECTIVE.test(param)) {
204
+ sectionLineFound = true
205
+ line.before = indent.replace(/ |\t/, '')
206
+ config = configWas
207
+ // separate sections with an extra newline
208
+ // https://github.com/cyjake/ssh-config/issues/23#issuecomment-564768248
209
+ if (lastLine && lastLine.after === '\n') lastLine.after += '\n'
210
+ config.push(line)
211
+ config = (line as Section).config = new SSHConfig()
212
+ } else {
213
+ config.push(line)
214
+ }
215
+ lastLine = line
216
+ }
217
+
218
+ return configWas
219
+ }
220
+
221
+ /**
222
+ * Prepend new section to existing ssh config.
223
+ * @param {Object} opts
224
+ */
225
+ prepend(opts: Record<string, string | string[]>, beforeFirstSection = false) {
226
+ const indent = getIndent(this)
227
+ let config: SSHConfig = this
228
+ let i = 0
229
+
230
+ // insert above known sections
231
+ if (beforeFirstSection) {
232
+ while (i < this.length && !('config' in this[i])) {
233
+ i += 1
234
+ }
235
+
236
+ if (i >= this.length) { // No sections in original config
237
+ return this.append(opts)
238
+ }
239
+ }
240
+
241
+ // Prepend new section above the first section
242
+ let sectionLineFound = false
243
+ let processedLines = 0
244
+
245
+ for (const param in opts) {
246
+ processedLines += 1
247
+ const value = opts[param]
248
+ const line: Directive = {
249
+ type: LineType.DIRECTIVE,
250
+ param,
251
+ separator: ' ',
252
+ value,
253
+ before: '',
254
+ after: '\n',
255
+ }
256
+
257
+ if (RE_SECTION_DIRECTIVE.test(param)) {
258
+ line.before = indent.replace(/ |\t/, '')
259
+ config.splice(i, 0, line)
260
+ config = (line as Section).config = new SSHConfig()
261
+ sectionLineFound = true
262
+ continue
263
+ }
264
+
265
+ // separate from previous sections with an extra newline
266
+ if (processedLines === Object.keys(opts).length) {
267
+ line.after += '\n'
268
+ }
269
+
270
+ if (!sectionLineFound) {
271
+ config.splice(i, 0, line)
272
+ i += 1
273
+
274
+ // Add an extra newline if a single line directive like Include
275
+ if (RE_SINGLE_LINE_DIRECTIVE.test(param)) {
276
+ line.after += '\n'
277
+ }
278
+ continue
279
+ }
280
+
281
+ line.before = indent
282
+ config.push(line)
283
+ }
284
+
285
+ return config
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Parse ssh config text into structured object.
291
+ */
292
+ export function parse(text: string): SSHConfig {
293
+ let i = 0
294
+ let chr = next()
295
+ let config: SSHConfig = new SSHConfig()
296
+ let configWas = config
297
+
298
+ function next() {
299
+ return text[i++]
300
+ }
301
+
302
+ function space(): Space {
303
+ let spaces = ''
304
+
305
+ while (RE_SPACE.test(chr)) {
306
+ spaces += chr
307
+ chr = next()
308
+ }
309
+
310
+ return spaces as Space
311
+ }
312
+
313
+ function linebreak() {
314
+ let breaks = ''
315
+
316
+ while (RE_LINE_BREAK.test(chr)) {
317
+ breaks += chr
318
+ chr = next()
319
+ }
320
+
321
+ return breaks
322
+ }
323
+
324
+ function parameter() {
325
+ let param = ''
326
+
327
+ while (chr && /[^ \t=]/.test(chr)) {
328
+ param += chr
329
+ chr = next()
330
+ }
331
+
332
+ return param
333
+ }
334
+
335
+ function separator(): Separator {
336
+ let sep = space()
337
+
338
+ if (chr === '=') {
339
+ sep += chr
340
+ chr = next()
341
+ }
342
+
343
+ return (sep + space()) as Separator
344
+ }
345
+
346
+ function value() {
347
+ let val = ''
348
+ let quoted = false
349
+ let escaped = false
350
+
351
+ while (chr && !RE_LINE_BREAK.test(chr)) {
352
+ // backslash escapes only double quotes
353
+ if (escaped) {
354
+ val += chr === '"' ? chr : `\\${chr}`
355
+ escaped = false
356
+ }
357
+ // ProxyCommand ssh -W "%h:%p" firewall.example.org
358
+ else if (chr === '"' && (!val || quoted)) {
359
+ quoted = !quoted
360
+ }
361
+ else if (chr === '\\') {
362
+ escaped = true
363
+ }
364
+ else {
365
+ val += chr
366
+ }
367
+ chr = next()
368
+ }
369
+
370
+ if (quoted || escaped) {
371
+ throw new Error(`Unexpected line break at ${val}`)
372
+ }
373
+
374
+ return val.trim()
375
+ }
376
+
377
+ function comment(): Comment {
378
+ const type = LineType.COMMENT
379
+ let content = ''
380
+
381
+ while (chr && !RE_LINE_BREAK.test(chr)) {
382
+ content += chr
383
+ chr = next()
384
+ }
385
+
386
+ return { type, content, before: '', after: '' }
387
+ }
388
+
389
+ // Host *.co.uk
390
+ // Host * !local.dev
391
+ // Host "foo bar"
392
+ function values() {
393
+ const results: string[] = []
394
+ let val = ''
395
+ let quoted = false
396
+ let escaped = false
397
+
398
+ while (chr && !RE_LINE_BREAK.test(chr)) {
399
+ if (escaped) {
400
+ val += chr === '"' ? chr : `\\${chr}`
401
+ escaped = false
402
+ }
403
+ else if (chr === '"') {
404
+ quoted = !quoted
405
+ }
406
+ else if (chr === '\\') {
407
+ escaped = true
408
+ }
409
+ else if (quoted) {
410
+ val += chr
411
+ }
412
+ else if (/[ \t]/.test(chr)) {
413
+ if (val) {
414
+ results.push(val)
415
+ val = ''
416
+ }
417
+ // otherwise ignore the space
418
+ }
419
+ else {
420
+ val += chr
421
+ }
422
+
423
+ chr = next()
424
+ }
425
+
426
+ if (quoted || escaped) {
427
+ throw new Error(`Unexpected line break at ${results.concat(val).join(' ')}`)
428
+ }
429
+ if (val) results.push(val)
430
+ return results.length > 1 ? results : results[0]
431
+ }
432
+
433
+ function directive() {
434
+ const type = LineType.DIRECTIVE
435
+ const param = parameter()
436
+ // Host "foo bar" baz
437
+ const multiple = RE_MULTI_VALUE_DIRECTIVE.test(param)
438
+ const result: Directive = {
439
+ type,
440
+ param,
441
+ separator: separator(),
442
+ quoted: !multiple && chr === '"',
443
+ value: multiple ? values() : value(),
444
+ before: '',
445
+ after: '',
446
+ }
447
+ if (!result.quoted) delete result.quoted
448
+ if (/^Match$/i.test(param)) {
449
+ const criteria = {}
450
+ for (let i = 0; i < result.value.length; i += 2) {
451
+ const keyword = result.value[i]
452
+ const value = result.value[ i + 1]
453
+ criteria[keyword] = value
454
+ }
455
+ (result as Match).criteria = criteria
456
+ }
457
+ return result
458
+ }
459
+
460
+ function line() {
461
+ const before = space()
462
+ const node = chr === '#' ? comment() : directive()
463
+ const after = linebreak()
464
+
465
+ node.before = before
466
+ node.after = after
467
+
468
+ return node
469
+ }
470
+
471
+ while (chr) {
472
+ let node = line()
473
+
474
+ if (node.type === LineType.DIRECTIVE && RE_SECTION_DIRECTIVE.test(node.param)) {
475
+ config = configWas
476
+ config.push(node)
477
+ config = (node as Section).config = new SSHConfig()
478
+ }
479
+ else if (node.type === LineType.DIRECTIVE && !node.param) {
480
+ // blank lines at file end
481
+ config[config.length - 1].after += node.before
482
+ }
483
+ else {
484
+ config.push(node)
485
+ }
486
+ }
487
+
488
+ return configWas
489
+ }
490
+
491
+ /**
492
+ * Stringify structured object into ssh config text
493
+ */
494
+ export function stringify(config: SSHConfig): string {
495
+ let str = ''
496
+
497
+ function formatValue(value: string | string[] | Record<string, any>, quoted: boolean) {
498
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
499
+ const result: string[] = []
500
+ for (const key in value) result.push(key, value[key])
501
+ value = result
502
+ }
503
+ if (Array.isArray(value)) {
504
+ return value.map(chunk => formatValue(chunk, RE_SPACE.test(chunk))).join(' ')
505
+ }
506
+ return quoted ? `"${value}"` : value
507
+ }
508
+
509
+ function formatDirective(line) {
510
+ const quoted = line.quoted
511
+ || (RE_QUOTE_DIRECTIVE.test(line.param) && RE_SPACE.test(line.value))
512
+ const value = formatValue(line.value, quoted)
513
+ return `${line.param}${line.separator}${value}`
514
+ }
515
+
516
+ const format = line => {
517
+ str += line.before
518
+
519
+ if (line.type === LineType.COMMENT) {
520
+ str += line.content
521
+ }
522
+ else if (line.type === LineType.DIRECTIVE && MULTIPLE_VALUE_PROPS.includes(line.param)) {
523
+ [].concat(line.value).forEach(function (value, i, values) {
524
+ str += formatDirective({ ...line, value })
525
+ if (i < values.length - 1) str += `\n${line.before}`
526
+ })
527
+ }
528
+ else if (line.type === LineType.DIRECTIVE) {
529
+ str += formatDirective(line)
530
+ }
531
+
532
+ str += line.after
533
+
534
+ if (line.config) {
535
+ line.config.forEach(format)
536
+ }
537
+ }
538
+
539
+ config.forEach(format)
540
+
541
+ return str
542
+ }
543
+
544
+ export default Object.assign(SSHConfig, { parse, stringify })
package/types/index.d.ts DELETED
@@ -1,46 +0,0 @@
1
- declare enum ELine {
2
- DIRECTIVE = 1,
3
- COMMENT = 2,
4
- }
5
-
6
- interface Directive {
7
- type: ELine.DIRECTIVE;
8
- before: string;
9
- after: string;
10
- param: string;
11
- separator: ' ' | '=';
12
- value: string;
13
- }
14
-
15
- interface Section extends Directive {
16
- config: SSHConfig<Line>;
17
- }
18
-
19
- interface Comment {
20
- type: ELine.COMMENT;
21
- content: string;
22
- }
23
-
24
- type Line = Section | Directive | Comment;
25
-
26
- declare class SSHConfig<T> extends Array<T> {
27
- static parse(text: string): SSHConfig<Line>;
28
- static stringify(config: SSHConfig<Line>): string;
29
-
30
- static DIRECTIVE: ELine.DIRECTIVE;
31
- static COMMENT: ELine.COMMENT;
32
-
33
- toString(): string;
34
-
35
- compute(host: string): Record<string, string>;
36
-
37
- find<T>(this: SSHConfig<T>, predicate: (line: T, index: number, config: T[]) => boolean): T;
38
- find(options: Record<string, string>): Line | Section;
39
-
40
- remove(options: Record<string, string>): Line | Section;
41
-
42
- append(options: Record<string, string>): SSHConfig<Line>;
43
- prepend(options: Record<string, string>): SSHConfig<Line>;
44
- }
45
-
46
- export default class extends SSHConfig<Line> {}