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