Add server side lock key sync heuristic

Based on QEMU's behaviour.
diff --git a/common/rfb/VNCSConnectionST.cxx b/common/rfb/VNCSConnectionST.cxx
index 53dd364..de41e8f 100644
--- a/common/rfb/VNCSConnectionST.cxx
+++ b/common/rfb/VNCSConnectionST.cxx
@@ -34,10 +34,12 @@
 #include <rfb/Security.h>
 #include <rfb/screenTypes.h>
 #include <rfb/fenceTypes.h>
+#include <rfb/ledStates.h>
 #include <rfb/ServerCore.h>
 #include <rfb/ComparingUpdateTracker.h>
 #include <rfb/KeyRemapper.h>
 #include <rfb/Encoder.h>
+#define XK_LATIN1
 #define XK_MISCELLANY
 #define XK_XKB_KEYS
 #include <rfb/keysymdef.h>
@@ -560,6 +562,73 @@
     }
   }
 
+  // Avoid lock keys if we don't know the server state
+  if ((server->ledState == ledUnknown) &&
+      ((key == XK_Caps_Lock) ||
+       (key == XK_Num_Lock) ||
+       (key == XK_Scroll_Lock))) {
+    vlog.debug("Ignoring lock key (e.g. caps lock)");
+    return;
+  }
+  // Always ignore ScrollLock though as we don't have a heuristic
+  // for that
+  if (key == XK_Scroll_Lock) {
+    vlog.debug("Ignoring lock key (e.g. caps lock)");
+    return;
+  }
+
+  if (down && (server->ledState != ledUnknown)) {
+    // CapsLock synchronisation heuristic
+    // (this assumes standard interaction between CapsLock the Shift
+    // keys and normal characters)
+    if (((key >= XK_A) && (key <= XK_Z)) ||
+        ((key >= XK_a) && (key <= XK_z))) {
+      bool uppercase, shift, lock;
+
+      uppercase = (key >= XK_A) && (key <= XK_Z);
+      shift = pressedKeys.find(XK_Shift_L) != pressedKeys.end() ||
+              pressedKeys.find(XK_Shift_R) != pressedKeys.end();
+      lock = server->ledState & ledCapsLock;
+
+      if (lock == (uppercase == shift)) {
+        vlog.debug("Inserting fake CapsLock to get in sync with client");
+        server->desktop->keyEvent(XK_Caps_Lock, true);
+        server->desktop->keyEvent(XK_Caps_Lock, false);
+      }
+    }
+
+    // NumLock synchronisation heuristic
+    // (this is more cautious because of the differences between Unix,
+    // Windows and macOS)
+    if (((key >= XK_KP_Home) && (key <= XK_KP_Delete)) ||
+        ((key >= XK_KP_0) && (key <= XK_KP_9)) ||
+        (key == XK_KP_Separator) || (key == XK_KP_Decimal)) {
+      bool number, shift, lock;
+
+      number = ((key >= XK_KP_0) && (key <= XK_KP_9)) ||
+                (key == XK_KP_Separator) || (key == XK_KP_Decimal);
+      shift = pressedKeys.find(XK_Shift_L) != pressedKeys.end() ||
+              pressedKeys.find(XK_Shift_R) != pressedKeys.end();
+      lock = server->ledState & ledNumLock;
+
+      if (shift) {
+        // We don't know the appropriate NumLock state for when Shift
+        // is pressed as it could be one of:
+        //
+        // a) A Unix client where Shift negates NumLock
+        //
+        // b) A Windows client where Shift only cancels NumLock
+        //
+        // c) A macOS client where Shift doesn't have any effect
+        //
+      } else if (lock == (number == shift)) {
+        vlog.debug("Inserting fake NumLock to get in sync with client");
+        server->desktop->keyEvent(XK_Num_Lock, true);
+        server->desktop->keyEvent(XK_Num_Lock, false);
+      }
+    }
+  }
+
   // Turn ISO_Left_Tab into shifted Tab.
   VNCSConnectionSTShiftPresser shiftPresser(server->desktop);
   if (key == XK_ISO_Left_Tab) {
diff --git a/common/rfb/VNCServer.h b/common/rfb/VNCServer.h
index 982a4ff..c5335ad 100644
--- a/common/rfb/VNCServer.h
+++ b/common/rfb/VNCServer.h
@@ -74,6 +74,10 @@
 
     // setName() tells the server what desktop title to supply to clients
     virtual void setName(const char* name) = 0;
+
+    // setLEDState() tells the server what the current lock keys LED
+    // state is
+    virtual void setLEDState(unsigned int state) = 0;
   };
 }
 #endif
diff --git a/common/rfb/VNCServerST.cxx b/common/rfb/VNCServerST.cxx
index ec5e962..28f6a62 100644
--- a/common/rfb/VNCServerST.cxx
+++ b/common/rfb/VNCServerST.cxx
@@ -58,6 +58,7 @@
 #include <rfb/Security.h>
 #include <rfb/KeyRemapper.h>
 #include <rfb/util.h>
+#include <rfb/ledStates.h>
 
 #include <rdr/types.h>
 
@@ -74,7 +75,7 @@
 
 VNCServerST::VNCServerST(const char* name_, SDesktop* desktop_)
   : blHosts(&blacklist), desktop(desktop_), desktopStarted(false),
-    blockCounter(0), pb(0),
+    blockCounter(0), pb(0), ledState(ledUnknown),
     name(strDup(name_)), pointerClient(0), comparer(0),
     cursor(new Cursor(0, 0, Point(), NULL)),
     renderedCursorInvalid(false),
@@ -458,6 +459,11 @@
   }
 }
 
+void VNCServerST::setLEDState(unsigned int state)
+{
+  ledState = state;
+}
+
 // Other public methods
 
 void VNCServerST::approveConnection(network::Socket* sock, bool accept,
diff --git a/common/rfb/VNCServerST.h b/common/rfb/VNCServerST.h
index 00f77c7..2dfdbbd 100644
--- a/common/rfb/VNCServerST.h
+++ b/common/rfb/VNCServerST.h
@@ -101,6 +101,7 @@
     virtual void setCursor(int width, int height, const Point& hotspot,
                            const rdr::U8* data);
     virtual void setCursorPos(const Point& p);
+    virtual void setLEDState(unsigned state);
 
     virtual void bell();
 
@@ -209,6 +210,7 @@
     int blockCounter;
     PixelBuffer* pb;
     ScreenSet screenLayout;
+    unsigned int ledState;
 
     CharArray name;