[Из песочницы] Wake on Lan бот для Telegram

После открытия API ботов для Telegram их популяция начала стремительно расти. Я решил не отставать и обзавестись собственным для возможности удаленно включать компьютер. Для разработки был выбран язык C#, а в качестве хостинга выбор пал на Azure.

Задача

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

Примечание: в статье не рассматриваются получение токена для бота и деплой сайта на Azure. На эти вопросы без труда находится ответ на хабре или Google.

Немного теории и сетевые настройки


Для включения компьютера будет использоваться технология Wake on Lan. Поскольку бот будет находиться в облаке, нам нужно отправлять пакет на внешний адрес. В связи с этим, нужно позаботиться о том, чтобы пакет в итоге попал в локальную сеть и достиг нужной сетевой платы. Для этого нужно пробросить порт на маршрутизаторе:

337d02ab31924985a8cbcc7085fb3e9c.png

Почему 7 порт и такой адрес?
Обычно, чтобы включить компьютер в локальной сети с помощью Wake on Lan, требуется отправить «волшебный пакет» на широковещательный адрес локальной сети, на порт 7/UDP (в некоторых случаях могут использоваться и другие порты). Когда сетевая карта получает «свой» пакет, она подает сигнал на включение компьютера.

Пакет состоит из набора байт в следующем порядке: первые 6 байтов — нулевые, затем идет последовательность байт из мак-адреса сетевой карты, который повторяется 16 раз. Собственно, именно по MAC адресу сетевая карта и понимает, что нужно включить именно ее компьютер.

Мы будем формировать такой пакет и отправлять его на наш внешний адрес (например, на адрес домашнего роутера), после чего пакет должен будет маршрутизироваться на broadcast адрес локальной сети.

В моем случае пробрасывается 7 порт (доступный из интернета) на шировещательный адрес сети (10.10.10.255).

Пишем код


Для работы с API бота была выбрана библиотека Telegram.Bot. Для получения обновлений будем использовать вариант с вебхуком. В этом случае каждое сообщение или событие, которые будет получать бот, будут отправлены на указанный URL.

В качестве хостинга было решено использовать Azure, так как он подходит по всем параметрам:

  • всегда доступен
  • на бесплатном тарифе есть сертификат, который позволяет обращаться к сайту по HTTPS (боты могут отправлять обновления только по зашифрованным соединениям)
  • быстрая и удобная публикация сайта прямо из Visual Studio


Для начала создаем класс, который будет возвращать нам объект для работы с ботом:

public static class Bot
{
    private static Api _bot;

    /// 
    /// Получаем бота, а если он еще
    /// не инициализирован - инициализируем
    /// и возвращаем
    /// 
    public static Api Get()
    {
        if (_bot != null) return _bot;
        _bot = new Api(Config.BotApiKey);
        _bot.SetWebhook(Config.WebHookUrl);
        return _bot;
    }
}


Настройки достаем из класса Config

Config.cs
public static class Config
{
    /// 
    /// Настройки для бота храним в настройках приложения
    /// 
    private static readonly NameValueCollection Appsettings = ConfigurationManager.AppSettings;

    /// 
    /// Полученный токен для бота
    /// 
    public static string BotApiKey
    {
        get { return Appsettings["BotApiKey"]; }
    }

    /// 
    /// URL, на который должны приходить все обновления от бота
    /// 
    public static string WebHookUrl
    {
        get { return Appsettings["WebHookUrl"]; }
    }
}



Теперь нам нужно формировать и отправлять пакет. За это будет отвечать класс WakeOnLan

/// 
/// Может отправлять "волшебные" пакеты для включения удаленного компьютера
/// 
public static class WakeOnLan
{
    public static void Up(string ip, string mac, int? port = null)
    {
        var client = new UdpClient();
        var data = new byte[102];

        for (var i = 0; i <= 5; i++) // первые шесть байт - нулевые
            data[i] = 0xff;

        var macDigits = GetMacDigits(mac);
        if (macDigits.Length != 6)
            throw new ArgumentException("Incorrect MAC address supplied!");

        const int start = 6;
        for (var i = 0; i < 16; i++) // создаем нужную последовательность байт для пакета
            for (var x = 0; x < 6; x++)
                data[start + i * 6 + x] = (byte)Convert.ToInt32(macDigits[x], 16);

        client.Send(data, data.Length, ip, port ?? 7); // отправляем пакет
    }

    private static string[] GetMacDigits(string mac) // парсим MAC
    {
        return mac.Split(mac.Contains("-") ? '-' : ':');
    }

    public static bool ValidateMac(string mac) // простая проверка на валидность MAC адреса
    {
        return GetMacDigits(mac).Length == 6;
    }
}

Пакеты формируем и умеем отправлять. Осталось научить бота отвечать на команду, к примеру, /wol.
Для простоты реализована команда с параметрами, т.е. пользователь должен будет ввести примерно следующее
/wol 1.2.3.4 01:02:03:04:05:06 7
для того, чтобы отправить пакет на адрес 1.2.3.4, на 7 порт и разбудить компьютер с MAC адресом 01:02:03:04:05:06

public async void Handle(Message message)
{
    var text = message.Text.Split(' ');
    if (text.First() != "/wol") return;
    switch (text.Count())
    {
        case 1:
        case 2:
            await _bot.SendTextMessage(message.Chat.Id, "Пример использования: /wol 1.2.3.4 01:02:03:04:05:06 7");
            break;
        default:
            if (!WakeOnLan.ValidateMac(text[2]))
                await _bot.SendTextMessage(message.Chat.Id, "Неверный MAC адрес");
            else
            {
                try
                {
                    WakeOnLan.Up(text[1], text[2], GetPort(text));
                    await _bot.SendTextMessage(message.Chat.Id, "Пакет отправлен!");
                }
                catch (Exception)
                {
                    await _bot.SendTextMessage(message.Chat.Id, "Произошла ошибка :(");
                }
            }
            break;
    }
}

/// 
/// Получаем порт из параметров
/// 
private static int? GetPort(IReadOnlyList text)
{
    int port;
    if (text.Count == 4 && int.TryParse(text[3], out port))
        return port;
    return null;
}


Отлично, осталось лишь создать контроллер, который будет принимать обновления от бота:

public class MessageController : ApiController
{
    [Route(@"api/message/wol")]
    public OkResult Post([FromBody]Update value)
    {
        Task.Run(() => new Handler().Handle(value.Message));
        return Ok();
    }
}


После «заливки» приложения в Azure проверяем бота:
6e44e1bcc47a4ccd81739500d82e8808.PNG

Ссылки:
WoL бот в Telegram
Проект на GitHub
API ботов Telegram
Библиотека Telegram.Bot (GitHub)

© Habrahabr.ru