Авторизироваться на vk.com с помощью Dart

Мне необходимо войти в аккаунт на vk.com через мое Flutter приложение (Без создания приложения в ВК и получения access_token). Я проанализировал запросы, которые делает браузер, и выяснил, что сначала необходимо заполнить формы:

{act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email: +79999999999, pass: 0123456789}

Значения и ключи находятся внутри html (name:value) :

<div id="index_login" class="page_block index_login">
    <form method="post" name="login" id="index_login_form" action="https://login.vk.com/?act=login">
      <input type="hidden" name="act" id="act" value="login">
      <input type="hidden" name="role" value="al_frame">
      <input type="hidden" name="expire" id="index_expire_input" value="">
      <input type="hidden" name="_origin" value="https://vk.com">
      <input type="hidden" name="ip_h" value="abcdefgh0123456789">
      <input type="hidden" name="lg_h" value="0123456789abcdefgh">
      <input type="text" class="big_text" name="email" id="index_email" value="" placeholder="Телефон или email">
      <input type="password" class="big_text" name="pass" id="index_pass" value="" placeholder="Пароль" onkeyup="toggle('index_expire', !!this.value);toggle('index_forgot', !this.value)">
      <button id="index_login_button" class="index_login_button flat_button button_big_text">Войти</button>
      <div class="forgot">
        <div class="checkbox" id="index_expire" onclick="checkbox(this);ge('index_expire_input').value=isChecked(this)?1:'';" role="checkbox" aria-checked="false" tabindex="0">Чужой компьютер</div>
        <a id="index_forgot" class="index_forgot" href="/restore" target="_top">Забыли пароль?</a>
      </div>
    </form>
  </div>

А тут находится значение для ключа to (оно всегда равно aW5kZXgucGhw):

window.handlePageParams && handlePageParams({"id":0,"loc":"","noleftmenu":1,"wrap_page":1,"width":960,"width_dec":0,"width_dec_footer":0,"top_home_link_class":"TopHomeLink TopHomeLink--logoWithText","body_class":"index_page new_header_design ","to":"aW5kZXgucGhw","counters":[],"pvbig":0,"pvdark":1});addEvent(document, 'click', onDocumentClick);

Headers я взял из запроса, который сделал браузер (скопировал все, кроме cookie).

Далее по нажатию кнопки "Войти" браузер делает POST запрос по адресу: https://login.vk.com/?act=login (который так же находится внутри html) и передает словарь вышеупомянутых параметров. В ответ приходит 302 код состояния http (т.е. redirect как я понял). Headers, полученные браузером и моим приложением почти совпадают, за исключением location.

location в браузере : https://vk.com/login.php?act=slogin&to=aW5kZXgucGhw&s=1&__q_hash=149c8d2a4784c9a019d4fb720469378e

location в Dart : https://vk.com/login?&to=&s=0&m=1&email=

ОБНОВЛЕНО

Видимо мой POST запрос неверно составлен или что-то в этом роде, раз ответы от сервера отличаются.

Вот мой код на Dart:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';

Future<void> login() async {
  Map<String, String> headers = {
    'accept':
    'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
    'cache-control': 'max-age=0',
    //no cookies
    'referer': 'https://vk.com',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent':
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
  };

  String email='+79999999999'; //email or phone
  String password='0123456789';

  http.Client client = http.Client();

  try {
    http.Response uriResponse =
    await client.get('https://vk.com/', headers: headers);

    // get params for POST request
    String html = uriResponse.body;

    String start = '<form method="POST" name="login" id="quick_login_form"',
        end = "toggle('quick_forgot', !this.value)";
    html = html.substring(html.indexOf(start) + start.length);
    html = html.substring(0, html.indexOf(end));

    start = 'action="';
    end = '"';
    String action = html.substring(html.indexOf(start) + start.length);
    html = action.substring(action.indexOf(end) + end.length);
    action = action.substring(0, action.indexOf(end));
    print(action); // https://login.vk.com/?act=login

    List<String> parts = html.split('<input type="');
    Map<String, String> forms = {};
    parts.removeAt(0);
    String name, value;
    parts.forEach((e) {
      if (e.startsWith('hidden')) {
        start = 'name="';
        end = '"';
        name = e.substring(e.indexOf(start) + start.length);
        name = name.substring(0, name.indexOf(end));
        start = 'value="';
        value = e.substring(e.indexOf(start) + start.length);
        value = value.substring(0, value.indexOf(end));
        forms[name] = value;
      } else if (e.contains('name="')) {
        start = 'name="';
        end = '"';
        name = e.substring(e.indexOf(start) + start.length);
        name = name.substring(0, name.indexOf(end));
        if (name == 'email') {
          value = email;
          forms[name] = value;
        } else if (name == 'pass') {
          value = password;
          forms[name] = value;
        }
      }
    });

    html = uriResponse.body;
    start = 'handlePageParams(';
    html = html.substring(html.indexOf(start) + start.length);
    start = '"to":"';
    html = html.substring(html.indexOf(start) + start.length);
    forms['to'] = html.substring(0, html.indexOf('"'));

    print(forms
        .toString()); // {act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email: +79999999999, pass: 0123456789}

    //headers['content-length']='210'; error...
    headers['content-type']='application/x-www-form-urlencoded';
    headers['origin']='https://vk.com';
    headers['referer']='https://vk.com/';
    headers['sec-fetch-dest']='iframe';

    var resp = await client.post(
      action,
      headers: headers,
      body: jsonEncode(forms),
    );

    print(resp.statusCode); // 302
    print(resp.headers); // {connection: keep-alive, x-powered-by: KPHP/7.4.106227, location: https://vk.com/login?&to=&s=0&m=1&email=, cache-control: no-store, set-cookie: remixir=DELETED; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=.vk.com; secure; HttpOnly; SameSite=None,remixlang=0; expires=Sat, 26 Feb 2022 20:14:15 GMT; path=/; domain=.vk.com; secure; SameSite=None, date: Thu, 25 Feb 2021 20:39:20 GMT, content-encoding: gzip, strict-transport-security: max-age=15768000, content-length: 20, p3p: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT", content-type: text/html; charset=windows-1251, server: kittenx}

  } finally {
    client.close();
  }
}

ДОБАВЛЕНО

Реализация на Python:

import lxml.html
import requests

url='https://vk.com/'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language':'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
    'Accept-Encoding':'gzip, deflate',
    'Connection':'keep-alive',
    'DNT':'1'
}

session = requests.session()
data = session.get(url, headers=headers)
page = lxml.html.fromstring(data.content)

form = page.forms[0]
form.fields['email'] =  'EMAIL/PHONE'
form.fields['pass'] = 'PASSWORD'

response = session.post(form.action, data=form.form_values())
print('onLoginDone' in response.text)

Ответы (1 шт):

Автор решения: Byte Byte

Все оказалось проще... Неделя была потрачена не зря)))

Необходимы были библиотеки: dio, dio_cookie_manager, cookie_jar.

Одной из главных и самых глупых ошибок было использование обычного Map вместо FormData для отправки POST запроса.

import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';

Future login() async {
  var dio = Dio();
  var cookieJar = CookieJar();
  dio.interceptors.add(CookieManager(cookieJar));
  dio.options.followRedirects = true;

  const String phone = 'PHONE';
  const String password = 'PASSWORD';

  Map<String, String> headers = HashMap();
  headers.addAll({
    'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
    'DNT': '1',
  });

  String url = 'https://vk.com/';
  dio.options.headers = headers;
  cookieJar.loadForRequest(Uri.parse('https://login.vk.com/'));
  Response response = await dio.get(url);

  String remixstid = response.headers['set-cookie'].toString();
  remixstid = remixstid
      .substring(remixstid.indexOf('remixstid') + 'remixstid'.length + 1);
  remixstid = remixstid.substring(0, remixstid.indexOf(';'));

  String remixlgk = response.headers['set-cookie'].toString();
  remixlgk =
      remixlgk.substring(remixlgk.indexOf('remixlgk') + 'remixlgk'.length + 1);
  remixlgk = remixlgk.substring(0, remixlgk.indexOf(';'));

  String remixsid = response.headers['set-cookie'].toString();
  remixsid =
      remixsid.substring(remixsid.indexOf('remixsid') + 'remixsid'.length + 1);
  remixsid = remixsid.substring(0, remixsid.indexOf(';'));

  String remixusid = response.headers['set-cookie'].toString();
  remixusid = remixusid.substring(remixusid.indexOf('remixusid')+'remixusid'.length+1);
  remixusid=remixusid.substring(0,remixusid.indexOf(';'));

  String html = response.data;
  html = html.substring(html.indexOf('id="quick_login_form"'));
  html = html.substring(0, html.indexOf('</form>'));

  String start = 'action="', end = '"';
  String action = html.substring(html.indexOf(start) + start.length);
  html = action.substring(action.indexOf(end) + end.length);
  action = action.substring(0, action.indexOf(end));

  List<String> parts = html.split('<input type="');
  Map<String, String> forms = HashMap();
  parts.removeAt(0);
  String name, value;
  parts.forEach((e) {
    if (e.startsWith('hidden')) {
      start = 'name="';
      end = '"';
      name = e.substring(e.indexOf(start) + start.length);
      name = name.substring(0, name.indexOf(end));
      start = 'value="';
      value = e.substring(e.indexOf(start) + start.length);
      value = value.substring(0, value.indexOf(end));
      forms[name] = value;
    } else if (e.contains('name="')) {
      start = 'name="';
      end = '"';
      name = e.substring(e.indexOf(start) + start.length);
      name = name.substring(0, name.indexOf(end));
      if (name == 'email') {
        value = phone;
        forms[name] = value;
      } else if (name == 'pass') {
        value = password;
        forms[name] = value;
      } else
        print('No such constant!');
    }
  });

  forms['to'] = 'bG9naW4/bT0xJmVtYWlsPWFzJnRvPWFXNWtaWGd1Y0dodw--';

  var forms2 = FormData.fromMap(forms);
  response = await dio.post(
    'https://login.vk.com/',
    queryParameters: {'act': 'login'},
    data: forms2,
    options: Options(
      followRedirects: true,
      validateStatus: (status) {
        return status < 500;
      },
    ),
  );

  List<String> lst = [
    'remixbdr=0',
    'remixsid=$remixsid',
    'remixusid=$remixusid',
    'remixstid=$remixstid',
    'remixflash=0.0.0',
    'remixscreen_width=1920',
    'remixscreen_height=1080',
    'remixscreen_dpr=1',
    'remixscreen_depth=24',
    'remixscreen_orient=1',
    'remixscreen_winzoom=1',
    'remixseenads=0',
    'remixlang=0',
    'remixlhk=$remixlgk',
  ];

  response.headers.forEach((name, values) {
    if (name.contains('remixq')) {
      lst.add(name + '=' + values.join());
    }
  });

  dio.options.headers['cookie'] = lst.join('; ');

  String location = response.headers['location'].toString();
  int length = location.length - 1;
  location = location.substring(1, length);

  Map<String, String> queryParams = {};

  String tmp = location.substring(location.indexOf('?') + 1);
  location = location.substring(0, location.indexOf('?'));

  List<String> params = tmp.split('&');
  params.forEach((element) {
    List<String> kv = element.split('=');
    queryParams.putIfAbsent(kv[0], () => kv[1]);
  });

  queryParams['to'] = 'aW5kZXgucGhw';

  response = await dio.get(location, queryParameters: queryParams);
  print('LOGIN : '+response.data.toString().contains('onLoginDone').toString());
}

→ Ссылка