From 63daa63c4755408e50f9fbea0b6d0ba47fbe8ca1 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Thu, 13 Apr 2023 16:41:01 +0200 Subject: [PATCH] Warn users if Windows not updated for longer than two months Closes: #128 Signed-off-by: Simon Rozman --- .gitmodules | 3 + CHANGES.md | 3 + eduVPN/{SelfUpdate.cs => CGo.cs} | 46 ++++++-- eduVPN/CGoInt64Ptr.cs | 22 ++++ eduVPN/{CGoPtrPair.cs => CGoPtrPtr.cs} | 2 +- eduVPN/Engine.cs | 14 +-- eduVPN/Resources/Strings.Designer.cs | 10 ++ eduVPN/Resources/Strings.ar.resx | 4 + eduVPN/Resources/Strings.de.resx | 4 + eduVPN/Resources/Strings.es-ES.resx | 4 + eduVPN/Resources/Strings.es.resx | 4 + eduVPN/Resources/Strings.fr.resx | 4 + eduVPN/Resources/Strings.nb.resx | 4 + eduVPN/Resources/Strings.nl.resx | 4 + eduVPN/Resources/Strings.pt-PT.resx | 4 + eduVPN/Resources/Strings.resx | 4 + eduVPN/Resources/Strings.sl.resx | 4 + eduVPN/Resources/Strings.tr.resx | 4 + eduVPN/Resources/Strings.uk.resx | 4 + .../Pages/SelfUpdateProgressPage.cs | 4 +- .../ViewModels/Pages/SelfUpdatePromptPage.cs | 2 +- eduVPN/ViewModels/Windows/ConnectWizard.cs | 9 ++ eduVPN/eduVPN.csproj | 5 +- eduvpn-windows/cgo.go | 31 ++++- eduvpn-windows/go.mod | 8 +- eduvpn-windows/go.sum | 3 + eduvpn-windows/healthcheck/healthcheck.go | 111 ++++++++++++++++++ .../healthcheck/healthcheck_test.go | 40 +++++++ lxn-win | 1 + 29 files changed, 334 insertions(+), 28 deletions(-) rename eduVPN/{SelfUpdate.cs => CGo.cs} (89%) create mode 100644 eduVPN/CGoInt64Ptr.cs rename eduVPN/{CGoPtrPair.cs => CGoPtrPtr.cs} (95%) create mode 100644 eduvpn-windows/healthcheck/healthcheck.go create mode 100644 eduvpn-windows/healthcheck/healthcheck_test.go create mode 160000 lxn-win diff --git a/.gitmodules b/.gitmodules index ac5a6839..b1cc06a3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ [submodule "eduvpn-common"] path = eduvpn-common url = https://github.com/Amebis/eduvpn-common.git +[submodule "lxn-win"] + path = lxn-win + url = https://github.com/Amebis/lxn-win.git diff --git a/CHANGES.md b/CHANGES.md index ad1befec..63b63877 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ## [Unreleased](https://github.com/Amebis/eduVPN/compare/3.3.8...HEAD) +- Warn users if Windows not updated for longer than two months +- Fixes: #128 + ## [3.3.8](https://github.com/Amebis/eduVPN/compare/3.3.7...3.3.8) (2023-04-12) diff --git a/eduVPN/SelfUpdate.cs b/eduVPN/CGo.cs similarity index 89% rename from eduVPN/SelfUpdate.cs rename to eduVPN/CGo.cs index 7bc39e3a..acb126b9 100644 --- a/eduVPN/SelfUpdate.cs +++ b/eduVPN/CGo.cs @@ -18,7 +18,7 @@ namespace eduVPN { - public class SelfUpdate + public class CGo { #region Data types @@ -220,7 +220,7 @@ public void Dispose() /// /// Available self-update description /// - public class Package : JSON.ILoadableItem + public class SelfUpdatePackage : JSON.ILoadableItem { #region Fields @@ -259,7 +259,7 @@ public class Package : JSON.ILoadableItem #region Constructors - public Package(Uri baseUri) + public SelfUpdatePackage(Uri baseUri) { BaseUri = baseUri; } @@ -292,16 +292,26 @@ public void Load(object obj) #endregion + #region Fields + + /// + /// Used to convert Unix timestamps into + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + static readonly DateTimeOffset Epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, new TimeSpan(0, 0, 0)); + + #endregion + #region Methods [DllImport("eduvpn_windows.dll", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair check_selfupdate( + static extern CGoPtrPtr check_selfupdate( [MarshalAs(UnmanagedType.LPUTF8Str)] string url, [MarshalAs(UnmanagedType.LPUTF8Str)] string allowedSigners, [MarshalAs(UnmanagedType.LPUTF8Str)] string productId, IntPtr ctx); - public static Package Check(ResourceRef discovery, string productId, CancellationToken ct = default) + public static SelfUpdatePackage CheckSelfUpdate(ResourceRef discovery, string productId, CancellationToken ct = default) { using (var ctx = new CGoContext(ct)) { @@ -321,7 +331,7 @@ public static Package Check(ResourceRef discovery, string productId, Cancellatio { if (r.r1 != IntPtr.Zero) throw new Exception((string)m.MarshalNativeToManaged(r.r1)); - var p = new Package(discovery.Uri); + var p = new SelfUpdatePackage(discovery.Uri); p.Load(eduJSON.Parser.Parse((string)m.MarshalNativeToManaged(r.r0), ct)); return p; } @@ -356,7 +366,7 @@ static extern string download_and_install_selfupdate( IntPtr ctx, SetProgress setProgress); - public static void DownloadAndInstall( + public static void DownloadAndInstallSelfUpdate( IEnumerable uris, byte[] hash, string installerArguments, @@ -377,6 +387,28 @@ public static void DownloadAndInstall( } } + [DllImport("eduvpn_windows.dll", CallingConvention = CallingConvention.Cdecl)] + static extern CGoInt64Ptr get_last_update_timestamp(IntPtr ctx); + + public static DateTimeOffset GetLastUpdateTimestamp(CancellationToken ct = default) + { + using (var ctx = new CGoContext(ct)) + { + var m = CGoToManagedStringMarshaller.GetInstance(null); + var r = get_last_update_timestamp(ctx.Handle); + try + { + if (r.r1 != IntPtr.Zero) + throw new Exception((string)m.MarshalNativeToManaged(r.r1)); + return Epoch.AddSeconds(r.r0); + } + finally + { + m.CleanUpNativeData(r.r1); + } + } + } + #endregion } } diff --git a/eduVPN/CGoInt64Ptr.cs b/eduVPN/CGoInt64Ptr.cs new file mode 100644 index 00000000..5fa8e450 --- /dev/null +++ b/eduVPN/CGoInt64Ptr.cs @@ -0,0 +1,22 @@ +/* + eduVPN - VPN for education and research + + Copyright: 2017-2023 The Commons Conservancy + SPDX-License-Identifier: GPL-3.0+ +*/ + +using System; +using System.Runtime.InteropServices; + +namespace eduVPN +{ + /// + /// A blittable struct to allow (C.int64_t, *C.char) CGo function return types + /// + [StructLayout(LayoutKind.Sequential)] + struct CGoInt64Ptr + { + public long r0; + public IntPtr r1; + } +} diff --git a/eduVPN/CGoPtrPair.cs b/eduVPN/CGoPtrPtr.cs similarity index 95% rename from eduVPN/CGoPtrPair.cs rename to eduVPN/CGoPtrPtr.cs index 7855524b..1d96647c 100644 --- a/eduVPN/CGoPtrPair.cs +++ b/eduVPN/CGoPtrPtr.cs @@ -14,7 +14,7 @@ namespace eduVPN /// A blittable struct to allow (*C.char, *C.char) CGo function return types /// [StructLayout(LayoutKind.Sequential)] - struct CGoPtrPair + struct CGoPtrPtr { public IntPtr r0; public IntPtr r1; diff --git a/eduVPN/Engine.cs b/eduVPN/Engine.cs index b3d4346b..1c04e821 100644 --- a/eduVPN/Engine.cs +++ b/eduVPN/Engine.cs @@ -358,7 +358,7 @@ public static void Deregister() } [DllImport("eduvpn_common.dll", EntryPoint = "ExpiryTimes", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _ExpiryTimes(); + static extern CGoPtrPtr _ExpiryTimes(); /// /// Returns the different Unix timestamps regarding expiry. @@ -497,7 +497,7 @@ public static void RemoveOwnServer(Uri url) } [DllImport("eduvpn_common.dll", EntryPoint = "CurrentServer", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _CurrentServer(); + static extern CGoPtrPtr _CurrentServer(); public static string CurrentServer() { @@ -516,7 +516,7 @@ public static string CurrentServer() } [DllImport("eduvpn_common.dll", EntryPoint = "ServerList", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _ServerList(); + static extern CGoPtrPtr _ServerList(); public static string ServerList() { @@ -535,7 +535,7 @@ public static string ServerList() } [DllImport("eduvpn_common.dll", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair GetConfig( + static extern CGoPtrPtr GetConfig( /*[MarshalAs(UnmanagedType.I4)]*/ ServerType type, [MarshalAs(UnmanagedType.LPUTF8Str)] string id, int pTCP, @@ -649,7 +649,7 @@ public static void SetSecureInternetLocation(string countryCode) } [DllImport("eduvpn_common.dll", EntryPoint = "DiscoServers", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _DiscoServers(); + static extern CGoPtrPtr _DiscoServers(); /// /// Gets the servers list from the discovery server. @@ -675,7 +675,7 @@ public static string DiscoServers() } [DllImport("eduvpn_common.dll", EntryPoint = "DiscoOrganizations", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _DiscoOrganizations(); + static extern CGoPtrPtr _DiscoOrganizations(); /// /// Gets the organizations list from the discovery server. @@ -750,7 +750,7 @@ public static void SetSupportWireGuard(bool support) } [DllImport("eduvpn_common.dll", EntryPoint = "SecureLocationList", CallingConvention = CallingConvention.Cdecl)] - static extern CGoPtrPair _SecureLocationList(); + static extern CGoPtrPtr _SecureLocationList(); /// /// Returns all the available locations diff --git a/eduVPN/Resources/Strings.Designer.cs b/eduVPN/Resources/Strings.Designer.cs index 57f9cff6..0ab19ff6 100644 --- a/eduVPN/Resources/Strings.Designer.cs +++ b/eduVPN/Resources/Strings.Designer.cs @@ -428,5 +428,15 @@ internal static string WarningDefaultGatewayIsVPN { return ResourceManager.GetString("WarningDefaultGatewayIsVPN", resourceCulture); } } + + /// + /// Looks up a localized string similar to Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. + ///Your organization might opt to prevent connections from insecure computers in the future.. + /// + internal static string WarningWindowsUpdatesStalled { + get { + return ResourceManager.GetString("WarningWindowsUpdatesStalled", resourceCulture); + } + } } } diff --git a/eduVPN/Resources/Strings.ar.resx b/eduVPN/Resources/Strings.ar.resx index 9a62b93d..4f48548e 100644 --- a/eduVPN/Resources/Strings.ar.resx +++ b/eduVPN/Resources/Strings.ar.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.de.resx b/eduVPN/Resources/Strings.de.resx index 7328dcbb..4c1a1097 100644 --- a/eduVPN/Resources/Strings.de.resx +++ b/eduVPN/Resources/Strings.de.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.es-ES.resx b/eduVPN/Resources/Strings.es-ES.resx index 85c4048a..6fa5c2e4 100644 --- a/eduVPN/Resources/Strings.es-ES.resx +++ b/eduVPN/Resources/Strings.es-ES.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.es.resx b/eduVPN/Resources/Strings.es.resx index f8845990..620968b4 100644 --- a/eduVPN/Resources/Strings.es.resx +++ b/eduVPN/Resources/Strings.es.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.fr.resx b/eduVPN/Resources/Strings.fr.resx index bcbdabb1..b914e868 100644 --- a/eduVPN/Resources/Strings.fr.resx +++ b/eduVPN/Resources/Strings.fr.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.nb.resx b/eduVPN/Resources/Strings.nb.resx index b852574a..5479c2e7 100644 --- a/eduVPN/Resources/Strings.nb.resx +++ b/eduVPN/Resources/Strings.nb.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.nl.resx b/eduVPN/Resources/Strings.nl.resx index c64ac2f8..1ff32a84 100644 --- a/eduVPN/Resources/Strings.nl.resx +++ b/eduVPN/Resources/Strings.nl.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.pt-PT.resx b/eduVPN/Resources/Strings.pt-PT.resx index 2602541f..8067b4db 100644 --- a/eduVPN/Resources/Strings.pt-PT.resx +++ b/eduVPN/Resources/Strings.pt-PT.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.resx b/eduVPN/Resources/Strings.resx index 85c4048a..6fa5c2e4 100644 --- a/eduVPN/Resources/Strings.resx +++ b/eduVPN/Resources/Strings.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.sl.resx b/eduVPN/Resources/Strings.sl.resx index 85f0fb94..00823fef 100644 --- a/eduVPN/Resources/Strings.sl.resx +++ b/eduVPN/Resources/Strings.sl.resx @@ -243,4 +243,8 @@ Vaš računalnik že uporablja tunel VPN za privzeti promet. Dodatna povezava VPN verjetno ne bo delovala. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.tr.resx b/eduVPN/Resources/Strings.tr.resx index 42ab56e6..ed002109 100644 --- a/eduVPN/Resources/Strings.tr.resx +++ b/eduVPN/Resources/Strings.tr.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/Resources/Strings.uk.resx b/eduVPN/Resources/Strings.uk.resx index d5801f10..aadc3db7 100644 --- a/eduVPN/Resources/Strings.uk.resx +++ b/eduVPN/Resources/Strings.uk.resx @@ -243,4 +243,8 @@ Your computer is using a VPN tunnel to handle the default traffic already. Another VPN connection will likely not work. + + Your computer did not install any Windows Updates for a while. Please check the update status and/or upgrade your computer to a newer and supported release of Windows. +Your organization might opt to prevent connections from insecure computers in the future. + \ No newline at end of file diff --git a/eduVPN/ViewModels/Pages/SelfUpdateProgressPage.cs b/eduVPN/ViewModels/Pages/SelfUpdateProgressPage.cs index 1f974eb8..4fc36231 100644 --- a/eduVPN/ViewModels/Pages/SelfUpdateProgressPage.cs +++ b/eduVPN/ViewModels/Pages/SelfUpdateProgressPage.cs @@ -103,8 +103,8 @@ public override void OnActivate() selfUpdate.DoWork += (object sender, DoWorkEventArgs e) => { selfUpdate.ReportProgress(0); - SelfUpdate.DownloadAndInstall(DownloadUris, Hash, Arguments, ct, - new SelfUpdate.SetProgress((float value) => selfUpdate.ReportProgress((int)Math.Floor(value * 100)))); + CGo.DownloadAndInstallSelfUpdate(DownloadUris, Hash, Arguments, ct, + new CGo.SetProgress((float value) => selfUpdate.ReportProgress((int)Math.Floor(value * 100)))); }; // Self-update progress. diff --git a/eduVPN/ViewModels/Pages/SelfUpdatePromptPage.cs b/eduVPN/ViewModels/Pages/SelfUpdatePromptPage.cs index 0f091af5..0700e5f9 100644 --- a/eduVPN/ViewModels/Pages/SelfUpdatePromptPage.cs +++ b/eduVPN/ViewModels/Pages/SelfUpdatePromptPage.cs @@ -142,7 +142,7 @@ public void DiscoverVersions() Wizard.TryInvoke((Action)(() => Wizard.TaskCount++)); try { - var package = SelfUpdate.Check( + var package = CGo.CheckSelfUpdate( Properties.SettingsEx.Default.SelfUpdateDiscovery, Properties.Settings.Default.SelfUpdateBundleId, Window.Abort.Token); diff --git a/eduVPN/ViewModels/Windows/ConnectWizard.cs b/eduVPN/ViewModels/Windows/ConnectWizard.cs index aa0f909c..4c723ccc 100644 --- a/eduVPN/ViewModels/Windows/ConnectWizard.cs +++ b/eduVPN/ViewModels/Windows/ConnectWizard.cs @@ -353,6 +353,15 @@ public ConnectWizard() }, 24 * 60 * 60 * 1000)); // Repeat every 24 hours + actions.Add(new KeyValuePair( + () => + { + var ts = CGo.GetLastUpdateTimestamp(Abort.Token); + if (DateTimeOffset.UtcNow - ts > TimeSpan.FromDays(60)) + throw new Exception(Resources.Strings.WarningWindowsUpdatesStalled); + }, + 24 * 60 * 60 * 1000)); // Repeat every 24 hours + // TODO: Migrate eduVPN settings to eduvpn-common. // TODO: Support preconfigured Institute Access and Secure Internet to eduvpn-common. //var str = Engine.ServerList(); diff --git a/eduVPN/eduVPN.csproj b/eduVPN/eduVPN.csproj index 0e102d3a..2539e93a 100644 --- a/eduVPN/eduVPN.csproj +++ b/eduVPN/eduVPN.csproj @@ -41,8 +41,9 @@ + - + @@ -53,7 +54,7 @@ - + diff --git a/eduvpn-windows/cgo.go b/eduvpn-windows/cgo.go index 3d81d0e3..ca7e78c6 100644 --- a/eduvpn-windows/cgo.go +++ b/eduvpn-windows/cgo.go @@ -25,8 +25,10 @@ import ( "strings" "unsafe" + "github.com/Amebis/eduVPN/eduvpn-windows/healthcheck" "github.com/Amebis/eduVPN/eduvpn-windows/selfupdate" "github.com/jedisct1/go-minisign" + "github.com/lxn/win" ) type mycontext struct { @@ -80,6 +82,10 @@ func goStringZ(strz *C.char) []string { return tab } +func cError(err error) *C.char { + return C.CString(err.Error()) +} + //export check_selfupdate func check_selfupdate(url *C.char, allowedSigners *C.char, productId *C.char, ctx C.uintptr_t) (pkg *C.char, err *C.char) { _allowedSigners := goStringZ(allowedSigners) @@ -88,13 +94,13 @@ func check_selfupdate(url *C.char, allowedSigners *C.char, productId *C.char, ct v := strings.Split(str, "|") k, err := minisign.NewPublicKey(v[0]) if err != nil { - return nil, C.CString(err.Error()) + return nil, cError(err) } s := selfupdate.TrustedSigner{PublicKey: k} if len(v) > 1 { x, err := strconv.Atoi(v[1]) if err != nil { - return nil, C.CString(err.Error()) + return nil, cError(err) } s.AlgorithmMask = selfupdate.AlgorithmMask(x) } else { @@ -104,11 +110,11 @@ func check_selfupdate(url *C.char, allowedSigners *C.char, productId *C.char, ct } p, err2 := selfupdate.Check(C.GoString(url), signers, C.GoString(productId), goContext(ctx)) if err2 != nil { - return nil, C.CString(err2.Error()) + return nil, cError(err2) } pStr, err2 := json.Marshal(p) if err2 != nil { - return nil, C.CString(fmt.Errorf("failed converting to JSON: %w", err2).Error()) + return nil, cError(fmt.Errorf("failed converting to JSON: %w", err2)) } return C.CString(string(pStr)), nil } @@ -133,7 +139,22 @@ func download_and_install_selfupdate( setProgress C.set_progress) (err *C.char) { err2 := selfupdate.DownloadAndInstall(goStringZ(urls), (*selfupdate.Hash)(unsafe.Pointer(hash)), C.GoString(installerArguments), goContext(ctx), progressIndicator{setProgress: setProgress}) if err2 != nil { - return C.CString(err2.Error()) + return cError(err2) } return nil } + +//export get_last_update_timestamp +func get_last_update_timestamp(ctx C.uintptr_t) (timestamp C.int64_t, err *C.char) { + var session3 *win.IUpdateSession3 + hr := win.CoCreateInstance(&win.CLSID_UpdateSession, nil, win.CLSCTX_INPROC_SERVER, &win.IID_IUpdateSession3, (*unsafe.Pointer)(unsafe.Pointer(&session3))) + if win.FAILED(hr) { + return 0, cError(fmt.Errorf("failed to create UpdateSession: %#v", hr)) + } + defer session3.Release() + t, err2 := healthcheck.MostRecentUpdateTimestamp(session3, goContext(ctx)) + if err2 != nil { + return 0, cError(err2) + } + return C.int64_t(t.Unix()), nil +} diff --git a/eduvpn-windows/go.mod b/eduvpn-windows/go.mod index 34e04786..0eee73ad 100644 --- a/eduvpn-windows/go.mod +++ b/eduvpn-windows/go.mod @@ -2,9 +2,11 @@ module github.com/Amebis/eduVPN/eduvpn-windows go 1.20 -require github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776 - require ( - golang.org/x/crypto v0.6.0 // indirect + github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776 golang.org/x/sys v0.5.0 + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + golang.org/x/crypto v0.6.0 // indirect ) + +replace github.com/lxn/win => ../lxn-win diff --git a/eduvpn-windows/go.sum b/eduvpn-windows/go.sum index fd6f1ee4..8e8f2e82 100644 --- a/eduvpn-windows/go.sum +++ b/eduvpn-windows/go.sum @@ -1,6 +1,9 @@ github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776 h1:WXhZ7psl6HhDDW58rDWIJE6oB0ETjaQA4U6d8U7lMyg= github.com/jedisct1/go-minisign v0.0.0-20230211184525-1f273d8dc776/go.mod h1:09CTTv5TZgz94QHts03Xnuzy5LmxCE8BNqQRFigO5gA= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/eduvpn-windows/healthcheck/healthcheck.go b/eduvpn-windows/healthcheck/healthcheck.go new file mode 100644 index 00000000..8922cda5 --- /dev/null +++ b/eduvpn-windows/healthcheck/healthcheck.go @@ -0,0 +1,111 @@ +/* + eduVPN - VPN for education and research + + Copyright: 2023 The Commons Conservancy + SPDX-License-Identifier: GPL-3.0+ +*/ + +package healthcheck + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/lxn/win" +) + +const ( + secondsPerMinute = 60 + secondsPerHour = 60 * secondsPerMinute + secondsPerDay = 24 * secondsPerHour +) + +var variantDATEToUnixDiff = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Sub(time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)).Seconds() + +func MostRecentUpdateTimestamp(session3 *win.IUpdateSession3, ctx context.Context) (t time.Time, err error) { + criteria := win.SysAllocString("") + if criteria == nil { + panic("SysAllocString failed") + } + defer win.SysFreeString(criteria) + col, hr := session3.QueryHistory(criteria, 0, 0xffff) + if win.FAILED(hr) { + return time.Time{}, fmt.Errorf("failed to query update history: %x", hr) + } + defer col.Release() + count, hr := col.Count() + if win.FAILED(hr) { + return time.Time{}, fmt.Errorf("failed to query update history count: %x", hr) + } + for i := int32(0); i < count; i++ { + select { + case <-ctx.Done(): + return time.Time{}, ctx.Err() + default: + } + entry, hr := col.Item(i) + if win.FAILED(hr) { + return time.Time{}, fmt.Errorf("failed to query update: %x", hr) + } + defer entry.Release() + if op, hr := entry.Operation(); win.FAILED(hr) || op != win.UOInstallation { + continue + } + if rc, hr := entry.ResultCode(); win.FAILED(hr) || rc != win.ORCSucceeded && rc != win.ORCSucceededWithErrors { + continue + } + date, hr := entry.Date() + if win.FAILED(hr) { + continue + } + date *= secondsPerDay + date -= variantDATEToUnixDiff + sec, subsec := math.Modf(date) + nsec := math.Trunc(subsec * 1e+9) + t2 := time.Unix(int64(sec), int64(nsec)) + if t2.Unix() > t.Unix() { + t = t2 + } + } + return t, nil +} + +func UpdateHistory(session3 *win.IUpdateSession3, ctx context.Context) (history []*win.IUpdateHistoryEntry, err error) { + criteria := win.SysAllocString("") + if criteria == nil { + panic("SysAllocString failed") + } + defer win.SysFreeString(criteria) + col, hr := session3.QueryHistory(criteria, 0, 0xffff) + if win.FAILED(hr) { + return nil, fmt.Errorf("failed to query update history: %x", hr) + } + defer col.Release() + count, hr := col.Count() + if win.FAILED(hr) { + return nil, fmt.Errorf("failed to query update history count: %x", hr) + } + history = make([]*win.IUpdateHistoryEntry, 0, count) + for i := int32(0); i < count; i++ { + select { + case <-ctx.Done(): + ReleaseIUpdateHistoryEntries(history) + return nil, ctx.Err() + default: + } + entry, hr := col.Item(i) + if win.FAILED(hr) { + continue + } + history = append(history, entry) + } + return history, nil +} + +func ReleaseIUpdateHistoryEntries(history []*win.IUpdateHistoryEntry) { + for i := range history { + history[i].Release() + } +} diff --git a/eduvpn-windows/healthcheck/healthcheck_test.go b/eduvpn-windows/healthcheck/healthcheck_test.go new file mode 100644 index 00000000..c527339d --- /dev/null +++ b/eduvpn-windows/healthcheck/healthcheck_test.go @@ -0,0 +1,40 @@ +/* + eduVPN - VPN for education and research + + Copyright: 2023 The Commons Conservancy + SPDX-License-Identifier: GPL-3.0+ +*/ + +package healthcheck + +import ( + "testing" + "unsafe" + + "github.com/lxn/win" +) + +func TestHealthCheck(t *testing.T) { + hr := win.CoInitializeEx(nil, win.COINIT_APARTMENTTHREADED|win.COINIT_SPEED_OVER_MEMORY) + if win.FAILED(hr) { + t.Errorf("Error initializing COM: %#v", hr) + } + defer win.CoUninitialize() + var session3 *win.IUpdateSession3 + hr = win.CoCreateInstance(&win.CLSID_UpdateSession, nil, win.CLSCTX_INPROC_SERVER, &win.IID_IUpdateSession3, (*unsafe.Pointer)(unsafe.Pointer(&session3))) + if win.FAILED(hr) { + t.Errorf("Failed to create UpdateSession: %#v", hr) + } + defer session3.Release() + + _, err := MostRecentUpdateTimestamp(session3) + if err != nil { + t.Errorf("Failed to get last update timestamp: %#v", err) + } + + history, err := UpdateHistory(session3) + if err != nil { + t.Errorf("Failed to enumerate update history: %#v", err) + } + defer ReleaseIUpdateHistoryEntries(history) +} diff --git a/lxn-win b/lxn-win new file mode 160000 index 00000000..8c028faf --- /dev/null +++ b/lxn-win @@ -0,0 +1 @@ +Subproject commit 8c028faf83e033693cba4fcb7f068371e7cbf029