We get information about the user's workplace

0. Foreword
It all started with the next call from a user who proudly said: “Everything’s broken,” and from my “attempts” to remotely find the PC on which this user is working ..
The solution was planned to be simple to the point of frenzy and pack on your knee. Since most of our employees work under Windows and all workstations are included in the domain, a solution search vector was set. Initially, it was planned to write a small script. His task was to collect basic information about the system and the employee who works for this system. The set of information is minimal. Namely: login, name of the workstation and its ip. The result of the work is saved on the server, and the script itself is "hanged" on the user through the GPO.
In this implementation, there were significant shortcomings in the form of:
- information could be obtained only by going to the server (its network folder where the file was stored), which is not always convenient
- keep the file up to date
- receive data in real time
After reflection, the decision came: to use the bot in Telegram. Having resorted to a little sleight of hand, the script was rewritten into a small program for sending information to the chat, for the place of "boring" recording to a file on the server. (+ some more parameters were added that were recovering to the bot)

PS The data shown on the image is censored to preserve trade secrets.
But such an approach solved only the problem with the availability of information, while preserving the remaining disadvantages of the old approach.
It was necessary to change something. It was decided to write a full-fledged client-server application.
The concept is simple. We are writing a server that will serve incoming connections from the client and send him the requested information.
1. We write the server
First, select the protocol for "communication". The choice is not great - UDP / TCP. I decided in favor of TCP. The benefits are obvious:
- provides reliable communication
- single session data exchange
Let's start by creating a user class.
public class User
{
public string Name { get; set; }
public string PC { get; set; }
public string IP { get; set; }
public string Version { get; set; }
public byte[] Screen { get; set; }
}Initially, it had only 3 properties. But during the development process, the view server code changed. There was a new functionality. A version has become necessary for client and server compatibility. I did not take the version from the assembly, deciding that it was redundant. Also, it became possible to make a screen of the user’s screen.
Constructor:
public User(string name, string pc, string ip, string version)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
}We do not always need to transfer a screenshot. Therefore, we create constructor overload:
public User(string name, string pc, string ip, string version, byte[] screen)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
this.Screen = screen;
}Running a little ahead, I will say that initially the data was transmitted through BinaryWriter "line by line" and without casting to a common data type. Which was very inconvenient when adding new features to the application. Rewriting the data sending function has added the ability to serialize it. Now the User object could be represented in three forms:
- Binary
- Json
- XML
[Serializable, DataContract]
public class User
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string PC { get; set; }
[DataMember]
public string IP { get; set; }
[DataMember]
public string Version { get; set; }
[DataMember]
public byte[] Screen { get; set; }
public User(string name, string pc, string ip, string version)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
}
public User(string name, string pc, string ip, string version, byte[] screen)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
this.Screen = screen;
}
public byte[] GetBinary()
{
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, this);
return stream.ToArray();
}
}
public byte[] GetXML()
{
XmlSerializer formatter = new XmlSerializer(typeof(User));
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, this);
return stream.ToArray();
}
}
public byte[] GetJSON()
{
DataContractJsonSerializer jsonFormatter = new DataContractJsonSerializer(typeof(User));
using (MemoryStream stream = new MemoryStream())
{
jsonFormatter.WriteObject(stream, this);
return stream.ToArray();
}
}
}In order to be able to deserialize the binary User object, I had to transfer it to a separate library and use it in the program through it.
I also want to pay attention to the stream that we get at the output. An array of bytes is returned through the ToArray method. Its minus - it creates a copy of the stream in memory. But this is not critical, in contrast to using the GetBuffer method, which does not return a clean data array to us, but the entire stream (the point is that the memory allocated for the stream may not be completely filled), as a result we get an increase in the array. Unfortunately, I did not see this nuance right away. But only with a detailed analysis of the data.
The ClientObject class is responsible for processing our connections.
public class ClientObject
{
public TcpClient client;
[Flags]
enum Commands : byte
{
GetInfoBin = 0x0a,
GetInfoJSON = 0x0b,
GetInfoXML = 0x0c,
GetScreen = 0x14,
GetUpdate = 0x15,
GetTest = 0xff
}
public ClientObject(TcpClient tcpClient)
{
client = tcpClient;
}
protected void Sender(TcpClient client, byte[] data)
{
try
{
Logger.add("Sender OK 0xFF");
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(data);
writer.Flush();
writer.Close();
}
catch (Exception e)
{
Logger.add(e.Message + "0xFF");
}
}
protected byte[] _Info ()
{
return new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version, _Screen()).GetBinary();
}
protected byte[] _Info(string type)
{
User.User tmp = new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version);
switch (type)
{
case "bin": return tmp.GetBinary();
case "json": return tmp.GetJSON();
case "xml": return tmp.GetXML();
}
return (new byte[1] { 0x00 });
}
protected byte[] _Screen()
{
Bitmap bm = new Bitmap(Screen.PrimaryScreen.Bounds.Width,
Screen.PrimaryScreen.Bounds.Height);
Graphics gr = Graphics.FromImage(bm as Image);
gr.CopyFromScreen(0, 0, 0, 0, bm.Size);
using (MemoryStream stream = new MemoryStream())
{
bm.Save(stream, ImageFormat.Jpeg);
return stream.ToArray();
}
}
protected byte[] _Test()
{
return Encoding.UTF8.GetBytes("Test send from server");
}
public void CmdUpdate(Process process)
{
Logger.add("Command from server: Update");
try
{
string fileName = "Update.exe", myStringWebResource = null;
WebClient myWebClient = new WebClient();
myStringWebResource = Settings.UrlUpdate + fileName;
myWebClient.DownloadFile(myStringWebResource, fileName);
Process.Start("Update.exe", process.Id.ToString());
}
catch (Exception e)
{
Logger.add(e.Message);
}
finally
{
Logger.add("Command end");
}
}
public void _Process()
{
try
{
BinaryReader reader = new BinaryReader(this.client.GetStream());
byte cmd = reader.ReadByte();
Logger.add(cmd.ToString());
switch ((Commands)cmd)
{
case Commands.GetInfoBin: Sender(this.client, _Info("bin")); break;
case Commands.GetInfoJSON: Sender(this.client, _Info("json")); break;
case Commands.GetInfoXML: Sender(this.client, _Info("xml")); break;
case Commands.GetScreen: Sender(this.client, _Screen()); break;
case Commands.GetUpdate: CmdUpdate(Process.GetCurrentProcess()); break;
case Commands.GetTest: Sender(this.client, _Test()); break;
default: Logger.add("Incorrect server command "); break;
}
reader.Close();
}
catch (Exception e)
{
Logger.add(e.Message + " 0x2F");
}
finally
{
Logger.add("Client close connect");
this.client.Close();
MemoryManagement.FlushMemory();
}
}
static string GetIp()
{
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
return host.AddressList.FirstOrDefault(ip => ip.AddressFamily ==
AddressFamily.InterNetwork).ToString();
}
}It describes all the commands that come from the client. Commands are implemented very simply. It was understood that the client could be any device, program or processing server. Therefore, the nature of the response is determined by receiving one byte:
[Flags]
enum Commands : byte
{
GetInfoBin = 0x0a,
GetInfoJSON = 0x0b,
GetInfoXML = 0x0c,
GetScreen = 0x14,
GetUpdate = 0x15,
GetTest = 0xff
}You can quickly add new functionality or build complex behavior logic, which will be determined by only 1 byte using a bit mask. For convenience, all bytes are given to readable commands.
The Sender method is responsible for sending data, which receives a TcpClient object and a data set as an array of bytes.
protected void Sender(TcpClient client, byte[] data)
{
try
{
Logger.add("Sender OK");
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(data);
writer.Flush();
writer.Close();
}
catch (Exception e)
{
Logger.add(e.Message);
}
}Everything is also quite restrained. We create a BinaryWriter from a stream from TcpClient, write an array of bytes into it, clean it and close it.
The User object is responsible for creating the ._Info method, which has overload
protected byte[] _Info ()
{
return new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version, _Screen()).GetBinary();
}
protected byte[] _Info(string type)
{
User.User tmp = new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version);
switch (type)
{
case "bin": return tmp.GetBinary();
case "json": return tmp.GetJSON();
case "xml": return tmp.GetXML();
}
return (new byte[1] { 0x00 });
}We initialize a new instance of User, fill out the constructor, and immediately call the .GetBinary method to obtain serialized data. We need overload if we want to explicitly indicate what type of data we want to receive.
The ._Screen method, is responsible for creating a screenshot of the desktop.
Of the interesting. Here you can highlight the CmdUpdate method. It accepts the
current process as an input :
CmdUpdate(Process.GetCurrentProcess());This method implements updates to our server at the command of the client. Inside it, a WebClient object is created, which the assistant program downloads withserver / sitethe specified source required to update the server itself. Then it starts it and passes as the input parameter, the ID of the current process:
string fileName = "Update.exe", myStringWebResource = null;
WebClient myWebClient = new WebClient();
myStringWebResource = Settings.UrlUpdate + fileName;
myWebClient.DownloadFile(myStringWebResource, fileName);
Process.Start("Update.exe", process.Id.ToString());The entry point for the handler is ._Process. It creates a BinaryReader and reads the command byte from it. Depending on the byte received, one or another operation is performed. At the end, we complete the client’s work and clear the memory.
To get obj TcpClient, we will be using TcpListener in a perpetual loop using .AcceptTcpClient. We pass the received client object to our handler. By launching it in a new thread, to avoid blocking the main thread
static TcpListener listener;
try
{
listener = new TcpListener(IPAddress.Parse("127.0.0.1"), Settings.Port);
listener.Start();
Logger.add("Listener start");
while (true)
{
TcpClient client = listener.AcceptTcpClient();
ClientObject clientObject = new ClientObject(client);
Task clientTask = new Task(clientObject._Process);
clientTask.Start();
MemoryManagement.FlushMemory();
}
}
catch (Exception ex)
{
Logger.add(ex.Message);
}
finally
{
Logger.add("End listener");
if (listener != null)
{
listener.Stop();
Logger.add("Listener STOP");
}
}The server also has a couple of helper classes: Logger and Settings
static public class Settings
{
static public string Version { get; set; }
static public string Key { set; get; }
static public string UrlUpdate { get; set; }
static public int Port { get; set; }
static public bool Log { get; set; }
static public void Init(string version, string key, string urlUpdate, int port, bool log)
{
Version = version;
Key = key;
UrlUpdate = urlUpdate;
Port = port;
Log = log;
}
}In the future, it is planned to save and read settings from a file.
The Logger class allows us to save events that occurred during program execution to a file. It is possible to disable logging through the settings.
static class Logger
{
static Stack log_massiv = new Stack();
static string logFile = "log.txt";
static public void add(string str)
{
log_massiv.Push(time() + " - " + str);
write(log_massiv, logFile, Settings.Log);
}
private static void write(Stack strs, string file, bool log)
{
if (log)
{
File.AppendAllLines(file, strs);
log_massiv.Clear();
}
}
private static string time()
{
return
DateTime.Now.Day + "." +
DateTime.Now.Month + "." +
DateTime.Now.Year + " " +
DateTime.Now.Hour + ":" +
DateTime.Now.Minute + ":" +
DateTime.Now.Second;
}
} 2. Client
Will be, but a little later.