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

в 15:47, , рубрики: .net, C#, администрирование windows, разработка под windows

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

Автор: ruhex0

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js