Своя система лицензирования шаблонов в связке с GAS (Google Apps Script)

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
Всем привет, хоть я сейчас и загружен очень сильно на проекте по телеграм, да еще и сопли по колено, нашел таки время отписать коротенькую статейку по поводу защиты своих шаблонов.

Интро:
Весь код, который тут представлен, написан на C# за исключением скриптов гугл ). Тем кто не разбирается в Шарпе, рекомендую уделить время по изучать его, ну или использовать готовое решение в своих целях. Я сам так часто поступаю ;-).

Интро2:
Сидел писал статью и на Складчике пришло уведомление, что появился новый ответ в отслеживаемой теме, как оказалось апнули довольно старую тему по защите шаблонов от уважаемого @Moadip, оценив его идею и свои наработки пришел к выводу, что скорее всего он меня натолкнул на этот краеугольный камень ))). Отмечу что наши идеи схожи, но реализации разные.

Стартуем:

Итак, с чего начнем - для тех кто не знает, шаблоны можно зашифровать и установить в них права стандартным модулем zennoposter "Шифрование"


Казалось бы этого достаточно, но не тут то было )) многие, кто оказывает услуги по разработке шаблонов, сталкивались с нерадивыми заказчиками, которые получали решение, а оплачивать отказывались; пару топиков тут уже проскакивали с гневом шаблонописцев. Что делать? Можно воспользоваться личным кабинетом, разделом боты и не заморачиваться :-) для тех кто не в курсе смотреть сюда https://userarea.zennolab.com/lk/login.aspx



- Ну вот теперь та проблема решена же!

Да, можно и так защитить свой труд, но ведь все мы ушлые ребятки и не хотим морозить 10-20-100 баксов на счету в ЛК ЗенноЛаба (@nuaru ни чего личного :-). Я просто хочу рассказать, что делаю сам и ни в коей мере не призываю отказаться от выписки шаблонов через ЛК! )


А мы с вами тем временем пойдем по другому пути. Итак, давайте создадим пустой проект и накидаем в него пару кубиков :-), ну 3 если быть точным.



Думаю тут нет вопросов ни у кого...

Идем далее, давайте подумаем как бы нам защищаться, та?
Самый очевидный путь собрать какие то данные из окружения зенки и/или ПК и что-то с ними сделать.
На ум приходит сразу то, что зенка привязана к e-mail, который не меняется. Давайте его и будем использовать.
А как нам его получить? Да очень просто:

Код:
return Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();
Да все верно, выдираем из реестра почту привязанную к зенке!

Думаем дальше:
Хочу, чтобы я мог ограничивать работу шаблона в каком то периоде: часы, дни, годы. Ну и отлично :-) Значит, нам нужно сохранять дату каким-то образом.

О! Забыли подумать над тем, что на каждом компе время может отличаться(разные часовые пояса), ну для этого у наст есть стандартизация времени по-Гринвичу и, чтобы получить текущую дату по Гринвичу, нужно выполнить такой код:

Код:
return DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm");
Почитайте те, кто не знаком с классом DateTime https://msdn.microsoft.com/ru-ru/library/k494fzbf(v=vs.100).aspx - узнаете много интересного! ;-)

Отличненько, что далее?
Теперь нам нужно каким-то образом где-то проверить, а может шаблон работать или нет. Другими словами, нам нужно где-то что-то посмотреть и узнать, что шаблон может запускаться.

Ну, так давайте уже знакомиться с сервисом google script app!
Сперва создадим пустую гугл табличку и посмотрим на вкладку инструменты:



Конечно же тыкайте на "Редактор скриптов", я думал вы уже тыкнули :-). Главное потом нажмите сюда

У вас, скорее всего, там будет пусто, а у меня уже есть пару боевых скриптов.

Полагаю, что вы же ткнули "Создание проекта" и увидели вот такую картинку:



стираем там все, что написано и пишем код:

Java:
function doPost() {

}
function doGet() {

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

Что дальше? А давайте исправим doGet так, чтобы он нам возвращал те данные, которые мы ему передали. Исправьте скрипт следующим образом:

Java:
function doPost(e) {

}
function doGet(e) {
  var s = e.parameter.mail + " " + e.parameter.date;
  //строчка ниже просто возвращает то, что пришло гет запросом
  return ContentService.createTextOutput(s).setMimeType(ContentService.MimeType.TEXT);
}
Немного пояснений:
Как вы видите из кода, функция doGet принимает один объект - e, который содержит в себе все параметры, переданные запросом в УРЛ. В нашем случае использовано 2 параметра mail и date.

Ну вот, пришла пора выставить скрипт наружу. Для этого сделаем следующее:
Нажмем на кнопку.



Теперь выполним следующие действия:


Версия: новый
Как запускать: От моего имени
Кто имеет доступ: Все, включая анонимных


И, нажимаем "Развернуть", после чего гугл попросит нас подтвердить разрешение на внесение изменений, мы, конечно же, соглашаемся! А может и не попросит :-) У меня перестал просить, наверное от того, что я все скрипты храню в одной таблице.

Копируем урл скрипта. Он появится в отдельном окошке, но если вы забыли его скопировать, то нажмите "Публикация" - "Развернуть как веб приложение" и вверху увидите заветный УРЛ скрипта.



ВАЖНО! Если вы внесли какие-то изменения в скрипт, то вам нужно провести процедуру публикации и указать версию "НОВЫЙ".

Для тех, кто любит читать по Англицки, кидаю фулдок по веб АПП https://developers.google.com/apps-script/guides/web

Урл скопировали, давайте вернемся к шаблону.

В кубик C# добавим такой код, запустим его на выполнение и посмотрим, что получили:

C#:
//Где опубликован скрипт
string licUrl = "https://script.google.com/ ТУТ ДОЛЖЕН БЫТЬ УРЛ ВАШЕГО СКРИПТА";
//Параметры полученые шаблоном
string mail = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();
string date = DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm");
//Форматируем параметры
string parameter = string.Format("?mail={0}&date={1}", mail, date);
//отправляем запрос
string response = ZennoPoster.HttpGet(licUrl + parameter, "", "UTF-8", InterfacesLibrary.Enums.Http.ResponceType.BodyOnly);
return response;
Вставили? Запустили? Получили свой емаил и текущую дату?
Отлично! Двигаемся далее.

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

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



Как вы видите на скрине, я добавил еще одно поле - "HASH". А зачем спросите вы? Так будет проще искать нужную строчку в таблице. Т.е. вместо того, чтобы делать два запроса к таблице (Сначала найдем все строки по полю Client, а потом из этих строк будем искать TemplateName), мы будем искать по одному полю под названием "HASH", а значение этого поля будет рассчитано таким образом:

C#:
return Convert.ToString(mail + project.Name).GetMD5Hash();
То бишь соединим строковое значения емайл адреса клиента и название шаблона, и возьмем хэш от полученной строки!

ОК! Теперь давайте научим наш гугло скрипт искать данные в таблице, а для этого нам потребуется немного изменить его следующим образом:

Java:
function doGet(e) {
  var h = e.parameter.HASH; // возмем хешь из параметров
  var client = getClientPermit(h); // найдем клиента
  return ContentService.createTextOutput(JSON.stringify(client)).setMimeType(ContentService.MimeType.TEXT);//ответим шаблону
}

//Получим разрешение
function getClientPermit(h){
  //вот прям ща создадим объект в который сложим результат поиска пользователя
  var Clients = getClientsObject(getTebleRows().getDataRange().getValues()); //Прочитаем все таблицу
  return searchClientFromClientsHASH(h, Clients);  //вернем объект
}

//Эта функция создает удобный для нас массив объектов содержащий строки таблички
function getClientsObject(range) {
  var rowObjects = [];
    for (var i = 1; i < range.length; i++) {
      var row = {Client: range[i][0], ExpiredDate: range[i][1], TemplateName: range[i][2], HASH:range[i][3]};
      rowObjects.push(row);
    }
  return rowObjects;
}

//Эта функция ищет нам данные о клиенте по хешу
function searchClientFromClientsHASH(hash, Clients){
  for (var i = 0; i < Clients.length; i++) {
  if (Clients[i].HASH == hash)
     return Clients[i];
  }
  return {error: 'Клиент не найден!', code: 0};
}

function getTebleRows(){
  // Откроем таблицу по ИД
  var tid = 'вставьте свой'; // это ид таблицы, он берется из урл самой таблицы
  var sheet = SpreadsheetApp.openById(tid).getSheetByName('Clients');
  return sheet;
}
Скрипт мы поправили, теперь давайте немного под шаманим шаблон, заменим содержимое C# кубика следующим содержанием:

C#:
//Где опубликован скрипт, я думаю понятно что нужно вставить урл вашего скрипта
string licUrl = "https://script.google.com/a/zhag/ваш ид скрипта/exec";
//Параметры полученые шаблоном
string mail = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();
string date = DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm");
//Форматируем параметры
string parameter = string.Format("?mail={0}&date={1}&HASH={3}", mail, date, Convert.ToString(mail + project.Path));
//отправляем запрос
string response = ZennoPoster.HttpGet(licUrl + parameter, "", "UTF-8", InterfacesLibrary.Enums.Http.ResponceType.BodyOnly);
return response;

А мы все ближе! К чему? ....

Что теперь мы получаем? а мы получаем JSON в котором описан наш клиент, ну вы в данном случае. Это отлично! Но мы сразу позаботимся о том что, возможна ситуация когда клиента нет в списке клиентов. И если мы не смогли найти по хешу клиента тогда шаблон получит вот такой JSON:

Код:
{"error":"Клиент не найден!","code":0}
Давайте его обработаем, и для этого изменим в C# кубике все строки на код ниже:

C#:
JavaScriptSerializer ser = new JavaScriptSerializer();
//Где опубликован скрипт
string licUrl = "https://script.google.com/a/z";
//Параметры полученые шаблоном
string mail = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();
string date = DateTime.UtcNow.ToString("MM/dd/yyyy HH:mm");
//Форматируем параметры
string parameter = string.Format("?mail={0}&date={1}&HASH={3}", mail, date, Convert.ToString(mail + project.Path));
//отправляем запрос
string response = ZennoPoster.HttpGet(licUrl + parameter, "", "UTF-8", InterfacesLibrary.Enums.Http.ResponceType.BodyOnly);

//Сделаем словарь из ответа нашего гугл скрипта
Dictionary<string, dynamic> d = ser.Deserialize<Dictionary<string, dynamic>>(response);

//проверем чтобы мы нашли клиента 100%
if(d.ContainsKey("error")){//Если не нашли клиента
    ZennoPoster.StopTask(Guid.Parse(project.TaskId)); // остановим выполнение шаблона
    throw new Exception(d["error"] + " Oбратитесь в телеграм к разработчику @zhagru");
}

return response;

Лирическое отступление для тех кто все проделывает шаг за шагом:

Чтобы у вас все заработало вам нужно в шаблоне добавить модуль "Добавить ссылки из GAC", и добавить либу "System.Web.Extensions", я нарисовал путь мышкой на скриншоте:


Я кукарача! )))))))))))))))
Думаем далее! Мы общаемся с нашим "Сервером" лицензирования, нашим "Софтом", но все что мы передаем ему, мы передаем в открытом виде, все параметры можно подделать и сломать нашу систему защиты. Я не буду объяснять как это сделать, я расскажу как от этого защитится.

Короче так, мы нашему серверу, вы заметили, я стал называть скрипт в гугло табличке сервером лицензирования, это не спроста. И так нашему серверу поступают данные: емаил, дата текущая и хеш. Как нам должен ответить сервер чтобы избежать подделки данных? - а все просто: Сервер должен возвращать в ответе хеш доступа и состояние лицензии. Давайте подумаем как....

А давайте, наш сервер будет высчитывать количество секунд, которое осталось по его данным на использование шаблона. Но не просто так, а в виде какого нибудь хеша. И вот тут мы понимаем что гугло скрипты особо та и не умеют рассчитывать стойки МД5. Но это поправимо. Добавьте код приведенный ниже в наш гугло скрипт.



Java:
String.prototype.MD5 = function(charset, toByte) {

charset = charset || Utilities.Charset.UTF_8;

var digest = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, this, charset);

if(toByte) return digest;

var __ = '';

for (i = 0; i < digest.length; i++) {

var byte = digest[i];

if (byte < 0) byte += 256;

var bStr = byte.toString(16);

if (bStr.length == 1) bStr = '0' + bStr;

__ += bStr;

}

return __;

}

Этот код я сдернул в сети, с какого-то сайта очень древней наружности, и разбирать его мы с вами не будем, скажу одно он позволяет получить хешь от обекта, что нас устаревает на 100500%!

Ну так что на счет расчетов? Давайте думать: мы знаем текущее время у клиента, так как передаем его в запросе, мы знаем время когда кончится лицензия у клиента, это хранится в таблице. Ну так давайте получим разницу этих дат в секундах, и от полученного возьмем хеш. Для этого исправим код гугл скрипта на следующий:


Java:
function doGet(e) {
  var salt = "сольМоя"; //это дополнителная строка которую добовляем при получении хеша, чтоб его ни кто не мог расчитать без нас
  var h = e.parameter.HASH; // возмем хешь из параметров
  var client = getClientPermit(h); // найдем клиента
  var sicret  = salt.MD5() + getSecondsWork(client.ExpiredDate, e.parameter.date).toString().MD5();// составим сикретные данные
  client.s = sicret; // добавим хеш к ответу сервера
  return ContentService.createTextOutput(JSON.stringify(client)).setMimeType(ContentService.MimeType.TEXT);//ответим шаблону
}

//Получим разрешение
function getClientPermit(h){
  //вот прям ща создадим объект в который сложим результат поиска пользователя
  var Clients = getClientsObject(getTebleRows().getDataRange().getValues()); //Прочитаем все таблицу
  return searchClientFromClientsHASH(h, Clients);  //вернем объект
}

//Эта функция создает удобный для нас массив объектов содержащий строки таблички
function getClientsObject(range) {
  var rowObjects = [];
    for (var i = 1; i < range.length; i++) {
      var row = {Client: range[i][0], ExpiredDate: range[i][1], TemplateName: range[i][2], HASH:range[i][3]};
      rowObjects.push(row);
    }
  return rowObjects;
}

//Эта функция ищет нам данные о клиенте по хешу
function searchClientFromClientsHASH(hash, Clients){
  for (var i = 0; i < Clients.length; i++) {
  if (Clients[i].HASH == hash)
     return Clients[i];
  }
  return {error: 'Клиент не найден!', code: 0};
}

function getTebleRows(){
  // Откроем таблицу по ИД
  var tid = 'вставьСвойИдТаблици'; // это ид таблицы, он берется из урл самой таблицы
  var sheet = SpreadsheetApp.openById(tid).getSheetByName('Clients');
  return sheet;
}

//Расчитаем сколько секунд осталось по лицензии
function getSecondsWork(startDate, endDate){//, endDate){
  // startDate - это дата которую мы получили от клиента
  // endDate - эту дату мы берем из таблицы
  return (startDate - new Date(Date.parse(endDate)))/1000;
}

String.prototype.MD5 = function(charset, toByte) {

charset = charset || Utilities.Charset.UTF_8;

var digest = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, this, charset);

if(toByte) return digest;

var __ = '';

for (i = 0; i < digest.length; i++) {

var byte = digest[i];

if (byte < 0) byte += 256;

var bStr = byte.toString(16);

if (bStr.length == 1) bStr = '0' + bStr;

__ += bStr;

}

return __;

}

И внесем исправления в код кубика C# всамом шаблоне:

C#:
JavaScriptSerializer ser = new JavaScriptSerializer();


//Где опубликован скрипт
string licUrl = "вставьСвойУрлСкрипта";
//Параметры полученые шаблоном
string mail = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();

string date = DateTime.UtcNow.ToString("s")+"Z";
//Форматируем параметры
string parameter = string.Format("?mail={0}&date={1}&HASH={2}", mail, date, Convert.ToString(mail + project.Name).GetMD5Hash());
//отправляем запрос
string response = ZennoPoster.HttpGet(licUrl + parameter, "", "UTF-8", InterfacesLibrary.Enums.Http.ResponceType.BodyOnly);
project.SendInfoToLog(response);
//Сделаем словарь из ответа нашего гугл скрипта
Dictionary<string, dynamic> d = ser.Deserialize<Dictionary<string, dynamic>>(response);

//проверем чтобы мы нашли клиента 100%
if(d.ContainsKey("error")){//Если не нашли клиента
    ZennoPoster.StopTask(Guid.Parse(project.TaskId)); // остановим выполнение шаблона
    throw new Exception(d["error"] + " Oбратитесь в телеграм к разработчику @zhagru");
}

//расчитываем количество секунд оставшихся до конца лицензии
TimeSpan t =  DateTime.Parse(d["ExpiredDate"]) - DateTime.Parse(date);
int i = (int)t.TotalMilliseconds/1000; // проверим что еще есть время для работы, если тут получим отрицательное число, то лицензия кончилась

//проверяем можно работать или нет
if(i<0) throw new Exception(d["ExpiredDate"] + " закончилась лицензия");
string salt = "сольМоя"; // это секретное слово которое должно совпадать с секретным словом в гугл скрипте
project.SendInfoToLog(Convert.ToString(salt + i.ToString()).GetMD5Hash());
if(d["s"] == salt.GetMD5Hash() + i.ToString().GetMD5Hash()){//сверим хеши
    return "";
}
else
    throw new Exception(d["ExpiredDate"] + " закончилась лицензия");



Вот ребятки такая штука получилась. Шаблон и текст гугло скрипта приложу.
 

Вложения

Для запуска проектов требуется программа ZennoPoster.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...

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

Последнее редактирование:

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
Инструкция ФАСТСТАРТ по этой статье:

1. Качаем аттач.
2. Открываем шаблон в ПроджектМейкере
3. Открываем кубик JS и копируем все из него.
4. Идем в гуглДокс, создаем табличку, в шапке можете ввести любые названия, но порядок данных не изменяйте иначе не будет работать.
5. Создаем скрипт (Инструменты - Редактор скриптов - создать новый)
6. Вставляем в него все что скопировали на шаге 3. Меняем СОЛЬ ОБЯЗАТЕЛЬНО!!!
7. Публикуем как веб приложение, копируем урл.
8. Вставляем УРЛ в кубик C#, и тут тоже меняем соль!!!
9. Профит.

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

UPD1:
Дабы избежать перевода времени на клиенте вставил функцию сравнения времени на клиенте со временем на сервере. Она выглядит вот так:

Java:
//Проверка расхождения времени клиент-сервер, функции передаем дату в строке.
function getCDTimeDiff(dateTimeClient){
  var d = (Date.parse(dateTimeClient) - new Date())/1000;
  if(d > -1000){//если разница клиент сервер менее 16 минут
    return true;//работаем
  }
  else{
    return false;//отдыхаем
  }
}
В связи с этим если время разное тогда в ответе сервер будет слать ошибку, для этого функцию doGet переделаем вот так:

Java:
function doGet(e) {
  var salt = "AsTsA"; //это дополнителная строка которую добовляем при получении хеша, чтоб его ни кто не мог расчитать без нас
  var h = e.parameter.HASH; // возмем хешь из параметров
  var client = getClientPermit(h); // найдем клиента
  var sicret  = getSecondsWork(client.ExpiredDate, e.parameter.date).toString() + salt;// составим секретные данные
  client.s = sicret.MD5(); // добавим хеш к ответу сервера

  if(!getCDTimeDiff(e.parameter.date)){
    client.error = 'Большая разница во времени между клиентом и сервером!';
  }
  return ContentService.createTextOutput(JSON.stringify(client)).setMimeType(ContentService.MimeType.TEXT);//ответим шаблону
}
В обработчике в шаблоне ни чего не изменилось.
Обновил шаблон в аттаче, снипет JS содержит последнюю версию кода.

UPD2: Сказ о том что GAS имеет ограничения

Вчера писал статью, да забыл сделать ВАЖНОЕ отступление!!! GAS имеет ограничения по количеству запросов в сутки. На моем аккаунте gBisness оно равно 10к, на обычном аккаунте 5к в сутки.

Так давайте ченить придумаем :-) чтобы не получилось так что один клиент выжрет весь лимит запросов оставив остальных без доступа к лицензии.

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

А где мы будем брать срок истечения сессии? - Ну так сервер нам его должен присылать!

Делаем!
Идем в нашу табличку, добавляем в ней еще одно поле "ExpiredSession". Сделали? А подумали что туда будем писать? Я предлагаю вам писать туда количество МИНУТ в течении которых сессия будет считаться живой, и шаблон не будет стучать серверу. Почему минуты? - Все просто, эта величина универсальна при выписке демо лицензии, допустим на 2 часа, и выписке лицензии на месяц. в первом случае мы поставим параметр ExpiredSession = 30 минутам, а во втором ExpiredSession = (=1*24*60) те формулу которая автоматом рассчитает сколько минут в одних сутках, если это вам покажется тоже часто, тогда первую цифирьку увеличивайте по своему усмотрению.

Теперь исправим гугло скрипт!
Мы будем отдавать это поле клиенту, но не просто это поле, а будем отдавать дату и время после которой шаблон ОБЯЗАТЕЛЬНО должен запросить состояние лицензии на сервере.
Исправим код функции getClientsObject следующим образом:

Java:
//Эта функция создает удобный для нас массив объектов содержащий строки таблички
function getClientsObject(range) {
  var rowObjects = [];
    for (var i = 1; i < range.length; i++) {
      var row = {
        Client: range[i][0],
        ExpiredDate: range[i][1],
        TemplateName: range[i][2],
        HASH:range[i][3],
      };
     
      row.ExpiredSession = new Date(new Date().setMinutes(new Date().getMinutes() + range[i][4]));
     
      rowObjects.push(row);
    }
  return rowObjects;
}

Отлично данные от сервера изменили, наступает магия внутри шаблона!
Думаем.

Сессией будем считать весь массив полученных данных от сервера, т.е. переменную response, но хранить локально на компе у клиента, эти данные не сикурно, он просто может взять и изменить данные, да так что шаблон вообще 100500 лет не будет запрашивать состояние лицензии.

Тут нам на помощь приходит C# класс System.Security. Я тут умничаю, а на самом деле я просто спросил у гугаля - "ОК гугл! C# как зашифровать строку?" и он мне ответил ВОТ ТАК , я с этим гуглом часто общаюсь :-)
Значит тыкаем на первую строчку, читаем, все нас устаревает.

Делаем!
Файлик лицензии будем хранить рядышком с шаблоном, называть его будем хешь от переменной project.Name, ну и конечно все это будем шифровать.

Добавим в OwnCodeUsing блок на в кладку "Общий код" такое содержимое:
Код:
namespace ZennoLab.OwnCode
{
 
    public class zCrypto
    {
        static readonly string PasswordHash = "[email protected]@Sw0rd";
        static readonly string SaltKey = "[email protected]&KEY";
        static readonly string VIKey = "@1B2c3D4e5F6g7H8";
     
        public static string Encrypt(string plainText)
        {
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            byte[] keyBytes = new Rfc2898DeriveBytes(PasswordHash, Encoding.ASCII.GetBytes(SaltKey)).GetBytes(256 / 8);
            var symmetricKey = new RijndaelManaged() { Mode = CipherMode.CBC, Padding = PaddingMode.Zeros };
            var encryptor = symmetricKey.CreateEncryptor(keyBytes, Encoding.ASCII.GetBytes(VIKey));

            byte[] cipherTextBytes;

            using (var memoryStream = new MemoryStream())
            {
                using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                {
                    cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                    cryptoStream.FlushFinalBlock();
                    cipherTextBytes = memoryStream.ToArray();
                    cryptoStream.Close();
                }
                memoryStream.Close();
            }
            return Convert.ToBase64String(cipherTextBytes);
        }
     
        public static string Decrypt(string encryptedText)
        {
            byte[] cipherTextBytes = Convert.FromBase64String(encryptedText);
            byte[] keyBytes = new Rfc2898DeriveBytes(PasswordHash, Encoding.ASCII.GetBytes(SaltKey)).GetBytes(256 / 8);
            var symmetricKey = new RijndaelManaged() { Mode = CipherMode.CBC, Padding = PaddingMode.None };

            var decryptor = symmetricKey.CreateDecryptor(keyBytes, Encoding.ASCII.GetBytes(VIKey));
            var memoryStream = new MemoryStream(cipherTextBytes);
            var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
            byte[] plainTextBytes = new byte[cipherTextBytes.Length];

            int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
            memoryStream.Close();
            cryptoStream.Close();
            return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount).TrimEnd("\0".ToCharArray());
        }
    }
}
В том же OwnCodeUsing но на первой вкладке добавим строчку:
C#:
using System.Security.Cryptography;
using System.Web.Script.Serialization;
using ZennoLab.OwnCode;
И отредактируем наш основной обработчик в шаблоне (я говорю про кубик C# самый первый) таким образом:

C#:
JavaScriptSerializer ser = new JavaScriptSerializer();

/*---------------------ОБЯЗАТЕЛЬНО ЗАМЕНИТЕ Кодовое слово в строке ниже, а так же замените его в гугл скрипте--------------------------------*/
string salt = "AsTsA"; // это секретное слово которое должно совпадать с секретным словом в гугл скрипте

//определим клиента
string mail = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("SOFTWARE\\ZennoLab").GetValue("login").ToString();
//Проверим существует ли фаил лицензии и почитаем данные сначала из него
string pathToLic = project.Path + project.Name.GetMD5Hash() + ".lic";
if(File.Exists(pathToLic)){
    string lic = "";
    Dictionary<string, dynamic> dLic = new Dictionary<string, dynamic>();
    try{
        List<string> l = new List<string>();
        l.AddRange(File.ReadAllLines(pathToLic));//читаем лицензию в список
        lic = zCrypto.Decrypt(l[0]);//дешифруем лицензию
        //сверим хеши, чтобы клиент не менял ни чего в лицензии
        if(l[1] == Convert.ToString(lic + salt).GetMD5Hash()){//если хеши не совпадают значит клиент ковырял лицензию
            dLic = ser.Deserialize<Dictionary<string, dynamic>>(lic);
            if(dLic.ContainsKey("ExpiredSession") //существуют данные по истечению сессии?
                && dLic.ContainsKey("ExpiredDate")//существуют данные по истечению лицензии?
                && dLic.ContainsKey("Client")//Существуют данные о клиенте
                ){
                if(DateTime.Parse(dLic["ExpiredSession"]) > DateTime.UtcNow //если сессия не закончилась
                    && DateTime.Parse(dLic["ExpiredDate"]) > DateTime.UtcNow // и если лицензия не закончилась
                    && dLic["Client"] == mail //и если клиент тот
                    ){
                    //разрешаем работать
    //                    project.SendInfoToLog(dLic["ExpiredSession"] + " " dLic["ExpiredDate"] + " ");
                    return "";
                };
            }
        }
    }
    catch(Exception e){
        File.Delete(pathToLic);
        project.SendWarningToLog("Фаил лицензии поврежден. Запросим новый на сервере!");
    }
}

//Где опубликован скрипт
string licUrl = "";

string date = DateTime.UtcNow.ToString("s")+"Z";
//Форматируем параметры
string parameter = string.Format("?mail={0}&date={1}&HASH={2}", mail, date, Convert.ToString(mail + project.Name).GetMD5Hash());
//отправляем запрос
string response = ZennoPoster.HttpGet(licUrl + parameter, "", "UTF-8", InterfacesLibrary.Enums.Http.ResponceType.BodyOnly);

//Сделаем словарь из ответа нашего гугл скрипта
Dictionary<string, dynamic> d = new Dictionary<string, dynamic>();
try{ 
    d = ser.Deserialize<Dictionary<string, dynamic>>(response);
    //если словарь получилось сотавить тогда запишим ответ от сервера в фаил лицензии
    File.AppendAllText(pathToLic, zCrypto.Encrypt(response) + Environment.NewLine + Convert.ToString(response + salt).GetMD5Hash());
}
catch{
    throw new Exception("Ошибка на сервере лицензирования, свяжитесь с разработчиком.");
}
//проверем чтобы мы нашли клиента 100%
if(d.ContainsKey("error")){//Если не нашли клиента
    ZennoPoster.StopTask(Guid.Parse(project.TaskId)); // остановим выполнение шаблона
    throw new Exception(d["error"] + " Oбратитесь в телеграм к разработчику @zhagru");
}

//расчитываем количество секунд оставшихся до конца лицензии
TimeSpan t =  DateTime.Parse(d["ExpiredDate"]) - DateTime.Parse(date);
int i = (int)t.TotalMilliseconds/1000; // проверим что еще есть время для работы, если тут получим отрицательное число, то лицензия кончилась

//проверяем можно работать или нет
if(i<0) throw new Exception(d["ExpiredDate"] + " закончилась лицензия");

string gh =  i.ToString() + salt;

if(d["s"] == gh.GetMD5Hash()){//сверим хеши
    return "";
}
else
    throw new Exception(d["ExpiredDate"] + " закончилась лицензия");
На что хочу обратить ваше внимание!
В самой лицензии две строки. перва содержит зашифрованные данные в строковом формате полученные от сервера. А вторая содержит хеш от этих данных с солью. Такая реализация исключает подделку файла лицензии!

Выводы епт:
1. Вы получили легко масштабируемую систему лицензирования, за которую не придется платить ни копейки!
2. все ваши продажи, лицензии соберутся со временем в одном месте, что очень удобно )). При этом отключить лицензию косячному клиенту вы сможете без особых усилий!
3. Вы можете вставить такой кубик перед каждым модулем вашего шаблона, и тем самым поддерживать один продукт в котором много функций (регер, спамер, парсер), из гугло таблички.
4. Считаю что решение достаточно сикурно и надежно. Правда я его реализовал часов за 10 и не тестил особо.

К этому сообщению приложил второй вариант реализации системы, с сохранением локальной сессии.
 

Вложения

Последнее редактирование:

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
Я делал намного проще,
все в шабе -
определение мыла и для каждого мыла прописан дедлайн в формате unixtime
дергается текущий unixtime у зенны {-TimeNow.UnixTime-}
(сначала дергал с левого реса, вот там в теории можно было подменить),
высчитывается остаток подписки и логика пуск/стоп,
при продлении просто в шабе меняется дедлайн на нужный, обнова шаблона у клиента (в боксах через админку автоматом, в постерах перекачать шаблон по статической ссылке - шабы и так регулярно обновляются и скачиваются)
 
Последнее редактирование:
  • Спасибо
Реакции: kibnet и orka13

alekwuy

Client
Регистрация
06.04.2013
Сообщения
1 626
Благодарностей
450
Баллы
83
Я делал намного проще,
все в шабе -
определение мыла и для каждого мыла прописан дедлайн в формате unixtime
дергается текущий unixtime у зенны, высчитывается остаток подписки и логика пуск/стоп,
при продлении просто в шабе меняется дедлайн на нужный, обнова шаблона (в боксах через админку автоматом, в постерах перекачать шаблон по статической ссылке - шабы и так регулярно обновляются и скачиваются)
а подделать ответ твоего сервера?
 

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
Кстати дергать мыло лучше из переменной окружения {-Environment.CurrentUser-}
т.к. в реестре можно также подделать (после запуска зенно) - например простеньким скриптиком
импорта/экспорта реестра) - может кто-то захочет прикинуться админом :-)
 
Последнее редактирование:

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
Я делал намного проще,
Действительно все просто. но в моей реализации я могу отрубить клиенту шаблон ранее чем закончится подписка :-) так сказать я управляю лицензиями почти точно так же как и в ЛК Зенки.
а подделать ответ твоего сервера?
В моей реализации в ответе клиенту передается параметр s - который содержит хешь от остатка секунд подписки + соль. Так что подделать его не вариант, он при каждом обращении будет разный.
Кстати дергать мыло лучше из переменной окружения {-Environment.CurrentUser-}
т.к. в реестре можно также подделать (после запуска зенно) - например простеньким скриптиком
импорта/экспорта реестра)
а как ты думаешь откуда этот макрос {-Environment.CurrentUser-} берет то самое мыло? :-) Думаю тут не поспоришь ССЫЛКА

В любом случае если клиент каким то образом изменит мыло, то шаблон будет ему отвечать что такого клиента нет.
 
  • Спасибо
Реакции: orka13

AgentRassilok

Известная личность
Регистрация
08.11.2016
Сообщения
1 273
Благодарностей
449
Баллы
83
круто
 

Juniorcpa

Client
Регистрация
27.05.2014
Сообщения
1 314
Благодарностей
609
Баллы
113
Всё круто, но хромать система начинает, когда сначала у юзера была зенка в демо установлена на почту блабла@mail.ru, а потом он купил зеннобокс (допустим) на почту яжир@mail.ru
В реестре у него останется первая почта (блабла@mail.ru). Вечно наколяет эта система.
 

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
Действительно все просто. но в моей реализации я могу отрубить клиенту шаблон ранее чем закончится подписка :-) так сказать я управляю лицензиями почти точно так же как и в ЛК Зенки.
В этом есть плюс, но обычно раньше дедлайна отключать не требуется.
В боксе шаб проработает до перезагрузки бокса (и автообновления шаблона).
а как ты думаешь откуда этот макрос {-Environment.CurrentUser-} берет то самое мыло? :-) Думаю тут не поспоришь ССЫЛКА
В любом случае если клиент каким то образом изменит мыло, то шаблон будет ему отвечать что такого клиента нет.
Если подставить в реестр чужое мыло - то зенно не авторизуется,
если подставить в реестр после авторизации, то шаб будет дергать из реестра подставное мыло, а в переменной окружения будет то, с которого ZP успешно авторизовался, т.е. валидное (не проверял, но имхо)


P.S. Я не успоряю твоего варианта, просто привел свой, возможно, он менее надежен и защищен.
Анатолий, знаком с твоими продуктами, :ay:
Желаю удачи в конкурсе и остальном!
 

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
Всё круто, но хромать система начинает, когда сначала у юзера была зенка в демо установлена на почту блабла@mail.ru, а потом он купил зеннобокс (допустим) на почту яжир@mail.ru
В реестре у него останется первая почта (блабла@mail.ru). Вечно наколяет эта система.
при переустановке в реестре перепишется мыло.
Если зенка загрузится, то система будет работать нормально
 

Juniorcpa

Client
Регистрация
27.05.2014
Сообщения
1 314
Благодарностей
609
Баллы
113
при переустановке в реестре перепишется мыло.
Если зенка загрузится, то система будет работать нормально
У меня просто есть опыт с этой фигней и я даже рапортовал о ней. Нет, мыло в реестре не переписывается.
 

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
У меня просто есть опыт с этой фигней и я даже рапортовал о ней. Нет, мыло в реестре не переписывается.
Если не перепишется, то и зенка не запустится - следовательно, это проблема не шаблона и системы в ней.
 

zennoX

Client
Регистрация
05.04.2014
Сообщения
454
Благодарностей
116
Баллы
43
Если подставить в реестр чужое мыло - то зенно не авторизуется,
если подставить в реестр после авторизации, то шаб будет дергать из реестра подставное мыло, а в переменной окружения будет то, с которого ZP успешно авторизовался, т.е. валидное (не проверял, но имхо)
Проверил, так и есть..

 

sergey_l

Client
Регистрация
06.12.2016
Сообщения
18
Благодарностей
5
Баллы
3
Забыл файло залить :-)

в самом шаблоне вы найдете полную реализацию сего лицедейства. Вот вам краткая инструкция по быстрому старту:
1. Качаем аттач.
2. Открываем шаблон в ПроджектМейкере
3. Открываем кубик JS и копируем все из него.
4. Идем в гуглДокс, создаем табличку, в шапке можете ввести любые названия, но порядок данных не изменяйте иначе не будет работать.
5. Создаем скрипт (Инструменты - Редактор скриптов - создать новый)
6. Вставляем в него все что скопировали на шаге 3. Меняем СОЛЬ ОБЯЗАТЕЛЬНО!!!
7. Публикуем как веб приложение, копируем урл.
8. Вставляем УРЛ в кубик C#, и тут тоже меняем соль!!!
9. Профит.
Шаблон выдает ошибку не достаточно прав
 

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43

nole

Client
Регистрация
19.11.2010
Сообщения
346
Благодарностей
159
Баллы
43
Юзер переводит время своего пк и будет получать валидную разницу времени от сервера
Ну или поднимает вебсервер, перехватывает запросы к серверу и подделывает ответ на валидный, даже если там хэш, то берет валидный хэш пока шаблон работает
Как шаб на это отреагирует?
 

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
Юзер переводит время своего пк и будет получать валидную разницу времени от сервера
Вот это забыл добавить. Ща подправлю. А суть такая:
Клиент нам шлет свою локальную дату в ЮТЦ формате. На сервере гугл скриптом мы проверяем расхождение дат, и если оно более, допустим одного часа. Тогда сервер говорит что с клиентом что то не то и не дает запуститься шаблону.

то берет валидный хэш пока шаблон работает
Как шаб на это отреагирует?
Валидный хешь клиент ни когда не получит так как он состоит из соли + количество секунд действия лицензии. Он рассчитывается при каждом обращении и всегда уникален, так же можно в этот хешь еще добавить параметров :-) допустим мыло или стринг от даты, ну это уже совсем параноя. МД5 достаточно стойкий алгоритм который просчитать можно разве что на квантовом ПК :-).
 

nole

Client
Регистрация
19.11.2010
Сообщения
346
Благодарностей
159
Баллы
43
Валидный хешь клиент ни когда не получит так как он состоит из соли + количество секунд действия лицензии. Он рассчитывается при каждом обращении и всегда уникален, так же можно в этот хешь еще добавить параметров :-) допустим мыло или стринг от даты, ну это уже совсем параноя. МД5 достаточно стойкий алгоритм который просчитать можно разве что на квантовом ПК :-).
Хэш и не нужно подделывать, нужно подделывать ответ сервера
Например, ситуация: покупаю у тебя лицензию, получаю валидный ответ, который приходит от твоего сервера, перехватываю все последующие запросы к этому серверу и возвращаю этот валидный хэш, в итоге шаблон получает количество секунд лицензии всегда одно и то же и работает бесконечно
 

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
перехватываю все последующие запросы к этому серверу и возвращаю этот валидный хэш
Я же выше написал что при каждом обращении сервер генерит другой хеш!!! он всегда уникален. То что ты перехватишь первый ответ, ну или любой последующий валидный, тебе ни чего не даст ровным счетом!
Отвечу более развернуто, чтобы наступило понимание:

На клиенте:
1. Получаем текущую дату и храним ее значение в переменной
C#:
string date = DateTime.UtcNow.ToString("s")+"Z";
На сервере:
2. Получаем дату из п.1 в пост запросе, проверяем ее на разность дат м/у сервером и клиентом, если разница приемлемая то работаем далее, иначе говорим клиенту что он косячит.
3. Высчитываем количество секунд которые остались у лицензии, берем хешь от этих секунд и соли. Возвращаем этот хеш и дату окончания лицензии клиенту.

На клиенте:
4. Если в ответе от сервера нет ошибок, то работаем, иначе выводим клиенту ошибку:
а. Не нашли клиента в списке
б. Большая разница во времени м/у сервером и клиентом
5. Ошибок нет, тогда высчитываем разницу м/у датой в п.1 и датой которая пришла в ответе, от полученого и соли берем хеш.
Сравниваем хешь который получили от сервера и тот который расчитали в шаблоне. Если они совпадают то работаем, иначе говорим клиенту что лицуха кончилась.

Я надеюсь теперь понятно? Если не понятно, тогда рекомендую протестировать мое решение, ведь по условиям конкурса шаблон открыт ;-)
 
  • Спасибо
Реакции: nole

sergey_l

Client
Регистрация
06.12.2016
Сообщения
18
Благодарностей
5
Баллы
3
Не знаю что я делаю не так при запросе к таблице выдает ошибку:
Не удается вызвать метод "getDataRange" объекта null. На строке 17
 

ZHAG

Client
Регистрация
01.05.2014
Сообщения
228
Благодарностей
210
Баллы
43
Не знаю что я делаю не так при запросе к таблице выдает ошибку:
Не удается вызвать метод "getDataRange" объекта null. На строке 17
А как у тебя лист называется на котором данные хранишь?
Он должен называться "Clients"
 

sergey_l

Client
Регистрация
06.12.2016
Сообщения
18
Благодарностей
5
Баллы
3
А как у тебя лист называется на котором данные хранишь?
Он должен называться "Clients"
Поправил, заработало!:bo:
Теперь получаю ответ:
{"error":"Большая разница во времени между клиентом и сервером!","code":0,"s":"2d549b91b73fd867a8479d68b6860147"}
 

kapelan28

Client
Регистрация
22.09.2015
Сообщения
457
Благодарностей
166
Баллы
43
по гугл-таблице подскажите - ее сохранять с каким доступом? Где брать ID таблицы для указания в скрипте? (простите за нубские вопросы, но не работал с гугл-таблицами).
 

sergey_l

Client
Регистрация
06.12.2016
Сообщения
18
Благодарностей
5
Баллы
3
по гугл-таблице подскажите - ее сохранять с каким доступом? Где брать ID таблицы для указания в скрипте? (простите за нубские вопросы, но не работал с гугл-таблицами).
В старт посте есть скрин.
А ID нужно брать из УРЛа
 

kapelan28

Client
Регистрация
22.09.2015
Сообщения
457
Благодарностей
166
Баллы
43
В старт посте есть скрин.
может невнимательно смотрел, но не вижу я там в скрине информации по типу сохранения страницы с таблицей. По скрипту - все понятно, вопросов нет, а вот с таблицей - не понял.
 

Adigen

Client
Регистрация
28.07.2014
Сообщения
828
Благодарностей
610
Баллы
93
Может быть я чего-то не понимаю, но:
1. Соль у нас константа, значит достаточно после покупки отснифить один раз ответ, чтобы получить МД5 от ключа.
2. Дальше просто перенаправляем запросы идущие на https://script.google.com/ на свой обработчик, и там отлично высчитываем все остальное.

Тут надо использовать ассиметричное шифрование, например RSA.
Если не прав поправьте.
 

Кто просматривает тему: (Всего: 1, Пользователи: 0, Гости: 1)