-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathSshConfigParser.cs
More file actions
176 lines (157 loc) · 7.13 KB
/
Copy pathSshConfigParser.cs
File metadata and controls
176 lines (157 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Flow.Launcher.Plugin.QuickSSH
{
/// <summary>
/// Parses ~/.ssh/config and extracts Host entries as structured <see cref="SshProfile"/> objects.
/// </summary>
public static class SshConfigParser
{
/// <summary>
/// Parses the SSH config file and returns a dictionary of Host alias → <see cref="SshProfile"/>.
/// Wildcard patterns (containing * or ?) are skipped.
/// Supports both space-separated and '='-separated key/value pairs as per ssh_config(5).
/// Multiple host aliases on a single Host line are each stored as separate entries.
/// </summary>
public static Dictionary<string, SshProfile> Parse(string? configPath = null)
{
configPath ??= Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".ssh", "config");
var profiles = new Dictionary<string, SshProfile>(StringComparer.OrdinalIgnoreCase);
if (!File.Exists(configPath))
return profiles;
// Current Host block state
var currentAliases = new List<string>();
string? hostName = null;
string? user = null;
string? port = null;
string? identityFile = null;
bool identitiesOnly = false;
var localForwards = new List<string>();
var remoteForwards = new List<string>();
string? dynamicForward = null;
string? proxyJump = null;
string? proxyCommand = null;
foreach (var rawLine in File.ReadLines(configPath))
{
var line = rawLine.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
continue;
if (!TrySplitKeyValue(line, out var key, out var value))
continue;
if (key.Equals("Host", StringComparison.OrdinalIgnoreCase))
{
// Flush previous Host block before starting a new one.
if (currentAliases.Count > 0)
{
foreach (var alias in currentAliases)
AddEntry(profiles, alias, hostName, user, port, identityFile,
identitiesOnly, localForwards, remoteForwards, dynamicForward,
proxyJump, proxyCommand);
}
// Reset block state
currentAliases.Clear();
hostName = user = port = identityFile = dynamicForward =
proxyJump = proxyCommand = null;
identitiesOnly = false;
localForwards = new List<string>();
remoteForwards = new List<string>();
var aliases = value.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (aliases.Any(a => a.Contains('*') || a.Contains('?')))
continue; // skip wildcard blocks
currentAliases.AddRange(aliases);
}
else if (currentAliases.Count > 0)
{
switch (key.ToLowerInvariant())
{
case "hostname": hostName = value; break;
case "user": user = value; break;
case "port": port = value; break;
case "identityfile": identityFile = value; break;
case "identitiesonly":
identitiesOnly = value.Equals("yes", StringComparison.OrdinalIgnoreCase);
break;
case "localforward": localForwards.Add(value); break;
case "remoteforward": remoteForwards.Add(value); break;
case "dynamicforward": dynamicForward = value; break;
case "proxyjump": proxyJump = value; break;
case "proxycommand": proxyCommand = value; break;
}
}
}
// Flush the last Host block.
if (currentAliases.Count > 0)
{
foreach (var alias in currentAliases)
AddEntry(profiles, alias, hostName, user, port, identityFile,
identitiesOnly, localForwards, remoteForwards, dynamicForward,
proxyJump, proxyCommand);
}
return profiles;
}
private static bool TrySplitKeyValue(string line, out string key, out string value)
{
var eqIdx = line.IndexOf('=');
var spIdx = line.IndexOfAny(new[] { ' ', '\t' });
int sepIdx;
bool isEquals;
if (eqIdx >= 0 && (spIdx < 0 || eqIdx <= spIdx))
{
sepIdx = eqIdx;
isEquals = true;
}
else if (spIdx >= 0)
{
sepIdx = spIdx;
isEquals = false;
}
else
{
key = value = string.Empty;
return false;
}
key = line.Substring(0, sepIdx).TrimEnd();
value = isEquals
? line.Substring(sepIdx + 1).TrimStart()
: line.Substring(sepIdx + 1).Trim();
// Handle "Key = Value": space won the separator race but value still starts with "="
if (!isEquals && value.StartsWith("="))
value = value.Substring(1).TrimStart();
return !string.IsNullOrEmpty(key);
}
private static void AddEntry(
Dictionary<string, SshProfile> profiles,
string alias,
string? hostName,
string? user,
string? port,
string? identityFile,
bool identitiesOnly,
List<string> localForwards,
List<string> remoteForwards,
string? dynamicForward,
string? proxyJump,
string? proxyCommand)
{
var profile = new SshProfile
{
Type = "ssh",
HostName = hostName ?? alias,
User = string.IsNullOrEmpty(user) ? null : user,
Port = string.IsNullOrEmpty(port) || port == "22" ? null : port,
IdentityFile = string.IsNullOrEmpty(identityFile) ? null : identityFile,
IdentitiesOnly = identitiesOnly,
LocalForward = localForwards.Count > 0 ? new List<string>(localForwards) : null,
RemoteForward = remoteForwards.Count > 0 ? new List<string>(remoteForwards) : null,
DynamicForward = string.IsNullOrEmpty(dynamicForward) ? null : dynamicForward,
ProxyJump = string.IsNullOrEmpty(proxyJump) ? null : proxyJump,
ProxyCommand = string.IsNullOrEmpty(proxyCommand) ? null : proxyCommand,
};
profiles[alias] = profile;
}
}
}