seb-win-refactoring/SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs
2024-06-10 19:39:58 +02:00

381 lines
10 KiB
C#

/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using SafeExamBrowser.SystemComponents.Contracts.Network.Events;
using SafeExamBrowser.WindowsApi.Contracts;
using Windows.Devices.Enumeration;
using Windows.Devices.WiFi;
using Windows.Foundation;
using Windows.Networking.Connectivity;
using Windows.Security.Credentials;
using Timer = System.Timers.Timer;
namespace SafeExamBrowser.SystemComponents.Network
{
public class NetworkAdapter : INetworkAdapter
{
private readonly object @lock = new object();
private readonly ConcurrentDictionary<string, object> attempts;
private readonly ILogger logger;
private readonly INativeMethods nativeMethods;
private readonly List<WirelessNetwork> wirelessNetworks;
private WiFiAdapter adapter;
private Timer timer;
private bool HasWirelessAdapter => adapter != default;
public ConnectionStatus Status { get; private set; }
public ConnectionType Type { get; private set; }
public event ChangedEventHandler Changed;
public event CredentialsRequiredEventHandler CredentialsRequired;
public NetworkAdapter(ILogger logger, INativeMethods nativeMethods)
{
this.attempts = new ConcurrentDictionary<string, object>();
this.logger = logger;
this.nativeMethods = nativeMethods;
this.wirelessNetworks = new List<WirelessNetwork>();
}
public void ConnectToWirelessNetwork(string name)
{
var isFirstAttempt = !attempts.TryGetValue(name, out _);
var network = default(WiFiAvailableNetwork);
lock (@lock)
{
network = wirelessNetworks.FirstOrDefault(n => n.Name == name)?.Network;
}
if (network != default)
{
if (isFirstAttempt || network.IsOpen())
{
ConnectAutomatically(network);
}
else
{
ConnectWithAuthentication(network);
}
Changed?.Invoke();
}
else
{
logger.Warn($"Could not find wireless network '{name}'!");
}
}
public IEnumerable<IWirelessNetwork> GetWirelessNetworks()
{
lock (@lock)
{
return new List<WirelessNetwork>(wirelessNetworks);
}
}
public void Initialize()
{
const int FIVE_SECONDS = 5000;
timer = new Timer(FIVE_SECONDS);
timer.Elapsed += (o, args) => Update();
timer.AutoReset = true;
InitializeAdapter();
NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
NetworkInformation.NetworkStatusChanged += NetworkInformation_NetworkStatusChanged;
Update();
logger.Info("Started monitoring the network adapter.");
}
public void StartWirelessNetworkScanning()
{
timer?.Start();
if (HasWirelessAdapter)
{
_ = adapter.ScanAsync();
}
}
public void StopWirelessNetworkScanning()
{
timer?.Stop();
}
public void Terminate()
{
NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged -= NetworkChange_NetworkAvailabilityChanged;
NetworkInformation.NetworkStatusChanged -= NetworkInformation_NetworkStatusChanged;
if (HasWirelessAdapter)
{
adapter.AvailableNetworksChanged -= Adapter_AvailableNetworksChanged;
}
if (timer != default)
{
timer.Stop();
}
logger.Info("Stopped monitoring the network adapter.");
}
private void Adapter_AvailableNetworksChanged(WiFiAdapter sender, object args)
{
Update(false);
}
private void Adapter_ConnectCompleted(WiFiAvailableNetwork network, IAsyncOperation<WiFiConnectionResult> operation, AsyncStatus status)
{
var connectionStatus = default(WiFiConnectionStatus?);
if (status == AsyncStatus.Completed)
{
connectionStatus = operation.GetResults()?.ConnectionStatus;
}
else
{
logger.Error($"Failed to complete connection operation! Status: {status}.");
}
if (connectionStatus == WiFiConnectionStatus.Success)
{
attempts.TryRemove(network.Ssid, out _);
logger.Info($"Successfully connected to wireless network {network.ToLogString()}.");
}
else if (connectionStatus == WiFiConnectionStatus.InvalidCredential)
{
attempts.TryAdd(network.Ssid, default);
logger.Info($"Failed to connect to wireless network {network.ToLogString()} due to invalid credentials. Retrying...");
Task.Run(() => ConnectToWirelessNetwork(network.Ssid));
}
else
{
Status = ConnectionStatus.Disconnected;
logger.Error($"Failed to connect to wireless network {network.ToLogString()}! Reason: {connectionStatus}.");
}
Update();
}
private void ConnectAutomatically(WiFiAvailableNetwork network)
{
logger.Info($"Attempting to automatically connect to {(network.IsOpen() ? "open" : "protected")} wireless network {network.ToLogString()}...");
adapter.ConnectAsync(network, WiFiReconnectionKind.Automatic).Completed = (o, s) => Adapter_ConnectCompleted(network, o, s);
Status = ConnectionStatus.Connecting;
}
private void ConnectWithAuthentication(WiFiAvailableNetwork network)
{
if (TryGetCredentials(network.Ssid, out var credentials))
{
logger.Info($"Attempting to connect to protected wirless network {network.ToLogString()}...");
adapter.ConnectAsync(network, WiFiReconnectionKind.Automatic, credentials).Completed = (o, s) => Adapter_ConnectCompleted(network, o, s);
Status = ConnectionStatus.Connecting;
}
else
{
Status = ConnectionStatus.Disconnected;
Update();
}
}
private void InitializeAdapter()
{
try
{
// Requesting access is required as of fall 2024 and must be granted manually by the user, otherwise all wireless functionality will
// be denied by the system (see also https://learn.microsoft.com/en-us/windows/win32/nativewifi/wi-fi-access-location-changes).
var task = WiFiAdapter.RequestAccessAsync().AsTask();
var status = task.GetAwaiter().GetResult();
if (status == WiFiAccessStatus.Allowed)
{
var findAll = DeviceInformation.FindAllAsync(WiFiAdapter.GetDeviceSelector()).AsTask();
var devices = findAll.GetAwaiter().GetResult();
if (devices.Any())
{
var id = devices.First().Id;
var getById = WiFiAdapter.FromIdAsync(id).AsTask();
logger.Debug($"Found {devices.Count()} wireless network adapter(s).");
adapter = getById.GetAwaiter().GetResult();
adapter.AvailableNetworksChanged += Adapter_AvailableNetworksChanged;
logger.Debug($"Successfully initialized wireless network adapter '{id}'.");
}
else
{
logger.Info("Could not find a wireless network adapter.");
}
}
else
{
logger.Error($"Access to the wireless network adapter has been denied ({status})!");
}
}
catch (Exception e)
{
logger.Error("Failed to initialize wireless network adapter!", e);
}
}
private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
logger.Debug("Network address changed.");
Update();
}
private void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
logger.Debug($"Network availability changed ({(e.IsAvailable ? "available" : "unavailable")}).");
Update();
}
private void NetworkInformation_NetworkStatusChanged(object sender)
{
logger.Debug("Network status changed.");
Update();
}
private bool TryGetCredentials(string network, out PasswordCredential credentials)
{
var args = new CredentialsRequiredEventArgs { NetworkName = network };
credentials = new PasswordCredential();
CredentialsRequired?.Invoke(args);
if (args.Success)
{
if (!string.IsNullOrEmpty(args.Password))
{
credentials.Password = args.Password;
}
if (!string.IsNullOrEmpty(args.Username))
{
credentials.UserName = args.Username;
}
}
return args.Success;
}
private bool TryGetCurrentWirelessNetwork(out string name)
{
name = default;
if (HasWirelessAdapter)
{
try
{
var getProfile = adapter.NetworkAdapter.GetConnectedProfileAsync().AsTask();
var profile = getProfile.GetAwaiter().GetResult();
if (profile?.IsWlanConnectionProfile == true)
{
name = profile.WlanConnectionProfileDetails.GetConnectedSsid();
}
}
catch
{
}
}
return name != default;
}
private void Update(bool rescan = true)
{
try
{
var currentNetwork = default(WirelessNetwork);
var hasConnection = nativeMethods.HasInternetConnection();
var isConnecting = Status == ConnectionStatus.Connecting;
var networks = new List<WirelessNetwork>();
var previousStatus = Status;
if (HasWirelessAdapter)
{
hasConnection &= TryGetCurrentWirelessNetwork(out var current);
foreach (var network in adapter.NetworkReport.AvailableNetworks.FilterAndOrder())
{
var wirelessNetwork = network.ToWirelessNetwork();
if (network.Ssid == current)
{
currentNetwork = wirelessNetwork;
wirelessNetwork.Status = ConnectionStatus.Connected;
}
networks.Add(wirelessNetwork);
}
if (rescan)
{
_ = adapter.ScanAsync();
}
}
lock (@lock)
{
wirelessNetworks.Clear();
wirelessNetworks.AddRange(networks);
}
Type = HasWirelessAdapter ? ConnectionType.Wireless : (hasConnection ? ConnectionType.Wired : ConnectionType.Undefined);
Status = hasConnection ? ConnectionStatus.Connected : (isConnecting ? ConnectionStatus.Connecting : ConnectionStatus.Disconnected);
LogNetworkChanges(previousStatus, currentNetwork);
}
catch (Exception e)
{
logger.Error("Failed to update network adapter!", e);
}
Changed?.Invoke();
}
private void LogNetworkChanges(ConnectionStatus previousStatus, WirelessNetwork currentNetwork = default)
{
if (previousStatus != ConnectionStatus.Connected && Status == ConnectionStatus.Connected)
{
logger.Info($"Connection established ({Type}{(currentNetwork != default ? $", {currentNetwork.Name}" : "")}).");
}
if (previousStatus != ConnectionStatus.Disconnected && Status == ConnectionStatus.Disconnected)
{
logger.Info("Connection lost.");
}
}
}
}