Конвертация скрипта Bash в код С# для отправки СМС через usb модем HUAWEI E3372

    Несколько моих проектов отправляют СМС и в последнее время после обновлений сервера, а может и ряда других причин, отправка СМС стала почти невозможной.

    Старенький USB модем HUAWEI (марку не буду разглашать) перестал стабильно висеть на одном COM порту и временами переподключался на другие порты, совсем отключался и терял антенну.

    Да и ситуация с библиотекой GSMComm была непонятной и болезненной.
    GSMComm — это пакет для телефонов GSM, в основном для выполнения задач, связанных с SMS.
    www.nuget.org/packages/GSMComm последня версия 1.21.1 от 10.10.2015 года.
    Поиск по интерент показал, что есть возможность использовать встроенный функционал WEB API новых модемов HUAWEI, более эффективно, чем старый подход с AT командами реализованный в GSMComm.

    Выяснилось, что есть прекрасный usb модем HUAWEI E3372, который почти хакерским способом способен отправлять СМС как из скрипта (Curl + Bash), так и из кода (Python, Perl), и, как я предположил, из C#.

    Самое печальное, что компания HUAWEI не предоставляет никакой документации как это сделать и все найденные методы имели экспериментальный харктер и зависели от семейства устройств.

    В общем, опираясь на найденный материал, не гарантирующий работу кода с момедом, был приобретен HUAWEI E3372.

    Не углублясь в эксперименты с Python или Perl я решил попробовать разобраться с вариантами Bash + Curl.

    В общем, после нескольких экспериментов был найден работающий код под MS Windows 10 + Git Bash for MS Windows.

    Скрипт

    curl -b session.txt -c session.txt http://192.168.8.1/html/index.html > /dev/null 2>&1
    
    #TOKEN=$(curl -s -b session.txt -c session.txt http://192.168.8.1/html/smsinbox.html)
    TOKEN=$(echo $TOKEN | cut -d'"' -f 10)
    echo $TOKEN > token.txt
    
    NUMBER=$1
    MESSAGE=$2
    
    LENGTH=${#MESSAGE}
    TIME=$(date +"%Y-%m-%d %T")
    TOKEN=$(<token.txt)
    
    SMS="<?xml version='1.0' encoding='UTF-8'?><request><Index>-1</Index><Phones><Phone>$NUMBER</Phone></Phones><Sca/><Content>$MESSAGE</Content><Length>$LENGTH</Length><Reserved>1</Reserved><Date>$TIME</Date></request>"
    
    echo $SMS
    
    curl -v -b session.txt -c session.txt -H "X-Requested-With: XMLHttpRequest" --data "$SMS" http://192.168.8.1/api/sms/send-sms --header "__RequestVerificationToken: $TOKEN" --header "Content-Type:text/xml" --header "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" --header "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3"

    Скрипт удивительным образом работал. И это было уже счастье, так как деньги были потрачены на модем не зря!

    Осталось только понять КАК же он работает. Почитав документацию по Curl и Bash (ну по bash я не читал так догадался) прояснилась работа скрипта. Привожу этот же скрипт с моими комментариями.

    Скрипт с комментариями

    # https://stackoverflow.com/questions/28070500/grab-current-sessions-cookie-with-curl/28070870
    # Содержимое файла session.txt определяется опцией -b
    # Сделать GET запрос и получить куки в первый раз и записать их в файл
    curl -b session.txt -c session.txt http://192.168.8.1/html/index.html > /dev/null 2>&1
    
    # Сделать GET запрос и получить куки во второй раз и записать в файл и сохранить содержимое страницы HTML в переменную TOKEN как строку
    TOKEN=$(curl -b session.txt -c session.txt http://192.168.8.1/html/smsinbox.html)
    
    # Извлчеь из переменной значение метатега из <meta name="csrf_token" content="b/XNeODpHCthQXEOEjBNkICn2n7e9v4e"/> и перезаписать в ту же переменную TOKEN
    TOKEN=$(echo $TOKEN | cut -d'"' -f 10)
    
    # Отобразить на экране
    echo "$TOKEN"
    
    # сохранить подстроку в файле
    echo $TOKEN > token.txt
    
    # Получить два параметра командной строки: (1) номер телефона и (2) текст СМС
    NUMBER=$1
    MESSAGE=$2
    
    # Получить количество символов в тексте
    LENGTH=${#MESSAGE}
    
    # Получить текущее время и отформатировать его
    TIME=$(date +"%Y-%m-%d %T")
    
    # Загрузить содержимое файла в переменную
    TOKEN=$(<token.txt)
    
    # Сфоромировать текст для отправки СМС как XML 
    SMS="<?xml version='1.0' encoding='UTF-8'?><request><Index>-1</Index><Phones><Phone>$NUMBER</Phone></Phones><Sca/><Content>$MESSAGE</Content><Length>$LENGTH</Length><Reserved>1</Reserved><Date>$TIME</Date></request>"
    
    # Отобразить переменную с текстом СМС на экране 
    echo $SMS
    
    # Сделать POST для отправки СМС
    curl -v -b session.txt -c session.txt -H "X-Requested-With: XMLHttpRequest" --data "$SMS" http://192.168.8.1/api/sms/send-sms --header "__RequestVerificationToken: $TOKEN" --header "Content-Type:text/xml"
    

    Скажу честно, что СМС на русском я не смог добиться. Приходит абракадабра. Так что этот вопрос остался открытым и если у кого-то есть желание закрыть тему отправки СМС на русском — милости просим, дерзайте.

    Понимание работы скрипта принесло свою пользу и приблизило к написанию кода на C#.

    Было понятно, что в нем должно быть также 3 запроса и должны они делать то же самое что и благословенный Curl. Поэтому в коде приведен Curl, а ниже, аналогичный ему C# код.

    Код С# для WinForms

    private void button1_Click(object sender, EventArgs e)
    {
       var ip = "192.168.8.1"; // IP адрес который выдает модем в браузере после установки
       var phone = "+70000000000"; // Номер телефона
       var msg = "Привет!!! СМС работает!!!";
    
       var result = SendSMS(ip, phone, msg);
       if (result)
       {
           //TODO  Сохранить в БД, например
       }
       else
       {
           //TODO  Сохранить в БД, например
       }
    }
    
    private bool SendSMS(string ip, string phone, string msg)
    {
        try
        {
            /* curl -b session.txt -c session.txt http://192.168.8.1/html/index.html > /dev/null 2>&1 */
    
            Cookie firstCookie = null;
            Cookie secondCookie = null;
            string token = string.Empty;
    
            //В первый раз получить куки
            var cookieContainer = new CookieContainer();
            var uri = new Uri($"http://{ip}/html/index.html");
            using (var httpClientHandler = new HttpClientHandler { CookieContainer = cookieContainer })
            {
                using (var httpClient = new HttpClient(httpClientHandler))
                {
                    httpClient.GetAsync(uri).Wait();
                    var all = cookieContainer.GetCookies(uri);
                    firstCookie = all[0];
                }
            }
    
            /*
            TOKEN=$(curl -s -b session.txt -c session.txt http://192.168.8.1/html/smsinbox.html)
            TOKEN=$(echo $TOKEN | cut -d'"' -f 10)
            echo $TOKEN > token.txt
             */
    
    	    // И спользуя куки из первого запроса получить страницу и извлечь из нее токен
            if (firstCookie != null)
            {
                var cookieContainer2 = new CookieContainer();
                cookieContainer2.Add(firstCookie); // Поместить в конейнер куки из первого запроса к сайту
                var uri2 = new Uri($"http://{ip}/html/smsinbox.html");
                using (var httpClientHandler = new HttpClientHandler
                {
                    CookieContainer = cookieContainer2
                })
                {
                    using (var httpClient = new HttpClient(httpClientHandler))
                    {
                        var html = httpClient.GetStringAsync(uri2).Result; // Получить страницу HTML
    
                        var all = cookieContainer2.GetCookies(uri2);
                        secondCookie = all[0];
    
                        var doc = new HtmlAgilityPack.HtmlDocument(); // Используем HtmlAgilityPack чтобы преобразовать текст HTML в структурный вид 
                        doc.LoadHtml(html);
                        var items = doc.DocumentNode.SelectNodes("//meta");
                        if (items.Count >= 2) // Получить второй по счету meta тег.
                        {
                            token = items[1].GetAttributeValue("content", ""); // Получить значение метатега. Не спрашивайтепочему второй метатаг с токеном рабочий - не знаю )))
                        }
                    }
                }
    
                // Когда в наличии есть куки и токен делаем отправку СМС через запрос POST
                if (!string.IsNullOrEmpty(token))
                {
                    var msgLength = msg.Length;
                    var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); //    TIME=$(date +"%Y-%m-%d %T")
                    var sms = $"<?xml version='1.0' encoding='UTF-8'?><request><Index>-1</Index><Phones><Phone>{phone}</Phone></Phones><Sca/><Content>{msg}</Content><Length>{msgLength}</Length><Reserved>1</Reserved><Date>{time}</Date></request>";
    
                    /*# Сделать POST для отправки СМС
                      curl -v -b session.txt -c session.txt -H "X-Requested-With: XMLHttpRequest" --data "$SMS" 
                        http://192.168.8.1/api/sms/send-sms  --header "__RequestVerificationToken: $TOKEN"  --header "Content-Type:text/xml" 
                    */
    
                    var uri3 = new Uri($"http://{ip}/api/sms/send-sms");
                    var client = new RestSharp.RestClient { BaseUrl = uri3 }; // Используем RestSharp для запроса (дело вкуса)
                    var request = new RestSharp.RestRequest(RestSharp.Method.POST);
                    // Формируем свой заголовой запроса - ничего лишненго все по примеру из Curl
    			    request.AddHeader("__RequestVerificationToken", token);
                    var ses = secondCookie.ToString();
                    request.AddCookie("cookie", ses);
                    request.AddHeader("Content-Type", "text/xml");
                    request.AddHeader("X-Requested-With", "XMLHttpRequest");
                    request.AddParameter("text/html", sms, RestSharp.ParameterType.RequestBody);
                    RestSharp.IRestResponse response = client.Execute(request);
    
                    if (response.IsSuccessful)
                    {
                        var xmlDoc = new XmlDocument();
                        xmlDoc.LoadXml(response.Content); // <?xml version="1.0" encoding="UTF-8"?><response> OK </response>
                        var responseElemenets = xmlDoc.GetElementsByTagName("response");
                        var resultOK = responseElemenets[0].InnerXml.ToLower();
                        return resultOK == "ok"; // Ну вот и признак того, что СМС отправлено, но без отчета о доставке. 
                    }
                }
            }
        }
        catch (Exception)
        {
            //TODO в лог ошибку;
        }
    
        return false;
    }

    Как только у вас в руках рабочий C# код вы всегда можете его улучшить.

    В моем случае он работает как часы и для некоторого солидного количества СМС в минуту вполне годится.

    Надеюсь эта статься принесла пользу и в профессиональном и экономическом смыслах.

    Душевно благодарю!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 8

      +2

      1)сделать метод async, чтобы избавиться от wait
      2)не собирать xml как интерполяцию, т.к. если у вас в тексте будет например кавычка или символ <, xml будет невалидна

        –4
        Это не конечный вариант кода. )))

        1)сделать метод async, чтобы избавиться от wait

        Конечно, на усмотрение разработчика

        2)не собирать xml как интерполяцию, т.к. если у вас в тексте будет например кавычка или символ <, xml будет невалидна

        Ну тут все жестко, другой код и не приходит, кавычки исключены, все вшито в софт модема

        Смело можете доработать и запускать платный сервис по отправке СМС! )))
        0
        В свое время довелось попользоваться GSMComm. Хорошая библиотека и гораздо более надежная и менее костыльная, чем то что предложено в этой статье.

        А вот назвать consumer'овские GSM модемы надежными не поднимается рука. Через достаточно непродолжительные промежутки времени у тех что я использовал отваливалась сеть и требовалось передергивать модем в usb порт. В итоге проблема решилась покупкой промышленного GSM модема.
        • UFO just landed and posted this here
            0
            Если через родной интерфейс sms кирилицей отправляется нормально, то проверяйте кодировки коих в windows для русского языка целых три (cp866, cp1251 и UTF-8). В вызовах WinApi еще UTF-16. Если winforms, то рискну предположить что текст при вводе cp1251, модему же он нужен в UTF-8. Точно также скорее всего входящие смс придется перекодировать обратно. Когда мне нужно было такое, то со скриптом я тащил еще iconv специальную версию для windows.
              0
              а кто нибудь может порекомендовать решение как с модема переадресовать пришедшие смски на емайл
              0

              С русскими буквами легко не будет. Описанным методом отправки, это скорее всего не возможно принципиально. У Вас идет отправка нескольких полей (телефон, сообщение), на основе которых формируется в модеме команда AT+CMGS. Кодировки кроме латиницы представляются в кодировке UCS2, и команду нужно в другом режиме использовать. Отправка в модемных командах:
              ЛАТИНИЦА:
              AT+CMGF=1 [Enter]
              AT+CMGS=+79261234567 [Enter]
              hello habr, this is test message [Ctrl+Z]
              КИРИЛЛИЦА:
              AT+CMGF=0 [Enter]
              AT+CMGS=84 [Enter]
              0011000B919762214365F70008C146043F04400438043204350442002004450430043
              10440002C0020044D0442043E00200442043504410442043E0432043E043500200441
              043E043E043104490435043D04380435 [Ctrl+Z]
              Я давно на Delphi делал отправщик через COM с русскими буквами на Delphi на основе статьи: https://habr.com/ru/post/133085/ .

              Only users with full accounts can post comments. Log in, please.