Обмен между 1С7.7 и Честным знаком через API. Часть 2.

Продолжаю описание обмена с Честным знаком. В предыдущей статье был описан алгоритм получения списка сертификатов из личного хранилища.

Этап 2. Установка подключения состоит из основных блоков:

  1. Отправка запроса на подключение. В ответ мы получаем от сервера честного знака пару uuid-data.
  2. Подпись данных и отправка их обратно на сервер. В ответ мы получим токен сессии, с которым в дальнейшем будем отправлять документы.

Ниже приведены основные процедуры и функции получения токена соединения с сервером Честного знака.

В целях уменьшения приведенного кода в этой статье и последующих, процедуры и функции работы с JSON приведены в отдельной статье JSON и 1С7.7.

Функция ПолучитьТокенСесии(АдресСервера, ОтпечатокСертификата)
	Перем ДанныеДляАвторизации, uuid, code;
	Перем Сигнатура; 
        Перем URL, сзJSONЗапрос;
	Перем ОтветСервера, ОтветСервераСЗ;
	Перем ТекстСообщения;
	
	ДанныеДляАвторизации = ОтправитьЗапросНаАвторизацию(АдресСервера);
	Если ДанныеДляАвторизации = 0 Тогда
		Возврат 0;
	КонецЕсли;
	uuid = ДанныеДляАвторизации.Получить("uuid");
	code = ДанныеДляАвторизации.Получить("data");
	
	Сигнатура = ПодписатьТекст(code, ОтпечатокСертификата, 1);
	Если ПустоеЗначение(Сигнатура) = 1 Тогда
		Возврат 0;
	КонецЕсли;
	Сигнатура = ПолучитьПубличнуюЧастьСертификата(Сигнатура); 

	сзJSONЗапрос	= СоздатьОбъект("СписокЗначений");
	сзJSONЗапрос.Установить("uuid", uuid);
	сзJSONЗапрос.Установить("data", Сигнатура);		

	URL = АдресСервера;
	ОтветСервера = HTPP_ВыполнитьЗапрос("POST", URL + "/auth/simpleSignIn", сзJSONЗапрос,,,);
	Если ПустоеЗначение(ОтветСервера) = 0 Тогда
		ОтветСервераСЗ	= JSONВСписок(ОтветСервера);
	КонецЕсли;
	Если ПустоеЗначение(ОтветСервераСЗ) = 1 Тогда
		Возврат 0;
	КонецЕсли;
	
	Результат = 0;
	Если ПустоеЗначение(ОтветСервераСЗ.Получить("token")) = 0 Тогда
		Результат = ОтветСервераСЗ.Получить("token");
	Иначе
		ТекстСообщения = ОтветСервераСЗ.Получить("error_message");
		Если ПустоеЗначение(ТекстСообщения) = 0 Тогда
			Сообщить(ТекстСообщения, "!!");
		КонецЕсли;
	КонецЕсли;

	Возврат Результат;
	
КонецФункции

Функция ОтправитьЗапросНаАвторизацию(АдресСервера)
	Перем Соединение, URL, RequestTimeout;
	Перем Результат, ОтветСервера;
	Перем ТекстСообщения;
	
	URL = АдресСервера + "/auth/key";
	Соединение = HTPP_ПолучитьОбъект("GET", URL);
	Если Соединение = 0 Тогда
		Возврат 0;
	КонецЕсли;
	Соединение.Send();
	RequestTimeout = 40;
	Попытка
	    Результат = Соединение.WaitForResponse(RequestTimeout);
	Исключение
		Результат = 0;
		ТекстСообщения = "Превышено время ожидания ответа сервера авторизации! " + ОписаниеОшибки();
		Сообщить(ТекстСообщения, "!!");
	КонецПопытки;
	
	Если Результат = -1 Тогда
		ОтветСервера = СокрЛП(Соединение.ResponseText());
		Результат = JSONВСписок(ОтветСервера);
		Если ТипЗначенияСтр(Результат) = "СписокЗначений" Тогда
			Если (ПустоеЗначение(Результат.Получить("uuid")) = 1) или (ПустоеЗначение(Результат.Получить("data")) = 1) Тогда
				Результат = 0;
			КонецЕсли;
		Иначе
			Результат = 0;
		КонецЕсли;
		Если Результат = 0 Тогда
			ТекстСообщения = "От серевера получен некорректный ответ!";
			Сообщить(ТекстСообщения, "!!");
		КонецЕсли;
	Иначе
		ТекстСообщения = "Нет ответа от сервера авторизации!";
		Сообщить(ТекстСообщения, "!!");
	КонецЕсли;
	
	Возврат Результат;
	
КонецФункции

//****************************
// Создает и возвращает объект WinHttp
// Параметры:
//		Нет
// Возвращаемое значение:
//		Объект WinHttp или 0
Функция HTPP_ПолучитьОбъект(ТипЗапроса, URL)
	Перем ТекстСообщения;
	
	Попытка
		Результат = СоздатьОбъект("WinHttp.WinHttpRequest.5.1");
		Результат.Option(2, "utf-8");
		Результат.SetTimeouts(0, 0, 0, 0);
		Результат.Open(ТипЗапроса, URL, 1);
		Результат.SetRequestHeader("Content-Type", "application/json");
		Результат.SetRequestHeader("Accept-Language", "ru");
		Результат.setRequestHeader("Content-Language", "ru");
	Исключение
		ТекстСообщения = "Не удалось создать объект <WinHttp>!";
		Сообщить(ТекстСообщения, "!!");
		Результат = 0;
	КонецПопытки;
	
	Возврат Результат;
	
КонецФункции

Функция ПодписатьТекст(ТекстДляПодписи, ОтпечатокСертификата, БезBOM = 1, ВернутьКакСтроку = 0)
	Перем ФайлИсходный, ФайлПодписанный;
	Перем ВремТекст;
	
	ФайлИсходный = КаталогВременныхФайлов() + "infile.txt";
	Если ФС.СуществуетФайл(ФайлИсходный) = 1 Тогда
		Попытка
			ФС.УдалитьФайл(ФайлИсходный);
		Исключение
			ФайлИсходный = КаталогВременныхФайлов() + "infile" + СформироватьGIUD() + ".txt";
		КонецПопытки;
	КонецЕсли;
	ФайлПодписанный = КаталогВременныхФайлов() + "outfile.txt";
	Если ФС.СуществуетФайл(ФайлПодписанный) = 1 Тогда
		Попытка
			ФС.УдалитьФайл(ФайлПодписанный);
		Исключение
			ФайлПодписанный = КаталогВременныхФайлов() + "ФайлПодписанный" + СформироватьGIUD() + ".txt";
		КонецПопытки;
	КонецЕсли;
	
	Если Прав(ТекстДляПодписи, 4) = ".xml" Тогда
		ФайлИсходный = ТекстДляПодписи;
		ВыполнитьПодписаниеФайла(ФайлИсходный, ФайлПодписанный, Подпись_ПолучитьСертификатПоОтпечатку(Отпечаток));
		Если ФС.СуществуетФайл(ФайлПодписанный) = 1 Тогда
			Возврат ФайлПодписанный;
		Иначе
			Возврат "";
		КонецЕсли;
	Иначе
		Если БезBOM = 1 Тогда
			ВремТекст = СоздатьОбъект("Текст");
			ВремТекст.ДобавитьСтроку(ТекстДляПодписи);
			ВремТекст.Записать(ФайлИсходный);
		Иначе
			АдоДБСтрим = СоздатьОбъект("ADODB.Stream");
		    АдоДБСтрим.Mode = 3;
		    АдоДБСтрим.Type = 2;//2текст
			АдоДБСтрим.charset="utf-8";
		    АдоДБСтрим.Open();
			АдоДБСтрим.WriteText(ТекстДляПодписи);
			АдоДБСтрим.Position=0;
			АдоДБСтрим.SaveToFile(ФайлИсходный, 2);
			АдоДБСтрим.Close();
		КонецЕсли;
		ВыполнитьПодписаниеФайла(ФайлИсходный, ФайлПодписанный, Подпись_ПолучитьСертификатПоОтпечатку(Отпечаток));
		Если ФС.СуществуетФайл(ФайлПодписанный) = 1 Тогда
			Если ВернутьКакСтроку = 1 Тогда
				Возврат РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ФайлПодписанный);
			Иначе
				Возврат ФайлПодписанный;
			КонецЕсли;
		Иначе
			Возврат "";
		КонецЕсли;
	КонецЕсли;
	
КонецФункции

//****************************
// Подписывает файл с помощью скрипта
// Параметры:
//		ИмяФайлаВходящего 	- имя файла, который требуется подписать
//		ИмяФайлаРезультат 	- имя файла-результат (подписанный исходный файл)
//		Сертификат 		- сертификат для подписи
// Возвращаемое значение:
//		1 - успешно, 0 - ошибка
Функция ВыполнитьПодписаниеФайла(ИмяФайлаВходящего, ИмяФайлаРезультат, Сертификат)
	Перем JS, ТекстСкрипта;
	Перем ТекстСообщения;
	
	Попытка
		JS=СоздатьОбъект("MSScriptControl.ScriptControl");
		JS.Language="jscript";
		JS.Timeout=-1;
	Исключение
		ТекстСообщения = "Не удалось создать объект MSScriptControl.ScriptControl! " + ОписаниеОшибки();
		Сообщить(ТекстСообщения, "!!");
		Возврат 0;
	КонецПопытки;
	
	Попытка
		
		ТекстСкрипта="function SignFile(FileName, OutFileName, Cert)
		|{
		|   InStream=new ActiveXObject(""ADODB.Stream"");
		|   InStream.Type=1; // binary data
		|   InStream.Mode=3; // read/write
		|   InStream.Open();
		|   InStream.LoadFromFile(FileName);
		|   InData=InStream.Read(-1);
		|
		|   Signer=new ActiveXObject(""CAPICOM.Signer"");
		|   Signer.Certificate=Cert;
		|   Signer.Options=2; // CAPICOM_CERTIFICATE_INCLUDE_END_ENTITY_ONLY
		|   SignedData=new ActiveXObject(""CAPICOM.SignedData"");
		|   SignedData.Content=InData;
		|   OutSignedData=SignedData.Sign(Signer,0,0);
		|
		|   OutStream=new ActiveXObject(""ADODB.Stream"");
		|   OutStream.CharSet=""utf-8"";
		|   OutStream.Type=2; // text data
		|   OutStream.Mode=3; // read/write
		|   OutStream.Open();
		|   OutStream.WriteText(OutSignedData);
		|   OutStream.SaveToFile(OutFileName,2);
		|   OutStream.Close();
		|
		| return(0);
		|}
		|";

		JS.AddCode(ТекстСкрипта);
		Рез = JS.Modules("Global").CodeObject.SignFile(ИмяФайлаВходящего, ИмяФайлаРезультат, Сертификат);
	Исключение
		ТекстСообщения = "Произошла ошибка при подписи файла! " + ОписаниеОшибки();
		Сообщить(ТекстСообщения, "!!");
		Возврат 0;
	КонецПопытки;
	
	Возврат 1;
	
КонецФункции

//****************************
// Функция ищет в хранилище сертификат по отпечатку
// Параметры:
//		Отпечаток - строка, отпечаток сертификата
// Возвращаемое значение:
//		Сертификат (COM-объект)
Функция Подпись_ПолучитьСертификатПоОтпечатку(Отпечаток)
	Перем oStore, текСертификат, текОтпечаток;
	
	oStore = Подпись_ПолучитьОбъектХранилищеСертификатов();
	Если oStore = 0 Тогда
		Возврат "";
	КонецЕсли;
	
	Результат 	= ""; // Найденный сертификат (Com-объект)
	
	Certs = oStore.Certificates;
	Для СчСер = 1 По Certs.Count Цикл
		текСертификат = Certs.Item (СчСер); 
		текОтпечаток  = текСертификат.Thumbprint; // возвращается отпечаток в шестнадцатеричном виде
		Если ВРЕГ(текОтпечаток) = ВРЕГ(Отпечаток) Тогда
			Результат = текСертификат;
			Прервать;
		КонецЕсли;
	КонецЦикла;
	oStore.Close(); // Закрыть хранилище сертификатов и освободить объект
	
	Возврат Результат;
	
КонецФункции

//****************************
// Выполнить HTTP запрос
// Параметры: 
//  ТипЗапроса="POST",Url,сзJSONЗапрос,КлючСессии="",сзЗаголовки=""
// Возвращаемое значение:
//  Ответ сервера
Функция HTPP_ВыполнитьЗапрос(ТипЗапроса="POST", URL, сзJSONЗапрос, КлючСессии="", сзЗаголовки="", НеОбр429 = 0) 
	Перем WinHttp, RequestTimeout, РезультатHTTP;
	Перем ИмяЗаголовка, ЗначениеЗаголовка, Счетчик;
	Перем Статус, СтатусТекст, Ответ, сзОтвет;
	Перем ТекстСообщения;
	
	WinHttp = HTPP_ПолучитьОбъект(ТипЗапроса, URL);
	Если WinHttp = 0 Тогда
		Возврат 0;
	КонецЕсли;
	Если ПустоеЗначение(КлючСессии) = 0 Тогда
		WinHttp.SetRequestHeader ("Authorization", "Bearer " + КлючСессии);
	КонецЕсли;
	
	Если ТипЗначенияСтр(сзJSONЗапрос) = "СписокЗначений" Тогда
		ЗапросJSON	= ЗначениеВJSON(сзJSONЗапрос);
	ИначеЕсли ТипЗначенияСтр(сзJSONЗапрос) = "Строка" Тогда
		ЗапросJSON	= СокрЛП(сзJSONЗапрос);
	Иначе
		ЗапросJSON = "";
	КонецЕсли;
	Если ПустоеЗначение(сзЗаголовки) = 0 Тогда
		Для Счетчик = 1 По сзЗаголовки.РазмерСписка() Цикл
			ИмяЗаголовка = "";
			ЗначениеЗаголовка = сзЗаголовки.ПолучитьЗначение(Счетчик, ИмяЗаголовка);
			Если (ПустоеЗначение(ИмяЗаголовка) = 0) И (ПустоеЗначение(ЗначениеЗаголовка) = 0) Тогда
				WinHttp.SetRequestHeader (ИмяЗаголовка, ЗначениеЗаголовка);
			КонецЕсли;
		КонецЦикла;
	КонецЕсли;
	
	WinHttp.Send(ЗапросJSON);
	
	RequestTimeout = 40;
	Попытка
		РезультатHTTP = WinHttp.WaitForResponse(RequestTimeout);
	Исключение
		РезультатHTTP = 0;
	КонецПопытки;
	
	Если РезультатHTTP = -1 Тогда
		Статус = WinHttp.status();
		СтатусТекст = WinHttp.statusText();
		Ответ = HTPP_ПолучитьОтветСервера(WinHttp);
		
		Если Статус <> 200 Тогда
			Если Статус = 307 Тогда//перенаправление
				URL = WinHttp.GetResponseHeader("Location");
				Если ПустоеЗначение(Url) = 0 Тогда
					Возврат HTPP_ВыполнитьЗапрос(ТипЗапроса, URL, сзJSONЗапрос, КлючСессии, сзЗаголовки);
				КонецЕсли;
			КонецЕсли;
			Если (Статус = 429) И (НеОбр429 = 0) Тогда //слишком много запросов
				ВыдержатьПаузу(5);
				Возврат HTPP_ВыполнитьЗапрос(ТипЗапроса, Url, сзJSONЗапрос, КлючСессии, сзЗаголовки);
			КонецЕсли;
			Если ПустоеЗначение(Ответ) = 0 Тогда
				Возврат Ответ;
			Иначе
				Возврат 0;
			КонецЕсли;
		КонецЕсли;
	Иначе
		ТекстСообщения = "Не удалось выполнить HTTP-запрос! " + ОписаниеОшибки();
		Сообщить(ТекстСообщения, "!!");
		Ответ = 0;
	КонецЕсли;
	
	Возврат Ответ;
	
КонецФункции 

//****************************
// Фозвращает ответ сервера после выполнения HTTP запроса
// Параметры: 
//  WinHttp - объект "WinHttp.WinHttpRequest.5.1"
// Возвращаемое значение:
//  ОтветСервера - строка
Функция HTPP_ПолучитьОтветСервера(WinHttp)
	Перем Stream, Скрипт;
	Перем ИмяВременногоФайла, ИмяВременногоФайлаПерекодированного;
	Перем СтримВход, СтримВыход, Байт, РазмерДанных;
	Перем тмпТекст;
	Перем ТекстСообщения;
	
	ИмяВременногоФайла = КаталогВременныхФайлов() + СформироватьGIUD() + ".txt";
	
	Результат = "";
	Попытка
		// Получаем ответ от сервера с помощью
		// java-скрипта, чтобы не потерять символы utf-8
		Stream = СоздатьОбъект("ADODB.Stream"); 
		Stream.Mode = 3; 
		Stream.Type = 1; 
		Stream.Open(); 
		Скрипт = СоздатьОбъект("MSScriptControl.ScriptControl"); 
		Скрипт.Language = "javascript"; 
		Скрипт.AddObject("WinHttp", WinHttp); 
		Скрипт.AddObject("Stream",  Stream); 
		Скрипт.Eval("Stream.Write(WinHttp.ResponseBody)"); 
		Stream.SaveToFile(ИмяВременногоФайла, 2);
		Stream.Close();
		
		ИмяВременногоФайлаПерекодированного = РаботаСФайлами_Перекодировка(ИмяВременногоФайла, "utf-8", "windows-1251");
		Если ПустоеЗначение(ИмяВременногоФайлаПерекодированного) = 1 Тогда
			ТекстСообщения = "Не удалось перекодировать ответ сервера!";
			Сообщить(ТекстСообщения, "!!");
			Результат = "";
		Иначе
			Результат = РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяВременногоФайлаПерекодированного);
		КонецЕсли;
	Исключение
		ТекстСообщения = "Не удалось получить ответ от сервера!";
		Сообщить(ТекстСообщения, "!!");
		Результат = "";
	КонецПопытки;
	
	Попытка
		ФС.УдалитьФайл(ИмяВременногоФайла);
		ФС.УдалитьФайл(ИмяВременногоФайлаПерекодированного);
	Исключение
		//
	КонецПопытки;
	
	Возврат Результат;
	
КонецФункции

Функция РаботаСФайлами_Перекодировка(ИмяФайла, КодировкаВход, КодировкаВыход)

	Результат = ИмяФайла + "_k";
	
	Попытка
	    Байт = 255;
	    СтримВход = СоздатьОбъект("ADODB.Stream");
	    СтримВход.Type = 2;
	    СтримВход.charset = КодировкаВход;
	    СтримВход.Open();
	    СтримВход.LoadFromFile(ИмяФайла);
	    СтримВыход = СоздатьОбъект("ADODB.Stream");
	    СтримВыход.Type = 2;
	    СтримВыход.charset = КодировкаВыход;
	    СтримВыход.LineSeparator = -1;
	    СтримВыход.Open();
	    РазмерДанных = СтримВход.size;
	    Пока СтримВход.EOS = 0 Цикл
	        СтримВыход.WriteText(СтримВход.ReadText(Байт), ?(Байт=-2,1,0));
	    КонецЦикла;
	    СтримВыход.SaveToFile(Результат, 2);
	    СтримВход.Close();
	    СтримВыход.Close();
	Исключение
	    ТекстСообщения = "Не удалось получить изменить кодировку!";
	    Сообщить(ТекстСообщения, "!!");
	    Результат = "";
	КонецПопытки;
	
	Возврат Результат;
	
КонецФункции

Функция РаботаСФайлами_ПолучитьТекстФайлаКакСтроку(ИмяФайла, БезБОМ = 0)
	Перем ВсегоСтрок, Счетчик;
	
	Результат = "";
	
	Попытка
		тмпТекст = СоздатьОбъект("Текст");
		тмпТекст.Открыть(ИмяФайла);
		ВсегоСтрок = тмпТекст.КоличествоСтрок();
		Для Счетчик = 1 по ВсегоСтрок Цикл
			текСтрока = тмпТекст.ПолучитьСтроку(Счетчик);
			Если (Счетчик = 1) И (БезБОМ = 1) Тогда
				текСтрока = Сред(текСтрока, 4);
			КонецЕсли;
			Результат = Результат + текСтрока;
		КонецЦикла;
	Исключение
		//;
	КонецПопытки;
	
	Возврат Результат;
	
КонецФункции

Процедура ВыдержатьПаузу(Секунд = 1)
	Перем Система;
	
	Система = СоздатьОбъект("Система");
	Система.Уснуть(Секунд*1000);
	
КонецПроцедуры

Функция СформироватьGIUD()
	
	ОбъектGUID = СоздатьОбъект("GUID");
	ОбъектGUID.Новый();
	
	Возврат ОбъектGUID.ВСтроку();
	
КонецФункции 

На этом подключение к Честному знаку завершено. Мы получили токен сессии для дальнейшего использования при передаче документа.

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

Ну в следующей статье я опишу пример отправки документа в формате JSON в Честный знак.

Если вы заметили ошибку, неточность или у вас появились вопросы, пишите в комментариях, я обязательно вам отвечу и приму все ваши замечания.

Author: admin

Добавить комментарий