Added session recording. Added CapsLock key mapping.

This commit is contained in:
Jared Goodwin 2019-03-18 12:31:10 -07:00
parent ea8281e787
commit b658264229
18 changed files with 274 additions and 43 deletions

4
.gitignore vendored
View File

@ -265,4 +265,6 @@ __pycache__/
/Remotely_Server/wwwroot/Downloads/*.appimage
/Remotely_Server/wwwroot/Downloads/CurrentAgentVersion.txt
/Remotely_Server/Server.db
/Remotely_Agent/Resources/*
/Remotely_Agent/Resources/*
/Remotely_Server/Recordings/*
/Remotely_Server/ffmpeg.exe

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Remotely_Library.Models
{
public class AttendedSessionInfo
{
public string SignalRConnectionID { get; set; }
public string Password { get; set; }
}
}

View File

@ -99,7 +99,7 @@ namespace Remotely_ScreenCast.Capture
// continue;
//}
while (viewer.PendingFrames > 30)
while (viewer.PendingFrames > 10)
{
await Task.Delay(1);
}

View File

@ -51,10 +51,11 @@ namespace Remotely_ScreenCast
public static async Task HandleConnection()
{
OutgoingMessages.SendDeviceInfo(ServiceID, Environment.MachineName).Wait();
if (Mode == AppMode.Unattended)
{
OutgoingMessages.SendServiceID(ServiceID).Wait();
var desktopName = Win32Interop.GetCurrentDesktop();
if (desktopName.ToLower() != CurrentDesktopName.ToLower())
{

View File

@ -47,9 +47,9 @@ namespace Remotely_ScreenCast.Sockets
await Connection.SendAsync("NotifyViewersRelaunchedScreenCasterReady", viewerIDs);
}
internal async Task SendServiceID(string serviceID)
internal async Task SendDeviceInfo(string serviceID, string machineName)
{
await Connection.SendAsync("ReceiveServiceID", serviceID);
await Connection.SendAsync("ReceiveDeviceInfo", serviceID, machineName);
}
internal async Task SendConnectionFailedToViewers(List<string> viewerIDs)

View File

@ -278,6 +278,12 @@ namespace Win32
case "Backspace":
keyCode = (short)VirtualKey.BACK;
break;
case "Tab":
keyCode = (short)VirtualKey.TAB;
break;
case "CapsLock":
keyCode = (short)VirtualKey.CAPITAL;
break;
case "Delete":
keyCode = (short)VirtualKey.DELETE;
break;

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Remotely_Server.Models
{
public class RemoteControlFrame
{
public RemoteControlFrame(byte[] frameBytes, int left, int top, int width, int height, string viewerID, string machineName, DateTime startTime)
{
this.FrameBytes = frameBytes;
this.Left = left;
this.Top = top;
this.Width = width;
this.Height = height;
this.ViewerID = viewerID;
this.MachineName = machineName;
this.StartTime = startTime;
}
public byte[] FrameBytes { get; private set; }
public int Left { get; private set; }
public int Top { get; private set; }
public int Width { get; private set; }
public int Height { get; private set; }
public string ViewerID { get; private set; }
public string MachineName { get; private set; }
public DateTime StartTime { get; private set; }
}
}

View File

@ -59,6 +59,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.2" PrivateAssets="All" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.2.0" />
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
</ItemGroup>
@ -1699,4 +1700,11 @@
<ProjectReference Include="..\Remotely_Library\Remotely_Library.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Drawing">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Drawing.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -17,10 +17,9 @@ namespace Remotely_Server.Services
public string DefaultPrompt => Config["ApplicationOptions:DefaultPrompt"];
public string DBProvider => Config["ApplicationOptions:DBProvider"];
public bool AllowSelfRegistration => bool.Parse(Config["ApplicationOptions:AllowSelfRegistration"]);
public bool UseDomainAuthentication => bool.Parse(Config["ApplicationOptions:UseDomainAuthentication"]);
public bool ShowMessageOfTheDay => bool.Parse(Config["ApplicationOptions:ShowMessageOfTheDay"]);
public double DataRetentionInDays => double.Parse(Config["ApplicationOptions:DataRetentionInDays"]);
public double RemoteControlSessionLimit => double.Parse(Config["ApplicationOptions:RemoteControlSessionLimit"]);
public bool RecordRemoteControlSessions => bool.Parse(Config["ApplicationOptions:RecordRemoteControlSessions"]);
public string SmtpHost => Config["ApplicationOptions:SmtpHost"];
public int SmtpPort => int.Parse(Config["ApplicationOptions:SmtpPort"]);

View File

@ -111,16 +111,6 @@ namespace Remotely_Server.Services
}
await Groups.AddToGroupAsync(Context.ConnectionId, RemotelyUser.Organization.ID);
await Clients.Caller.SendAsync("UserOptions", RemotelyUser.UserOptions);
if (AppConfig.ShowMessageOfTheDay)
{
try
{
var wc = new WebClient();
var message = await wc.DownloadStringTaskAsync(new Uri("https://remotely.lucency.co/api/messageoftheday"));
await Clients.Caller.SendAsync("DisplayConsoleHTML", message);
}
catch { }
}
await base.OnConnectedAsync();
}

View File

@ -12,12 +12,17 @@ namespace Remotely_Server.Services
{
public class RCBrowserSocketHub : Hub
{
public RCBrowserSocketHub(DataService dataService, IHubContext<RCDeviceSocketHub> rcDeviceHub, IHubContext<DeviceSocketHub> deviceHub, ApplicationConfig appConfig)
public RCBrowserSocketHub(DataService dataService,
IHubContext<RCDeviceSocketHub> rcDeviceHub,
IHubContext<DeviceSocketHub> deviceHub,
ApplicationConfig appConfig,
RemoteControlSessionRecorder rcSessionRecorder)
{
this.DataService = dataService;
this.RCDeviceHub = rcDeviceHub;
this.AppConfig = appConfig;
this.DeviceHub = deviceHub;
RCSessionRecorder = rcSessionRecorder;
}
public static ConcurrentDictionary<string, RemotelyUser> OrganizationConnectionList { get; set; } = new ConcurrentDictionary<string, RemotelyUser>();
private ApplicationConfig AppConfig { get; set; }
@ -47,6 +52,7 @@ namespace Remotely_Server.Services
private DataService DataService { get; }
private IHubContext<DeviceSocketHub> DeviceHub { get; }
private RemoteControlSessionRecorder RCSessionRecorder { get; }
private IHubContext<RCDeviceSocketHub> RCDeviceHub { get; }
private string RequesterName
{
@ -125,6 +131,11 @@ namespace Remotely_Server.Services
}
}
await RCDeviceHub.Clients.Client(ScreenCasterID).SendAsync("ViewerDisconnected", Context.ConnectionId);
if (AppConfig.RecordRemoteControlSessions)
{
RCSessionRecorder.EncodeFrames(Context.ConnectionId);
}
}
public async Task SelectScreen(int screenIndex)
@ -150,6 +161,7 @@ namespace Remotely_Server.Services
TimeStamp = DateTime.Now,
Message = $"Remote control session requested by {requesterName}. " +
$"Connection ID: {Context.ConnectionId}. User ID: {Context.UserIdentifier}. " +
$"Screen Caster ID: {screenCasterID}." +
$"Login ID (if logged in): {Context?.User?.Identity?.Name}. " +
$"Requester IP Address: " + Context?.GetHttpContext()?.Connection?.RemoteIpAddress?.ToString()
});

View File

@ -1,29 +1,37 @@
using Remotely_Library.Models;
using Remotely_Library.Services;
using Remotely_Server.Data;
using Remotely_Server.Data;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using System.Drawing;
namespace Remotely_Server.Services
{
public class RCDeviceSocketHub : Hub
{
public static ConcurrentDictionary<string, string> AttendedSessionList { get; set; } = new ConcurrentDictionary<string, string>();
public static object SaveLock { get; } = new object();
public RCDeviceSocketHub(DataService dataService,
IHubContext<BrowserSocketHub> browserHub,
IHubContext<RCBrowserSocketHub> rcBrowserHub,
IHubContext<DeviceSocketHub> deviceSocketHub)
IHubContext<DeviceSocketHub> deviceSocketHub,
RemoteControlSessionRecorder rcSessionRecorder,
ApplicationConfig appConfig)
{
DataService = dataService;
BrowserHub = browserHub;
RCBrowserHub = rcBrowserHub;
DeviceHub = deviceSocketHub;
RCSessionRecorder = rcSessionRecorder;
AppConfig = appConfig;
}
private IHubContext<DeviceSocketHub> DeviceHub { get; }
public RemoteControlSessionRecorder RCSessionRecorder { get; }
public ApplicationConfig AppConfig { get; }
private DataService DataService { get; }
private IHubContext<BrowserSocketHub> BrowserHub { get; }
private IHubContext<RCBrowserSocketHub> RCBrowserHub { get; }
@ -45,6 +53,61 @@ namespace Remotely_Server.Services
Context.Items["ServiceID"] = value;
}
}
private string MachineName
{
get
{
if (Context.Items.ContainsKey("MachineName"))
{
return Context.Items["MachineName"] as string;
}
else
{
return null;
}
}
set
{
Context.Items["MachineName"] = value;
}
}
private Size CurrentScreenSize
{
get
{
if (Context.Items.ContainsKey("CurrentScreenSize"))
{
return (Size)Context.Items["CurrentScreenSize"];
}
else
{
return Size.Empty;
}
}
set
{
Context.Items["CurrentScreenSize"] = value;
}
}
private DateTime StartTime
{
get
{
if (Context.Items.ContainsKey("StartTime"))
{
return (DateTime)Context.Items["StartTime"];
}
else
{
return DateTime.Now;
}
}
set
{
Context.Items["StartTime"] = value;
}
}
private List<string> ViewerList
{
get
@ -59,6 +122,7 @@ namespace Remotely_Server.Services
public override Task OnConnectedAsync()
{
StartTime = DateTime.Now;
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
@ -81,9 +145,10 @@ namespace Remotely_Server.Services
}
await base.OnDisconnectedAsync(exception);
}
public void ReceiveServiceID(string serviceID)
public void ReceiveDeviceInfo(string serviceID, string machineName)
{
ServiceID = serviceID;
MachineName = machineName;
}
public void ViewerDisconnected(string viewerID)
{
@ -103,14 +168,21 @@ namespace Remotely_Server.Services
public async Task SendScreenSize(int width, int height, string rcBrowserHubConnectionID)
{
CurrentScreenSize = new Size(width, height);
await RCBrowserHub.Clients.Client(rcBrowserHubConnectionID).SendAsync("ScreenSize", width, height);
}
public Task SendScreenCapture(byte[] captureBytes, string rcBrowserHubConnectionID, int left, int top, int width, int height, DateTime captureTime)
{
if (AppConfig.RecordRemoteControlSessions)
{
RCSessionRecorder.SaveFrame(captureBytes, left, top, CurrentScreenSize.Width, CurrentScreenSize.Height, rcBrowserHubConnectionID, MachineName, StartTime);
}
return RCBrowserHub.Clients.Client(rcBrowserHubConnectionID).SendAsync("ScreenCapture", captureBytes, left, top, width, height, captureTime);
}
public async Task NotifyRequesterUnattendedReady(string browserHubConnectionID)
{
await BrowserHub.Clients.Client(browserHubConnectionID).SendAsync("UnattendedSessionReady", Context.ConnectionId);

View File

@ -0,0 +1,125 @@
using Microsoft.AspNetCore.Hosting;
using Remotely_Server.Data;
using Remotely_Server.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Remotely_Server.Services
{
public class RemoteControlSessionRecorder
{
private static bool IsProcessing { get; set; }
private static ConcurrentQueue<RemoteControlFrame> FrameQueue { get; } = new ConcurrentQueue<RemoteControlFrame>();
private static ConcurrentDictionary<string, Bitmap> CumulativeFrames { get; } = new ConcurrentDictionary<string, Bitmap>();
private static object LockObject { get; } = new object();
private IHostingEnvironment HostingEnv { get; }
private DataService DataService { get; }
public RemoteControlSessionRecorder(IHostingEnvironment hostingEnv, DataService dataService)
{
HostingEnv = hostingEnv;
DataService = dataService;
}
internal void SaveFrame(byte[] frameBytes, int left, int top, int width, int height, string viewerID, string machineName, DateTime startTime)
{
var rcFrame = new RemoteControlFrame(frameBytes, left, top, width, height, viewerID, machineName, startTime);
FrameQueue.Enqueue(rcFrame);
lock (LockObject)
{
if (!IsProcessing)
{
IsProcessing = true;
Task.Run(StartProcessing);
}
}
}
internal void StartProcessing()
{
while (FrameQueue.Count > 0)
{
if (FrameQueue.TryDequeue(out var frame))
{
if (!CumulativeFrames.ContainsKey(frame.ViewerID))
{
CumulativeFrames[frame.ViewerID] = new Bitmap(frame.Width, frame.Height);
}
var saveDir = Directory.CreateDirectory(GetSaveFolder(frame));
var saveFile = Path.Combine(
saveDir.FullName,
$"frame-{(Directory.GetFiles(saveDir.FullName).Length + 1).ToString()}.jpg");
var bitmap = CumulativeFrames[frame.ViewerID] as Bitmap;
using (var graphics = Graphics.FromImage(bitmap))
{
using (var ms = new MemoryStream(frame.FrameBytes))
{
using (var saveImage = Image.FromStream(ms))
{
graphics.DrawImage(saveImage, frame.Left, frame.Top);
}
}
}
bitmap.Save(saveFile, ImageFormat.Jpeg);
}
}
lock (LockObject)
{
IsProcessing = false;
}
}
private string GetSaveFolder(RemoteControlFrame frame)
{
return Path.Combine(
HostingEnv.ContentRootPath,
"Recordings",
frame.StartTime.Year.ToString().PadLeft(4, '0'),
frame.StartTime.Month.ToString().PadLeft(2, '0'),
frame.StartTime.Day.ToString().PadLeft(2, '0'),
frame.MachineName,
frame.ViewerID,
frame.StartTime.ToString("HH.mm.ss.fff"));
}
internal void EncodeFrames(string viewerID)
{
var recordingDirs = Directory.GetDirectories(Path.Combine(
HostingEnv.ContentRootPath,
"Recordings",
DateTime.Now.Year.ToString().PadLeft(4, '0'),
DateTime.Now.Month.ToString().PadLeft(2, '0')),
viewerID,
SearchOption.AllDirectories);
foreach (var dir in recordingDirs)
{
foreach (var subDir in Directory.GetDirectories(dir))
{
try
{
System.Diagnostics.Process.Start("ffmpeg", $"-y -i \"{Path.Combine(subDir, "frame-%d.jpg")}\" \"{Path.Combine(subDir, "Recording.mp4")}\"").WaitForExit();
foreach (var file in Directory.GetFiles(subDir, "*.jpg"))
{
File.Delete(file);
}
}
catch (Exception ex)
{
DataService.WriteEvent(ex);
}
}
}
}
}
}

View File

@ -94,6 +94,7 @@ namespace Remotely_Server
services.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<EmailSender>();
services.AddScoped<DataService>();
services.AddScoped<RemoteControlSessionRecorder>();
services.AddSingleton<ApplicationConfig>();
services.AddSingleton<RandomGenerator>();
}

View File

@ -13,8 +13,7 @@
"DefaultPrompt": "~>",
"DBProvider": "SQLite",
"AllowSelfRegistration": true,
"UseDomainAuthentication": false,
"ShowMessageOfTheDay": true,
"RecordRemoteControlSessions": false,
"DataRetentionInDays": 90,
"RemoteControlSessionLimit": 1,
"SmtpHost": "",

View File

@ -22,8 +22,6 @@ export class RCBrowserSockets {
});
this.Connection.closedCallbacks.push((ev) => {
UI.Screen2DContext.clearRect(0, 0, UI.ScreenViewer.width, UI.ScreenViewer.height);
console.log("Connection closed.");
UI.StatusMessage.innerHTML = "Connection closed.";
UI.ScreenViewer.setAttribute("hidden", "hidden");
UI.ConnectBox.style.removeProperty("display");
});
@ -134,6 +132,7 @@ export class RCBrowserSockets {
this.Connection.stop();
});
hubConnection.on("ScreenCasterDisconnected", () => {
UI.StatusMessage.innerHTML = "The host has disconnected.";
this.Connection.stop();
});
hubConnection.on("RelaunchedScreenCasterReady", (newClientID) => {

File diff suppressed because one or more lines are too long

View File

@ -30,8 +30,6 @@ export class RCBrowserSockets {
})
this.Connection.closedCallbacks.push((ev) => {
UI.Screen2DContext.clearRect(0, 0, UI.ScreenViewer.width, UI.ScreenViewer.height);
console.log("Connection closed.");
UI.StatusMessage.innerHTML = "Connection closed.";
UI.ScreenViewer.setAttribute("hidden", "hidden");
UI.ConnectBox.style.removeProperty("display");
});
@ -143,6 +141,7 @@ export class RCBrowserSockets {
this.Connection.stop();
});
hubConnection.on("ScreenCasterDisconnected", () => {
UI.StatusMessage.innerHTML = "The host has disconnected.";
this.Connection.stop();
});
hubConnection.on("RelaunchedScreenCasterReady", (newClientID: string) => {