WebAPI

mail@pastecode.io avatar
unknown
kotlin
2 years ago
39 kB
14
Indexable
package me.cok28rus.module.drom.network

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import me.cok28rus.module.drom.*
import me.cok28rus.module.drom.network.exception.DromException
import me.cok28rus.module.drom.network.model.DromApiError
import me.cok28rus.module.drom.network.model.DromError
import me.cok28rus.module.drom.network.model.DromProfile
import me.cok28rus.module.drom.model.Device
import me.cok28rus.module.drom.model.Session
import me.cok28rus.module.drom.util.RandomUtils
import me.cok28rus.module.drom.util.SignatureUtil
import me.cok28rus.module.drom.util.TimeUtils
import me.cok28rus.module.proxy.model.Proxy
import okhttp3.*
import org.jsoup.Jsoup
import java.lang.System.currentTimeMillis
import java.util.concurrent.TimeUnit

internal class DromWeb {
    /**
     * JSON маппер
     */
    private val mapper = jacksonObjectMapper()

    /**
     * HTTP-клиент для запросов в сеть
     */
    private lateinit var client: OkHttpClient

    /**
     * Данные девайса
     */
    private lateinit var device: Device

    /**
     * Хранилище куков
     */
    private val cookieStore = mutableMapOf<String, String>()

    /**
     * Параметр отражающий время задержки запросов до сервера
     */
    private var serverTimeOffset = 0L

    /**
     * Метод для подготовки клиента к регистрации
     */
    private fun init(proxy: Proxy) {
        // Откатываем хранилище с куками
        resetCookieStore()

        // Переустанавливаем данные об устройстве
        resetDevice()

        // Создаем новый HTTP клиент
        val newClient = OkHttpClient.Builder()
            .callTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .cookieJar(object : CookieJar {
                override fun loadForRequest(url: HttpUrl): List<Cookie> {
                    return listOf()
                }

                override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
                    cookies.forEach { cookie ->
                        if (cookie.value.trim().isEmpty() || cookie.value.contains("deleted")) {
                            cookieStore.remove(cookie.name)
                        } else {
                            cookieStore[cookie.name] = cookie.value
                        }
                    }
                }
            })
            .addInterceptor { chain ->
                val timestampBefore = currentTimeMillis()
                val response = chain.proceed(chain.request())
                val timestampAfter = currentTimeMillis()

                val date = response.headers["Date"]

                if (null != date && response.code != 304) {
                    serverTimeOffset =
                        (((timestampAfter - timestampBefore) / 2) + timestampAfter) - TimeUtils.time(date)
                }

                response
            }
            .build()

        // Сохраняем новый клиент и прокси
        client = newClient.withProxy(proxy)
    }

    /**
     * Метод для подготовки новых данных об устройсве
     */
    private fun resetDevice() {
        device = Device(
            brand = "samsung",
            model = "SM-N975F",
            build = "N2G48H",
            osVersion = "7.1.2"
        )
    }

    /**
     * Метод для обнуления хранилища куков до первоначального состояния
     */
    private fun resetCookieStore() {
        // Очищаем хранилище
        cookieStore.clear()
        // Устанавливаем неизменяемые параметры
        cookieStore.putAll(
            mapOf(
                "mobile" to "1",
                "app_name" to "drom_android",
                "app_version" to Constants.APP_VERSION,
                "device_id" to RandomUtils.randomAndroidId(),
                "mobileapp" to "1",
                "googlePayAvailable" to "",
                "app_google_auth_enable" to "1",
                "dark_theme_forced" to "0"
            )
        )
    }

    /**
     * Метод регистрации
     *
     * @param phone Номер телефона на который будет зарегистрирован аккаунт
     * @param proxy Прокси через который будут осуществляться все действия
     * @param verifyCodeHandler Функция обратного вызова для получения кода из СМС, которое поступит на номер из параметра [phone]
     */
    fun signIn(phone: String, proxy: Proxy, verifyCodeHandler: () -> String): Session {
        init(proxy)

        return try {
            try {
                saveRingAndIpGeo()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось получить обязательные параметры.", e)
            }

            try {
                sendStartEvent()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось отправить событие с регистрацией устройства.", e)
            }

            val csrf = try {
                fetchCsrfToken()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось получить обязательный параметр \"csrf\".", e)
            }

            try {
                saveNewSegSession()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось получить обязательный параметр \"segSession\".", e)
            }

            val (signCsrf, signCode) = try {
                sign(phone, csrf)
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось отправить СМС с кодом на номер \"$phone\".", e)
            }

            try {
                checkCode(phone, verifyCodeHandler(), signCsrf, signCode)
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось пройти проверку кода из СМС.", e)
            }

            try {
                checkSign()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось пройти проверку входа.", e)
            }

            try {
                checkAuthority()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось пройти проверку полномочий.", e)
            }

            val profile = try {
                fetchProfile()
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось загрузить профиль.", e)
            }

            val (latitude, longitude) = fetchRandomCoordinates()

            val (recRegionId, recCityId) = try {
                fetchLocation(latitude, longitude)
            } catch (e: Exception) {
                throw IllegalStateException("Не удалось определить локацию по координатам.", e)
            }

            Session(
                uid = fetchFromCookieStore("uid"),
                login = fetchFromCookieStore("login"),
                boobs = fetchAuthTokenFromCookieStore(),
                pony = fetchFromCookieStore("pony"),
                segSession = fetchFromCookieStore("segSession"),
                ring = fetchRingFromCookieStore(),
                cookieCityId = fetchFromCookieStore("cookie_cityid").toInt(),
                cookieRegionId = fetchFromCookieStore("cookie_regionid").toInt(),
                recSysRegionId = recRegionId,
                recSysCityId = recCityId,
                myGeo = fetchFromCookieStore("my_geo").toInt(),
                deviceId = fetchFromCookieStore("device_id"),
                deviceBrand = device.brand,
                deviceModel = device.model
            )
        } catch (e: Exception) {
            throw RuntimeException("Не удалось создать сессию в сервисе Drom.", e)
        }
    }

    /**
     * Метод для получения случайных координат
     * @return [Pair.first] - latitude, [Pair.second] - longitude
     */
    private fun fetchRandomCoordinates(): Pair<Double, Double> {
        return RandomUtils.randomCoordinates()
    }

    /**
     * Метод для получения `токена авторизации` из хранилища
     *
     * @throws NoSuchElementException В случае если токен отсутсвует
     */
    @Throws(NoSuchElementException::class)
    private fun fetchAuthTokenFromCookieStore(): String {
        return fetchFromCookieStore("boobs")
    }

    /**
     * Метод для получения `идентификатора устройства` из хранилища
     *
     * @throws NoSuchElementException В случае если идентификатор отсутсвует
     */
    @Throws(NoSuchElementException::class)
    private fun fetchDeviceIdFromCookieStore(): String {
        return fetchFromCookieStore("device_id")
    }

    /**
     * Метод для получения параметра `Ring` из хранилища
     *
     * @throws NoSuchElementException В случае если параметр отсуствует
     */
    @Throws(NoSuchElementException::class)
    private fun fetchRingFromCookieStore(): String {
        return fetchFromCookieStore("ring")
    }

    /**
     * Метод для получения параметра по ключу из хранилища куков.
     */
    @Throws(IllegalArgumentException::class)
    private fun fetchFromCookieStore(key: String): String {
        return cookieStore[key]
            ?: throw IllegalArgumentException("В хранилище отсутсвует параметр \"$key\".")
    }

    /**
     * Метод для генерации заголовка `User-Agent` под требования API
     *
     * Строка включает в себя параметры:
     *
     * * `Название приложения`, например: `DromAuto`
     * * `Версия приложения`, например: `5.12.0`
     * * `Название платформы`, например: `Android`
     * * `Бренд устройства`, например: `samsung`
     * * `Модель устройства`, например: `SM-N975F`
     * * `Пока неизвестный мне параметр`, например: `2.25`
     *
     * Пример: `DromAuto/5.12.0 (Android; samsung; SM-N975F; 2.25)`
     */
    private fun userAgentForApi(): String {
        return buildApiUserAgent(device.brand, device.model)
    }

    /**
     * Метод для генерации заголовка `User-Agent` под требования `android браузера`
     *
     * Пример: `Mozilla/5.0 (Linux; Android 7.1.2; SM-N975F Build/N2G48H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/68.0.3440.70 Mobile Safari/537.36 Android DromAuto/5.12.0(759) SM-N975F`
     */
    private fun userAgentForWeb(): String {
        return "Mozilla/5.0 (Linux; Android ${device.osVersion}; ${device.model} Build/${device.build}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/68.0.3440.70 Mobile Safari/537.36 Android DromAuto/${Constants.APP_VERSION}(759) ${device.model}"
    }

    /**
     * Метод получает и сохраняет в хранилище обязательный параметр `Ring` и `гео-данные IP-адреса`
     *
     * Эти параметры трубуются для всех запросов к серверу `Drom`!
     * Передаются через заголовки `Cookie` и `Ring`
     *
     * @throws IllegalStateException Если сервер не вернул или клиент по какой-то причине не сохранил параметры в хранилище
     */
    @Throws(IllegalStateException::class, RuntimeException::class)
    private fun saveRingAndIpGeo() {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("User-Agent", userAgentForApi())
            .add("App-Build-Version", Constants.APP_VERSION)
            .add("Os", "android")
            .build()

        val params = mapOf(
            "deviceId" to fetchDeviceIdFromCookieStore(),
            "recSysDeviceId" to fetchDeviceIdFromCookieStore(),
            "app_id" to Constants.APP_ID,
            "timestamp" to currentTimeMillis()
        )

        val url = generateUrl("https://api.drom.ru/v1.3/mycars/fetch", params)

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                if (!cookieStore.containsKey("ring")) {
                    throw IllegalStateException("Параметр \"ring\" небыл сохранен в хранилище.")
                }

                if (!cookieStore.containsKey("cookie_cityid")) {
                    throw IllegalStateException("Параметр \"cookie_cityid\" небыл сохранен в хранилище.")
                }

                if (!cookieStore.containsKey("cookie_regionid")) {
                    throw IllegalStateException("Параметр \"cookie_regionid\" небыл сохранен в хранилище.")
                }

                if (!cookieStore.containsKey("my_geo")) {
                    throw IllegalStateException("Параметр \"my_geo\" небыл сохранен в хранилище.")
                }

                return@use
            } else if (null != response.body) {
                val tree = mapper.readTree(response.body!!.string())

                if (tree.has("error")) {
                    val exception = try {
                        val error = mapper.readValue<DromApiError>(tree["error"].toString())
                        RuntimeException(error.payload.message)
                    } catch (e: Exception) {
                        throw IllegalArgumentException("Сервер вернул ошибку, но формат неизвестный.", e)
                    }

                    throw exception
                }
            }

            throw RuntimeException("Неизвестная ошибка при получении параметров: \"ring\", \"cookie_cityid\", \"cookie_regionid\", \"my_geo\".")
        }
    }

    /**
     * Отправка события с параметрами нового устройства на сервер о том, что приложение запущено
     *
     * `Данный метод является обязательным, поскольку без него устройство будет невалидным и выдача контактов будет ограниченным`
     */
    private fun sendStartEvent() {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "veryFirstHit",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Project", "drom_auto")
            .add("Device-Id", fetchDeviceIdFromCookieStore())
            .add("Ring", fetchRingFromCookieStore())
            .add("User-Agent", userAgentForApi())
            .add("App-Build-Version", Constants.APP_VERSION)
            .add("Os", "android")
            .build()

        val params = mapOf(
            "recSysDeviceId" to fetchDeviceIdFromCookieStore(),
            "app_id" to Constants.APP_ID,
            "timestamp" to currentTimeMillis(),
        )

        val body = mapOf(
            "data" to listOf(
                mapOf(
                    "action" to "Старт",
                    "appBuildVersion" to Constants.APP_VERSION,
                    "category" to "Приложение",
                    "extra" to mapOf<String, String>(),
                    "section" to "common",
                    "timeOffset" to serverTimeOffset,
                    "timestamp" to TimeUtils.format(currentTimeMillis()),
                )
            )
        )

        val url = generateUrl("https://api.drom.ru/v1.2/stat/appLog", params)

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .postJson(mapper.writeValueAsString(body))
            .build()

        client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val tree = mapper.readTree(response.body!!.string())

                val success = tree["success"]?.asBoolean()
                    ?: throw IllegalArgumentException("Сервер вернул ответ в недопустимом формате.")

                if (success.not()) {
                    throw RuntimeException("Сервер отклонил событие.")
                }
            } else {
                throw RuntimeException("Неизвестная ошибка при попытке отправить событие.")
            }
        }
    }

    /**
     * Метод для получения профиля авторизованного аккаунта
     */
    private fun fetchProfile(): DromProfile {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "signFrom",
            "segSession",
            "boobs",
            "logged_in",
            "pony",
            "login",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("X-Auth-Token", fetchAuthTokenFromCookieStore())
            .add("Ring", fetchRingFromCookieStore())
            .add("User-Agent", userAgentForApi())
            .add("App-Build-Version", Constants.APP_VERSION)
            .add("Os", "android")
            .build()

        val params = mapOf(
            "recSysDeviceId" to fetchDeviceIdFromCookieStore(),
            "app_id" to Constants.APP_ID,
            "timestamp" to currentTimeMillis()
        )

        val url = generateUrl("https://api.drom.ru/v1.2/user", params)

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val tree = mapper.readTree(response.body!!.string())

                if (tree.has("data")) {
                    if (tree["data"].has("error")) {
                        val error = mapper.readValue<DromError>(tree["data"].toString())
                        throw DromException(error)
                    } else {
                        mapper.readValue(tree["data"].toString())
                    }
                } else throw IllegalArgumentException("Сервер вернул ответ в недопустимом формате.")
            } else {
                throw RuntimeException("Неизвестная ошибка при попытке получить профиль.")
            }
        }
    }

    /**
     * Метод для получения идентификаторов региона и города по координатам
     *
     * Параметры `REGION_ID` и `CITY_ID` требуются для следующих запросов:
     * * `ПОИСК ОБЪЯВЛЕНИЙ`
     * * `ПОЛУЧЕНИЕ ОБЪЯВЛЕНИЯ ПО ИДЕНТИФИКАТОРУ`
     * * `ПОЛУЧЕНИЕ КОНТАКТОВ ОБЪЯВЛЕНИЯ`
     *
     * @param latitude Широта
     * @param longitude Долгота
     *
     * @return [Pair.first] - region_id, [Pair.second] - city_id
     */
    private fun fetchLocation(latitude: Double, longitude: Double): Pair<Int, Int> {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "signFrom",
            "segSession",
            "boobs",
            "logged_in",
            "pony",
            "login",
            "uid",
            "la",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Ring", fetchRingFromCookieStore())
            .add("User-Agent", userAgentForApi())
            .add("App-Build-Version", Constants.APP_VERSION)
            .add("Os", "android")
            .build()

        val params = mapOf(
            "lat" to latitude,
            "lon" to longitude,
            "recSysDeviceId" to fetchDeviceIdFromCookieStore(),
            "app_id" to Constants.APP_ID,
            "timestamp" to currentTimeMillis()
        )

        val url = generateUrl("https://api.drom.ru/v1.1/geo/whereami", params)

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val tree = mapper.readTree(response.body!!.string())

                val regionId = tree["idRegion"]?.asInt()
                    ?: throw IllegalArgumentException("Сервер вернул ответ, но идентификатор региона отсутсвует.")

                val cityId = tree["idCity"]?.asInt()
                    ?: throw IllegalArgumentException("Сервер вернул ответ, но идентификатор города отсутсвует.")

                Pair(regionId, cityId)
            } else {
                throw RuntimeException("Неизвестная ошибка при попытке получить локацию по координатам.")
            }
        }
    }

    /**
     * Метод сохраняет обязательный параметр `segSession` в хранилище куков
     */
    private fun saveNewSegSession() {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "veryFirstHit",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "boobs",
            "pony",
            "signFrom",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Ring", fetchRingFromCookieStore())
            .add("User-Agent", userAgentForApi())
            .add("App-Build-Version", Constants.APP_VERSION)
            .add("Os", "android")
            .build()

        val params = mapOf(
            "recSysDeviceId" to fetchDeviceIdFromCookieStore(),
            "app_id" to Constants.APP_ID,
            "timestamp" to currentTimeMillis()
        )

        val url = generateUrl("https://api.drom.ru/v1.1/reviews/constants", params)

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                cookieStore["segSession"]
                    ?: throw IllegalStateException("Сервер вернул ответ, но параметр \"segSession\" небыл сохранян в хранилище.")
            } else {
                throw RuntimeException("Неизвестная ошибка при попытке получить параметр \"segSession\".")
            }
        }
    }

    /**
     * Метод для отправки SMS сообщения с кодом на номер телефона из параметра [phone]
     *
     * @param phone Номер телефона
     * @param csrfToken CSRF токен (токен можно получить через метод [fetchCsrfToken])
     *
     * @return [Pair.first] Новый CSRF-токен, [Pair.second] Код доступа авторизации
     */
    private fun sign(phone: String, csrfToken: String): Pair<String, String> {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "boobs",
            "pony",
            "signFrom",
            "segSession"
        ) + mapOf(
            "boobs" to "",
            "pony" to "",
            "signFrom" to "",
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Upgrade-Insecure-Requests", "1")
            .add("User-Agent", userAgentForWeb())
            .add("Referer", "https://my.drom.ru/sign?from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result")
            .add("X-Requested-With", "ru.farpost.dromfilter")
            .build()

        val form = FormBody.Builder()
            .add("csrfToken", csrfToken)
            .add("radio", "reg")
            .add("sign", phone)
            .build()

        val request = Request.Builder()
            .url("https://my.drom.ru/sign?from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result")
            .headers(headers)
            .post(form)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val newCsrfToken = Jsoup.parse(response.body!!.string()).getElementById("csrfToken")?.`val`()
                    ?: throw IllegalArgumentException("Сервер вернул ответ, но в ответе отсутсвует \"csrf\" параметр.")

                val signCode = "^https://my\\.drom\\.ru/sign/code/(.*)\\?sign".toRegex().let { regex ->
                    if (!regex.containsMatchIn(response.request.url.toString())) {
                        throw RuntimeException("Неудалось отправить код подтверждения.")
                    } else {
                        regex.find(response.request.url.toString())!!.destructured.component1()
                    }
                }

                Pair(newCsrfToken, signCode)
            } else {
                throw RuntimeException("Неизвестная ошибка при попытке оотправить код подтверждения.")
            }
        }
    }

    /**
     * Проверка кода подтверждения из СМС
     *
     * @param phone Номер телефона на который было отправлено СМС
     * @param code Проверочный код из СМС
     * @param csrf CSRF-токен из метода [sign]
     * @param signCode Код доступа к авторизации из метода [sign]
     */
    private fun checkCode(phone: String, code: String, csrf: String, signCode: String): Boolean {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "signFrom",
            "segSession",
            "PHPSESSID"
        ) + mapOf(
            "darkThemeForced" to "0",
            "boobs" to "",
            "pony" to "",
        )

        val referer =
            "https://my.drom.ru/sign/code/${signCode}?sign=$phone&from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result"

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Upgrade-Insecure-Requests", "1")
            .add("User-Agent", userAgentForWeb())
            .add("Referer", referer)
            .add("X-Requested-With", "ru.farpost.dromfilter")
            .build()

        val form = FormBody.Builder()
            .add("password", code)
            .add("submit", "Подтвердить")
            .add("csrfToken", csrf)
            .add("sent", "")
            .build()

        val url =
            "https://my.drom.ru/sign/code/${signCode}?sign=$phone&from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result"

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .post(form)
            .build()

        client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val body = response.body!!.string()

                if (body.contains("Неверный код", true)) {
                    throw RuntimeException("Неверный код")
                }
            } else if (response.code == 303) {
                val location = response.header("location")
                    ?: throw RuntimeException("Сервер вернул ответ, но в ответе отсутсвует заголовок \"Location\".")

                return location.contains("^app://drom-auto/auth_result$".toRegex())
            } else {
                throw RuntimeException("Неизвестная ошибка при проверке кода из СМС.")
            }
        }

        return false
    }

    /**
     * Метод проверки входа в аккаунт
     */
    private fun checkSign(): Boolean {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "boobs",
            "pony",
            "login",
            "logged_in",
            "signFrom",
            "segSession",
            "PHPSESSID"
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Upgrade-Insecure-Requests", "1")
            .add("User-Agent", userAgentForWeb())
            .add("X-Requested-With", "ru.farpost.dromfilter")
            .build()

        val url = "https://my.drom.ru/sign?from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result"

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.code == 302) {
                val location = response.header("Location")
                    ?: throw RuntimeException("Сервер вернул ответ, но в ответе отсутсвует заголовок \"Location\".")

                location.contains("^app://drom-auto/auth_result$".toRegex())
            } else {
                throw RuntimeException("Неизвестная ошибка при проверке авторизации.")
            }
        }
    }

    /**
     * Проверка полномочий
     */
    private fun checkAuthority() {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "boobs",
            "pony",
            "signFrom",
            "segSession"
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Upgrade-Insecure-Requests", "1")
            .add("User-Agent", userAgentForWeb())
            .add("X-Requested-With", "ru.farpost.dromfilter")
            .build()

        val url = "https://my.drom.ru/checkAuthority?return=app%3A%2F%2Fdrom-auto%2Fauth_result"

        val request = Request.Builder()
            .url(url)
            .headers(headers)
            .build()

        client.newCall(request).execute().use { response ->
            if (response.code == 302) {
                val location = response.header("location")
                    ?: throw IllegalArgumentException("Сервер вернул ответ, но в ответе отсутсвует заголовок \"Location\".")

                if (!location.contains("^app://drom-auto/auth_result$".toRegex())) {
                    throw RuntimeException("Сервер вернул недопустимый формат заголовка \"Location\".")
                }

                if (!cookieStore.containsKey("uid")) {
                    throw IllegalArgumentException("Неудалось сохранить обязательный параметр \"uid\" при проверке полномочий.")
                }
            } else {
                throw RuntimeException("Произошла неизвестная ошибка при проверке полномочий.")
            }
        }
    }

    /**
     * Получение CSRF токена для операции [sign]
     *
     * @return [String] CSRF-токен
     */
    private fun fetchCsrfToken(): String {
        val cookies = cookieStore.filterByKeys(
            "mobile",
            "app_name",
            "app_version",
            "mobileapp",
            "googlePayAvailable",
            "device_id",
            "app_google_auth_enable",
            "ring",
            "veryFirstHit",
            "cookie_cityid",
            "cookie_regionid",
            "my_geo",
            "dark_theme_forced",
            "boobs",
            "pony",
            "uid",
            "signFrom",
        ) + mutableMapOf(
            "darkThemeForced" to "0",
            "boobs" to "",
            "pony" to "",
            "uid" to "",
            "signFrom" to ""
        )

        val headers = Headers.Builder()
            .cookies(cookies)
            .add("Upgrade-Insecure-Requests", "1")
            .add("User-Agent", userAgentForWeb())
            .add("X-Requested-With", "ru.farpost.dromfilter")
            .build()

        val request = Request.Builder()
            .url("https://my.drom.ru/sign?from=drom&mode=api&return=app%3A%2F%2Fdrom-auto%2Fauth_result")
            .headers(headers)
            .build()

        val client = client.newBuilder()
            .followRedirects(false)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful && null != response.body) {
                val content = response.body?.string()
                    ?: throw IllegalStateException("При получении CSRF токена, сервер вернул пустое тело ответа.")

                Jsoup.parse(content).getElementById("csrfToken")?.`val`()
                    ?: throw RuntimeException("Сервер вернул ответ, но в ответе отсутсвует параметр \"CSRF\".")
            } else {
                throw RuntimeException("Произошла неизвестная ошибка при получении CSRF токена.")
            }
        }
    }

    /**
     * Генерация ссылки с параметрами и подписью
     *
     * @param endpoint URL без параметров
     * @param params Параметры которые будут добавлены к ссылке
     *
     * @return [String] Готовая ссылка с query-параметрами и сигнатурой
     */
    private fun generateUrl(endpoint: String, params: Map<String, Any>): String {
        return "$endpoint?${buildHttpQuery(params)}&secret=${SignatureUtil.secretMain(params)}"
    }
}