
0. Предисловие
Все началось с очередного звонка пользователя, который с гордостью сообщил сообщил: — „Всё сломалось“, и с моих „потуг“ удаленно найти PC, на котором работает данный пользователь..
Решение планировало быть простым до безумия и собираться на коленке. Так-как большинство наших сотрудников работают под "виндой" и все рабочие станции входят в домен, был задан вектор поиска решения. Изначально, планировалось написать небольшой скрипт. В его задачу входило собрать базовую информацию о системе и сотруднике, который за этой системой работает. Набор информации минимальный. А именно: логин, название рабочей станции и ее ip. Результат работы сохраняем на сервере, а сам скрипт "вешаем" на пользователя через GPO.
В такой реализации были существенные недостатки в виде:
- получить информацию можно было бы только зайдя на сервер (его сетевую папку где хранился файл), что не всегда удобно
- поддерживать файл в актуальном состоянии
- получать данные в реальном времени
После раздумий пришло решение: использовать бота в Telegram. Прибегнув к небольшой ловкости рук, скрипт был переписан в небольшую программку для отправки информации в чат, за место "скучной" записи в файл на сервере. (+ были добавлены еще некоторые параметры которые оправлялись боту)

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
Будет, но чуть позже.
