Позднее Ctrl + ↑

3 часть, «кропотливая работа и долгожданный успех»

1 часть, «как нельзя относиться к корпоративным клиентам»
2 часть, «анализ, получение исходных данных»

Сделай шаг — дорога появится сама собой…

и мы приступим...

вот требования graphisoft к сложности паролей:
Ваш пароль должен:

  • содержать не менее 6 символов
  • содержать не менее одной буквы
  • содержать не менее одной заглавной буквы
  • содержать не менее одной цифры
  • отличаться от вашего адреса электронной почты
  • не содержать пробелов, знаков препинания, специальных или национальных символов.

для начала хочу показать небольшую функцию для генерации паролей, фишка в том, что пароль должен содержать минимум две цифры, да-да... хрен вы зарегистрируетесь с одной цифрой, короче ловите функцию

function randomPassword() {
    $alphabet = "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789";
    $pass = array();
    $alphaLength = strlen($alphabet) - 1;
    for ($i = 0; $i < 10; $i++) {
        $n = rand(0, $alphaLength);
        $pass[] = $alphabet[$n];
    }
	$password = implode($pass);
	$password = substr_replace($password, rand(0,9), rand(1,3), 0);
	$password = substr_replace($password, rand(0,9), rand(4,7), 0);
    return $password;
}

сразу оговорюсь, при регистрации 150 учёток ни разу не споткнулась 😇

ну, вроде всё готово для реализации проекта.

Настоятельно рекомендую зайти на первую страницу и сохранить cookie, чтобы было максимально похоже на действия человека. Так же часто не отправляйте запросы, я регистрировал по одному сотруднику раз в пять минут, все таки есть опасность словить captcha. Большая активность — подозрительна. А так, раз в пять минут за ночь и пол дня всех зарегистрировал.

Не буду грузить, расписывая каждое действие, пройдусь по самым главным моментам.
При отправке данных используется три метода передачи:

  • GET
  • POST
  • PUT

пришлось их все учитывать в одной функции, чтобы не плодить сущностей

так же данные формируются разными форматами

  • application/json
  • application/x-www-form-urlencoded

я сейчас выложу мою монстровую функцию

function get_url($url, $method, $ctype, $referer, $data) {
	// $url - ссылка, на страницу
	// $method - метод передачи (GET, POST или PUT)
	// $ctype - 1, 2 или 3, смотри ниже
	// $referer - страница, с которой мы, якобы, перешли
	// $data - данные, если отправляем форму
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
	if ($data!=='') {
		if ($ctype == 1) {
			// если тип данных json
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
		} else if ($ctype == 2) {
			// если тип данных www-form
			curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($data));
		} else if ($ctype == 3) {
			// сложная конструкция, когда нам надо работать с полученным заголовком, чтобы вытащить оттуда token авторизации
			curl_setopt($ch, CURLOPT_HEADER, 1);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
			curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($data));
		}
	}
	curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');
	// формируем заголовок запроса
	$headers = array();
	$headers[] = 'Connection: keep-alive';
	$headers[] = 'Sec-Ch-Ua: \" Not;A Brand\";v=\"99\", \"Opera\";v=\"79\", \"Chromium\";v=\"93\"';
	$headers[] = 'Dnt: 1';
	$headers[] = 'Sec-Ch-Ua-Mobile: ?0';
	$headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 OPR/79.0.4143.22';
	if ($ctype == 1) {
		// если тип данных json
		$headers[] = 'Content-Type: application/json; charset=UTF-8';
	} else {
		// если тип данных www-form
		$headers[] = 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8';
	}
	$headers[] = 'Accept: application/json, text/javascript, */*; q=0.01';
	$headers[] = 'X-Requested-With: XMLHttpRequest';
	$headers[] = 'Sec-Ch-Ua-Platform: \"Windows\"';
	$headers[] = 'Sec-Fetch-Site: same-origin';
	$headers[] = 'Sec-Fetch-Mode: cors';
	$headers[] = 'Sec-Fetch-Dest: empty';
	$headers[] = 'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7';
	$headers[] = 'Origin: '.$referer;
	$headers[] = 'Referer: '.$referer;
	// используем сформированный заголовок
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	// подключаем файл с cookie, сразу в него сохраняем и из него читаем
	curl_setopt($ch, CURLOPT_COOKIEFILE, dirname(__FILE__).'/cookie.txt');
	curl_setopt($ch, CURLOPT_COOKIEJAR, dirname(__FILE__).'/cookie.txt');
	$response = curl_exec($ch);
	// используем глобальную переменную, чтобы получить token авторизации, который возвращается в header
	global $header;
	$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
	$header = substr($response, 0, $header_size);
	// возвращаем результатом полученный текст
	return $response;
	if (curl_errno($ch)) {
		echo 'Error:' . curl_error($ch);
	}
	curl_close($ch);
}

вот такая страхолюдина получилась в итоге

все действия по шагам:

  1. заполняем форму регистрации и отправляем запрос
    • открываем главную страницу
    • переходим на страницу личного кабинета
    • проверяем, зарегистрирован ли уже e-mail
    • генерируем пароль
    • отправляем форму с данными
  2. подтверждаем регистрацию
    • результатом отправки регистрационной формы будет json с кодом подтверждения регистрации (ура! не надо лезть в почту пользователя и искать письмо)
    • в этом же json получаем graphisoft id пользователя, потребуется позже
    • подтверждаем регистрацию двумя запросами, как оказалось второй запрос тоже необходим
  3. заходим в кабинет администратора и отправляем запрос на присоединение пользователя
    • аккуратно логинемся в кабинет, получаем json с данными организации, на потребуется graphisoft id организации, для создания запроса на присоединение пользователя
    • формируем и отправляем запрос
  4. заходим в кабинет пользователя и подтверждаем запрос
    • аккуратно логинемся в кабинет пользователя
    • формируем запрос и подтверждаем присоединение к компании

ну и сам код:

<?php

// данные регистрируемого сотрудника
$fname = 'Иван';
$sname = 'Петров';
$email = 'user@domain.ru';
$pasword = 'koo0ovo5Kamu';

// данные администратора компании
$admin_login = 'admin@domain.ru';
$admin_password = 'P@5$w0rD';

$header = '';

function get_url($url, $method, $ctype, $referer, $data) {
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
	if ($data!=='') {
		if ($ctype == 1) {
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
		} else if ($ctype == 2) {
			curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($data));
		} else if ($ctype == 3) {
			curl_setopt($ch, CURLOPT_HEADER, 1);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
			curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($data));
		}
	}
	curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');
	$headers = array();
	$headers[] = 'Connection: keep-alive';
	$headers[] = 'Sec-Ch-Ua: \" Not;A Brand\";v=\"99\", \"Opera\";v=\"79\", \"Chromium\";v=\"93\"';
	$headers[] = 'Dnt: 1';
	$headers[] = 'Sec-Ch-Ua-Mobile: ?0';
	$headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 OPR/79.0.4143.22';
	if ($ctype == 1) {
		$headers[] = 'Content-Type: application/json; charset=UTF-8';
	} else {
		$headers[] = 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8';
	}
	$headers[] = 'Accept: application/json, text/javascript, */*; q=0.01';
	$headers[] = 'X-Requested-With: XMLHttpRequest';
	$headers[] = 'Sec-Ch-Ua-Platform: \"Windows\"';
	$headers[] = 'Sec-Fetch-Site: same-origin';
	$headers[] = 'Sec-Fetch-Mode: cors';
	$headers[] = 'Sec-Fetch-Dest: empty';
	$headers[] = 'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7';
	$headers[] = 'Origin: '.$referer;
	$headers[] = 'Referer: '.$referer;
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt($ch, CURLOPT_COOKIEFILE, dirname(__FILE__).'/cookie.txt');
	curl_setopt($ch, CURLOPT_COOKIEJAR, dirname(__FILE__).'/cookie.txt');
	$response = curl_exec($ch);
	global $header;
	$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
	$header = substr($response, 0, $header_size);
	return $response;
	if (curl_errno($ch)) {
		echo 'Error:' . curl_error($ch);
	}
	curl_close($ch);
}

// удаляем старые куки
if (file_exists(dirname(__FILE__).'/cookie.txt')) {
   unlink(dirname(__FILE__).'/cookie.txt');
}		
// заходим на главную
get_url('https://graphisoft.com/ru','GET',1,'','');
// переходим на форму пользователя
get_url('https://graphisoftid.graphisoft.com/Account/ServiceLogin?Application=GRAPHISOFT&ReturnUrl=https://graphisoft.com/ru','GET',1,'https://graphisoftid.graphisoft.com/','');
// переходим на форму регистрации
get_url('https://graphisoftid.graphisoft.com/#/register/3/https%3a%2f%2fgraphisoft.com%2fru','GET',1,'https://graphisoftid.graphisoft.com/','');

//проверяем e-mail
$check_mail = get_url('https://graphisoftid.graphisoft.com/api/user/CheckEmail/','PUT',2,'https://graphisoftid.graphisoft.com/',array("EmailAddress" => $email));
if ($check_mail == 'true') {
	$data = array('applicationName' => null,
				  'customDataObject' => null,
				  'redirectUrl' => null,
				  'emailHintLocalizationKey' => null,
				  'firstName' => $fname,
				  'lastName' => $sname,
				  'isValidated' => true,
				  'email' => $email,
				  'password' => $pasword,
				  'confirmPassword' => $pasword,
				  'isPrivacyPolicyAccepted' => true,
				  'companyName' => 'ООО "Рога и Копыта"',
				  'countryId' => 186,
				  'federated' => false,
				  'callbackUrl' => 'https://graphisoft.com/ru',
				  'isDirty' => true,
				  'errors' => array());

	//echo json_encode($data)."\n";

	$ecoded_json = get_url('https://graphisoftid.graphisoft.com/api/user/CreateUser','POST',1,'https://graphisoftid.graphisoft.com/',$data);

	$jsonObj = json_decode($ecoded_json);

	if ($jsonObj === null && json_last_error() !== JSON_ERROR_NONE) {
	   echo "у меня не получилось отправить регистрационную форму с данными...  извините\n";
	} else {
		// удаляем старые куки
		unlink(dirname(__FILE__).'/cookie.txt');
		get_url($jsonObj->{'VerificationURL'}.$jsonObj->{'VerificationCode'},'GET',1,'','');
		$gsid_url = get_url('https://graphisoftid.graphisoft.com/api/user/VerifyUser','PUT',2,'https://graphisoftid.graphisoft.com/',array("code" => $jsonObj->{'VerificationCode'}));
		$parts = parse_url($gsid_url);
		parse_str($parts['query'], $query);
		$user_gsid = $query['gsid'];
		if ($user_gsid !== '') {
			echo "пользователь ".$email." успешно зарегистрирован (Graphisoft sid: ".$user_gsid.")\n";
			// удаляем старые куки
			unlink(dirname(__FILE__).'/cookie.txt');
			// заходим на главную
			get_url('https://graphisoft.com/ru','GET',1,'','');
			get_url('https://graphisoftid.graphisoft.com/','GET',1,'','');
			// авторизуемся под пользователем
			$data = array('email' => $admin_login,
						  'errors' => array(),
						  'isDirty' => true,
						  'isNullo' => false,
						  'password' => $admin_password);			
			$json_response = get_url('https://graphisoftid.graphisoft.com/api/Authenticate/Login','POST',1,'https://graphisoftid.graphisoft.com/',$data);
			$obj = json_decode($json_response);
			$companyGsId = $obj->{'Company'}->{'GsId'};
			// Загружаем список подключенных пользователей
			foreach ($obj->{'Company'}->{'CompanyUsers'} as $user) {
				$users_list[] = array("FirstName" => $user->{'FirstName'},"LastName" => $user->{'LastName'},"EmailAddress" => $user->{'EmailAddress'},"GsId" => $user->{'GsId'});
			}


			$data = array('companyGsId' => $companyGsId,
						  'email' => $email,
						  'errors' => array(),
						  'isDirty' => true,
						  'requestedGraphisoftUserId' => $user_gsid);
			get_url('https://graphisoftid.graphisoft.com/api/user/SendInvitation','PUT',2,'https://graphisoftid.graphisoft.com/',$data);
			echo "выслано приглашение присоединиться к компании\n";
			// удаляем старые куки
			unlink(dirname(__FILE__).'/cookie.txt');
			// переходим на форму пользователя
			get_url('https://graphisoftid.graphisoft.com/','GET',1,'','');
			// авторизуемся под пользователем
			$data = array('email' => $email,
						  'errors' => array(),
						  'isDirty' => true,
						  'isNullo' => false,
						  'password' => $pasword);			
			$json_response = get_url('https://graphisoftid.graphisoft.com/api/Authenticate/Login','POST',1,'https://graphisoftid.graphisoft.com/',$data);
			$obj = json_decode($json_response);
			$user_gsid = $obj->{'GraphisoftUser'}->{'GsId'};
			if ($user_gsid == '') {
				echo "не смог зайти под пользователем в личный кабинет\n";
			} else {
				$data = array('actionId' => 2,
							  'companyGsId' => $companyGsId,
							  'graphisoftUserGsId' => $user_gsid,
							  'reactionId' => 1);
				get_url('https://graphisoftid.graphisoft.com/api/user/ProcessingPendingRequest','PUT',1,'https://graphisoftid.graphisoft.com/',$data);
				echo "сотрудник подтвердил приглашение\n";
			}
		} else {
			echo "у меня не получилось подтвердить регистрацию...  извините\n";
		}
	}
} else {
	echo "$email уже зарегистрирован в системе\n";
}
// удаляем старые куки
unlink(dirname(__FILE__).'/cookie.txt');
?>

спасибо тем, кто дочитал до конца

2 часть, «анализ, получение исходных данных»

1 часть, «как нельзя относиться к корпоративным клиентам»

Всегда начинайте со сбора информации, это важно! Исходные данные определят направление и общую схему работы вашего решения.

Любая задача решается по принципу «от общего к частному».

давайте рассмотрим алгоритм действия администратора:

  1. регистрация пользователя на сайте graphisoft.com
  2. подтверждение регистрации в посте пользователя
  3. авторизация на сайте graphisoft.com под административной учетной записью
  4. отправка приглашения пользователю
  5. авторизация на сайте graphisoft.com под пользовательской учетной записью
  6. подтверждение приглашения

Вроде, не сложно... да и в процессе тестовой регистрации я не заметил использования captcha. Это очень важно, иначе полная автоматизация была бы невозможна.

Какие данные нам нужны, чтобы проделать весь вышеописанный алгоритм?
данные пользователя:

  • имя
  • фамилия
  • e-mail
  • пароль (пароль будем генерировать с учетом требований безопасности graphisoft)

данные администратора:

  • e-mail
  • пароль

Осталось выбрать инструмент для реализации задуманного, я решил остановиться на php, т. к. у нас уже есть некий фундамент автоматизации рутинных процессов, который имеет web интерфейс и уже написан на php. В качестве хранилища информации выступит база MariaDB 10.

Использовать будем curl, это «наше всё» для работы с web серверами.

Начнем аккуратно, тут спешка не нужна, будем максимально повторять действия пользователя:
для начала откроем главную страницу и сохраним все cookies. Они будут, при первом открытии сайта вам сразу предложат принять cookie.

файл для хранения cookie так и назовём «cookie.txt»

очень удобно в браузере копировать все необходимые ключи для curl сразу в буфер обмена

curl 'https://graphisoftid.graphisoft.com/' \
  -H 'Connection: keep-alive' \
  -H 'sec-ch-ua: " Not;A Brand";v="99", "Opera";v="79", "Chromium";v="93"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'DNT: 1' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 OPR/79.0.4143.50' \
  -H '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' \
  -H 'Sec-Fetch-Site: none' \
  -H 'Sec-Fetch-Mode: navigate' \
  -H 'Sec-Fetch-User: ?1' \
  -H 'Sec-Fetch-Dest: document' \
  -H 'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7' \
  --compressed

для ленивых предлагаю воспользоваться online сервисом https://incarnate.github.io/curl-to-php/

Сконвертируем ключи командной строки curl в код php и получим следующее:

// Generated by curl-to-PHP: http://incarnate.github.io/curl-to-php/
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://graphisoftid.graphisoft.com/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');

curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate');

$headers = array();
$headers[] = 'Connection: keep-alive';
$headers[] = 'Sec-Ch-Ua: \" Not;A Brand\";v=\"99\", \"Opera\";v=\"79\", \"Chromium\";v=\"93\"';
$headers[] = 'Sec-Ch-Ua-Mobile: ?0';
$headers[] = 'Sec-Ch-Ua-Platform: \"Windows\"';
$headers[] = 'Dnt: 1';
$headers[] = 'Upgrade-Insecure-Requests: 1';
$headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 OPR/79.0.4143.50';
$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';
$headers[] = 'Sec-Fetch-Site: none';
$headers[] = 'Sec-Fetch-Mode: navigate';
$headers[] = 'Sec-Fetch-User: ?1';
$headers[] = 'Sec-Fetch-Dest: document';
$headers[] = 'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

$result = curl_exec($ch);
if (curl_errno($ch)) {
    echo 'Error:' . curl_error($ch);
}
curl_close($ch);

осталось рассмотреть механизм php работы с почтовым ящиком, нам нужен по сути только механизм IMAP, не буду вас сильно грузить, оставлю готовый скрипт, который открывает почтовый ящик пользователя в режиме readonly и находит последнее письмо (если их несколько) от «register-noreply@graphisoft.com», расшифровывает тело письма и вытаскивает код подтверждения. Результатом работы скрипта будет ссылка на подтверждение регистрации или ничего, если письма не было.

<?php

$auth_mail = 'username@mail.com';
$auth_pass = 'P@$$woRd';

function get_string_between($string, $start, $end){
    $string = ' ' . $string;
    $ini = strpos($string, $start);
    if ($ini == 0) return '';
    $ini += strlen($start);
    $len = strpos($string, $end, $ini) - $ini;
    return substr($string, $ini, $len);
}

$imap   = imap_open('{imap.mail.com:993/imap/ssl}INBOX', $auth_mail, $auth_pass, OP_READONLY);
if ($imap) {
	$some = imap_search($imap, 'FROM "register-noreply@graphisoft.com"', SE_UID);
	if (count($some)>0) {
		$mail_body = base64_decode(imap_body($imap, $some[0]));
		$key = get_string_between($mail_body,'<span style="font-weight:bold;">','</span>');
		echo 'https://graphisoftid.graphisoft.com/#/verification/'.$key."\n";
	}
}
?>

Вроде, все попробовали, ко всему готовы... В процессе реализации обязательно что-то всплывёт... но, в целом мы готовы!

3 часть, «кропотливая работа и долгожданный успех»

1 часть, «как нельзя относиться к корпоративным клиентам»

Да, сегодня будет кинут камень в огород Graphisoft.

Небольшая предыстория из прошлого, как все начиналось...

Являясь руководителем IT отдела крупного архитектурного бюро столкнулся с неприятной ситуацией. Начиная с 25 версии ArchiCAD для работы сотруднику необходимо логинется под своим Graphisoft id.

— Ну, что же такого? спросите вы, многие вендоры просят авторизовываться в своих программных комплексах, вспомним Adobe или Autodesk. Но! Хотелось бы иметь какой-то адекватный механизм для массового создания учетных записей сотрудников, их как-никак около 150 человек.

Это, как вы себе представляете? Разослать инструкцию с картинками, чтобы каждый сотрудник сам, повторюсь, сам зашёл на сайт Graphisoft, заполнил форму, потом перешёл в свою почту и кликнув по ссылке из письма подтвердил регистрацию. Это утопия...
Даже половина не справится, кто-то скажет «не царское это дело», кто-то по скудоумию не осилит. Но, если даже 30% преодолеют — считайте успех. На самом деле — провал. ☹️

А в IT отделе нет таких ресурсов, чтобы каждого регистрировать вручную.

Беда...

Звонок другу? На разумный вопрос поддержка отвечает годным лайфхаком, годным с их точки зрения: «Сделайте один Graphisoft id на всю компанию и всех сотрудников под ним залогинте».

— извините, а что делать когда сотрудник уволится? менять пароль, и всем заново логинется в программе? Честно говоря, сомнительное предложение, с учётом сегодняшней текучки кадров. Я даже представил:

начало рабочего дня, массовая рассылка: — Внимание! Вчера уволился Пётр Архигад, перед началом работы в программе ArchiCAD всем перелогинеться!

И смешно и грустно, как мем, только наоборот.

Проблема есть, проблему надо решать...

Вызов принят, напоминаю, автоматизация — наше всё!

2 часть, «анализ, получение исходных данных»