Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#3.5

Windows Remote Desktop Application

4.96/5 (33 votes)
18 Apr 2015CPOL11 min read 130.5K   13.9K  
Client server program using a single TCP port to control a remote desktop

Introduction

This project is a full remote desktop application which is very similar to Microsoft remote desktop that was written using VS2010 and C# and consists of two fully working modules that make up the client server components needed for a remote desktop.

I recently purchased two HP 7 Stream tablets for the kids only to find that my fingers were a bit too big to administer the devices and that my eyesight was not as good as it used to be so I cranked up Microsoft remote desktop so that I could lock these devices down using my laptop and soon discovered that the free Bing 8 version of Windows would not allow this to work without upgrading Windows.

Well, if Microsoft thought that I was about to pay them to upgrade these devices just to get remote desktop working more than the devices cost in the first place, then they were very wrong indeed so I soon set about building my own remote desktop for free.

The full source code included in the above RNCRemoteDesktop.zip download plus the Client/Server application .EXE files are included at the root of the zip file if you would like a free remote desktop and also note that no 3rd party DLLs have been used in the creation of this project.

Server Component

The server runs a service on a new thread from a hidden form that waits for a TCP request to come in on port 4000 by default using a System.Net.Sockets.TcpListener that then connects to the client and enters a loop that takes a screen shot of the screen that is then serialized and sent back across the wire as a System.IO.MemoryStream using the code below.

C#
BinaryFormatter bFormat = new BinaryFormatter();
Bitmap screeny = new Bitmap(this.ScreenServerX + 
this.Padding, this.ScreenServerY + this.Padding, this.ImageResoloution);
Graphics theShot = Graphics.FromImage(screeny);
System.Drawing.Size Sz = new System.Drawing.Size
(this.ScreenServerX + this.Padding, this.ScreenServerY + this.Padding);
theShot.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, 
Screen.PrimaryScreen.Bounds.Y, this.Padding, this.Padding, Sz, CopyPixelOperation.SourceCopy);
Color C = WinCursor.CaptureCursor
(ref X, ref Y, theShot, this.ScreenServerX, this.ScreenServerY);
screeny.SetPixel(0, 0, C);//Set a pixels color that is based on the current mouse pointer
if (this.IsMetro)//Windows metro mode
   screeny.SetPixel(0, 1, Color.Black);//Set another hidden pixel 
		//so the client know we are in Metro mode
else
   screeny.SetPixel(0, 1, Color.Red);
MemoryStream MS = Encrypt(screeny);
bFormat.Serialize(CStream, MS);
theShot.Dispose();
screeny.Dispose();

Note from the above code that two pixels in the image are changed so that we can send additional data back to the client including the type of current Windows mouse pointer which I will come back to in more detail later.

The Windows desktop screen is not like a PC-Paint image and is more like Photoshop that contains layers which means that the above code will not capture any annoying Windows pop-up messages that ask if you would like to run a process as the administrator so the best thing here to do here is to just turn the UAC security settings off in Windows which is most likely turned off already because it's about the only way to make applications not written by Microsoft work in Windows without getting lots of warning messages.

The service also starts another thread that binds to the main socket to wait for incoming requests that are pushed to it from the client when a keystroke is made or the mouse cursor is moved or clicked on the client that then need to be replicated on the server using Sendkeys(ch); or Cursor.Position = new Point(x, y);

RNC remote desktop

Windows Cursor Trouble!

So far, taking a screen capture of layer one of the screen and sending keystrokes has been relatively straight forward, but that's about to change with the cursor because the mouse pointer is not shown on layer one of the screen so therefore it is not captured when taking a screen-shot.

Here, I was expecting to simply read the Windows mouse cursor state as an I-Beam, Wait or North South pointer and to then convert the number to a colour that could then be used to set a hidden pixel colour in the main image that is sent back across the wire to the client and displayed but I soon discovered that programs like PC-Paint use custom cursors and the only way to deal with these types of cursors is to capture the cursor as an image using Windows API calls and to then embed the image in the main image ready to be sent back to the client. The code I used to achieve this is shown below:

C#
[DllImport("user32.dll")]
    static extern bool GetCursorInfo(out CURSORINFO pci);
    public static Color CaptureCursor(ref int X, ref int Y, Graphics theShot, int ScreenServerX, int ScreenServerY)
    {//We return a color so that it can be embedded in a bitmap to be returned to the client program
        IntPtr C = Cursors.Arrow.Handle;
        CURSORINFO pci;
        pci.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(CURSORINFO));
        if (GetCursorInfo(out pci))
        {
            X = pci.ptScreenPos.x;
            Y = pci.ptScreenPos.y;
            if (pci.hCursor == Cursors.Default.Handle) return Color.Red;
            else if (pci.hCursor == Cursors.WaitCursor.Handle) return Color.Green;
            else if (pci.hCursor == Cursors.Arrow.Handle) return Color.Blue;
            else if (pci.hCursor == Cursors.IBeam.Handle) return Color.White;
            else if (pci.hCursor == Cursors.Hand.Handle) return Color.Violet;
            else if (pci.hCursor == Cursors.SizeNS.Handle) return Color.Yellow;
            else if (pci.hCursor == Cursors.SizeWE.Handle) return Color.Orange;
            else if (pci.hCursor == Cursors.SizeNESW.Handle) return Color.Aqua;
            else if (pci.hCursor == Cursors.SizeNWSE.Handle) return Color.Pink;
            else if (pci.hCursor == Cursors.PanEast.Handle) return Color.BlueViolet;
            else if (pci.hCursor == Cursors.HSplit.Handle) return Color.Cyan;
            else if (pci.hCursor == Cursors.VSplit.Handle) return Color.DarkGray;
            else if (pci.hCursor == Cursors.Help.Handle) return Color.DarkGreen;
            else if (pci.hCursor == Cursors.AppStarting.Handle) return Color.SlateGray;
            if (pci.flags == CURSOR_SHOWING)
            {//Custom cursor so add the mouse cursor to the main image.
                float XReal = pci.ptScreenPos.x * (float)ScreenServerX / (float)Screen.PrimaryScreen.Bounds.Width - 11;
                float YReal = pci.ptScreenPos.y * (float)ScreenServerY / (float)Screen.PrimaryScreen.Bounds.Height - 11;
                int x = Screen.PrimaryScreen.Bounds.X;
                var hdc = theShot.GetHdc();
                DrawIconEx(hdc, (int)XReal, (int)YReal, pci.hCursor, 0, 0, 0, IntPtr.Zero, DI_NORMAL);
                theShot.ReleaseHdc();
            }
            return Color.Black;
        }
        X = 0;
        Y = 0;
        return Color.Black;
    }

Keystrokes and Mouse

Not every keystroke on a keyboard can be sent to Windows using SendKeys.SendWait(Key); and a good example is CTRL-ALT-DEL which outside of Microsoft, no one can send but what about Caps-Lock or displaying the metro side-bar menu. Well, it turns out that the pin.invoke has the answer and the code I used is shown below.

C#
public const int KEYEVENTF_EXTENDEKEY = 1;
public const int KEYEVENTF_KEYUP = 2;
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern uint keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo); 
public static void CapsLock()
        {
            Server.keybd_event(0x14, 0x45, Server.KEYEVENTF_EXTENDEKEY, 0);
            Server.keybd_event(0x14, 0x45, Server.KEYEVENTF_EXTENDEKEY | Server.KEYEVENTF_KEYUP, 0);
        }
public static void ShowMetro()
        {//Shows the metro right sidebar menu
            Server.keybd_event((byte)Keys.LWin, 0, Server.KEYEVENTF_EXTENDEKEY, 0);
            Server.keybd_event((byte)Keys.C, 0, Server.KEYEVENTF_EXTENDEKEY, 0);
            Server.keybd_event((byte)Keys.LWin, 0, Server.KEYEVENTF_EXTENDEKEY | Server.KEYEVENTF_KEYUP, 0);
            Server.keybd_event((byte)Keys.C, 0, Server.KEYEVENTF_EXTENDEKEY | Server.KEYEVENTF_KEYUP, 0);
            Cursor.Position = new Point(Screen.PrimaryScreen.Bounds.Width - 30, 
            	Screen.PrimaryScreen.Bounds.Height /2); Thread.Sleep(20);
        }

Once again, for setting the mouse position and scrolling on the server, we need to resort to making Windows API calls again and you would think that given the size of the Windows framework that Microsoft would already have this covered but they don't, so here is the code that I needed to add.

C#
public const uint MOUSEEVENTF_WHEEL = 0x0800;
public const uint MOUSEEVENTF_HWHEEL = 0x01000;
public const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
public const uint MOUSEEVENTF_LEFTUP = 0x0004;
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern void mouse_event(long dwFlags, long dx, long dy, long cButtons, long dwExtraInfo);
public static void ScrollVertical(int Amount)
        {//Scroll left/right
            Server.mouse_event(Server.MOUSEEVENTF_HWHEEL, 0, 0, Amount, 0);
        }
public static void ScrollHorizontal(int Amount)
        {//Scroll up/down
            Server.mouse_event(Server.MOUSEEVENTF_HWHEEL, 0, 0, Amount, 0);
        }
//Left mouse up or down goes a bit like this
if (Command.StartsWith("LDOWN"))
       {
          mouse_event(MOUSEEVENTF_LEFTDOWN, Cursor.Position.X, Cursor.Position.Y, 0, 0);
       }
       else if (Command.StartsWith("LUP"))
       {
          mouse_event(MOUSEEVENTF_LEFTUP, Cursor.Position.X, Cursor.Position.Y, 0, 0);
        }.

Encryption and Compression

The command data that is sent from the client to the server is quite short and has been encrypted using a simple XOR-Shift of the string data and the same is also used when clipboard text is sent across the wire but this form of data protection might be fast but is far from being secure.

Image 2

Compressing image data that has already been compressed as is the case with the .PNG data captured during the screen-shot is not going to achieve very much in the way of compression so the only way to achieve any sort of compression is to reduce the colour depth of the image from 32-bit to 16-bit or to change the image resolution and both methods have been used in the program along with setting a plain black background wallpaper on the server that is switched back to the original background image when the client disconnects from the server.

I experimented using symmetric keys and Rijndael for encrypting the main images that are sent across the wire but removed the code because this thrashed the CPU on small tablet devices running Intel ATOM chipset (Chips are not getting fast these days, just the number has changed, Moore's Law is broken) and the extra workload resulted in the screen flickering. Using an SSL stream might be a better option here but I didn't try because distribution of certificates can be a bit of a pain so I opted for tweaking the serialized data instead.

Scaling Desktop Images to Fit Both Machines

You will notice from the above images that the client side of the program has both a vertical and horizontal scrollbar that can be used to send a message to the remote desktop server about how to scale the desktop image before sending it back across the wire.

I wanted to just calculate the necessary values for scaling to take into account both the size of the servers screen resolution using the code shown below which worked fine most time, but then I hit a problem.

C#
this.x=Screen.PrimaryScreen.Bounds.Width; 
this.y=Screen.PrimaryScreen.Bounds.Height

Another consideration to remember is that tablet PCs can flip between portrait and landscape mode, so the manual options allow for the best of both worlds but no one told me that the first time I thought my code had all gone wrong because someone had move the tablet PC.

Permissions

During development, I noticed that the remote desktop worked just fine on 3rd part applications and programs such as PC-Paint, but when I opened Windows task-manager and clicked on the tabs at the top of the manager, they didn't work and the same was happening for many of the apps in the Windows control panel.

This was due to the security context that the remote desktop server application was running in and if the server is not running as the administrator, then Windows is not going to play ball even if the mouse seems to move just fine and no warning is given by Windows after allowing the manager to open.

A Few Tip Bits of Information

If you scroll back up and take a look at the top image, then you will notice that the server component has the option of adding firewall rules to allow the service to work over the network and to also add a scheduled task to allow Windows to run the program on start-up.

Sounds easy, but code that worked on Windows 7 no longer works on version 8 and if it works on the enterprise edition and the user is this, sets that, has this security update fix KB9999 then it just might work on Windows 8 Bing edition but it's funny that Windows apps never seems to gets this trouble ???

Anyway, if you like to tweak windows using command line code then consider executing your scripts in a Windows power shell with admin right instead of a cmd.exe shell because it seems to work better so you might like this little bit of code.

C#
public static string ExecuteCommandAsAdmin(string command)
    {
        ProcessStartInfo psinfo = new ProcessStartInfo();
        psinfo.FileName = "powershell.exe";
        psinfo.Arguments = command;
        psinfo.RedirectStandardError = true;
        psinfo.RedirectStandardOutput = true;
        psinfo.UseShellExecute = false;
        using (Process proc = new Process())
        {
            proc.StartInfo = psinfo;
            proc.Start();
            string output = proc.StandardOutput.ReadToEnd();
            if (string.IsNullOrEmpty(output))
                output = proc.StandardError.ReadToEnd();
            return output;
        }
    }

The code below is a hack that uses the above function to add a shortcut to the desktop with the "Run as administrator" flag turned on for the current application.

C#
public static bool AddDesktopShortcut()
        {
            FileInfo FInfo = new FileInfo(Application.ExecutablePath);
            string FileNameLnk = @"C:\Users\" + Environment.UserName + 
            @"\Desktop\" + FInfo.Name.ToLower().Replace(".exe", "") + ".lnk";
            if (File.Exists(FileNameLnk)) return true;//Already created shortcut so return
            string Cmd = "$WshShell = New-Object -comObject WScript.Shell" + Environment.NewLine;
            Cmd += "$Shortcut = $WshShell.CreateShortcut('" + 
            FileNameLnk + "')" + Environment.NewLine;
            Cmd += "$Shortcut.TargetPath = '" + 
            Application.ExecutablePath + "';" + Environment.NewLine;
            Cmd += "$Shortcut.Description = 
            'Runs the program with admin rights';" + Environment.NewLine;
            Cmd += "$Shortcut.WorkingDirectory = '" + 
            Application.StartupPath + "';" + Environment.NewLine;
            Cmd += "$Shortcut.WindowStyle = 1;" + Environment.NewLine;
            Cmd += "$Shortcut.Save()" + Environment.NewLine;
            string ScriptResults = ScheduleTask.ExecuteCommandAsAdmin(Cmd);//Runs as a windows power script
            if (ScriptResults.Length == 0 && File.Exists(FileNameLnk))
            {
                using (FileStream fs = new FileStream(FileNameLnk, FileMode.Open, FileAccess.ReadWrite))
                {//We need to hack the shortcut file to give it administrator rights
                    fs.Seek(21, SeekOrigin.Begin);
                    fs.WriteByte(0x22);
                }
                return true;
            }
            return false;
        }

The Remote Desktop Client

The client side of the project is a piece of cake when compared to the server and is basically a form with a docked image for the main screen that is feed by images that are pushed to it from the server using a TCP socket with a few form GroupBoxes used to position the blue menu control that is displayed in the middle at the top of the page.

Image 3

If the form is maximized, then the title bar at the top of the page is removed and when the form is minimized, a signal command is sent to the server to inform it to stop sending images of the screen to reduce the CPU loading on the server.

Keystroke and mouse movement events are easy to capture for the form and they are simply packaged up and pushed to the server using the shared TCP socket as encrypted command strings along with the clip-board text if it changes.

When connect is pressed, a System.Net.Socket.TcpClient is used to connect to the server using the default port of 4000 and if successful, then a System.IO.Stream is attached to the stream for reading any pushed data from the server that could be in the form of an image or it could be clip-board text.

We also attach a System.IO.StreamWriter to the TcpClient so that the client can push any keystrokes or mouse move events to the server, but during the connection process, we first send our password and screen resolution of the client and a few other values to the server get things rolling.

Next, a new thread is started that enters a loop and binds to our connected stream and waits for any data to arrive so it all looks a bit like this.

C#
private void Connect()
{
this.Tcpclient = new TcpClient(TxtIPAddress.Text.ToString(), int.Parse(TxtPort.Text.ToString()));
Stream S = Tcpclient.GetStream();     //Listen on
StreamWriter SW = new StreamWriter(S);//Send on
SW.Write(Helper.XorString("CMD PASSWORD " + txtPassword.Text.Trim(),45,true) + "\n");
SW.Write(Helper.XorString("SCREEN "+Settings.ScreenServerX +" "+ 
	Settings.ScreenServerY,45,true)+"\n");
SW.Flush();
this.WindowState = FormWindowState.Maximized;
theThread = new Thread(new ThreadStart(startRead));
theThread.Start();
this.eventSender=SW;
}

private void startRead()
    while(true) //Loop 
    {
       Bitmap inImage = BitmapFromStream(Settings.Encrypted);//Wait for an image to arrive
       if (inImage ==null) inImage=BitmapFromStream(!Settings.Encrypted);//Made a mistake
       if (Settings.Scale) inImage = ResizeImage(inImage, 1920, 1080);//Not quite right
       resolutionX = inImage.Width;
       if (resolutionX>5) theImage.Image = (Image)inImage;//Display our screen grab
       if (Settings.SendKeysAndMouse && !GroupConected.Visible && resolutionX> 5)
           {
             Color C1 = inImage.GetPixel(0, 0);//Hidden pixel for servers cursor
             Color C2 = inImage.GetPixel(0, 1);//Hidden pixel for windows metro-mode
             Settings.IsMetro = (C2.R == 0 && C2.G == 0 && C2.B == 0);//Black i think
             this.Invoke((MethodInvoker)delegate() { this.Cursor = WinCursor.ColorToCursor(C1); });
           }
    }
}

private Bitmap BitmapFromStream(bool Encrypted)
{
    Bitmap Image = null;
    BinaryFormatter bFormat = new BinaryFormatter();
    MemoryStream MSin = null;
    MSin = bFormat.Deserialize(stream) as MemoryStream;//Will wait for data
    if (MSin == null || MSin.Length <200)
    {//Too small for an image 
        if (MSin != null)
        {//Looks like test has been sent
        string Data = UTF8Encoding.UTF8.GetString(MSin.ToArray());
        string ClipboardText =Helper.XorString( Data.Replace("#NL#", Environment.NewLine),34,false);
        if (ClipboardText.StartsWith("#CLIPBOARD#"))
            {//Yes its clip-board text
               LastClipboardText = ClipboardText.Substring(11);
               ClipboardAsync.SetText(LastClipboardText);
            }
         else if (ClipboardText.StartsWith("#INFO#"))//Screen information or something
               ReadInfo(ClipboardText);
               return new Bitmap(1, 1);
            }
          }
        MemoryStream MSout = Decrypt(MSin);
        Image = new Bitmap(MSout);
        Settings.Encrypted = true;
}//Not all the code is shown here but this should help

public static Cursor ColorToCursor(Color C)
{//Code for the client that pulls a pixel from the picture and converts it to a cursor
    if (C.ToArgb() == Color.Red.ToArgb()) return Cursors.Default;
    if (C.ToArgb() == Color.Green.ToArgb()) return Cursors.WaitCursor;
    if (C.ToArgb() == Color.Blue.ToArgb()) return Cursors.Arrow;
    if (C.ToArgb() == Color.White.ToArgb()) return Cursors.IBeam;
    if (C.ToArgb() == Color.Violet.ToArgb()) return Cursors.Hand;
    if (C.ToArgb() == Color.Yellow.ToArgb()) return Cursors.SizeNS;
    byte[] BB = RemoteClient.Properties.Resources.ResourceManager.GetObject("CursorUnknown") as byte[];
    return new Cursor(new System.IO.MemoryStream(BB));//Don't know what it is so use our custom cursor
}

Commands such as "Sleep" ,"Lock" ,"UnLock" are sent to the server using the functions shown below along with any keystrokes or mouse moves/clicks, but notice how we change the shift key below just to confuse anyone that might be looking at the network traffic.

C#
public void SendCommand(string Cmd)
   {
     SafeSendValue("CMD " + Cmd.ToUpper());
   }

public bool SafeSendValue(string Text)
   {
    int Shift = 45;
    if (Text.Length == 1) Shift = 77;//Change the shift for a single key press just to confuse 
    if (!Settings.Connected) return false;
    try
      {
       string T = Helper.XorString(Text, Shift, true);//Encrypt by XOR with shift
       eventSender.Write(T + "\n");
       eventSender.Flush();
       return true;
       }
       catch (Exception Ex)
       {
         return false;
        }
    }

Most of the code in the client is concerned with resizing the form and sending the new height and width to the server using commands described above and a little bit of code to try fake a Windows "Metro" mouse trap when the mouse is moved to the top right corner of the screen.

Deployment

Copy RemoteServer.exe to the server and run with admin rights and then enter a port number and a password and then press the "Firewall" button and check that a new rule has been created in the firewall. The program should create a shortcut for you and place it on the desktop ready to use with admin rights turned on. Press "Save settings" to start the service.

Copy RemoteClient.exe to the client machine, run as administrator and type in the password and port number plus the IP-Address of the server or enter the machine name and press connect. If you have trouble, then check that a rule has been added to the firewall.

Keeping a tick in the box for debugging will allow you to see the commands being sent between the client and the server or click the blue down icon to tweak the screen size using the scroll-bars provided.

Click the "Help" button for more details.

Future Development

It is possible to grab the image for just the active window to then send that back across the network which would make the program more responsive so long as the active window was not maximized which could well be worth adding along with allowing users to not just cut/copy/paste text as the program does now but to also include OLE object, images and files.

Security would benefit if the option of switching the socket stream to use an SSLStream was provided, but I would first need to see the impact on performance this change might have.

Maybe next time, I will try to do something like this that sends the desktop back as plain HTML to allow desktop icons to be clicked (even if the layout is not the same) and for the start button to be activated and just use a floating div to display an image containing the active desktop item.

Please feedback any bugs or questions and I will see if I can help!

- Dr Gadgit

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)