Remotely/Agent/Services/AgentHubConnection.cs
Jared Goodwin 6ddc20b322
Add UseHttpLogging option in appsettings. (#562)
* Add UseHttpLogging option.

* Default to false for UseHttpLogging.

* Fix log downloads.
2023-01-14 15:55:16 -08:00

538 lines
20 KiB
C#

using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Remotely.Agent.Extensions;
using Remotely.Agent.Interfaces;
using Remotely.Shared.Enums;
using Remotely.Shared.Models;
using Remotely.Shared.Services;
using Remotely.Shared.Utilities;
using Remotely.Shared.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace Remotely.Agent.Services
{
public interface IAgentHubConnection
{
bool IsConnected { get; }
Task Connect();
Task SendHeartbeat();
}
public class AgentHubConnection : IAgentHubConnection, IDisposable
{
private readonly IAppLauncher _appLauncher;
private readonly ChatClientService _chatService;
private readonly ConfigService _configService;
private readonly IDeviceInformationService _deviceInfoService;
private readonly IHttpClientFactory _httpFactory;
private readonly ILogger<AgentHubConnection> _logger;
private readonly ILogger _fileLogger;
private readonly ScriptExecutor _scriptExecutor;
private readonly Uninstaller _uninstaller;
private readonly IUpdater _updater;
private ConnectionInfo _connectionInfo;
private HubConnection _hubConnection;
private Timer _heartbeatTimer;
private bool _isServerVerified;
public AgentHubConnection(ConfigService configService,
Uninstaller uninstaller,
ScriptExecutor scriptExecutor,
ChatClientService chatService,
IAppLauncher appLauncher,
IUpdater updater,
IDeviceInformationService deviceInfoService,
IHttpClientFactory httpFactory,
IEnumerable<ILoggerProvider> loggerProviders,
ILogger<AgentHubConnection> logger)
{
_configService = configService;
_uninstaller = uninstaller;
_scriptExecutor = scriptExecutor;
_appLauncher = appLauncher;
_chatService = chatService;
_updater = updater;
_deviceInfoService = deviceInfoService;
_httpFactory = httpFactory;
_logger = logger;
_fileLogger = loggerProviders
.OfType<FileLoggerProvider>()
.FirstOrDefault()
?.CreateLogger(nameof(AgentHubConnection));
}
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
public async Task Connect()
{
while (true)
{
try
{
_logger.LogInformation("Attempting to connect to server.");
_connectionInfo = _configService.GetConnectionInfo();
if (_hubConnection is not null)
{
await _hubConnection.DisposeAsync();
}
_hubConnection = new HubConnectionBuilder()
.WithUrl(_connectionInfo.Host + "/hubs/service")
.WithAutomaticReconnect(new RetryPolicy())
.AddMessagePackProtocol()
.Build();
RegisterMessageHandlers();
_hubConnection.Reconnected += HubConnection_Reconnected;
await _hubConnection.StartAsync();
_logger.LogInformation("Connected to server.");
var device = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, _connectionInfo.OrganizationID);
var result = await _hubConnection.InvokeAsync<bool>("DeviceCameOnline", device);
if (!result)
{
// Orgnanization ID wasn't found, or this device is already connected.
// The above can be caused by temporary issues on the server. So we'll do
// nothing here and wait for it to get resolved.
_logger.LogError("There was an issue registering with the server. The server might be undergoing maintenance, or the supplied organization ID might be incorrect.");
await Task.Delay(TimeSpan.FromMinutes(1));
continue;
}
if (!await VerifyServer())
{
await Task.Delay(TimeSpan.FromMinutes(1));
continue;
}
if (await CheckForServerMigration())
{
continue;
}
_heartbeatTimer?.Dispose();
_heartbeatTimer = new Timer(TimeSpan.FromMinutes(5).TotalMilliseconds);
_heartbeatTimer.Elapsed += HeartbeatTimer_Elapsed;
_heartbeatTimer.Start();
await _hubConnection.SendAsync("CheckForPendingSriptRuns");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while connecting to server.");
await Task.Delay(5_000);
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
_heartbeatTimer?.Dispose();
}
public async Task SendHeartbeat()
{
try
{
var currentInfo = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, _connectionInfo.OrganizationID);
await _hubConnection.SendAsync("DeviceHeartbeat", currentInfo);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error while sending heartbeat.");
}
}
private async Task<bool> CheckForServerMigration()
{
var serverUrl = await _hubConnection.InvokeAsync<string>("GetServerUrl");
if (Uri.TryCreate(serverUrl, UriKind.Absolute, out var serverUri) &&
Uri.TryCreate(_connectionInfo.Host, UriKind.Absolute, out var savedUri) &&
serverUri.Host != savedUri.Host)
{
_connectionInfo.Host = serverUrl.Trim().TrimEnd('/');
_connectionInfo.ServerVerificationToken = null;
_configService.SaveConnectionInfo(_connectionInfo);
await _hubConnection.DisposeAsync();
return true;
}
return false;
}
private async void HeartbeatTimer_Elapsed(object sender, ElapsedEventArgs e)
{
await SendHeartbeat();
}
private async Task HubConnection_Reconnected(string arg)
{
_logger.LogInformation("Reconnected to server.");
var device = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, _connectionInfo.OrganizationID);
if (!await _hubConnection.InvokeAsync<bool>("DeviceCameOnline", device))
{
await Connect();
return;
}
if (await CheckForServerMigration())
{
await Connect();
return;
}
await _updater.CheckForUpdates();
}
private void RegisterMessageHandlers()
{
_hubConnection.On("ChangeWindowsSession", async (string viewerConnectionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, int targetSessionID) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning("Session change attempted before server was verified.");
return;
}
await _appLauncher.RestartScreenCaster(new List<string>() { viewerConnectionId }, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection, targetSessionID);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while handling ChangeWindowsSession.");
}
});
_hubConnection.On("Chat", async (string senderName, string message, string orgName, string orgId, bool disconnected, string senderConnectionID) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning("Chat attempted before server was verified.");
return;
}
await _chatService.SendMessage(senderName, message, orgName, orgId, disconnected, senderConnectionID, _hubConnection);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while handling chat message.");
}
});
_hubConnection.On("CtrlAltDel", () =>
{
if (!_isServerVerified)
{
_logger.LogWarning("CtrlAltDel attempted before server was verified.");
return;
}
User32.SendSAS(false);
});
_hubConnection.On("DeleteLogs", () =>
{
if (_fileLogger is FileLogger logger)
{
logger.DeleteLogs();
}
});
_hubConnection.On("ExecuteCommand", ((ScriptingShell shell, string command, string authToken, string senderUsername, string senderConnectionID) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning(
"Command attempted before server was verified. Shell: {shell}. Command: {command}. Sender: {senderConnectionID}",
shell,
command,
senderConnectionID);
return;
}
_ = _scriptExecutor.RunCommandFromTerminal(shell,
command,
authToken,
senderUsername,
senderConnectionID,
ScriptInputType.Terminal,
TimeSpan.FromSeconds(30),
_hubConnection);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while executing command.");
}
}));
_hubConnection.On("ExecuteCommandFromApi", (
ScriptingShell shell,
string authToken,
string requestID,
string command,
string senderUsername) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning(
"Command attempted before server was verified. Shell: {shell}. Command: {command}. Sender: {senderUsername}",
shell,
command,
senderUsername);
return;
}
_ = _scriptExecutor.RunCommandFromApi(shell, requestID, command, senderUsername, authToken, _hubConnection);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while executing command from API.");
}
});
_hubConnection.On("GetLogs", async (string senderConnectionId) =>
{
if (_fileLogger is not FileLogger logger)
{
await _hubConnection.InvokeAsync("SendLogs", "Logger is not of expected type.", senderConnectionId).ConfigureAwait(false);
return;
}
var logBytes = await logger.ReadAllBytes();
if (!logBytes.Any())
{
var message = "There are no log entries written.";
await _hubConnection.InvokeAsync("SendLogs", message, senderConnectionId).ConfigureAwait(false);
return;
}
for (var i = 0; i < logBytes.Length; i += 50_000)
{
var chunk = Encoding.UTF8.GetString(logBytes.Skip(i).Take(50_000).ToArray());
await _hubConnection.InvokeAsync("SendLogs", chunk, senderConnectionId).ConfigureAwait(false);
}
});
_hubConnection.On("GetPowerShellCompletions", async (string inputText, int currentIndex, CompletionIntent intent, bool? forward, string senderConnectionId) =>
{
try
{
var session = PSCore.GetCurrent(senderConnectionId);
var completion = session.GetCompletions(inputText, currentIndex, forward);
var completionModel = completion.ToPwshCompletion();
await _hubConnection.InvokeAsync("ReturnPowerShellCompletions", completionModel, intent, senderConnectionId).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while getting PowerShell completions.");
}
});
_hubConnection.On("ReinstallAgent", async () =>
{
try
{
await _updater.InstallLatestVersion();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while reinstalling agent.");
}
});
_hubConnection.On("UninstallAgent", () =>
{
try
{
_uninstaller.UninstallAgent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while uninstalling agent.");
}
});
_hubConnection.On("RemoteControl", async (string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning("Remote control attempted before server was verified.");
return;
}
await _appLauncher.LaunchRemoteControl(-1, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while starting remote control.");
}
});
_hubConnection.On("RestartScreenCaster", async (List<string> viewerIDs, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning("Remote control attempted before server was verified.");
return;
}
await _appLauncher.RestartScreenCaster(viewerIDs, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while restarting screen caster.");
}
});
_hubConnection.On("RunScript", (Guid savedScriptId, int scriptRunId, string initiator, ScriptInputType scriptInputType, string authToken) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning(
"Script run attempted before server was verified. Script ID: {savedScriptId}. Initiator: {initiator}",
savedScriptId,
initiator);
return;
}
_ = _scriptExecutor.RunScript(savedScriptId, scriptRunId, initiator, scriptInputType, authToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while running script.");
}
});
_hubConnection.On("TransferFileFromBrowserToAgent", async (string transferID, List<string> fileIDs, string requesterID, string authToken) =>
{
try
{
if (!_isServerVerified)
{
_logger.LogWarning("File upload attempted before server was verified.");
return;
}
_logger.LogInformation("File upload started by {requesterID}.", requesterID);
var sharedFilePath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "RemotelySharedFiles")).FullName;
foreach (var fileID in fileIDs)
{
var url = $"{_connectionInfo.Host}/API/FileSharing/{fileID}";
using var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", authToken);
using var response = await client.GetAsync(url);
var filename = response.Content.Headers.ContentDisposition.FileName;
var legalChars = filename.ToCharArray().Where(x => !Path.GetInvalidFileNameChars().Any(y => x == y));
filename = new string(legalChars.ToArray());
using var rs = await response.Content.ReadAsStreamAsync();
using var fs = new FileStream(Path.Combine(sharedFilePath, filename), FileMode.Create);
rs.CopyTo(fs);
}
await _hubConnection.SendAsync("TransferCompleted", transferID, requesterID);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while transfering file from browser to agent.");
}
});
_hubConnection.On("TriggerHeartbeat", async () =>
{
await SendHeartbeat().ConfigureAwait(false);
});
}
private async Task<bool> VerifyServer()
{
if (string.IsNullOrWhiteSpace(_connectionInfo.ServerVerificationToken))
{
_isServerVerified = true;
_connectionInfo.ServerVerificationToken = Guid.NewGuid().ToString();
await _hubConnection.SendAsync("SetServerVerificationToken", _connectionInfo.ServerVerificationToken).ConfigureAwait(false);
_configService.SaveConnectionInfo(_connectionInfo);
}
else
{
var verificationToken = await _hubConnection.InvokeAsync<string>("GetServerVerificationToken");
if (verificationToken == _connectionInfo.ServerVerificationToken)
{
_isServerVerified = true;
}
else
{
_logger.LogWarning("Server sent an incorrect verification token. Token Sent: {verificationToken}.", verificationToken);
return false;
}
}
return true;
}
private class RetryPolicy : IRetryPolicy
{
public TimeSpan? NextRetryDelay(RetryContext retryContext)
{
if (retryContext.PreviousRetryCount == 0)
{
return TimeSpan.FromSeconds(3);
}
var waitSeconds = Math.Min(30, retryContext.PreviousRetryCount * 5);
return TimeSpan.FromSeconds(waitSeconds);
}
}
}
}