Skip to content

Commit 27e09bf

Browse files
Merge pull request #8 from totalretribution/update-checker
2 parents f888af4 + 5a2b158 commit 27e09bf

23 files changed

Lines changed: 533 additions & 137 deletions

.vscode/launch.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"preLaunchTask": "build:cli",
1212
"program": "${workspaceFolder}/src/SshAgentEcho.Cli/bin/Debug/net10.0/ssh-agent-echo.dll",
1313
"args": [
14+
"--update",
1415
"--print",
1516
"--sync",
1617
"--force"

Assets/icon.png

50.1 KB
Loading

src/SshAgentEcho.Cli/Program.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ static int Main(string[] args) {
2626
Description = "Force sync even if CRC matches"
2727
};
2828

29+
Option<bool> updateOption = new("--update") {
30+
Description = "Check for updates"
31+
};
32+
2933
RootCommand rootCommand = new("ssh-agent-echo - A tool to sync SSH agent public keys to ssh config");
3034
rootCommand.Options.Add(printOption);
3135
rootCommand.Options.Add(syncOption);
3236
rootCommand.Options.Add(forceOption);
37+
rootCommand.Options.Add(updateOption);
3338

3439
var version = Assembly.GetEntryAssembly()?
3540
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
@@ -42,6 +47,23 @@ static int Main(string[] args) {
4247
Console.WriteLine(bar + "\n");
4348

4449
rootCommand.SetAction(parseResult => {
50+
bool isUpdate = parseResult.GetValue(updateOption);
51+
if (isUpdate) {
52+
var updateChecker = new Update();
53+
updateChecker.Check().Wait();
54+
if (updateChecker.IsNewVersionAvailable()) {
55+
Log.Info("A new version is available!");
56+
Log.Info($"Current version: {version}");
57+
Log.Info($"Latest version: {updateChecker.LatestVersion}");
58+
Log.Info($"Release URL: {updateChecker.GetReleaseURL()}");
59+
Log.Info($"Download URL: {updateChecker.GetDownloadUrl()}");
60+
// Log.Info($"Release notes: {updateChecker.ReleaseNotes}");
61+
} else {
62+
Log.Info("You are using the latest version.");
63+
}
64+
return;
65+
}
66+
4567
bool isVerbose = parseResult.GetValue(printOption);
4668
if (isVerbose) {
4769
var agent = new Agent();

src/SshAgentEcho.Core/Agent.cs

Lines changed: 30 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,33 @@
77

88
namespace SshAgentEcho.Core;
99

10-
public class Agent : IEnumerable<Agent.Identity>
11-
{
12-
public readonly record struct Identity(string Comment, string User, string Host, string? Name, string Hash, string Type, string Filename)
13-
{
14-
public override string ToString()
15-
{
10+
public class Agent : IEnumerable<Agent.Identity> {
11+
public readonly record struct Identity(string Comment, string User, string Host, string? Name, string Hash, string Type, string Filename) {
12+
public override string ToString() {
1613
return $"{Type} {Hash} {Comment}";
1714
}
1815
}
1916

2017
private readonly List<Identity> _identities = new();
2118
private int _cursor = -1;
2219

23-
public Agent()
24-
{
20+
public Agent() {
2521
PopulateIdentities();
2622
}
2723

28-
public void Refresh()
29-
{
24+
public void Refresh() {
3025
PopulateIdentities();
3126
}
3227

33-
public void PrintIdentities()
34-
{
35-
foreach (var identity in _identities)
36-
{
28+
public void PrintIdentities() {
29+
foreach (var identity in _identities) {
3730
Log.Info(identity);
3831
}
3932
}
4033

4134
public IReadOnlyList<Identity> GetIdentities() => _identities.ToList();
4235

43-
public Identity? Next()
44-
{
36+
public Identity? Next() {
4537
if (_cursor + 1 >= _identities.Count) return null;
4638
_cursor++;
4739
return _identities[_cursor];
@@ -52,8 +44,7 @@ public void PrintIdentities()
5244
public IEnumerator<Identity> GetEnumerator() => _identities.GetEnumerator();
5345
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
5446

55-
private String? GetOpenSshKey(SshAgentPrivateKey? identity)
56-
{
47+
private String? GetOpenSshKey(SshAgentPrivateKey? identity) {
5748
if (identity?.Key == null) return null;
5849

5950
var key = identity.Key;
@@ -70,38 +61,31 @@ public void PrintIdentities()
7061
return Convert.ToBase64String(publicKeyBlob);
7162
}
7263

73-
private string? ExtractChevronContent(string input)
74-
{
64+
private string? ExtractChevronContent(string input) {
7565
int start = input.IndexOf('<');
7666
int end = input.IndexOf('>');
77-
if (start >= 0 && end > start)
78-
{
67+
if (start >= 0 && end > start) {
7968
return input.Substring(start + 1, end - start - 1);
8069
}
8170
return null;
8271
}
8372

84-
private (string User, string Host, string? Name)? ProcessComment(string comment)
85-
{
73+
private (string User, string Host, string? Name)? ProcessComment(string comment) {
8674
string? user_host = comment.Trim();
8775
string user = "";
8876
string host = "";
8977
string? name = null;
90-
if (comment.Contains('<') || comment.Contains('>'))
91-
{
78+
if (comment.Contains('<') || comment.Contains('>')) {
9279
// Must have exactly one '<' and one '>'
93-
if (comment.Count(c => c == '<') != 1 || comment.Count(c => c == '>') != 1)
94-
{
80+
if (comment.Count(c => c == '<') != 1 || comment.Count(c => c == '>') != 1) {
9581
return null;
9682
}
9783
// '>' must come after '<'
98-
if (comment.IndexOf('<') > comment.IndexOf('>'))
99-
{
84+
if (comment.IndexOf('<') > comment.IndexOf('>')) {
10085
return null;
10186
}
10287
// '<' must not be the first character, the name must be at least 2 characters long.
103-
if (comment.IndexOf('<') < 2)
104-
{
88+
if (comment.IndexOf('<') < 2) {
10589
return null;
10690
}
10791
user_host = ExtractChevronContent(comment);
@@ -123,64 +107,50 @@ public void PrintIdentities()
123107
return (user, host, name);
124108
}
125109

126-
private string GenerateFileName(string? name, string host)
127-
{
110+
private string GenerateFileName(string? name, string host) {
128111
String filename = "";
129-
if (!string.IsNullOrEmpty(name))
130-
{
112+
if (!string.IsNullOrEmpty(name)) {
131113
filename = name;
132-
}
133-
else
134-
{
114+
} else {
135115
filename = host;
136116
}
137117
filename = filename.Replace(".", "_").Replace(" ", "_");
138118
filename += ".pub";
139119
return filename;
140120
}
141121

142-
private SshAgentPrivateKey[]? RequestIdentities()
143-
{
122+
private SshAgentPrivateKey[]? RequestIdentities() {
144123
var agent = new SshAgent();
145124

146-
try
147-
{
125+
try {
148126
return Task
149127
.Run(() => agent.RequestIdentities())
150128
.WaitAsync(TimeSpan.FromMinutes(3))
151129
.GetAwaiter()
152130
.GetResult();
153-
}
154-
catch (TimeoutException)
155-
{
131+
} catch (TimeoutException) {
156132
// Handle the fact that the agent is ghosting you
157-
Console.WriteLine("SSH Agent failed to respond within the timeout.");
158-
}
159-
catch (Exception ex)
160-
{
133+
Log.Error("SSH Agent failed to respond within the timeout.");
134+
} catch (Exception ex) {
161135
// Handle other potential connection/SSH errors
162-
Console.WriteLine($"An error occurred: {ex.Message}");
136+
Log.Error($"An error occurred: {ex.Message}");
163137
}
164138
return null;
165139
}
166140

167-
private void PopulateIdentities()
168-
{
141+
private void PopulateIdentities() {
169142
Log.Info("Querying SSH agent for identities...");
170143
this._identities.Clear();
171144
_cursor = -1;
172-
try
173-
{
145+
try {
174146
var agentIdentities = RequestIdentities();
175147

176-
if (agentIdentities == null)
177-
{
148+
if (agentIdentities == null) {
178149
Log.Warning("No identities found or failed to retrieve identities from SSH agent.");
179150
return;
180151
}
181152

182-
foreach (var id in agentIdentities)
183-
{
153+
foreach (var id in agentIdentities) {
184154
var hash = GetOpenSshKey(id);
185155
var type = id?.Key?.ToString();
186156
var comment = id?.Key.Comment;
@@ -195,9 +165,7 @@ private void PopulateIdentities()
195165
var identity = new Identity(Comment: comment, User: user, Host: host, Name: name, Hash: hash, Type: type, Filename: filename);
196166
this._identities.Add(identity);
197167
}
198-
}
199-
catch (Exception ex)
200-
{
168+
} catch (Exception ex) {
201169
Log.Error($"Error querying SSH agent: {ex.Message}");
202170
}
203171
}

src/SshAgentEcho.Core/SshAgentEcho.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="Octokit" Version="14.0.0" />
1011
<PackageReference Include="SSH.NET" Version="2025.1.0" />
1112
<PackageReference Include="SshNet.Agent" Version="2024.2.0.1" />
1213
</ItemGroup>

src/SshAgentEcho.Core/Update.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using Octokit;
2+
using System.Runtime.InteropServices;
3+
using System.Reflection;
4+
5+
namespace SshAgentEcho.Core;
6+
7+
public class Update {
8+
public string? LatestVersion { get; private set; } = null;
9+
public string? ReleaseNotes { get; private set; } = null;
10+
public ReleaseAsset? LatestAsset { get; private set; } = null;
11+
public Release? LatestRelease { get; private set; } = null;
12+
13+
private string _currentVersion;
14+
private string _repo;
15+
private string _owner;
16+
17+
public Update() {
18+
_repo = "ssh-agent-echo";
19+
_owner = "totalretribution";
20+
var version = Assembly.GetEntryAssembly()?
21+
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
22+
if (version == null) {
23+
_currentVersion = "0.0.0";
24+
} else {
25+
_currentVersion = version;
26+
}
27+
}
28+
29+
public bool IsNewVersionAvailable() {
30+
if (LatestVersion == null) return false;
31+
try {
32+
var current = Version.Parse(_currentVersion);
33+
var latest = Version.Parse(LatestVersion);
34+
return latest > current;
35+
} catch (Exception) {
36+
return false;
37+
}
38+
}
39+
40+
public string? GetDownloadUrl() {
41+
return LatestAsset?.BrowserDownloadUrl;
42+
}
43+
44+
public string? GetReleaseURL() {
45+
return LatestRelease?.HtmlUrl;
46+
}
47+
48+
public string? GetContentType() {
49+
return LatestAsset?.ContentType;
50+
}
51+
52+
public DateTime? GetReleaseUTC() {
53+
return LatestRelease?.PublishedAt?.UtcDateTime;
54+
}
55+
56+
public int? GetUpdateSize() {
57+
return LatestAsset?.Size;
58+
}
59+
60+
private ReleaseAsset? GetAsset(IReadOnlyList<ReleaseAsset>? assets = null) {
61+
if (assets == null) return null;
62+
string rid = RuntimeInformation.RuntimeIdentifier;
63+
var asset = assets.FirstOrDefault(a => a.Name.Contains(rid, StringComparison.OrdinalIgnoreCase));
64+
return asset;
65+
}
66+
67+
public async Task Check() {
68+
if (_repo == null || _owner == null || _currentVersion == null) {
69+
throw new InvalidOperationException("Repo, owner, and current version must be set.");
70+
}
71+
var client = new GitHubClient(new Octokit.ProductHeaderValue(_repo));
72+
73+
try {
74+
Log.Info("Checking for updates...");
75+
var latestRelease = await client.Repository.Release.GetLatest(_owner, _repo);
76+
var latestVersion = Version.Parse(latestRelease.TagName.TrimStart('v'));
77+
LatestVersion = latestVersion.ToString();
78+
ReleaseNotes = latestRelease.Body;
79+
var asset = GetAsset(latestRelease.Assets);
80+
if (asset == null) {
81+
throw new Exception("No suitable asset found for the current platform.");
82+
}
83+
LatestAsset = asset;
84+
LatestRelease = latestRelease;
85+
86+
} catch (Exception ex) {
87+
Log.Error($"An error occurred while checking for updates: {ex.Message}");
88+
LatestVersion = null;
89+
ReleaseNotes = null;
90+
LatestAsset = null;
91+
LatestRelease = null;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)