WebAPI
unknown
kotlin
a year ago
39 kB
13
Indexable
Never
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)}" } }