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)}"
}
}