Не задалось у меня общение с 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%system32driversetchosts
- Создать свой локальный DNS сервер
Первое реализуется довольно просто, файл hosts – это обычный текстовый файл, который без труда читается/изменяется/сохраняется, главное только иметь на это права. А их то у обычного пользователя нет, поэтому повышаем у нашего приложения «уровень управления учетными записями Windows» поставив в файле app.manifest значение для requestedExecutionLevel = requireAdministrator. Подробнее об этом можно прочитать здесь. Работаем с файлом хостов приблизительно так:
//открываем файл хостов
string strHosts = File.ReadAllText(Environment.SystemDirectory + "\drivers\etc\hosts");
string[] linesHostsOld = Regex.Split(strHosts, "rn|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%AppDataLocal%PROGRAM_NAME%***. Реализовано это при помощи стандартных возможностей Properties.Settings.Default. Пароли хранятся там-же, но в зашифрованном виде. Шифрование сделано используя DPAPI (на харбе по этой теме есть статья и вопрос).
Работу с настройками формы, с шифрованием, с таймерами, с параллельными процессами и всего прочего, что не касается изначальной темы я подробно описывать не буду. Кому очень интересно – всегда можно посмотреть исходный код.
Внешний вид получившегося велосипеда:
При первом запуске понадобится внимательно настроить все нужные параметры.
В минимальном варианте: на первом компьютере (с динамическим адресом) нужно будет настроить «интерфейсы», а на втором компьютере (на котором нам нужно знать динамический адрес) нужно будет внимательно настроить «хосты».
Планы по развитию.
- Увеличение поддерживаемых облачных хранилищ.
- Увеличение поддерживаемых почтовых протоколов
- Шифрование передаваемой информации.
Исходный код проекта и саму программу пока выложил на Яндекс.Диск.
Исходники можно скачать здесь: http://yadi.sk/d/iZNy9wA28E0-E
Бинарные файлы лежат тут: http://yadi.sk/d/kYpZIqdn8E-ui
На этом всё. Спасибо за внимание.
Автор: digitallez