Исходник Лоадер с правильной авторизацией с форума Xenforo

*BLUHGANG* vs godless braindead users 15v15 *shayd
Пользователь
Статус
Оффлайн
Регистрация
20 Мар 2020
Сообщения
100
Реакции[?]
52
Поинты[?]
0
Отличный гайд
 
Начинающий
Статус
Оффлайн
Регистрация
14 Сен 2021
Сообщения
42
Реакции[?]
21
Поинты[?]
0
На форуме есть очень много тем с авторизацией на форуме Xenforo, но большая часть из них имеет в себе те или иные недочете в коде и в алгоритмах получения данных о пользователе. Сегодня я постараюсь подробно расписать процесс привязки вашего приложения к сайту с использованием Xenforo API 2.1+. Код будет написан на C#, но при желании его можно переделать под C++ и других современных языков программирования.

1. Получаем ключ API для форума.
Для нормальной работы с api форума нам нужен соответствующий ключ, который можно создать в админ-панеле. В параметрах найдите вкладку "Ключи API" и откройте ее. Если пункт не появляется, то, скорее всего, у вас отключен api в конфиге форума. Это исправляется изменением строки $config['enableApi'] = false; на $config['enableApi'] = true;.
Создайте новый ключ API с настройками, как на следующем скриншоте:
При желании можно добавить еще несколько разрешенных областей, если вы уверены, что это не навредит безопасности проекта.

2. Пишем wrapper для API.
Для начала стоит проверить работоспособность api форума. По умолчанию эндроинты располагаются по пути http(s)://yourforum.com/api/endpoint, но если у вас не настроены friendly URLs, то скорее всего это http(s)://yourforum.com/index.php?api/endpoint. Для первых тестов советую использовать бесплатную программу
Пожалуйста, авторизуйтесь для просмотра ссылки.
, либо онлайн-сервис
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Нам нужно послать POST запрос на api/auth, указав в параметрах запроса наш логин (почту или юзернейм) и пароль. Также в заголовки необходимо добавить XF-Api-Key: Ваш ключ. Так сказано на официальном сайте Xenforo, и без этих данный сервер будет возвращать ошибку.
Если вы все сделали правильно, то в окне response должен массив данных в формате JSON. Вот пример ответа от моего сервера:
JSON:
{
    "success": true,
    "user": {
        "about": "",
        "activity_visible": true,
        "alert_optout": [],
        "allow_post_profile": "members",
        "allow_receive_news_feed": "everyone",
        "allow_send_personal_conversation": "members",
        "allow_view_identities": "everyone",
        "allow_view_profile": "everyone",
        "avatar_urls": {
            "o": null,
            "h": null,
            "l": null,
            "m": null,
            "s": null
        },
        "can_ban": false,
        "can_converse": false,
        "can_edit": false,
        "can_follow": false,
        "can_ignore": false,
        "can_post_profile": false,
        "can_view_profile": true,
        "can_view_profile_posts": true,
        "can_warn": false,
        "content_show_signature": true,
        "creation_watch_state": "watch_no_email",
        "custom_fields": {
            "skype": "",
            "facebook": "",
            "twitter": ""
        },
        "custom_title": "",
        "email": "mysuperemail@mail.ru",
        "email_on_conversation": false,
        "gravatar": "",
        "interaction_watch_state": "watch_no_email",
        "is_admin": false,
        "is_discouraged": false,
        "is_moderator": false,
        "is_staff": false,
        "is_super_admin": false,
        "last_activity": 1624800266,
        "location": "",
        "message_count": 0,
        "push_on_conversation": false,
        "push_optout": [],
        "reaction_score": 0,
        "receive_admin_email": false,
        "register_date": 1624788945,
        "secondary_group_ids": [],
        "show_dob_date": false,
        "show_dob_year": false,
        "signature": "",
        "timezone": "Europe/Moscow",
        "trophy_points": 0,
        "usa_tfa": false,
        "user_group_id": 5,
        "user_id": 5,
        "user_state": "valid",
        "user_title": "Customer",
        "username": "TestUser",
        "visible": true,
        "website": ""
    }
}
Многие авторы статей на нашем форуме на этом этапе обычно приступают к написанию клиента, который будет отправлять запросы по шаблону от Xenforo. Но я не советую так делать. Как-никак, в заголовках запроса участвует суперключ api форума, который может позволить злоумышленникам брутить пароли пользователей, либо использовать другие его важные методы, которые включены в админ-панеле. Хорошим решением будет использование дополнительной PHP страницы, которая послужит нам посредником в "отношениях" с сервером.

Вот простой код с использованием cURL. Можно было воспользоваться стандартными средствави PHP, но на моем хостинге, к сожалению, это не очень хотело работать.
PHP:
<?php
$url = "http://yourforum.com/api/auth";

$post_data = array (
    'login' => $_POST["login"],
    'password' => $_POST["password"],
);
if (!isset($_POST["login"]) && !isset($_POST["password"]))
    $post_data = array();
if (isset($_POST["login"]) && !isset($_POST["password"]))
    $post_data = array('login' => $_POST["login"]);
if (!isset($_POST["login"]) && isset($_POST["password"]))
    $post_data = array('password' => $_POST["password"]);

$ch = curl_init();

curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Accept: application/json',
    'XF-Api-Key: Ключ API'
));

curl_setopt($ch, CURLOPT_URL, $url);

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);

$output = curl_exec($ch);

curl_close($ch);

echo $output;
?>
Теперь нужно загрузить файл на наш хостинг и проверить его работоспособность с помощью вышеупомянутых сервисов. На этот раз достаточно указать только login и password.

3.1. Написание классов для нашего лоадера.
Для начала стоит позаботиться о правильной десериализации ответа от нашего сервера. Поскольку мы работаем с форматом данных JSON, нам необходимо написать класс, с помощью которого можно спарсить всю неоюходимую информацию. Очень легко это можно сделать с помощью программы, которую я выкладывал около полугода назад - https://yougame.biz/threads/173834/. Выходной файл нужно лишь немного подрежактировать, добавив поддержку отображения ошибок.
Конкретно мой класс можно скачать с пастбина:
Пожалуйста, авторизуйтесь для просмотра ссылки.
.

В принципе, на этом можно было перейти к написанию основного кода, но я считаю правильным добавить сохранение авторизационных данных в реестр компьютера. Конечно, можно было это сделать через дополнительный файл или настройки Settings.settings, но работа с реестром будет гораздо более надежной из-за отсутствия привязки к расположению файла.

C#:
using Microsoft.Win32;
using System;

namespace XenforoLoader
{
    class Session
    {
        public string login { get; private set; }
        public string password { get; private set; }

        public void LoadData()
        {
            RegistryKey directory = Registry.CurrentUser.CreateSubKey("SuperiorLoader");
            login = (string)directory.GetValue("login");
            password = (string)directory.GetValue("password");
            directory.Close();
        }

        public void SaveData(string newLogin, string newPassword)
        {
            login = newLogin;
            password = newPassword;

            RegistryKey directory = Registry.CurrentUser.CreateSubKey("SuperiorLoader");
            directory.SetValue("login", login);
            directory.SetValue("password", password);
            directory.Close();
        }

        public void ClearData()
        {
            login = null;
            password = null;

            RegistryKey directory = Registry.CurrentUser.CreateSubKey("SuperiorLoader");
            directory.SetValue("login", "");
            directory.SetValue("password", "");
            directory.Close();
        }

        public bool isAuthed()
        {
            return !String.IsNullOrEmpty(login) && !String.IsNullOrEmpty(password);
        }

        public Session()
        {
            LoadData();
        }
    }
}
Создавать ключ я советую в разделе HKEY_CURRENT_USER, но можете воспользоваться и HKEY_LOCAL_MACHINE, если хотите избавиться от привязки к пользователю компьютера.

3.2. Написание основной части программы.
Мне не очень хочется заострять внимание на каждой мелочи в коде, поэтому я просто добавлю комментарии к основным моментам в программе:
C#:
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using System.Threading;

namespace XenforoLoader
{
    class Program
    {
        static void Main(string[] args)
        {
            // Устанавливаем заголовок консоли и выводим логотип нашего лоадера.
            Console.Title = "Loader";
            Console.WriteLine(@" _                   _ _____        ");
            Console.WriteLine(@"| |                   | |___  |    ");
            Console.WriteLine(@"| |     ___   __ _  __| |  / /  _ __");
            Console.WriteLine(@"| |    / _ \ / _` |/ _` |  \  \| '__/");
            Console.WriteLine(@"| |___| (_) | (_| | (_| | __)  | |");
            Console.WriteLine(@"\_____|\___/ \__,_|\__,_|\____/|_|");
            Console.WriteLine("\nyougame.biz | vk.com/ugame | t.me/govthing\n\n");

            // Объявляем переменные с данными о пользователе и сохраненной "сессии". Мы прописали в конструкторе выгрузку данных из реестра, поэтому нет необхомости писать session.LoadData();
            User user = null;
            Session session = new Session();

            // Фактически, мы проверяем строки с логином и паролем на пустоту.
            if (session.isAuthed())
            {
                Console.WriteLine("Saved data found. Trying to log in...");
                string resp = POST("http://irval.yourforum.com/auth.php", $"login={session.login}&password={session.password}"); // Отправляем POST запрос на наш wrapper.
                Response response = JsonConvert.DeserializeObject<Response>(resp); // Десериализация ответа.
                if (response.success == true) // Если success == null, то запрос не удался и мы выведем текст ошибки.
                    user = response.User; // Заносим данные об авторизированном пользователе в user.
                else
                {
                    Console.WriteLine("Cannot auth with saved credentials: " + response.errors.First().message);
                    session.ClearData();
                   
                    Process.Start(Assembly.GetEntryAssembly().Location); // Перезапуск приложения.
                    Process.GetCurrentProcess().Kill();
                }
            }
            else
            {
                string login, password;
                Console.Write("[>] Enter username or email: ");
                login = Console.ReadLine();
                Console.Write("[>] Enter password: ");
                password = Console.ReadLine();

                string resp = POST("http://irval.yourforum.com/auth.php", $"login={login}&password={password}"); // POST запрос на наш wrapper. См. строки 34-45.
                Response response = JsonConvert.DeserializeObject<Response>(resp);
                if (response.success == true)
                {
                    user = response.User;
                    session.SaveData(login, password); // Записываем данные пользователя в реестр.
                }
                else
                    Console.WriteLine("Cannot auth with entered credentials: " + response.errors.First().message);
            }

            if (user == null) // Если мы не передали значение user, т.е. авторизация не удалась.
            {
                Console.ReadKey(); // Читаем один символ, после чего закрываем процесс.
                Process.GetCurrentProcess().Kill();
            }

            Console.WriteLine("You have successfully logged in.");
            Console.Title = $"Loader [{user.username}]"; // Добавлем имя пользователя в заголовок.
            Thread.Sleep(1500);
            Console.Clear();
            Console.WriteLine("Current user information:\n"); // Выводим некоторую информацию о пользователе Xenforo.
            Console.WriteLine($"Username: {user.username}\nEmail: {user.email}\nisStaff: {user.is_staff}\nisBanned: {user.is_banned}");
            string secondaryGroups = user.secondary_group_ids != null && user.secondary_group_ids.Count > 0 ? String.Join(", ", user.secondary_group_ids) : "No groups";
            Console.WriteLine($"MainGroup: {user.user_group_id}\nSecondaryGroups: {secondaryGroups}\nAvatar: {user.avatar_urls.m ?? "null"}");
            Console.Write("\n\nPress Delete to unlogin or any key to exit...");
           
            if (Console.ReadKey().Key == ConsoleKey.Delete) // Прроверка на нажатие Delete.
            {
                session.ClearData(); // Очищаем строки в реестре.
                Console.WriteLine("\nSuccessfully logged out.");
                Thread.Sleep(2500);
            }
            Process.GetCurrentProcess().Kill();
        }

        private static string POST(string Url, string Data) // Функция POST запроса.
        {
            try
            {
                System.Net.WebRequest req = System.Net.WebRequest.Create(Url);
                req.Method = "POST";
                req.Timeout = 100000;
                req.ContentType = "application/x-www-form-urlencoded";
                byte[] sentData = Encoding.GetEncoding(1251).GetBytes(Data);
                req.ContentLength = sentData.Length;
                System.IO.Stream sendStream = req.GetRequestStream();
                sendStream.Write(sentData, 0, sentData.Length);
                sendStream.Close();
                System.Net.WebResponse res = req.GetResponse();
                System.IO.Stream ReceiveStream = res.GetResponseStream();
                System.IO.StreamReader sr = new System.IO.StreamReader(ReceiveStream, Encoding.UTF8);
                //Кодировка указывается в зависимости от кодировки ответа сервера
                Char[] read = new Char[256];
                int count = sr.Read(read, 0, 256);
                string Out = String.Empty;
                while (count > 0)
                {
                    String str = new String(read, 0, count);
                    Out += str;
                    count = sr.Read(read, 0, 256);
                }
                return Out;
            }
            catch (Exception ex)
            {
                File.WriteAllText("crash.log", ex.Message); // Пишем лог-файл с ошибкой запроса.
                return null;
            }
        }
    }
}
Скриншоты работы программы:
Скачать готовые исходники клиента можно здесь:
Пожалуйста, авторизуйтесь для просмотра ссылки.
.
Отдельное спасибо HORMAN за предоставленный форум, на котором мы еще будем делать много интересного :)
Ссылку обнови на я.д пожалуйста
 
Начинающий
Статус
Оффлайн
Регистрация
15 Июн 2019
Сообщения
31
Реакции[?]
1
Поинты[?]
0
Не плохо, спасибо, изучаю.
 
Сверху Снизу