/*
Myrtille: A native HTML4/5 Remote Desktop Protocol client.
Copyright(c) 2014-2021 Cedric Coste
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
using System;
using System.Diagnostics;
using System.IO.Pipes;
using System.Security.AccessControl;
using Myrtille.Helpers;
using Myrtille.Services.Contracts;
namespace Myrtille.Web
{
public class RemoteSessionPipes
{
public RemoteSession RemoteSession { get; private set; }
// it's possible to have 2 ways pipes (duplex, using overlapped I/O), but it proven difficult to setup and raised concurrency access issues...
// to keep things simple, using separate pipes...
// TODO: the updates pipe is currently handling both text messages and raw images
// even if it's ok (with a strict data format), there might be a need for a separate messages pipe
// same thing for audio, if implemented
private NamedPipeServerStream _inputsPipe;
public NamedPipeServerStream InputsPipe { get { return _inputsPipe; } }
private NamedPipeServerStream _updatesPipe;
public NamedPipeServerStream UpdatesPipe { get { return _updatesPipe; } }
private NamedPipeServerStream _audioPipe;
public NamedPipeServerStream AudioPipe { get { return _audioPipe; } }
// each audio block is 32768 bytes (32 kb); the audio buffer is up to 6 blocks
private const int audioBufferSize = 196608;
public RemoteSessionPipes(RemoteSession remoteSession)
{
RemoteSession = remoteSession;
}
public void CreatePipes()
{
try
{
// close the pipes if already exist; they will be re-created below
DeletePipes();
// set the pipes access rights
var pipeSecurity = new PipeSecurity();
var pipeAccessRule = new PipeAccessRule(RemoteSession.Manager.HostClient.GetProcessIdentity(), PipeAccessRights.FullControl, AccessControlType.Allow);
pipeSecurity.AddAccessRule(pipeAccessRule);
// create the pipes
_inputsPipe = new NamedPipeServerStream(
"remotesession_" + RemoteSession.Id + "_inputs",
PipeDirection.InOut,
1,
RemoteSession.HostType == HostType.RDP ? PipeTransmissionMode.Byte : PipeTransmissionMode.Message,
PipeOptions.Asynchronous,
0,
0,
pipeSecurity);
_updatesPipe = new NamedPipeServerStream(
"remotesession_" + RemoteSession.Id + "_updates",
PipeDirection.InOut,
1,
RemoteSession.HostType == HostType.RDP ? PipeTransmissionMode.Byte : PipeTransmissionMode.Message,
PipeOptions.Asynchronous,
0,
0,
pipeSecurity);
// RDP only
if (RemoteSession.HostType == HostType.RDP)
{
_audioPipe = new NamedPipeServerStream(
"remotesession_" + RemoteSession.Id + "_audio",
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous,
audioBufferSize,
audioBufferSize,
pipeSecurity);
}
// wait for client connection
InputsPipe.BeginWaitForConnection(InputsPipeConnected, InputsPipe);
UpdatesPipe.BeginWaitForConnection(UpdatesPipeConnected, UpdatesPipe);
if (RemoteSession.HostType == HostType.RDP)
{
AudioPipe.BeginWaitForConnection(AudioPipeConnected, AudioPipe);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to create pipes, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
public void DeletePipes()
{
DisposePipe("remoteSession_" + RemoteSession.Id + "_inputs", ref _inputsPipe);
DisposePipe("remoteSession_" + RemoteSession.Id + "_updates", ref _updatesPipe);
if (RemoteSession.HostType == HostType.RDP)
{
DisposePipe("remoteSession_" + RemoteSession.Id + "_audio", ref _audioPipe);
}
}
private void InputsPipeConnected(IAsyncResult e)
{
try
{
if (InputsPipe != null)
{
InputsPipe.EndWaitForConnection(e);
// send connection settings
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendServerAddress, string.IsNullOrEmpty(RemoteSession.ServerAddress) ? "localhost" : RemoteSession.ServerAddress);
if (!string.IsNullOrEmpty(RemoteSession.VMGuid))
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendVMGuid, string.Concat(RemoteSession.VMGuid, string.Format(";EnhancedMode={0}", RemoteSession.VMEnhancedMode ? "1" : "0")));
if (!string.IsNullOrEmpty(RemoteSession.UserDomain))
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendUserDomain, RemoteSession.UserDomain);
if (!string.IsNullOrEmpty(RemoteSession.UserName))
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendUserName, RemoteSession.UserName);
if (!string.IsNullOrEmpty(RemoteSession.UserPassword))
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendUserPassword, RemoteSession.UserPassword);
if (!string.IsNullOrEmpty(RemoteSession.StartProgram))
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendStartProgram, RemoteSession.StartProgram);
// (re)sync the clipboard
if (!string.IsNullOrEmpty(RemoteSession.ClipboardText))
{
// send the clipboard text as unicode code points (same as done from the browser)
var clipboardUnicode = string.Empty;
foreach (var charValue in RemoteSession.ClipboardText)
{
clipboardUnicode += (string.IsNullOrEmpty(clipboardUnicode) ? string.Empty : "-") + char.ConvertToUtf32(charValue.ToString(), 0);
}
RemoteSession.Manager.SendCommand(RemoteSessionCommand.SendLocalClipboard, clipboardUnicode);
}
// connect the host client to the remote host; a fullscreen update will be sent upon connection
RemoteSession.Manager.SendCommand(RemoteSessionCommand.ConnectClient);
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to wait for connection on inputs pipe, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
private void UpdatesPipeConnected(IAsyncResult e)
{
try
{
if (UpdatesPipe != null)
{
UpdatesPipe.EndWaitForConnection(e);
ReadUpdatesPipe();
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to wait for connection on updates pipe, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
private void AudioPipeConnected(IAsyncResult e)
{
try
{
if (AudioPipe != null)
{
AudioPipe.EndWaitForConnection(e);
ReadAudioPipe();
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to wait for connection on audio pipe, remote session {0} ({1})", RemoteSession?.Id, exc);
}
}
private void ReadUpdatesPipe()
{
try
{
byte[] data;
while (UpdatesPipe != null && UpdatesPipe.IsConnected)
{
data = PipeHelper.ReadPipeData(UpdatesPipe, "remotesession_" + RemoteSession.Id + "_updates");
if (data != null && data.Length > 0)
{
RemoteSession.Manager.ProcessUpdatesPipeData(data);
}
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to read updates pipe, remote session {0} ({1})", RemoteSession?.Id, exc);
// there is a problem with the updates pipe, close the remote session in order to avoid it being stuck
RemoteSession.Manager.SendCommand(RemoteSessionCommand.CloseClient);
}
}
private void ReadAudioPipe()
{
try
{
byte[] data;
while (AudioPipe != null && AudioPipe.IsConnected)
{
data = PipeHelper.ReadPipeData(AudioPipe, "remotesession_" + RemoteSession.Id + "_audio", false, audioBufferSize);
if (data != null && data.Length > 0)
{
RemoteSession.Manager.ProcessAudioPipeData(data);
}
}
}
catch (Exception exc)
{
Trace.TraceError("Failed to read audio pipe, remote session {0} ({1})", RemoteSession?.Id, exc);
// there is a problem with the audio pipe, close the remote session in order to avoid it being stuck
RemoteSession.Manager.SendCommand(RemoteSessionCommand.CloseClient);
}
}
private void DisposePipe(string pipeName, ref NamedPipeServerStream pipe)
{
if (pipe != null)
{
try
{
// CAUTION! closing a pipe in use can make .NET to crash! disconnect it first...
if (pipe.IsConnected)
{
pipe.WaitForPipeDrain();
pipe.Disconnect();
}
pipe.Close();
}
catch (Exception exc)
{
Trace.TraceError("Failed to close pipe {0}, remote session {1} ({2})", pipeName, RemoteSession?.Id, exc);
}
finally
{
pipe.Dispose();
pipe = null;
}
}
}
}
}