- Регистрация
- 26.09.2010
- Сообщения
- 1 300
- Реакции
- 127
- Баллы
- 63
Для примера возьмем тренировочную форму регистрации на https://lessons.zennolab.com/ru/advanced. Для начала нам нужно поймать запрос в траффике. Нажимаем F12, переходим на вкладку траффика, заполняем и отправляем форму. В итоге видим вот такой POST запрос
К запросу браузер прицепляет нужные куки, а так же отправляет payload вида application/x-www-form-urlencoded. Конечно можно пойти казалось бы можно заполнить все поля, решить капчу и кликнуть на кнопку. Но есть путь куда проще и быстрее - отправить fetch запрос, который мало того все нужные куки сам из браузера возьмет, так еще и отпечаток у него будет как у браузера.
Для этого нам понадобится небольшая подготовка и немного теории. Начнем с Runtime.evaluate - метода CDP который позволяет передать произвольный JavaScript код в движок браузера и выполнить его в контексте страницы/фрейма/воркера. Этот метод принимает несколько параметров, сегодня нам будут нужны:
- expression - это сам JavaScript код для выполнения
- awaitPromise - дожидаться результата выполнения скрипта или нет
- returnByValue - вернуть результат или нет
C#:
static async Task<string> RunJS(CDPClient cdpClient, string expression, string sessionId, int timeoutMs = 10000)
{
try
{
object evaluationParams = new { expression, awaitPromise = true, returnByValue = true };
var response = await cdpClient.SendCommandAsync("Runtime.evaluate", evaluationParams, timeoutMs, sessionId: sessionId);
var result = response.ToString();
if (string.IsNullOrEmpty(result))
{
throw new Exception("RunJS ответ пустой");
}
return result;
}
catch (Exception ex)
{
throw new Exception($"RunJS Произошла ошибка: {ex.Message}");
}
}
Теперь подготовим метод-обертку над RunJS, который будет принимать входные параметры запроса, подготавливать JS скрипт, отправлять его и возвращать объект CDPFetchResponse с результатами:
C#:
static async Task<CDPFetchResponse> SendFetchRequestAsync(CDPClient cdpClient, string sessionId, string url, string method = "GET", Dictionary<string, string> customHeaders = null, string body = null, int timeoutMs = 10000, bool sendCredentials = true)
{
try
{
// Формируем объект настроек для fetch
var fetchOptions = new JObject
{
{ "method", method },
{ "credentials", sendCredentials ? "include" : "omit" } // Чтобы куки отправлялись даже на кросс-доменные запросы
};
// Добавляем заголовки, если они переданы
if (customHeaders != null && customHeaders.Count > 0)
{
var headersObj = new JObject();
foreach (var header in customHeaders)
{
headersObj[header.Key] = header.Value;
}
fetchOptions["headers"] = headersObj;
}
// Добавляем тело, если это POST/PUT и тело не пустое
if (!string.IsNullOrEmpty(body) && (method.Equals("POST", StringComparison.OrdinalIgnoreCase) || method.Equals("PUT", StringComparison.OrdinalIgnoreCase)))
{
fetchOptions["body"] = body;
}
// Шаблон JS кода, который мы заставим браузер выполнить
// Обернем в самовызывающуюся async функцию и ВЕРНЕМ Promise
string jsCode = $@"
(async () => {{
try {{
let response = await fetch('{url}', {fetchOptions.ToString(Newtonsoft.Json.Formatting.None)});
let resBody = await response.text();
return JSON.stringify({{ status: response.status, body: resBody }});
}} catch (error) {{
return JSON.stringify({{ status: 0, error: error.toString() }});
}}
}})()
";
// Выполняем JS и ждем завершения Promise
string responseBody = await RunJS(cdpClient, jsCode, sessionId, timeoutMs);
// Очистка ответа (RunJS может вернуть экранированный JSON-результат из Runtime.evaluate)
try
{
var parsedResponse = JObject.Parse(responseBody);
string actualValue = parsedResponse["result"]?["result"]?["value"]?.Value<string>();
if (actualValue != null)
{
var innerData = JObject.Parse(actualValue);
return new CDPFetchResponse
{
StatusCode = innerData["status"] != null ? innerData["status"].Value<int>() : 0,
Body = innerData["body"] != null ? innerData["body"].Value<string>() : "",
Error = innerData["error"] != null ? innerData["error"].Value<string>() : null
};
}
}
catch
{
}
return new CDPFetchResponse { StatusCode = 0, Error = "Failed to parse Runtime.evaluate response: " + responseBody };
}
catch (Exception ex)
{
throw new Exception($"SendFetchRequestAsync Ошибка: {ex.Message}");
}
}
public class CDPFetchResponse
{
public int StatusCode { get; set; }
public string Body { get; set; }
public string Error { get; set; }
}
Помимо самого cdp-клиента, сессии и ссылки в метод передаются тип запроса, необязательные параметры с хедерами и телом запроса, таймаут и параметр credentials который отвечает за то прикреплять ли куки к запросу или нет.
Теперь повторим наш тренировочный POST запрос. Сначала заходим на сайт, а потом делаем запрос:
C#:
string body = $"fname=Ivan&lname=Ivanov&email=zenno%40afewg.ru&pass1=123123&pass2=123123&captcha=DC9N4W&submit=%D0%A1%D0%BE%D0%B7%D0%B4%D0%B0%D1%82%D1%8C+%D0%B0%D0%BA%D0%BA%D0%B0%D1%83%D0%BD%D1%82";
var headers = new Dictionary<string, string>
{
{ "Content-Type", "application/x-www-form-urlencoded" },
};
var fetch = await SendFetchRequestAsync(cdpClient, firstPageSessionId, "https://lessons.zennolab.com/ru/adv_reg", "POST", headers, body, 10000, true);
Как видно тут в заголовках мы указали Content-Type, указали тело и тип запроса - POST. Запускаем наш код, пока все грузится успеваем нажать F12 в ZennoBrowser и смотрим траффик:
В итоге у нас получился запрос практически 1-в-1, не хватает только некоторых заголовков типа upgrade-insecure-requests, но в случае необходимости их можно самостоятельно указать в словаре headers.
И вроде бы казалось все легко и просто. Но на сайтах иногда бывает так что находясь на основном домене site.com запрос к внутреннему апи уходит либо на субдомен либо на другой домен. И тут то перед нами встает коварный CORS.
Чтобы воспроизвести подобную ситуацию мы зайдем не на https://lessons.zennolab.com/ru/advanced а просто на https://zennolab.com/ru/ и повторим наш запрос. в итоге получим:
Как работает этот злосчастный CORS в браузере? Сначала браузер отправляет серверу Preflight-запрос (OPTIONS), если в ответ сервер присылает заголовки Access-Control-Allow-Origin, Access-Control-Allow-Credentials с нужным содержимым (например Access-Control-Allow-Origin: * и отсутствующий Access-Control-Allow-Credentials) то основной запрос проходит, если же содержимое не то или их вовсе нет то основной запрос не проходит и мы видим CORS error как на скрине выше.
Что же делать в такой ситуации? На помощь нам приходит вся мощь CDP. Мы можем просто перехватить OPTIONS запрос, не отправлять его на сервер а вернуть браузеру 204 со всеми разрешающими CORS-заголовками. Тогда браузер ничего не заподозрит и отправит основной запрос. Для этого будем использовать CDP-домен fetch (не путать с самим запросом fetch из javascript). Включаем fetch домен и в настройках перехват и запросов к серверу и ответов от сервера. Запрос перехватываем для того чтобы перехватить OPTIONS о котором шла речь выше и вернуть браузеру нужный ответ. А перехват ответа нужен для того чтобы в ответ сервера прикрепить CORS-заголовки, чтобы браузер не ругался.
C#:
await cdpClient.SendCommandAsync("Fetch.enable", new
{
patterns = new[]
{
new { requestStage = "Request" },
new { requestStage = "Response" }
},
handleAuthRequests = false
}, sessionId: sessionId);
После этого когда буду идти запросы и приходить на них ответы будет случаться событие Fetch.requestPaused. Поэтому подписываемся на него
C#:
cdpClient.SubscribeEvent("Fetch.requestPaused", fetchRequestPausedHandler);
Тут в дело вступает обработчик fetchRequestPausedHandler, который как видно из названия будет обрабатывать все события Fetch.requestPaused. Чтобы не растягивать пост приводить его и сам метод для отправки запросов с обходом CORS целиком не буду (их можно будет увидеть в прикрепленном проекте), только обозначу некоторые моменты:
У каждого события есть свой requestId, зная его мы можем выполнять следующие команды (это не полный список):
1) Fetch.continueResponse/Fetch.continueRequest - продолжить запрос, при этом можно оставить все как есть а можно внести изменения в запрос (например поменять тело запроса или хедеры).
2) Fetch.fulfillRequest - вернуть ответ браузеру. На стадии запроса с помощью этой команды можно вообще исключить обращение к серверу, а сразу вернуть готовый ответ браузеру (как раз для нашего OPTIONS запроса), на стадии ответа можно перехватить ответ сервера, модифицировать его и вернуть браузеру.
3) Fetch.getResponseBody - чтобы перехватить тело ответа сервера, например перехватываем тело, в нем json, меняем в нем нужное значение и передаем браузеру через fullfillRequest
Так же помимо requestId у события есть responseStatusCode (если этот параметр не null значит мы перехватили ответ браузеру, в противном случае запрос) и requestUrl (тут думаю из названия все понятно).
Логика перехватчика примерно такая:
Перехватываем запрос/ответ ->
Парсим responseStatusCode, requestUrl и метод ->
Если requestUrl не содержит заданный нами урл, то значит просто пропускаем такие запросы/ответы (через continueResponse/continueRequest) ->
Если метод OPTIONS то значит мы поймали тот самый preflight запрос. Возвращаем его браузеру через fulfullRequest с responseCode 204:
C#:
if (reqMethod == "OPTIONS" && responseStatusCode == null)
{
await cdpClient.SendCommandAsync("Fetch.fulfillRequest", new
{
requestId = fetchId,
responseCode = 204, // No Content
responseHeaders = corsHeaders
}, sessionId: sessionId);
return;
}
Если поймали запрос или ответ то так же модифицируем заголовки для CORS и передаем ответ в браузер через fulfullRequest.
В итоге находясь на zennolab.com мы можем успешно выполнить запрос к lessons.zennolab.com обойдя CORS политики
Нюанс:
К сожалению CDP не позволяет переназначать некоторые Sec-Fetch-* заголовки которые регулируются на уровне сетевого стека браузера на С++. Например заголовок Sec-Fetch-User, когда переход произошел из-за клика юзера имеет значение ?1. Но если мы сделаем отправку так как описано выше и даже подставим/подменим этот заголовок, то по факту все равно ?1 там не будет. Но это все равно не мешает пользоваться fetch запросами, такие проверки наверное могут быть у каких-нибудь злых защит, а у более простых сайтов fetch'и чтобы дергать внутренние апишки залетают на ура. Чтобы обойти это нужно патчить исходники, я к сожалению пока еще не дорос до такого уровня (может быть еще через несколько конкурсов, кто знает), поэтому пока просьба к уважаемым разработчикам ZennoBrowser по возможности пропатчить этот момент в исходниках.
На этом все, всем спасибо за внимание!
Вложения
Последнее редактирование:


