Не задалось у меня общение с DynDNS сервисами буквально с первого дня знакомства. Грабли попадались на каждом шагу: регистрация, скачивание и запуск клиента, настройка клиента или роутера – везде были какие-то мелкие нюансы, недоговорки, недоделки или просто баги, что приводило к неработоспособности сервиса. В довесок ко всему, через время «эти ребята» вдруг перестают быть белыми, пушистыми и бесплатными — начинают слать спам, раз в месяц требовать разгадать капчу или заставляют проделывать еще какие-либо телодвижения, чтобы доказать что ты еще жив. Всё это привело к общей неприязни ко всем сервисам подобного рода. Так и возникла идея создать что-то своё, и чтоб обязательно «белое и пушистое».
От идеи до реализации прошло довольно много времени. В основном из-за непонимания «а что собственно мне надо?». Читал статьи на досуге, кумекал, и постепенно появился в голове список основных требований к велосипеду.
Основные положения.
Назначение: узнать IP адрес удаленного компьютера (например домашний компьютер).
Уровень паранойи: выше среднего! (то есть IP адрес должны знать только доверенные лица). Вот тут как раз и основное отличие, от подобных сервисов – я не хочу чтобы любой желающий мог получить адрес моего компьютера просто вбив в командной строке что-то типа «ping supercomp.dyndns.org».
Обязательные условия «пушистости»:
- Бесплатность (не забываем что и время тоже деньги).
- Стабильность.
- Простота готового решения для конечного пользователя.
Исходя из уточнения к первому условию, технологии решено было использовать только те, что лично мне более-менее известны — Windows, c#, ASP.NET.
Под влиянием статьи «Свой простой DynDNS сервер» была предпринята попытка написания небольшого сайта-посредника. Но, посмотрев на поразительно стабильную не стабильность бесплатных ASP.NET хостингов, от этой идеи было решено отказаться и в качестве посредников использовать бесплатные почтовые сервисы и облачные хранилища. Кстати, именно из упомянутой статьи была взята, показавшаяся здравой, идея с «возможностью хранения IP адресов всех интерфейсов клиента».
Вот как-то так и получилось, что это должно быть обычное виндовое си-шарповое приложение.
Выбор «хранилища»
Под хранилищем подразумевается некое место, где будет лежать наша информация. Место это должно быть защищено от посторонних взглядов, быть легкодоступно из любой точки и обязательно соответствовать трем «пушистым» требованиям.
Чтобы сильно не напрягаться, было решено остановиться на таких вариантах:
- Файловая система компьютера (например папка синхронизируемая каким-нибудь облачным клиентом) – сохранение или чтение проблем не вызывает абсолютно, вся работа с сетью лежит на клиенте облака.
- Почта – отправляются письма без проблем, а вот читать приходится через стороннюю бесплатную библиотеку.
- Облачное хранилище (имеется ввиду взаимодействие с облаком без установки клиента) – вполне осуществимо.
На третьем пункте остановимся, и рассмотрим возможные варианты.
Предварительный опрос друзей и знакомых показывал, что большинство ничего не имеют против Яндекс-Диска и Скай-Драйва. Поэтому они изначально рассматривались как основные претенденты. Но проведя пол дня в «активном поиске», оказалось, что далеко не каждый облачный сервис предоставляет вменяемое средство взаимодействия. Например, Скай-Драйв API с некоторых пор невозможно использовать в настольных приложениях, Гугл-Драйв API – без бутылки не разобраться, а у ДропБокс – я как-то вообще не нашел SDK для Windows. Использование не официальных или устаревших “API” даже не рассматривались, так как нет никакой гарантии что они завтра будут работать. Возможно я плохо искал, или не там и не то искал – не знаю, если у кого-то есть примеры, буду рад помощи. Последним гвоздём в проблему выбора облачного сервиса, стал тот факт, что для работы с Яндекс-диском из c# не нужны вообще никакие сторонние библиотеки.
На каком-то одном из этих трех типов хранения/передачи останавливаться не стал. Было решено сделать поддержку всех трёх, а что конкретно использовать – оставить на выбор пользователя. Ибо ситуации бывают разные – у кого-то порты закрыты и почта не работает, кому-то нельзя ставить программы облачных клиентов и т.д.
Общий алгоритм работы приложения.
Общий алгоритм работы прост как две копейки:
- Периодически сохраняем текстовые сообщения со всей нужной информацией в «хранилище»
- Периодически читаем сообщения, и показываем в удобном виде.
Перейдем к реализации идей в программном коде.
Получение внешнего адреса.
Тут все просто. В «интернетах» полно всяких сервисов, которые показывают ваш внешний адрес. Если мало уже существующих, то создать еще пару десятков не составит особого труда. Примерный код такой страницы на ASP.NET:
protected void Page_Load(object sender, EventArgs e) { LabelIp.Text = HttpContext.Current.Request.UserHostAddress; }
Вернемся к нашему приложению. Используя класс System.Net.WebClient скачиваем страничку с таким адресом в строку, разбираем её регулярным выражением и получаем нужную нам информацию:
WebClient webClient = new WebClient(); string strExternalIp = webClient.DownloadString("http://checkip.dyndns.org/"); strExternalIp = (new Regex(@"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")).Matches(strExternalIp)[0].ToString();
Получение свойств сетевых интерфесов.
В этом нам поможет класс System.Net.NetworkInformation.NetworkInterface, и его статический метод GetAllNetworkInterfaces(), который возвращает массив элементов своего-же типа NetworkInterface[]. Перебрав этот массив мы можем получить из объекта IPInterfaceProperties всю нужную нам информацию – IP адреса, маски, шлюзы, dns-сервера и т.д.:
NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); // перебираем все сетевые интерфейсы foreach (NetworkInterface nic in adapters) { string strInterfaceName = nic.Name; // наименование интерфейса string strPhysicalAddress = nic.GetPhysicalAddress().ToString(); //МАС - адрес string strAddr = string.Empty; // перебираем IP адреса IPInterfaceProperties properties = nic.GetIPProperties(); foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses) { strAddr = unicast.Address.ToString() + " / " + unicast.IPv4Mask; } // перебираем днс-сервера foreach (IPAddress dnsAddress in properties.DnsAddresses) { strAddr = dnsAddress.ToString(); } // перебираем шлюзы foreach (GatewayIPAddressInformation gatewayIpAddressInformation in properties.GatewayAddresses) { strAddr = gatewayIpAddressInformation.Address.ToString(); } }
Передача текстового сообщения в «хранилище».
Собрав всю необходимую информацию, отправляем её в «хранилище» в виде обычного текстового файла (в случае с почтой – просто сообщение).
С обычными файлами всё просто:
System.IO.File.WriteAllText("MyInterfaces.txt", strInterfaces);
С почтой тоже всё решается парой строк кода (метод запросто находится в интернете). Одна из возможных вариаций:
MailMessage mail = new MailMessage { From = new MailAddress(strMailAddress), // от кого Subject = strSubject, // тема письма Body = strBody, // тело письма IsBodyHtml = false }; mail.To.Add(new MailAddress(Settings.Default.strMailTo)); // кому SmtpClient client = new SmtpClient { Host = strSmtpServer, // адрес SMTP сервера Port = nSmtpServerPort, // порт SMTP сервера EnableSsl = isSmtpSsl, // нужно ли испльзовать SSL Credentials = new NetworkCredential(strEmailUserName, strMailPassword), // логин пароль DeliveryMethod = SmtpDeliveryMethod.Network }; client.Send(mail); // отправляем mail.Dispose();
А вот с облаками немного сложнее, общий смысл – создать правильный веб запрос в который впихнуть передаваемый текст:
// strFilePath - имя и путь к файлу на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strFilePath); // указываем логин и пароль (дважды!!! в разных местах) web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Method = "PUT"; web.ContentType = "application/binary"; web.ContentLength = buffer.Length; using (Stream myReqStream = web.GetRequestStream()) { // strContent - текст передаваемого файла byte[] buffer = Encoding.UTF8.GetBytes(strContent); myReqStream.Write(buffer, 0, buffer.Length); myReqStream.Flush(); } HttpWebResponse resp = (HttpWebResponse)web.GetResponse();
Здесь немного пришлось поплясать с кодировками, но методом «научного тыка» было установлено, что с UTF8 всё отлично работает.
Чтение сообщений из «хранилища»
Обычные файлы из обычной файловой системы читаются одной строкой. Но нам ведь не нужен просто один файл, да и имя его заранее может быть не известно, поэтому просматриваем всё содержимое папки, ищем файлы по указанной маске и обрабатываем их по очереди:
// просмотр всех файлов из указанной директории по указанной маске var files = Directory.EnumerateFiles("путь к папке", "*.txt"); strFileNames = files as string[] ?? files.ToArray(); foreach (string strFileName in strFileNames) { string message = File.ReadAllText(strFileName); // читаем содержимое файла // что-то делаем с прочитанным }
С чтением почты пришлось повозиться. Код затачивался под гугло-почту, поэтому возможно некорректная работа на других почтовиках. Именно гугло-почта и привела к использованию IMAP сервера (на данный момент хотмэйл этот протокол не поддерживает). Многие советовали использовать псевдо-бесплатную библиотеку (название не буду приводить), которая периодически вместо тела письма возвращала свою рекламу. Но это прямо нарушает «второе пушистое требование» — стабильность, а если платить, то «первое пушистое» – бесплатность. Поэтому я выбрал полностью бесплатную и вполне рабочую библиотеку в которой есть работа с IMAP серверами — «MailSystem.NET». Примеры использования можно найти на странице проекта, здесь же я приведу небольшой кусочек кода для получения письма:
Imap4Client imap = new Imap4Client(); imap.ConnectSsl("imap.gmail.com", 993); // подключаемся imap.Login("mail@google.com", "password");// авторизуемся Mailbox inbox = imap.SelectMailbox("inbox");// получаем папку входящих int[] nIdsUnread = inbox.Search("UNSEEN"); // получаем только непрочитанные int nUnreadCount = nIdsUnread.Length; // узнаем количество непрочитанных for (int i = 0; i < nUnreadCount; i++) { int idx = nIdsUnread[i]; // получаем индекс письма в папке входящих // получаем текст сообщения Message message = inbox.Fetch.MessageObject(idx); // message.Subject - содержит тему письма // message.BodyText.Text - содержит текст письма // обрабатываем полученную информацию }
Вот так можно прочитать письмо — всего десять строк кода, но они тянут за собой пять библиотек (DLL) в папку программы, и потом придется тягать их везде с собой.
Читать файлы из облачного хранилища даже проще чем их туда отсылать:
// strFilePath - имя и путь к файлу на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strFilePath); // указываем логин и пароль web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Method = "GET"; HttpWebResponse resp = (HttpWebResponse)web.GetResponse(); using (StreamReader sr = new StreamReader(resp.GetResponseStream())) { string text = sr.ReadToEnd(); // text - теперь содержит в себе текстовое содержимое файла }
Но этот пример прочитает только один файл, а нам нужно читать все файлы из указанной директории. Решается эта задача предварительным запросом списка файлов. Сервер вернет нам XML файл, и пройдясь по содержимому тегов <d:displayname> мы получим список файлов:
// strPath - путь к папке на сервере HttpWebRequest web = (HttpWebRequest)WebRequest.Create("https://webdav.yandex.ru/" + strPath); // указываем логин и пароль web.Credentials = new NetworkCredential("mail@yandex.ru", "password"); web.Headers.Add("Authorization: Basic " + Convert.ToBase64String(Encoding.Unicode.GetBytes("mail@yandex.ru" + ":" + "password"))); web.Accept = "*/*"; web.Headers.Add("Depth: 1"); web.Method = "PROPFIND"; List<string> retValue = new List<string>(); // в этот список попадут все файлы из указанной паки HttpWebResponse resp = (HttpWebResponse)web.GetResponse(); using (StreamReader sr = new StreamReader(resp.GetResponseStream())) { // сервер возвращает XML файл. Разбираем его содержимое: XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(sr.ReadToEnd()); XmlNodeList displaynames = xmlDoc.GetElementsByTagName("d:displayname"); int nCount = displaynames.Count; for (int i = 1; i < nCount; i++) { retValue.Add(displaynames[i].InnerText); } }
DNS
После получения всей информации из хранилища, встает вопрос – а что делать дальше? И вариантов не много:
- Показать пользователю всё полученное в удобном виде
- Организовать возможность доступа к удаленным компьютерам по имени
С первым всё просто – обычный список и пару колонок. А вот со вторым сложнее. Реализовать можно двумя путями:
- Редактирование файла %windir%\system32\drivers\etc\hosts
- Создать свой локальный DNS сервер
Первое реализуется довольно просто, файл hosts – это обычный текстовый файл, который без труда читается/изменяется/сохраняется, главное только иметь на это права. А их то у обычного пользователя нет, поэтому повышаем у нашего приложения «уровень управления учетными записями Windows» поставив в файле app.manifest значение для requestedExecutionLevel = requireAdministrator. Подробнее об этом можно прочитать здесь. Работаем с файлом хостов приблизительно так:
//открываем файл хостов string strHosts = File.ReadAllText(Environment.SystemDirectory + "\\drivers\\etc\\hosts"); string[] linesHostsOld = Regex.Split(strHosts, "\r\n|\r|\n"); // разбиваем на строки StringBuilder sbHostsNew = new StringBuilder(); // обрабатываем все строки foreach (string lineHosts in linesHostsOld) { sbHostsNew.AppendLine(lineHosts); } // добавляем в конец текущие значения хостов sbHostsNew.AppendLine("127.0.0.1 hello.world.com"); // сохраняем файл хостов File.WriteAllText(Environment.SystemDirectory + "\\drivers\\etc\\hosts", sbHostsNew.ToString());
Второй вариант у меня не удалось хорошо оттестировать, так-как всех пока-что полностью устраивает работоспособность первого метода. DNS сервер реализован помощи сторонней библиотеки «ARSoft.Tools.Net». Сильно не мудрим, и по этим примерам делаем свои функции, приблизительно так:
DnsServer _server = new DnsServer(IPAddress.Any, 10, 10, ProcessQuery); _server.Start(); // запуск сервера // запрос адреса у DNS сервера private static DnsMessageBase ProcessQuery( DnsMessageBase message, IPAddress clientAddress, ProtocolType protocol) { message.IsQuery = false; DnsMessage query = message as DnsMessage; if (query != null) { if (query.Questions.Count == 1) { if (query.Questions[0].RecordType == RecordType.A) { if (query.Questions[0].Name.Equals("hello.world.com", StringComparison.InvariantCultureIgnoreCase)) { IPAddress ip; if (IPAddress.TryParse("127.0.0.1", out ip)) { query.ReturnCode = ReturnCode.NoError; DnsRecordBase rec = new ARecord(strHostName, 0, ip); query.AnswerRecords.Add(rec); return message; } } } } } message.ReturnCode = ReturnCode.ServerFailure; return message; }
Готовое приложение.
Собрав вместе всё выше описанное, и добавив вызов нужных процедур по таймеру, получится некое подобие задуманной программы. Остается только доработать всё напильником, привести в божеский вид и можно показывать людям.
Все настройки (а их получилось не мало) приложение хранит в файле %PROGRAM_NAME%.exe.config а сам файл лежит где то в этом районе: %USERPROFILE%\AppData\Local\%PROGRAM_NAME%\***. Реализовано это при помощи стандартных возможностей Properties.Settings.Default. Пароли хранятся там-же, но в зашифрованном виде. Шифрование сделано используя DPAPI (на харбе по этой теме есть статья и вопрос).
Работу с настройками формы, с шифрованием, с таймерами, с параллельными процессами и всего прочего, что не касается изначальной темы я подробно описывать не буду. Кому очень интересно – всегда можно посмотреть исходный код.
Внешний вид получившегося велосипеда:
При первом запуске понадобится внимательно настроить все нужные параметры.
В минимальном варианте: на первом компьютере (с динамическим адресом) нужно будет настроить «интерфейсы», а на втором компьютере (на котором нам нужно знать динамический адрес) нужно будет внимательно настроить «хосты».
Планы по развитию.
- Увеличение поддерживаемых облачных хранилищ.
- Увеличение поддерживаемых почтовых протоколов
- Шифрование передаваемой информации.
Исходный код проекта и саму программу пока выложил на Яндекс.Диск.
Исходники можно скачать здесь: http://yadi.sk/d/iZNy9wA28E0-E
Бинарные файлы лежат тут: http://yadi.sk/d/kYpZIqdn8E-ui
На этом всё. Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/191172/
Добавить комментарий