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

Поделиться с друзьями
-->

Комментарии (15)


  1. IL_Agent
    17.05.2017 23:21

    Почему же не wcf, webapi или asp.net core наконец? :)


    1. ruhex0
      18.05.2017 04:53

      Предвидел такой вопрос. Изначально задумывалось написать за вечер и залить на пеки пользователей. Но все, переросло в нечто большее =) Wcf в перспективе.


  1. LoadRunner
    18.05.2017 08:57

    У меня совет автору для второй части — для скриншота активного окна лучше использовать Alt+PrintScreen. Порой коммерческой тайной может быть даже панель задач со всеми значками.


    1. ruhex0
      18.05.2017 09:05

      Спасибо, учту )


  1. aquamakc
    18.05.2017 09:12

    Почему-бы просто не выполнять WMI запросы удалённо?


  1. Usef
    18.05.2017 09:40

    Если не секрет, чем не устроил BGinfo?


  1. Zam_dev
    18.05.2017 10:54

    Зачем плодить конструкторы…

    public User(string name, string pc, string ip, string version)
    + public User(string name, string pc, string ip, string version, byte[] screen)
    = public User(string name, string pc, string ip, string version, byte[] screen=null)


  1. k0rsh
    18.05.2017 13:28

    Знакомое тело сервера. У Metanit'а такой пример был, кажется, для консольного чата.
    Но речь о другом.

    Почему, например, не сделать блок через using?

    protected void Sender(TcpClient client, byte[] data)
            {
                try
                {
                    Logger.add("Sender OK");
                    using(BinaryWriter writer = new BinaryWriter(client.GetStream()))
                    {
                        writer.Write(data);
                        writer.Flush();
                    }
                }
                catch (Exception e)
                {
                    Logger.add(e.Message);
                }
            }
    


    И еще вопрос — не увидел блокировок в логгере. Как он себя поведет, когда несколько потоков его дернут одновременно?


    1. ruhex0
      18.05.2017 13:38

      Да, вы правы. Сходство есть. До это-го не разу не работал с сетью в c#, пришлось учиться на лету.
      Можно через using. Так будет правильно и надежно.
      Проблему с одновременным вызовом не решал. Еще есть моменты которые нужно исправить. Но и логгер без внимания не оставлю )


      1. k0rsh
        18.05.2017 13:53

        Ну и, кстати, о логгере. Сдается мне, что передавать стек в метод write через ссылку strs и в этом же методе очищать его непосредственно как log_massiv.Clear() — это не очень правильная практика.

        Кроме того, не мешало бы завернуть File.AppendAllLines в блок try/catch на всякий случай.


      1. k0rsh
        18.05.2017 14:39

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

        class MyClass
         {
            protected int num;
        
            public void NumMethod(int num)
            {
                this.num = num;
            }
        }
        


        Еще позволю себе совет: в именовании переменных и методов лучше сразу стараться себя приучить делать это dotnet way :) Никаких подчеркиваний в именах переменных/методов, за исключением приватных полей класса:

        private ILogger _logger = null;
        protected Stack<string> logMassiv = new Stack<string>();
        protected int variableName = 0;
        
        public void MyMetod(int variableOne, int variableTwo);
        

        ну и т.д. :))


  1. aquamakc
    18.05.2017 13:49

    Но и логгер без внимания не оставлю

    Делал реализацию своего логера через потокобезопасную очередь. Суть такова:
    Логгер — синглтон, с единственным публичным методом а-ля «addLogRecord».
    В обработчике метода происходит добавление записи в очередь и запуск отдельного потока, если он не запущен.
    Сам процесс логирования осуществляется в отдельном потоке, который работает, если очередь не пустая.
    Этот поток забирает записи из очереди, производит их вывод (например, в файл) и умирает, если записей больше нет.

    Кстати, это очень плохой подход:
    DateTime.Now.Day + "." + 
                    DateTime.Now.Month + "." + 
                    DateTime.Now.Year + " " + 
                    DateTime.Now.Hour + ":" + 
                    DateTime.Now.Minute + ":" + 
                    DateTime.Now.Second;
    

    Лучше так:
    DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss");
    


    1. k0rsh
      18.05.2017 14:03

      Сам процесс логирования осуществляется в отдельном потоке, который работает, если очередь не пустая.
      Этот поток забирает записи из очереди, производит их вывод (например, в файл) и умирает, если записей больше нет.


      Для таких целей можно попробовать постоянно живущий BackgroundWorker и блокировку очереди сообщений посредством ReaderWriterLock. А чтобы не убивать каждый раз процесс, можно построить на базе AutoResetEvent флажок, который поднимается при записи сообщения в очередь и поднятия которого ждет фоновый процесс через WaitOne().


      1. aquamakc
        18.05.2017 14:06

        Конечно можно. Вариантов много. Суть не меняется — вынос процесса логирования в отдельный поток. С передачей в него динамической коллекции записей.
        Кстати, если используется потокобезопасная очередь ConcurrentQueue, то никаких внешних блокировок не надо делать.


  1. aquamakc
    18.05.2017 14:11

    Возвращаясь к логам, я бы порекомендовал ввести систему ранжирования сообщений. DEBUG, INFO, ALERT, ERROR и т.д. Это позволит задавать, как разные стили вывода сообщений, так и фильтровать избыточные. Ну в общем насколько хватит фантазии.