📜 ⬆️ ⬇️

Get information about the user's workplace

image


0. Preface


It all started with the next call of the user, who proudly said: - “Everything broke,” and with my “attempts” to remotely find the PC on which this user works.


The decision planned to be simple to madness and gather on the knee. So, as most of our employees work under the "Windows" and all workstations are included in the domain, a search vector of the solution 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 "hung" on the user through the GPO.


In this implementation there were significant drawbacks in the form:



After deliberation, the decision came: use a bot in the Telegram. Having resorted to a little manual dexterity, the script was rewritten into a small program to send information to the chat, for the place of the “boring” record in the file on the server. (+ some more parameters were added which were sent to the bot)


image
PS The data shown in the image is censored to preserve commercial secrets.


But even this approach solved only the problem with the availability of information, at the same time preserving the remaining drawbacks of the old approach.


It was necessary to change something. It was decided to write a full client-server application.
The concept is simple. We are writing a server that will serve incoming connections from the client and send it the requested information.


1. We write server


To begin, choose a protocol for "communication". The choice is not great - UDP / TCP. I decided in favor of TCP. The advantages are obvious:



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, he had only 3 properties. But during the development process, the server code of the form 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, having decided that it is redundant. It is also an opportunity to make a screen of the user.


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 screen shot. Therefore, we create a 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 bit in front, I’ll say that the data was initially transferred via BinaryWriter "line by line" and without being reduced to a common data type. What was very inconvenient when adding new features to the application. Rewriting the function of sending data led to the addition of the possibility of their serialization. Now the User object could be represented in three forms:



 [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 a binary User object, I had to put it into a separate library and use it in the program already through it.


I also want to draw attention to the flow that we get at the output. The byte array is returned via the ToArray method. His minus - he creates a copy of the stream in memory. But this is not critical, unlike using the GetBuffer method, which returns not a pure array of data, 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. And only with detailed data analysis.


The class ClientObject is responsible for handling 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. The implication was that the client could be any device, program or server processing. Therefore, the nature of the response is given by the receipt of 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 logic of behavior, which will be determined by the entire 1 byte using a bit mask. For convenience, all bytes are given to readable commands.


The Sender method is responsible for sending data, which takes as input the TcpClient object and the 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 pretty restrained. Create a BinaryWriter from a stream from TcpClient, write an array of bytes into it, clean it up and close it.


For creating a User object, the ._Info method is responsible for overloading.


 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, populate the constructor, and immediately call the .GetBinary method to get the serialized data. We will need overloading 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.


From interesting. Here you can select the CmdUpdate method. He takes to the entrance
current process:


 CmdUpdate(Process.GetCurrentProcess()); 

This method implements our server updates at the command of the client. Inside it, a WebClient object is created that downloads an assistant program with server / site specified source needed to update the server itself. Then it starts it and passes the current process ID as an input parameter:


 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 a command byte from it. Depending on the received byte, this or that operation is performed. At the end, we shut down the client and clear the memory.


To get obj TcpClient, we will be using the TcpListener in the perpetual loop using .AcceptTcpClient. The resulting client object, we pass to our handler. Running 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"); } } 

Another server has a couple of auxiliary 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 we plan to save and read settings from a file.


The Logger class allows us to save to the file the events that occurred during the execution of the program. It is possible, through the settings to disable the recording of logs.


 static class Logger { static Stack<string> log_massiv = new Stack<string>(); 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<string> 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


It will, but a little later.


Project sources on GitHub


')

Source: https://habr.com/ru/post/328914/


All Articles