Получаем информацию о рабочем месте пользователя

от автора

image

0. Предисловие

Все началось с очередного звонка пользователя, который с гордостью сообщил сообщил: — "Всё сломалось" и в моих "потугах" удаленно найти PC, на котором работает данный пользователь.

Решение планировало быть простым до безумия и собираться на коленке. Так-как большинство наших сотрудников работают под "виндой" и все рабочие станции входят в домен, был задан вектор поиска решения. Изначально, планировалось написать небольшой скрипт. В его задачу входило собрать базовую информацию о системе и сотруднике, который за этой системой работает. Набор информации минимальный. А именно: логин, название рабочей станции и ее ip. Результат работы сохраняем на сервере, а сам скрипт "вешаем" на пользователя через GPO.

В такой реализации были существенные недостатки в виде:

  • получить информацию можно было бы только зайдя на сервер (его сетевую папку где хранился файл), что не всегда удобно
  • поддерживать файл в актуальном состоянии
  • получать данные в реальном времени

После раздумий пришло решение: использовать бота в Telegram. Прибегнув к небольшой ловкости рук, скрипт был переписан в небольшую программку для отправки информации в чат, за место "скучной" записи в файл на сервере. (+ были добавлены еще некоторые параметры которые оправлялись боту)

image
P.S. Данные приведенные на изображение отцензурированы для сохранения коммерческой тайны.

Но и такой подход решил только проблему с доступностью информации, одновременно сохраняя остальные минусы старого подхода.

Нужно было что-то менять. Было решено написать полноценное клиент-серверное приложение.
Концепция проста. Пишем сервер который будет обслуживать входящие соединения от клиента и отсылать ему запрашиваемую информацию.

1. Пишем сервер

Для начала выбираем протокол для "общения". Выбор не велик — UDP/TCP. Я решил в пользу TCP. Преимущества очевидны:

  • обеспечивает надежную связь
  • обмен данными в рамках одной сессии

Начнем с создания класса пользователя

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; } }

Изначально он имел только 3 свойства. Но в процессе разработки, код сервера вида изменялся. Появлялся новый функционал. Версия стала необходима для совместимости клиента и сервера. Брать версию из сборки я не стал, решив что она избыточна. Так-же появилась возможность делать скрин экрана пользователя.

Конструктор:

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; }

Забегая немного в перед, скажу что изначально данные передавались через BinaryWriter "построчно" и без приведения к общему типу данных. Что было очень неудобно при добавление новых функций в приложение. Переписывание функции отправки данных привело к добавлению возможности их сериализации. Теперь объект User можно было представить в трех видах:

  • 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();             }         }      }

Что-бы иметь возможность десериализации бинарного объекта User, пришлось вынести его в отдельную библиотеку и использовать в программе уже через нее.

Также хочу обратить внимание на поток который мы получаем на выходе. Массив байтов возвращается через метод ToArray. Его минус — он создает копию стрима в памяти. Но это не критично, в отличие от использования метода GetBuffer, который возвращает нам не чистый массив данных, а полностью весь поток (смысл в том что память выделенная под поток может быть заполнена не полностью), в результате мы получаем увеличения массива. К сожалению этот нюанс я увидел не сразу. А только при детальном анализе данных.

За обработку наших соединений отвечает класс ClientObject

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();         }     }

В нем описываются все команды которые поступают от клиента. Команды реализованы очень просто. Подразумевалось что клиентом может выступать любое устройство, программа или сервер обработки. Поэтому характер ответа задается получением одного байта:

[Flags] enum Commands : byte {         GetInfoBin = 0x0a,         GetInfoJSON = 0x0b,         GetInfoXML = 0x0c,         GetScreen = 0x14,         GetUpdate = 0x15,         GetTest = 0xff }

Можно быстро добавить новый функционал или построить сложную логику поведения, которая будет определяться все-го 1 байтом используя битовую маску. Для удобства все байты приведены к читаемым командам.

За оправку данных отвечает метод Sender который принимает на вход объект TcpClient и набор данных в виде массива байтов.

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);             }         }

Тоже все довольно сдержанно. Создаем BinaryWriter из потока от TcpClient пишем в него массив байт, отчищаем и закрываем.

За создание объекта User, отвечает метод ._Info который имеет перегрузку

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 }); }

Инициализируем новый экземпляр User, заполняем конструктор и сразу вызываем метод .GetBinary для получения сериализованных данных. Перегрузка понадобиться нам, если мы хотим явно указать какой тип данных мы хотим получить.

Метод ._Screen, отвечает за создание скриншота рабочего стола.

Из интересного. Здесь можно выделить метод CmdUpdate. Он принимает на вход
текущей процесс:

CmdUpdate(Process.GetCurrentProcess());

Данный метод реализует обновления нашего сервера по команде клиента. Внутри него создается объект WebClient, который скачивает программу помощник с сервера/сайта указанного источника, необходимую для обновления самого сервера. После чего запускает ее и передает в качестве входного параметра, ID текущего процесса:

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());

Точкой входа, у обработчика выступает ._Process. Он создает BinaryReader и считывает из него байт команды. В зависимости от полученного байта, выполняется та или иная операция. В конце мы завершаем работу клиента и отчищаем память.

Получать obj TcpClient, мы будем с помощью TcpListener в вечном цикле, используя .AcceptTcpClient. Полученный объект клиента, мы передаем в наш обработчик. Запуская его в новом потоке, для избежания блокировки 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");         }                  }

Еще сервер имеет пару вспомогательных классов: Logger и 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;         }     }

В дальнейшем планируется возможность сохранения и считывание настроек из файла.

Класс Logger, позволяет нам сохранять в файл события которые возникли во время выполнения программы. Есть возможность, через настройки отключить запись логов.

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

Будет, но чуть позже.

Исходники проекта на GitHub

ссылка на оригинал статьи https://habrahabr.ru/post/328914/