diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index ce76468..3c087b4 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -13,8 +13,10 @@ jobs: runs-on: windows-latest steps: - - name: Check out repository + - name: Check out repository, with full history uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up .NET uses: actions/setup-dotnet@v4 @@ -33,22 +35,11 @@ jobs: run: | Add-Content $env:GITHUB_PATH "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64" - - name: Install code signing certificate - env: - base64Pfx: ${{ secrets.PFX_B64 }} - password: ${{ secrets.PFX_PASS }} - run: | - $securePassword = ConvertTo-SecureString -String $env:password -AsPlainText -Force - $pfxBytes = [System.Convert]::FromBase64String($env:base64Pfx) - $tempPfxPath = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempPfxPath, $pfxBytes) - Import-PfxCertificate -FilePath $tempPfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $securePassword - - - name: Run package script - shell: pwsh + - name: Build x64 and ARM64 run: | - ./package.ps1 - mkdir -Force dist + dotnet restore + dotnet msbuild TailscaleClient.csproj -t:Rebuild -p:Platform=x64 -p:Configuration=Release -p:OutDir="./dist/x64/" + dotnet msbuild TailscaleClient.csproj -t:Rebuild -p:Platform=arm64 -p:Configuration=Release -p:OutDir="./dist/arm64/" - name: Upload build artifacts uses: actions/upload-artifact@v4 @@ -58,7 +49,7 @@ jobs: release: name: Release Package - runs-on: ubuntu-latest + runs-on: windows-latest needs: build if: startsWith(github.ref, 'refs/tags/v') @@ -68,14 +59,31 @@ jobs: with: name: dist - - name: "Upload to R2" + - name: Install packaging tools + run: | + dotnet tool install -g nbgv + dotnet tool install -g vpk + + - name: Sync vpk to current version + run: | + vpk download http --url https://tsc.xirreal.dev/ + + - name: Package x64 + run: | + vpk pack --packId TailscaleClient --packVersion $(nbgv get-version -v SimpleVersion) --packDir ./x64 --channel win-x64 --framework net8.0-x64-runtime + + - name: Package arm64 + run: | + vpk pack --packId TailscaleClient --packVersion $(nbgv get-version -v SimpleVersion) --packDir ./arm64 --channel win-arm64 --framework net8.0-arm64-runtime + + - name: Upload to R2 env: AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto AWS_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }} run: | - aws s3 sync ./ s3://tsc/ + aws s3 sync ./Releases s3://tsc/ - name: Create a new release uses: softprops/action-gh-release@v2 diff --git a/.gitignore b/.gitignore index 4795340..1f0f61d 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,5 @@ MigrationBackup/ FodyWeavers.xsd # Custom dist folder -dist/ \ No newline at end of file +dist/ +codegen/ \ No newline at end of file diff --git a/App.xaml.cs b/App.xaml.cs index eae4a65..48ffed7 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,4 +1,6 @@ -using Microsoft.UI.Xaml; +using System; +using Microsoft.UI.Xaml; +using Velopack; namespace TailscaleClient; @@ -10,7 +12,26 @@ public partial class App : Application public App() { + VelopackApp.Build().Run(); InitializeComponent(); + + var mgr = new UpdateManager("https://tsc.xirreal.dev"); + + try + { + var newVersion = mgr.CheckForUpdates(); + if (newVersion == null) + { + return; + } + + mgr.DownloadUpdates(newVersion); + mgr.ApplyUpdatesAndRestart(newVersion); + } catch (Exception e) + { + // TODO: Show failed update bar, possibly a badge on settings? + return; + } } protected override void OnLaunched(LaunchActivatedEventArgs args) diff --git a/Assets/AccountCard.xaml b/Assets/AccountCard.xaml index cc0206e..63f4bc1 100644 --- a/Assets/AccountCard.xaml +++ b/Assets/AccountCard.xaml @@ -34,8 +34,8 @@ - - + + diff --git a/Assets/DeviceCard.xaml b/Assets/DeviceCard.xaml index f1b93d4..1d66864 100644 --- a/Assets/DeviceCard.xaml +++ b/Assets/DeviceCard.xaml @@ -23,7 +23,7 @@ - + diff --git a/Assets/DeviceCard.xaml.cs b/Assets/DeviceCard.xaml.cs index 8e22ad3..569383d 100644 --- a/Assets/DeviceCard.xaml.cs +++ b/Assets/DeviceCard.xaml.cs @@ -31,11 +31,9 @@ private static string FormatDaysUntilExpiry(int daysUntilExpiry) return daysUntilExpiry switch { - < - 0 => "Expired", + <0 => "Expired", 0 => "Key expiring today", _ => $"Expiring in {daysUntilExpiry} {daysText}" - }; } @@ -43,8 +41,7 @@ private static Windows.UI.Color FormatColorForExpiry(int daysUntilExpiry) { return daysUntilExpiry switch { - <= - 0 => Windows.UI.Color.FromArgb(255, 255, 0, 0), + <=0 => Windows.UI.Color.FromArgb(255, 255, 0, 0), _ => Windows.UI.Color.FromArgb(255, 255, 150, 0), }; } @@ -157,7 +154,7 @@ public DeviceCard(Core.Types.PeerInfo device, Dictionary + + + + + + + diff --git a/Assets/FlippedSwitch.xaml.cs b/Assets/FlippedSwitch.xaml.cs new file mode 100644 index 0000000..96cdbc8 --- /dev/null +++ b/Assets/FlippedSwitch.xaml.cs @@ -0,0 +1,64 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace TailscaleClient.Assets; + +public sealed partial class FlippedSwitch : UserControl, INotifyPropertyChanged +{ + public FlippedSwitch() + { + InitializeComponent(); + } + + public event PropertyChangedEventHandler PropertyChanged; + public void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public static readonly DependencyProperty IsOnProperty = + DependencyProperty.Register( + nameof(IsOn), + typeof(bool), + typeof(FlippedSwitch), + new PropertyMetadata(false, OnIsOnChanged)); + + public bool IsOn + { + get => (bool)GetValue(IsOnProperty); + set => SetValue(IsOnProperty, value); + } + + private static void OnIsOnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (FlippedSwitch)d; + control.UpdateStatusText(); + } + + public event RoutedEventHandler Toggled; + + private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e) + { + Toggled?.Invoke(sender, e); + UpdateStatusText(); + } + + private string _status = "Off"; + public string Status + { + get => _status; + set + { + _status = value; + OnPropertyChanged(); + } + } + + private void UpdateStatusText() + { + Status = ToggleSwitch.IsOn ? "On" : "Off"; + } +} diff --git a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-16.png b/Assets/Icons/AppIcon.altform-lightunplated_targetsize-16.png deleted file mode 100644 index c0ad129..0000000 Binary files a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-16.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-24.png b/Assets/Icons/AppIcon.altform-lightunplated_targetsize-24.png deleted file mode 100644 index b1af4cd..0000000 Binary files a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-24.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-256.png b/Assets/Icons/AppIcon.altform-lightunplated_targetsize-256.png deleted file mode 100644 index b9e2919..0000000 Binary files a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-256.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-32.png b/Assets/Icons/AppIcon.altform-lightunplated_targetsize-32.png deleted file mode 100644 index 12b1bd3..0000000 Binary files a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-32.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-48.png b/Assets/Icons/AppIcon.altform-lightunplated_targetsize-48.png deleted file mode 100644 index c361874..0000000 Binary files a/Assets/Icons/AppIcon.altform-lightunplated_targetsize-48.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-unplated_targetsize-16.png b/Assets/Icons/AppIcon.altform-unplated_targetsize-16.png deleted file mode 100644 index c81ab65..0000000 Binary files a/Assets/Icons/AppIcon.altform-unplated_targetsize-16.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-unplated_targetsize-24.png b/Assets/Icons/AppIcon.altform-unplated_targetsize-24.png deleted file mode 100644 index add5a90..0000000 Binary files a/Assets/Icons/AppIcon.altform-unplated_targetsize-24.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-unplated_targetsize-256.png b/Assets/Icons/AppIcon.altform-unplated_targetsize-256.png deleted file mode 100644 index 3bddadb..0000000 Binary files a/Assets/Icons/AppIcon.altform-unplated_targetsize-256.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-unplated_targetsize-32.png b/Assets/Icons/AppIcon.altform-unplated_targetsize-32.png deleted file mode 100644 index a764f3a..0000000 Binary files a/Assets/Icons/AppIcon.altform-unplated_targetsize-32.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.altform-unplated_targetsize-48.png b/Assets/Icons/AppIcon.altform-unplated_targetsize-48.png deleted file mode 100644 index 5ce2654..0000000 Binary files a/Assets/Icons/AppIcon.altform-unplated_targetsize-48.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.scale-100.png b/Assets/Icons/AppIcon.scale-100.png deleted file mode 100644 index 2f422eb..0000000 Binary files a/Assets/Icons/AppIcon.scale-100.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.scale-125.png b/Assets/Icons/AppIcon.scale-125.png deleted file mode 100644 index d94703e..0000000 Binary files a/Assets/Icons/AppIcon.scale-125.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.scale-150.png b/Assets/Icons/AppIcon.scale-150.png deleted file mode 100644 index a9a9a16..0000000 Binary files a/Assets/Icons/AppIcon.scale-150.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.scale-200.png b/Assets/Icons/AppIcon.scale-200.png deleted file mode 100644 index 93b7575..0000000 Binary files a/Assets/Icons/AppIcon.scale-200.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.scale-400.png b/Assets/Icons/AppIcon.scale-400.png deleted file mode 100644 index 1a98fb4..0000000 Binary files a/Assets/Icons/AppIcon.scale-400.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.targetsize-16.png b/Assets/Icons/AppIcon.targetsize-16.png deleted file mode 100644 index c81ab65..0000000 Binary files a/Assets/Icons/AppIcon.targetsize-16.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.targetsize-24.png b/Assets/Icons/AppIcon.targetsize-24.png deleted file mode 100644 index add5a90..0000000 Binary files a/Assets/Icons/AppIcon.targetsize-24.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.targetsize-256.png b/Assets/Icons/AppIcon.targetsize-256.png deleted file mode 100644 index 3bddadb..0000000 Binary files a/Assets/Icons/AppIcon.targetsize-256.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.targetsize-32.png b/Assets/Icons/AppIcon.targetsize-32.png deleted file mode 100644 index a764f3a..0000000 Binary files a/Assets/Icons/AppIcon.targetsize-32.png and /dev/null differ diff --git a/Assets/Icons/AppIcon.targetsize-48.png b/Assets/Icons/AppIcon.targetsize-48.png deleted file mode 100644 index 5ce2654..0000000 Binary files a/Assets/Icons/AppIcon.targetsize-48.png and /dev/null differ diff --git a/Assets/Icons/AppIconBase-Dark.ico b/Assets/Icons/AppIconBase-Dark.ico index 6512e53..2be61c8 100644 Binary files a/Assets/Icons/AppIconBase-Dark.ico and b/Assets/Icons/AppIconBase-Dark.ico differ diff --git a/Assets/Icons/AppIconBase-Light.ico b/Assets/Icons/AppIconBase-Light.ico index 4ffe50d..48ae47b 100644 Binary files a/Assets/Icons/AppIconBase-Light.ico and b/Assets/Icons/AppIconBase-Light.ico differ diff --git a/Core/API.cs b/Core/API.cs index e50b0ed..4a12be2 100644 --- a/Core/API.cs +++ b/Core/API.cs @@ -47,8 +47,7 @@ public static bool Init() { BaseAddress = new Uri("http://local-tailscaled.sock"), DefaultRequestHeaders = { { "User-Agent", "Go-http-client/1.1" }, - { "Tailscale-Cap", "95" }, // TODO: Get the real value i just - // copied it from tailscale-ipn packets + { "Tailscale-Cap", "113" }, // Latest (as of 2025/02/07) from https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go { "Accept-Encoding", "gzip" } }, Timeout = TimeSpan.FromSeconds(2) }; @@ -182,9 +181,9 @@ public static Types.Profile GetCurrentUser() return GET>("/localapi/v0/profiles/"); } - public static Types.Prefs GetPrefs() + public static Types.MaskedPrefs GetPrefs() { - return GET("/localapi/v0/prefs"); + return GET("/localapi/v0/prefs"); } public static void SwitchEmptyProfile() @@ -202,19 +201,19 @@ public static void DeleteProfile(string userId) DELETE($"localapi/v0/profiles/{userId}", new { }); } - public static void Start(Types.Prefs prefs) + public static void Start(Types.MaskedPrefs prefs) { POST("/localapi/v0/start", new { UpdatePrefs = prefs }); } - public static Types.Prefs UpdatePrefs(Types.Prefs prefs) + public static Types.MaskedPrefs UpdatePrefs(Types.MaskedPrefs prefs) { - return PATCH("/localapi/v0/prefs", prefs); + return PATCH("/localapi/v0/prefs", prefs); } - public static void CheckPrefs(Types.Prefs prefs) + public static string CheckPrefs(Types.MaskedPrefs prefs) { - POST("/localapi/v0/check-prefs", prefs); + return POST("/localapi/v0/check-prefs", prefs); } public static void Logout() @@ -227,7 +226,7 @@ public static void Login(string controlUrl) SwitchEmptyProfile(); var prefs = - new Types.Prefs() { WantRunning = true, ControlURL = controlUrl }; + new Types.MaskedPrefs() { WantRunning = true, ControlURL = controlUrl }; Start(prefs); POST("/localapi/v0/login-interactive", new { }); } @@ -241,11 +240,15 @@ public static void Connect() { var currentPrefs = GetPrefs(); currentPrefs.WantRunning = true; - currentPrefs.WantRunningSet = true; UpdatePrefs(currentPrefs); } public static void Disconnect() { - UpdatePrefs(new Types.Prefs { WantRunning = false, WantRunningSet = true }); + UpdatePrefs(new Types.MaskedPrefs { WantRunning = false }); + } + + public static Types.SuggestedExitNode GetSuggestedExitNode() + { + return GET("/localapi/v0/suggest-exit-node"); } } diff --git a/Core/Types.cs b/Core/Types.cs index 35241f5..3199022 100644 --- a/Core/Types.cs +++ b/Core/Types.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -122,39 +123,82 @@ public DateTime KeyExpiry { get; set; } + public object CapMap + { + get; set; + } + } + + public class SuggestedExitNode + { + public string ID + { + get; set; + } + + public string Name + { + get; set; + } - public override string ToString() + public Location Location { - return $"ID: {ID}, PublicKey: {PublicKey}, HostName: {HostName}, OS: {OS}, LastSeen: {LastSeen}, Online: {Online}"; + get; set; } } - public class User + public class Location { - public long ID + public string Country { get; set; } - public string LoginName + public string CountryCode { get; set; } - public string DisplayName + public string City { get; set; } - public string ProfilePicURL + + public string CityCode { get; set; } - public List Roles + + public double Latitude + { + get; set; + } + public double Longitude { get; set; } - public override string ToString() + public int Priority { - return $"ID: {ID}, LoginName: {LoginName}, DisplayName: {DisplayName}, ProfilePicURL: {ProfilePicURL}"; + get; set; + } +} + + public class User + { + public long ID + { + get; set; + } + public string LoginName + { + get; set; + } + public string DisplayName + { + get; set; + } + public string ProfilePicURL + { + get; set; } } @@ -172,11 +216,6 @@ public bool MagicDNSEnabled { get; set; } - - public override string ToString() - { - return $"Name: {Name}, MagicDNSSuffix: {MagicDNSSuffix}, MagicDNSEnabled: {MagicDNSEnabled}"; - } } public class Status @@ -233,11 +272,6 @@ public Dictionary ClientVersion { get; set; } - - public override string ToString() - { - return $"Version: {Version}, BackendState: {BackendState}, Self: [{Self}], PeerCount: {Peer?.Count ?? 0}, UserCount: {User?.Count ?? 0}"; - } } public class NetworkProfile @@ -250,11 +284,6 @@ public string DomainName { get; set; } - - public override string ToString() - { - return $"MagicDNSName: {MagicDNSName}, DomainName: {DomainName}"; - } } public class Profile @@ -291,11 +320,6 @@ public string ControlURL { get; set; } - - public override string ToString() - { - return $"ID: {ID}, Name: {Name}, NetworkProfile: [{NetworkProfile}], NodeID: {NodeID}, ControlURL: {ControlURL}"; - } } public class AppConnector @@ -318,115 +342,505 @@ public object Apply } } - public class Prefs + public class MaskedPrefs : IJsonOnDeserialized { + private string _controlUrl; + public bool ControlURLSet + { + get; set; + } public string ControlURL + { + get => _controlUrl; + set + { + _controlUrl = value; + ControlURLSet = true; + } + } + + private bool _routeAll; + public bool RouteAllSet { get; set; } public bool RouteAll { - get; set; + get => _routeAll; + set + { + _routeAll = value; + RouteAllSet = true; + } } - public bool AllowSingleHosts + + + + private string _exitNodeID; + public bool ExitNodeIDSet { get; set; } public string ExitNodeID + { + get => _exitNodeID; + set + { + _exitNodeID = value; + ExitNodeIDSet = true; + } + } + + private string _exitNodeIP; + public bool ExitNodeIPSet { get; set; } public string ExitNodeIP { - get; set; + get => _exitNodeIP; + set + { + _exitNodeIP = value; + ExitNodeIPSet = true; + } } - public string InternalExitNodePrior + + private bool _exitNodeAllowLANAccess; + public bool ExitNodeAllowLANAccessSet { get; set; } public bool ExitNodeAllowLANAccess + { + get => _exitNodeAllowLANAccess; + set + { + _exitNodeAllowLANAccess = value; + ExitNodeAllowLANAccessSet = true; + } + } + + private bool _corpDNS; + public bool CorpDNSSet { get; set; } public bool CorpDNS + { + get => _corpDNS; + set + { + _corpDNS = value; + CorpDNSSet = true; + } + } + + private bool _runSSH; + public bool RunSSHSet { get; set; } public bool RunSSH + { + get => _runSSH; + set + { + _runSSH = value; + RunSSHSet = true; + } + } + + private bool _runWebClient; + public bool RunWebClientSet { get; set; } public bool RunWebClient + { + get => _runWebClient; + set + { + _runWebClient = value; + RunWebClientSet = true; + } + } + + private bool _wantRunning; + public bool WantRunningSet { get; set; } public bool WantRunning + { + get => _wantRunning; + set + { + _wantRunning = value; + WantRunningSet = true; + } + } + + private bool _loggedOut; + public bool LoggedOutSet { get; set; } public bool LoggedOut + { + get => _loggedOut; + set + { + _loggedOut = value; + LoggedOutSet = true; + } + } + + private bool _shieldsUp; + public bool ShieldsUpSet { get; set; } public bool ShieldsUp + { + get => _shieldsUp; + set + { + _shieldsUp = value; + ShieldsUpSet = true; + } + } + + private object _advertiseTags; + public bool AdvertiseTagsSet { get; set; } public object AdvertiseTags + { + get => _advertiseTags; + set + { + _advertiseTags = value; + AdvertiseTagsSet = true; + } + } + + private string _hostname; + public bool HostnameSet { get; set; } public string Hostname + { + get => _hostname; + set + { + _hostname = value; + HostnameSet = true; + } + } + + private bool _notepadURLs; + public bool NotepadURLsSet { get; set; } public bool NotepadURLs + { + get => _notepadURLs; + set + { + _notepadURLs = value; + NotepadURLsSet = true; + } + } + + private bool _forceDaemon; + public bool ForceDaemonSet + { + get; set; + } + + public bool ForceDaemon + { + get => _forceDaemon; + set + { + _forceDaemon = value; + ForceDaemonSet = true; + } + } + + private object _advertiseRoutes; + public bool AdvertiseRoutesSet { get; set; } public object AdvertiseRoutes + { + get => _advertiseRoutes; + set + { + _advertiseRoutes = value; + AdvertiseRoutesSet = true; + } + } + + private List _advertiseServices; + public bool AdvertiseServicesSet + { + get; set; + } + + public List AdvertiseServices + { + get => _advertiseServices; + set + { + _advertiseServices = value; + AdvertiseServicesSet = true; + } + } + + private bool _noSNAT; + public bool NoSNATSet { get; set; } public bool NoSNAT + { + get => _noSNAT; + set + { + _noSNAT = value; + NoSNATSet = true; + } + } + + private bool _noStatefulFiltering; + public bool NoStatefulFilteringSet + { + get; set; + } + + public bool NoStatefulFiltering + { + get => _noStatefulFiltering; + set + { + _noStatefulFiltering = value; + NoStatefulFilteringSet = true; + } + } + + private int _netfilterMode; + public bool NetfilterModeSet { get; set; } public int NetfilterMode + { + get => _netfilterMode; + set + { + _netfilterMode = value; + NetfilterModeSet = true; + } + } + + private string _operatorUser; + public bool OperatorUserSet + { + get; set; + } + + public string OperatorUser + { + get => _operatorUser; + set + { + _operatorUser = value; + OperatorUserSet = true; + } + } + + private string _profileName; + public bool ProfileNameSet + { + get; set; + } + public string ProfileName + { + get => _profileName; + set + { + _profileName = value; + ProfileNameSet = true; + } + } + + private AutoUpdate _autoUpdate; + public AutoUpdateMaskedPrefs AutoUpdateSet { get; set; } public AutoUpdate AutoUpdate + { + get => _autoUpdate; + set + { + _autoUpdate = value; + AutoUpdateSet = new AutoUpdateMaskedPrefs { CheckSet = true, ApplySet = true }; // Good enough I think + } + } + + private AppConnector _appConnector; + public bool AppConnectorSet { get; set; } public AppConnector AppConnector + { + get => _appConnector; + set + { + _appConnector = value; + AppConnectorSet = true; + } + } + + private bool _postureChecking; + public bool PostureCheckingSet { get; set; } public bool PostureChecking + { + get => _postureChecking; + set + { + _postureChecking = value; + PostureCheckingSet = true; + } + } + + private string _netfilterKind; + public bool NetfilterKindSet { get; set; } public string NetfilterKind + { + get => _netfilterKind; + set + { + _netfilterKind = value; + NetfilterKindSet = true; + } + } + + private object _driveShares; + public bool DriveSharesSet { get; set; } public object DriveShares + { + get => _driveShares; + set + { + _driveShares = value; + DriveSharesSet = true; + } + } + + private object _config; + public bool ConfigSet { get; set; } public object Config + { + get => _config; + set + { + _config = value; + ConfigSet = true; + } + } + + // Unused in current Tailscale, see https://github.com/tailscale/tailscale/issues/12058 + public bool AllowSingleHosts { get; private set; } = true; + + // Internal debug mode. + private bool _egg; + public bool EggSet { get; set; } - public bool WantRunningSet + public bool Egg + { + get => _egg; + set + { + _egg = value; + EggSet = true; + } + } + + // Internal field, cannot be set by clients + public string InternalExitNodePrior + { + get; private set; + } + + public void OnDeserialized() + { + // Set all the Set properties to false, so we can signal our own changes + ControlURLSet = false; + RouteAllSet = false; + ExitNodeIDSet = false; + ExitNodeIPSet = false; + ExitNodeAllowLANAccessSet = false; + CorpDNSSet = false; + RunSSHSet = false; + RunWebClientSet = false; + WantRunningSet = false; + LoggedOutSet = false; + ShieldsUpSet = false; + AdvertiseTagsSet = false; + HostnameSet = false; + NotepadURLsSet = false; + ForceDaemonSet = false; + EggSet = false; + AdvertiseRoutesSet = false; + AdvertiseServicesSet = false; + NoSNATSet = false; + NoStatefulFilteringSet = false; + NetfilterModeSet = false; + OperatorUserSet = false; + ProfileNameSet = false; + AutoUpdateSet = null; + AppConnectorSet = false; + PostureCheckingSet = false; + NetfilterKindSet = false; + DriveSharesSet = false; + ConfigSet = false; + } + } + + + // Special treatment just for auto-update, for some reason. + public class AutoUpdateMaskedPrefs + { + public bool CheckSet { get; set; } - public bool ControlURLSet + public bool ApplySet { get; set; } @@ -529,6 +943,9 @@ public static List ParseWarningsFromJson(string json) { try { + if (root["Warnings"] == null) { + return warningsList; + } var warnings = root["Warnings"].AsObject(); // Iterate through each warning entry (ignoring the key) @@ -555,5 +972,4 @@ public static List ParseWarningsFromJson(string json) return warningsList; } } -} - +} \ No newline at end of file diff --git a/Core/Utils.cs b/Core/Utils.cs index c690a8c..f21b182 100644 --- a/Core/Utils.cs +++ b/Core/Utils.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; @@ -47,4 +49,20 @@ public static void InitializeColors() colors.Add("SeverityLow", GetSeverityColor("low")); colors.Add("SeverityUnknown", GetSeverityColor("unknown")); } + + public static string GetAppVersion() + { + // Option A: Use the AssemblyInformationalVersionAttribute + var attr = Assembly.GetExecutingAssembly() + .GetCustomAttribute(); + if (attr is not null) + { + return attr.InformationalVersion; + } + + // Fallback: File version (e.g. Major.Minor.Build.Revision) + var fileVersion = FileVersionInfo.GetVersionInfo( + Assembly.GetExecutingAssembly().Location).FileVersion; + return fileVersion ?? "Unknown"; + } } diff --git a/Package.appxmanifest b/Package.appxmanifest deleted file mode 100644 index 084a4fa..0000000 --- a/Package.appxmanifest +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - Tailscale Client - xirreal - Assets\Icons\AppIcon.png - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TailscaleClient.csproj b/TailscaleClient.csproj index 2f0bff4..6ac64f4 100644 --- a/TailscaleClient.csproj +++ b/TailscaleClient.csproj @@ -1,298 +1,58 @@  - - WinExe - net8.0-windows10.0.22000.0 - 10.0.19041.0 - TailscaleClient - app.manifest - x64;ARM64 - anycpu - win-x64;win-arm64 - true - true - true - False - True - SHA256 - True - False - True - Always - 0 - True - Tailscale Client - AppIconBase-Dark.png - true - TailscaleClient.Program - 10.0.19041.0 - 10.0.22000.38 - x64|arm64 - True - true - 9C3E8CCAC08122B756BC98E5559841BE87816AAA - http://timestamp.digicert.com - https://tsc.xirreal.dev - en-us - C:\Users\xirreal\Workspace\TailscaleClient\Dist - Assets\Icons\AppIconBase-Dark.ico - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - - True - \ - - - - - - $(DefaultXamlRuntime) - - - MSBuild:Compile - - - MSBuild:Compile - - - - - $(DefaultXamlRuntime) - Designer - - - $(DefaultXamlRuntime) - Designer - - - $(DefaultXamlRuntime) - Designer - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - - - MSBuild:Compile - - - MSBuild:Compile - - + + WinExe + net8.0-windows10.0.22000.0 + x64;arm64 + true + false + None + true - - - true - - - portable - - - portable - - - portable - - - portable - + TailscaleClient + TailscaleClient.Program + TailscaleClient + Assets\Icons\AppIconBase-Dark.ico + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TailscaleClient.sln b/TailscaleClient.sln deleted file mode 100644 index 11ba599..0000000 --- a/TailscaleClient.sln +++ /dev/null @@ -1,57 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34511.84 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TailscaleClient", "TailscaleClient.csproj", "{A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TailscaleClientInstaller", "TailscaleClientInstaller\TailscaleClientInstaller.vcxproj", "{CC541689-FD1C-400D-BAED-E3826FC0803D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM64 = Debug|ARM64 - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|ARM64.Build.0 = Debug|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x64.ActiveCfg = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x64.Build.0 = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x64.Deploy.0 = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x86.ActiveCfg = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x86.Build.0 = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Debug|x86.Deploy.0 = Debug|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|ARM64.ActiveCfg = Release|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|ARM64.Build.0 = Release|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|ARM64.Deploy.0 = Release|ARM64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x64.ActiveCfg = Release|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x64.Build.0 = Release|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x64.Deploy.0 = Release|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x86.ActiveCfg = Release|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x86.Build.0 = Release|x64 - {A2D20BB9-EDD6-42F1-84C3-BE32E97B40D3}.Release|x86.Deploy.0 = Release|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|ARM64.ActiveCfg = Debug|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|ARM64.Build.0 = Debug|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|x64.ActiveCfg = Debug|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|x64.Build.0 = Debug|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|x86.ActiveCfg = Debug|Win32 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Debug|x86.Build.0 = Debug|Win32 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|ARM64.ActiveCfg = Release|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|ARM64.Build.0 = Release|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|x64.ActiveCfg = Release|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|x64.Build.0 = Release|x64 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|x86.ActiveCfg = Release|Win32 - {CC541689-FD1C-400D-BAED-E3826FC0803D}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {06DA7CF8-503D-4E59-A0E2-CB61CB9D3A3C} - EndGlobalSection -EndGlobal diff --git a/TailscaleClientInstaller/TailscaleClientInstaller.vcxproj b/TailscaleClientInstaller/TailscaleClientInstaller.vcxproj deleted file mode 100644 index 06b8576..0000000 --- a/TailscaleClientInstaller/TailscaleClientInstaller.vcxproj +++ /dev/null @@ -1,139 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 17.0 - Win32Proj - {cc541689-fd1c-400d-baed-e3826fc0803d} - TailscaleClientInstaller - 10.0 - - - - Application - true - v143 - Unicode - - - Application - false - v143 - true - Unicode - - - Application - true - v143 - Unicode - - - Application - false - v143 - true - Unicode - - - - - - - - - - - - - - - - - - - - - - Level3 - true - WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - - - Windows - true - RequireAdministrator - - - - - Level3 - true - true - true - WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - - - Windows - true - true - true - RequireAdministrator - - - - - Level3 - true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - - - Windows - true - RequireAdministrator - - - - - Level3 - true - true - true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - - - Windows - true - true - true - RequireAdministrator - - - - - - - - - \ No newline at end of file diff --git a/TailscaleClientInstaller/main.cpp b/TailscaleClientInstaller/main.cpp deleted file mode 100644 index ba92a26..0000000 --- a/TailscaleClientInstaller/main.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#pragma comment(lib, "user32.lib") -#pragma comment(lib, "Shcore.lib") -#pragma comment(lib, "Shlwapi.lib") -#pragma comment(lib, "urlmon.lib") -#pragma comment(lib, "crypt32.lib") - -constexpr LPCWSTR remoteUrl = L"https://tsc.xirreal.dev/"; - -static void ShowMessage(const std::wstring& message, UINT type) { - SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); - ShellMessageBoxW(NULL, NULL, message.c_str(), L"Tailscale Client Installer", - MB_OK | type); -} - -static std::wstring GetErrorMessage(DWORD errorCode) { - LPVOID messageBuffer = nullptr; - DWORD formatFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS; - DWORD languageId = 0; - std::wstring errorMessage; - - if (FormatMessageW(formatFlags, NULL, errorCode, languageId, - (LPWSTR)&messageBuffer, 0, NULL)) { - errorMessage = (LPWSTR)messageBuffer; - LocalFree(messageBuffer); - } else { - errorMessage = L"Unknown error."; - } - return errorMessage; -} - -static bool DownloadFile(LPCWSTR resource, const std::wstring& path) { - HRESULT hr = URLDownloadToFile(NULL, resource, path.c_str(), 0, NULL); - if (FAILED(hr)) { - std::wcerr << L"[x] Error downloading file: " << GetErrorMessage(hr) - << std::endl; - ShowMessage( - L"Download failed.\n\nCheck your internet connection or try the " - L"offline installer instead.\n", - MB_ICONERROR); - return false; - } - return true; -} - -static bool CreateTempFolder(std::wstring& tempFolder) { - wchar_t tempPath[MAX_PATH]; - GetTempPath(MAX_PATH, tempPath); - - tempFolder = tempPath; - tempFolder += L"TailscaleClientInstaller"; - - DWORD attributes = GetFileAttributes(tempFolder.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES) { - std::wcout << L"[!] Temporary folder already exists. Cleaning up..." - << std::endl; - // Create double null-terminated string - wchar_t* tempFolderDoubleNull = new wchar_t[tempFolder.size() + 2]; - std::copy(tempFolder.begin(), tempFolder.end(), tempFolderDoubleNull); - tempFolderDoubleNull[tempFolder.size()] = L'\0'; - tempFolderDoubleNull[tempFolder.size() + 1] = L'\0'; - // Delete recursively - SHFILEOPSTRUCT fileOp = {0}; - fileOp.wFunc = FO_DELETE; - fileOp.pFrom = tempFolderDoubleNull; - fileOp.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT; - if (SHFileOperation(&fileOp)) { - std::wcerr << L"[x] Failed to delete temporary folder." << std::endl; - } - delete[] tempFolderDoubleNull; - } - return CreateDirectory(tempFolder.c_str(), NULL); -} - -static bool InstallCertificate(HCERTSTORE certificateStore, - PCCERT_CONTEXT certificateContext) { - if (!CertAddCertificateContextToStore(certificateStore, certificateContext, - CERT_STORE_ADD_REPLACE_EXISTING, - NULL)) { - std::cerr << "[x] Failed to add the certificate to the store." << std::endl; - ShowMessage( - L"Failed to install the certificate.\n\nPlease report this issue to " - L"the developer.\n", - MB_ICONERROR); - return false; - } - std::cout << "[-] Certificate successfully installed." << std::endl; - return true; -} - -static bool SetupCertificate(HCERTSTORE& certificateStore, - PCCERT_CONTEXT& certificateContext, - const std::wstring& certPath) { - certificateStore = - CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, NULL, - CERT_SYSTEM_STORE_LOCAL_MACHINE, L"TrustedPeople"); - if (!certificateStore) { - std::cerr << "[x] Failed to open Trusted People store." << std::endl; - ShowMessage( - L"This installer can only run as administrator.\n\nPlease restart the " - L"installer using administrator permissions.", - MB_ICONWARNING); - return false; - } - - // Attempt to load the certificate from file - HANDLE hFile = CreateFile(certPath.c_str(), GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - ShowMessage(L"Certificate file missing or inaccessible.", MB_ICONERROR); - return false; - } - - DWORD fileSize = GetFileSize(hFile, NULL); - BYTE* fileBuffer = new BYTE[fileSize]; - DWORD bytesRead = 0; - - if (!ReadFile(hFile, fileBuffer, fileSize, &bytesRead, NULL)) { - ShowMessage(L"Failed to read certificate file.", MB_ICONERROR); - delete[] fileBuffer; - CloseHandle(hFile); - return false; - } - certificateContext = CertCreateCertificateContext( - X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, fileBuffer, fileSize); - delete[] fileBuffer; - CloseHandle(hFile); - - if (!certificateContext) { - ShowMessage(L"Failed to parse the certificate.\n\nPlease contact support.", - MB_ICONERROR); - return false; - } - - if (!InstallCertificate(certificateStore, certificateContext)) { - CertFreeCertificateContext(certificateContext); - CertCloseStore(certificateStore, CERT_CLOSE_STORE_FORCE_FLAG); - return false; - } - - return true; -} - -int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE previousInstance, - LPWSTR args, int shouldShowCmd) { -#ifdef _DEBUG - AllocConsole(); - AttachConsole(GetCurrentProcessId()); - FILE *pConsoleOut, *pConsoleErr; - freopen_s(&pConsoleOut, "CONOUT$", "w", stdout); - freopen_s(&pConsoleErr, "CONOUT$", "w", stderr); -#endif - - std::cout << ">> Tailscale Client Installer - (c) 2024 xirreal\n" - << std::endl; - - std::wstring tempFolder; - if (!CreateTempFolder(tempFolder)) { - DWORD error = GetLastError(); - std::wcerr << L"[x] Failed to create temporary folder. " - << GetErrorMessage(error) << std::endl; -#ifdef _DEBUG - system("pause"); -#endif - - return -1; - } - - std::wcout << "[-] Temporary folder created: " << tempFolder << std::endl; - - std::wstring certPath = tempFolder + L"\\TailscaleClient.cer"; - std::wstring certUrl = remoteUrl; - certUrl += L"TailscaleClient.cer"; - if (!DownloadFile(certUrl.c_str(), certPath)) { -#ifdef _DEBUG - system("pause"); -#endif - return -1; - } - - HCERTSTORE certificateStore = NULL; - PCCERT_CONTEXT certificateContext = NULL; - if (!SetupCertificate(certificateStore, certificateContext, certPath)) { -#ifdef _DEBUG - system("pause"); -#endif - return -1; - } - - std::wstring appinstallerPath = - tempFolder + L"\\TailscaleClient.appinstaller"; - std::wstring appinstallerUrl = remoteUrl; - appinstallerUrl += L"TailscaleClient.appinstaller"; - if (!DownloadFile(appinstallerUrl.c_str(), appinstallerPath)) { -#ifdef _DEBUG - system("pause"); -#endif - return -1; - } - - HINSTANCE result = ShellExecute(NULL, L"open", appinstallerPath.c_str(), NULL, - NULL, SW_SHOWNORMAL); - if ((INT_PTR)result <= 32) { - DWORD error = GetLastError(); - std::cerr << "[x] Failed to open protocol URL." << std::endl; - std::wcerr << GetErrorMessage(error) << std::endl; - ShowMessage(L"Something went wrong during installation.\n\n" + - GetErrorMessage(error) + L"\n", - MB_ICONERROR); - } else { - std::cout << "[-] Successfully opened protocol URL." << std::endl; - } - - // Cleanup - CertFreeCertificateContext(certificateContext); - CertCloseStore(certificateStore, CERT_CLOSE_STORE_FORCE_FLAG); - DeleteFile(certPath.c_str()); - // DeleteFile(appinstallerPath.c_str()); // We cant actually delete this - // because windows requires it to install the app... for some reason - // RemoveDirectory(tempFolder.c_str()); - -#ifdef _DEBUG - system("pause"); - FreeConsole(); -#endif - - return 0; -} diff --git a/Views/Accounts.xaml b/Views/Accounts.xaml index bb51c46..6945b99 100644 --- a/Views/Accounts.xaml +++ b/Views/Accounts.xaml @@ -8,7 +8,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Views/AppSkeleton.xaml b/Views/AppSkeleton.xaml index 7ec12e3..e064c6c 100644 --- a/Views/AppSkeleton.xaml +++ b/Views/AppSkeleton.xaml @@ -40,18 +40,17 @@ PaneDisplayMode="Top" IsBackButtonVisible="Collapsed" SelectionChanged="OnNavigate" + Background="{StaticResource SystemControlTransparentBrush}" > - - diff --git a/Views/Home.xaml b/Views/Home.xaml index 00ea515..3e1bb8d 100644 --- a/Views/Home.xaml +++ b/Views/Home.xaml @@ -8,7 +8,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Views/Home.xaml.cs b/Views/Home.xaml.cs index 99df39f..a3599a7 100644 --- a/Views/Home.xaml.cs +++ b/Views/Home.xaml.cs @@ -15,7 +15,7 @@ public sealed partial class Home : Page, INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName] string propertyName = null) { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private string _username = "Guest"; @@ -66,15 +66,8 @@ private void UpdateUI() var pfp = _profile.UserProfile.ProfilePicURL; // the spacing really bothered me. - if (string.IsNullOrEmpty(UserName)) - { - UserNameBlock.Margin = new(-4, 5, 12, 0); - } - else - { - UserNameBlock.Margin = new(4, 5, 12, 0); - } - + UserNameBlock.Margin = new(string.IsNullOrEmpty(UserName) ? -4 : 4, 5, 12, 0); + if (Uri.TryCreate(pfp, UriKind.Absolute, out _)) { ProfilePicture = pfp; diff --git a/Views/Settings.xaml b/Views/Settings.xaml index b1e9095..73e1f70 100644 --- a/Views/Settings.xaml +++ b/Views/Settings.xaml @@ -3,26 +3,99 @@ x:Class="TailscaleClient.Views.Settings" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:TailscaleClient.Views" + xmlns:local="using:TailscaleClient.Assets" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - + + + diff --git a/Views/Settings.xaml.cs b/Views/Settings.xaml.cs index 76933a2..5d58894 100644 --- a/Views/Settings.xaml.cs +++ b/Views/Settings.xaml.cs @@ -1,28 +1,174 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Windows.ApplicationModel; +using TailscaleClient.Assets; +using TailscaleClient.Core; namespace TailscaleClient.Views; - -public sealed partial class Settings : Page +public sealed partial class Settings : Page, INotifyPropertyChanged { - private readonly string Version; + private Types.MaskedPrefs _prefs = null; + + public bool IncomingTrafficEnabled = false; + public bool TailscaleDnsEnabled = false; + public bool SubnetRoutesEnabled = false; + public string ExitNode = ""; + public string VersionText = Utils.GetAppVersion(); + + public event PropertyChangedEventHandler PropertyChanged; + public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private int _sourceIndex; + public int CurrentNode + { + get => _sourceIndex; + set + { + _sourceIndex = value; + NotifyPropertyChanged(); + } + } + + private List _peerIds = []; + private string _recommendedNode = ""; + + public void UpdateExitNodeList(Types.SuggestedExitNode suggestedPeer, List peers) + { + ExitNodeList.Items.Clear(); + + ExitNodeList.Items.Add(new ComboBoxItem + { + Content = "None", + Tag = "" + }); + + foreach (var device in peers) + { + var dnsName = device.DNSName; + var nickname = dnsName.Split(".")[0]; + if (nickname == "") + { + nickname = device.HostName; + } + + var item = new ComboBoxItem + { + Content = $"{(device.ID == suggestedPeer.ID ? "Recommended: " : "")}{(nickname == device.HostName ? nickname : $"{nickname} ({device.HostName})")}", + Tag = device.ID + }; + + ExitNodeList.Items.Add(item); + } + + CurrentNode = ExitNodeList.Items.Cast().ToList().FindIndex(x => x.Tag as string == ExitNode); + } - public static string GetAppVersion() + public void UpdateUI() { + _prefs = API.GetPrefs(); + IncomingTrafficEnabled = !_prefs.ShieldsUp; + TailscaleDnsEnabled = _prefs.CorpDNS; + SubnetRoutesEnabled = _prefs.RouteAll; + ExitNode = _prefs.ExitNodeID; + + var suggestedNode = new Types.SuggestedExitNode + { + ID = "UNAVAILABLE" + }; + try { - var version = Package.Current.Id.Version; - return string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); + suggestedNode = API.GetSuggestedExitNode(); + } catch (System.Exception e) + { + Debug.WriteLine(e); } - catch + + var status = API.GetStatus(); + var peers = (status.Peer ?? []).Select(x => x.Value).Where(x => x.ExitNodeOption).ToList(); + peers.Sort((x, y) => x.ID == suggestedNode.ID ? -1 : y.ID == suggestedNode.ID ? 1 : 0); + + var peerIds = peers.Select(x => x.ID).ToList(); + if(!peerIds.All(x => _peerIds.Contains(x)) || (suggestedNode.ID != "UNAVAILABLE" && suggestedNode.ID != _recommendedNode)) { - return "Unpackaged build"; + _peerIds = peerIds; + _recommendedNode = suggestedNode.ID; + UpdateExitNodeList(suggestedNode, peers); } } public Settings() { - this.InitializeComponent(); - Version = GetAppVersion(); + InitializeComponent(); + + Messaging.Instance.MessageReceived += (sender, e) => + { + if (e.Kind == Messaging.MessageKind.IPNBusUpdate && e.Key == "Prefs") + { + DispatcherQueue.TryEnqueue(() => { UpdateUI(); }); + } + else if (e.Kind == Messaging.MessageKind.ProfileSwitch) + { + DispatcherQueue.TryEnqueue(() => { UpdateUI(); }); + } + }; + + UpdateUI(); + } + + private void UpdatePrefsShields(object _sender, RoutedEventArgs e) + { + var sender = _sender as ToggleSwitch; + if(_prefs.ShieldsUp == !sender.IsOn) + { + return; + } + + _prefs.ShieldsUp = !sender.IsOn; + + API.UpdatePrefs(_prefs); + } + + private void UpdatePrefsDns(object _sender, RoutedEventArgs e) + { + var sender = _sender as ToggleSwitch; + if (_prefs.CorpDNS == sender.IsOn) + { + return; + } + _prefs.CorpDNS = sender.IsOn; + API.UpdatePrefs(_prefs); + } + + private void UpdatePrefsRoutes(object _sender, RoutedEventArgs e) + { + var sender = _sender as ToggleSwitch; + if (_prefs.RouteAll == sender.IsOn) + { + return; + } + _prefs.RouteAll = sender.IsOn; + API.UpdatePrefs(_prefs); + } + + private void ExitNodeChanged(object sender, SelectionChangedEventArgs e) + { + var selection = (sender as ComboBox).SelectedItem as ComboBoxItem; + + if (selection == null || (selection?.Tag as string) == ExitNode) + { + return; + } + + _prefs.ExitNodeID = selection!.Tag as string; + API.UpdatePrefs(_prefs); } } diff --git a/Views/TrayMenu.xaml.cs b/Views/TrayMenu.xaml.cs index 6c7074e..d205dbd 100644 --- a/Views/TrayMenu.xaml.cs +++ b/Views/TrayMenu.xaml.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.UI.Xaml.Controls; @@ -5,12 +7,26 @@ namespace TailscaleClient.Views; -[ObservableObject] -public sealed partial class TrayMenu : UserControl +public sealed partial class TrayMenu : UserControl, INotifyPropertyChanged { - [ObservableProperty] + public event PropertyChangedEventHandler PropertyChanged; + public void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + private string _appDisplayName = Constants.AppDisplayName; + public string AppDisplayName + { + get => _appDisplayName; + set + { + _appDisplayName = value; + OnPropertyChanged(); + } + } + public TrayMenu() { InitializeComponent(); diff --git a/app.manifest b/app.manifest deleted file mode 100644 index ea9da52..0000000 --- a/app.manifest +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - PerMonitorV2 - - - \ No newline at end of file diff --git a/package.ps1 b/package.ps1 deleted file mode 100644 index ccd746a..0000000 --- a/package.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -$projectFile = "TailscaleClient.csproj" -$outputDir = "./Dist" -$thumbprint = "9C3E8CCAC08122B756BC98E5559841BE87816AAA" -$baseUri = "https://tsc.xirreal.dev" - -if (Test-Path $outputDir) { - Remove-Item -Path $outputDir -Recurse -Force -} - -Write-Output "Building MSIX packages..." -dotnet restore $projectFile -dotnet msbuild $projectFile -t:Rebuild -p:Platform=x64 -p:Configuration=Release -p:OutDir="$outputDir/x64/" -dotnet msbuild $projectFile -t:Rebuild -p:Platform=arm64 -p:Configuration=Release -p:OutDir="$outputDir/arm64/" - -$appxManifest = Join-Path -Path $outputDir -ChildPath "x64/AppxManifest.xml" -[xml]$manifestXml = Get-Content $appxManifest -$identityNode = $manifestXml.Package.Identity -$publisher = $identityNode.Publisher -$version = $identityNode.Version -$appName = $identityNode.Name - -makeappx.exe pack /h SHA256 /d "./Dist/x64" /o /p "./Dist/msix/TailscaleClient_x64.msix" -makeappx.exe pack /h SHA256 /d "./Dist/arm64" /o /p "./Dist/msix/TailscaleClient_arm64.msix" -makeappx.exe bundle /bv $version /d "./Dist/msix" /p "./Dist/TailscaleClient.msixbundle" -signtool.exe sign /fd SHA256 /sha1 "$thumbprint" /t http://timestamp.digicert.com "./Dist/TailscaleClient.msixbundle" - -msbuild /t:TailscaleClientInstaller /p:Configuration=Release /p:OutDir="../Dist/" -Remove-Item -Path "$outputDir/TailscaleClientInstaller.pdb" -Force - -$cert = Get-ChildItem -Path Cert:/CurrentUser/My | Where-Object { $_.Thumbprint -eq $thumbprint } -$certPath = "$outputDir/TailscaleClient.cer" -Export-Certificate -Cert $cert -FilePath $certPath - -Write-Output "Creating .appinstaller file..." -$appInstallerFile = "$outputDir/TailscaleClient.appinstaller" -$appInstallerXml = @" - - - - - - - -"@ -$appInstallerXml | Out-File -FilePath $appInstallerFile -Encoding utf8 -Write-Output "AppInstaller file created at $appInstallerFile" - -Remove-Item -Path "$outputDir/x64" -Recurse -Force -Remove-Item -Path "$outputDir/arm64" -Recurse -Force -Remove-Item -Path "$outputDir/msix" -Recurse -Force - -Write-Output "Process completed successfully." diff --git a/version.json b/version.json new file mode 100644 index 0000000..ecb1945 --- /dev/null +++ b/version.json @@ -0,0 +1,3 @@ +{ + "version": "1.4" +} \ No newline at end of file