Создаём универсальный класс взаимодействия с вашим сервером

Уроки по PHP, Javascript и т.п.

Создаём универсальный класс взаимодействия с вашим сервером

Сообщение AlexProger » 01 сен 2012, 18:57

Делал я однажды заказ для заказчика (для себя ничего не люблю делать :cry: ) и понадобилось мне взаимодействовать с сервером и тут появилось 2 вопроса :
    1. Как это сделать удобнее
    2. Как это сделать безопаснее
Условия :
    1. На сервере 3 скрипта. (config.php, vkapi.class.php, core.php)
    2. Взаимодействовать надо только со скриптом core.php
    3. Надо чтобы были минимальные сложности в использовании
Приступаем к решению :
[Шаг 1: создаём среду для работы скрипта core.php]
Файл config.php :
  1.  
  2. <?php
  3. /*
  4. @Author : Alexandr Leutin {AlexProger, Xikke}
  5. */
  6. define("DB_USER","..."); //Имя пользователя БД
  7. define("DB_PASS","...");  //Пароль БД
  8. define("DB_NAME","..."); //Имя БД
  9. define("DB_HOST","localhost");
  10.  
  11. define("API_ID","...");  //ID прилы в вк
  12. define("API_SECRET","........."); //Секретный ключ прилы
  13.  
  14. function Connect()
  15. {
  16.     mysql_connect(DB_HOST, DB_USER, DB_PASS) or die (mysql_error());
  17.     mysql_select_db(DB_NAME) or die (mysql_error());
  18.     mysql_query("SET NAMES 'utf8'");   
  19. }
  20. ?>
  21.  

Единственное что хотелось бы выделить в данном скрипте это то что данные хранятся не в переменных, а в константах объявленных через define
Файл vkapi.class.php :
  1.  
  2. <?php
  3.  
  4. /**
  5.  * VKAPI class for vk.com social network
  6.  *
  7.  * @package server API methods
  8.  * @link http://vk.com/developers.php
  9.  * @autor Oleg Illarionov
  10.  * @version 1.0
  11.  */
  12.  
  13. class vkapi {
  14.     var $api_secret;
  15.     var $app_id;
  16.     var $api_url;
  17.    
  18.     function vkapi($app_id, $api_secret, $api_url = 'api.vk.com/api.php') {
  19.         $this->app_id = $app_id;
  20.         $this->api_secret = $api_secret;
  21.         if (!strstr($api_url, 'http://')) $api_url = 'http://'.$api_url;
  22.         $this->api_url = $api_url;
  23.     }
  24.    
  25.     function api($method,$params=false) {
  26.         if (!$params) $params = array();
  27.         $params['api_id'] = $this->app_id;
  28.         $params['v'] = '3.0';
  29.         $params['method'] = $method;
  30.         $params['timestamp'] = time();
  31.         $params['format'] = 'json';
  32.         $params['random'] = rand(0,10000);
  33.         ksort($params);
  34.         $sig = '';
  35.         foreach($params as $k=>$v) {
  36.             $sig .= $k.'='.$v;
  37.         }
  38.         $sig .= $this->api_secret;
  39.         $params['sig'] = md5($sig);
  40.         $query = $this->api_url.'?'.$this->params($params);
  41.         $res = file_get_contents($query);
  42.         return json_decode($res, true);
  43.     }
  44.    
  45.     function params($params) {
  46.         $pice = array();
  47.         foreach($params as $k=>$v) {
  48.             $pice[] = $k.'='.urlencode($v);
  49.         }
  50.         return implode('&',$pice);
  51.     }
  52. }
  53. ?>
  54.  

[Шаг 2 : готовимся к написанию скрипта core.php]
Для начала определим задачи и список функций выполняемых скриптом.
    1. Авторизация пользователя
    2. Получение его данных из БД
    3. Запись данных в БД
Определив задачи нам нужно спроектировать БД которая бы устраивала нас. Благо я это уже сделал :
  1.  
  2. CREATE TABLE IF NOT EXISTS `users` (
  3.   `id` int(9) auto_increment,
  4.   `uid` int(9),
  5.   `money` int(6) default '100',
  6.   `rait` int(8) default '0',
  7.   `last_enter` int(15),
  8.   PRIMARY KEY  (`id`),
  9.   UNIQUE KEY `uid` (`uid`)
  10. ) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
  11.  

Теперь я опишу что обозначают эти поля :
    id - номер в базе
    uid - ID пользователя в вк
    money - кол-во денег в приле
    rait - рейтинг в приле
    last_enter - UNIX время последнего захода (защитимся от захода в прилу через каждые 5 секунд)
Теперь определимся как скрипт будет узнавать что мы от него хотим?
Естественно передавая переменную act вот варианты её значения :
    AUTH_USR - пользователь хочет авторизироваться
    GET_USER_INFO - получаем данные о юзере
    UPDATE_USER_INFO - обновляем данные о юзере
[шаг 3: реализуем скрипт core.php]
Итак часть 1 : подключаем ранее написанные файлы :

Здесь обсуждать нечего если вы хоть чуть - чуть знакомы с PHP
Теперь получим обязательные данные которые нужны скрипту :
  1.  
  2. $uid = $_POST['viewer_id'];
  3. $auth = $_POST['auth_key'];
  4. $act = $_POST['act'];
  5. $key = $_POST['key'];
  6.  

Теперь я опишу что означают эти переменные :
    $uid - ID пользователя в вк
    $auth - auth_key пользователя
    $act - наша переменная обозначающая что мы хотим делать
    $key - ключ защиты переменной act
Здесь я хотел бы выделить последнюю переменную :
$key - это хэш защищающий от подмены не только $act, но и $uid и $auth.
Вот формула (защита идёт через MD5, хотя мы спокойно могли бы использовать и SHA1 и другие алгоритмы защиты данных, но такой задачи нам не ставилось и мы не ищем лёгких путей :mrgreen: )
Нам пора использовать полученные данные, я решил не расписывать каждый шаг и представил каркас ниже :
  1.  
  2. <?php
  3. include 'config.php';
  4. require 'vkapi.class.php';
  5. Connect();
  6. /*
  7.     @desc: Загрузка переменных
  8. */
  9. $uid  = $_POST['uid'];
  10. $auth = $_POST['auth'];
  11. $act  = $_POST['act'];
  12. $key  = $_POST['key'];
  13. /*
  14.     @desc: Обработка данных
  15. */
  16. if ($auth == md5(API_ID.'_'.$uid.'_'.API_SECRET))
  17. {
  18.     if (md5(md5($act).md5($auth).md5($uid)) == $key){
  19.         switch($act)
  20.         {
  21.             case "AUTH_USR":
  22.             //Авторизация
  23.             break;
  24.             case "GET_USER_INFO":
  25.             //Получение данных
  26.             break;
  27.             case "UPDATE_USER_INFO":
  28.             //Обновление данных
  29.             break;
  30.             default:
  31.             //Неизвестный акт
  32.             break;
  33.         }
  34.     }else{
  35.     //Ошибка в key (скорее всего подмена данных
  36.    
  37.     }
  38. }else{
  39. //Ошибка в auth_key
  40.  
  41. }
  42. ?>
  43.  

Если вы не поняли что делает этот каркас, значит вам надо учить PHP
Итак продолжим реализацию :
Теперь нам надо записать пользователя в БД, код представлен ниже :
  1.  
  2. $result = mysql_query('INSERT INTO `users` (`uid`, `last_enter`) VALUES ("'.$uid.'","'.time().'") ON DUPLICATE KEY UPDATE  `last_enter` = "'.time().'";');
  3. if (!$result){
  4. //Ошибка mysql запроса выводим её в ответ :
  5. exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  6. }else{
  7. //Выводим что всё хорошо
  8. exit('{"error":"0","auth":"1","hash":"'.md5(md5('1_auth_1')).'"}');
  9. }
  10.  

Обсудим что я сотворил выше :D
Я делаю запрос на занесение пользователя в базу и в случае ошибки выдаю пользователю в виде JSON ответа.
Думаю ничего сложного в этом нет поэтому приступим к рассмотрению второго вида акта : GET_USER_INFO
Я не буду разъяснять что мы делаем в этом акте т.к. это за меня сделал Александр (Тыц чтобы посмотреть урок) поэтому вот код :
  1.  
  2. $result = mysql_query('SELECT * FROM `users` WHERE `uid`='.$uid.';');
  3. if (!$result){
  4. //Ошибка mysql запроса выводим её в ответ :
  5. exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  6. }else{
  7. $data = mysql_fetch_assoc($result);     //mysql_fetch_assoc потому что нам надо обращаться к переменныем таким способом : $data['money'];
  8. //Создаём переменные которые надо вывести :
  9. $money = $data['money'];
  10. $rait  = $data['rait'];
  11. $hash  = md5(md5($money).md5($rait));
  12. //Выводим результат
  13. exit('{"error":"0","money":"'.$money.'","rait":"'.$rait.'","hash":"'.$hash.'"}');
  14. }
  15.  

Теперь напишем третий акт : UPDATE_USER_INFO.
Сейчас я немного отойду от темы и мы поговорим о безопасности этого акта.
Данный акт требует дополнительные данные (кол-во добавляемых едениц) и нам надо защитить эти данные. Поэтому мы будем принимать к этому методу не 2 параметра, а 3.
Параметры :
    1. money - это сколько надо добавить денег
    2. rait - это сколько надо добавить рейтинга
    3. checker - это хэш данных (защищает только money и rait и привязан к auth_key)
Формула составления хэша :
md5(money + auth + rait);
Вот код третьего акта :
  1.  
  2. $money = $_POST['m_add']; //Внимание : я использовал именование m_add, r_add вместо money и rait
  3. $rait  = $_POST['r_add']; //
  4. $hash  = $_POST['c_add']; //c_add это checker
  5. if (md5($money.$auth.$rait) == $hash){
  6.     $result = mysql_query('UPDATE `users` SET `money` = `money` + "'.$money.'", `rait` = `rait` + "'.$rait.'" WHERE `uid`='.$uid.';');
  7.     if (!$result)
  8.     {
  9.         exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  10.     }else{
  11.         //Обновляем данные и выводим их
  12.         $result_2 = mysql_query('SELECT * FROM `users` WHERE `uid`='.$uid.';');
  13.         if (!$result_2){
  14.             exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  15.         }else{
  16.             //Выводим обновлённые данные [код из акта 2]
  17.             $data = mysql_fetch_assoc($result);     //mysql_fetch_assoc потому что нам надо обращаться к переменныем таким способом : $data['money'];
  18.             //Создаём переменные которые надо вывести :
  19.             $money = $data['money'];
  20.             $rait  = $data['rait'];
  21.             $hash  = md5(md5($money).md5($rait));
  22.             //Выводим результат
  23.             exit('{"error":"0","money":"'.$money.'","rait":"'.$rait.'","hash":"'.$hash.'"}');
  24.         }
  25.     }
  26. }else{
  27. //Попытка обмана
  28. exit('{"error":"1","desc":"invalid hash"}');
  29. }
  30.  

Готово! Перед тем как приступить к клиенту напишем полный код файла core.php :
  1.  
  2. <?php
  3. include 'config.php';
  4. require 'vkapi.class.php';
  5. Connect();
  6. /*
  7.     @desc: Загрузка переменных
  8. */
  9. $uid  = $_POST['uid'];
  10. $auth = $_POST['auth'];
  11. $act  = $_POST['act'];
  12. $key  = $_POST['key'];
  13. /*
  14.     @desc: Обработка данных
  15. */
  16. if ($auth == md5(API_ID.'_'.$uid.'_'.API_SECRET))
  17. {
  18.     if (md5(md5($act).md5($auth).md5($uid)) == $key){
  19.         switch($act)
  20.         {
  21.             case "AUTH_USR":
  22.             //Авторизация
  23.                 $result = mysql_query('INSERT INTO `users` (`uid`, `last_enter`) VALUES ("'.$uid.'","'.time().'") ON DUPLICATE KEY UPDATE  `last_enter` = "'.time().'";');
  24.                 if (!$result){
  25.                     //Ошибка mysql запроса выводим её в ответ :
  26.                     exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  27.                 }else{
  28.                     //Выводим что всё хорошо
  29.                     exit('{"error":"0","auth":"1","hash":"'.md5(md5('1_auth_1')).'"}');
  30.                 }
  31.             break;
  32.             case "GET_USER_INFO":
  33.             //Получение данных
  34.                 $result = mysql_query('SELECT * FROM `users` WHERE `uid`='.$uid.';');
  35.                 if (!$result){
  36.                     //Ошибка mysql запроса выводим её в ответ :
  37.                     exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  38.                 }else{
  39.                     $data = mysql_fetch_assoc($result);     //mysql_fetch_assoc потому что нам надо обращаться к переменныем таким способом : $data['money'];
  40.                     //Создаём переменные которые надо вывести :
  41.                     $money = $data['money'];
  42.                     $rait  = $data['rait'];
  43.                     $hash  = md5(md5($money).md5($rait));
  44.                     //Выводим результат
  45.                     exit('{"error":"0","money":"'.$money.'","rait":"'.$rait.'","hash":"'.$hash.'"}');
  46.             }
  47.             break;
  48.             case "UPDATE_USER_INFO":
  49.             //Обновление данных
  50.                 $money = $_POST['m_add']; //Внимание : я использовал именование m_add, r_add вместо money и rait
  51.                 $rait  = $_POST['r_add']; //
  52.                 $hash  = $_POST['c_add']; //c_add это checker
  53.                 if (md5($money.$auth.$rait) == $hash){
  54.                 $result = mysql_query('UPDATE `users` SET `money` = `money` + "'.$money.'", `rait` = `rait` + "'.$rait.'" WHERE `uid`='.$uid.';');
  55.                 if (!$result)
  56.                 {
  57.                     exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  58.                 }else{
  59.                     //Обновляем данные и выводим их
  60.                     $result_2 = mysql_query('SELECT * FROM `users` WHERE `uid`='.$uid.';');
  61.                         if (!$result_2){
  62.                             exit('{"error":"1","desc":"mysql error '.mysql_error().'}');
  63.                         }else{
  64.                             //Выводим обновлённые данные [код из акта 2]
  65.                             $data = mysql_fetch_assoc($result);     //mysql_fetch_assoc потому что нам надо обращаться к переменныем таким способом : $data['money'];
  66.                             //Создаём переменные которые надо вывести :
  67.                             $money = $data['money'];
  68.                             $rait  = $data['rait'];
  69.                             $hash  = md5(md5($money).md5($rait));
  70.                             //Выводим результат
  71.                             exit('{"error":"0","money":"'.$money.'","rait":"'.$rait.'","hash":"'.$hash.'"}');
  72.                         }
  73.                     }
  74.                 }else{
  75.                     //Попытка обмана
  76.                     exit('{"error":"1","desc":"invalid hash"}');
  77.                 }
  78.             break;
  79.             default:
  80.             //Неизвестный акт
  81.             exit('{"error":"1","desc":"unknown act"}');
  82.             break;
  83.         }
  84.     }else{
  85.     //Ошибка в key (скорее всего подмена данных
  86.     exit('{"error":"1","desc":"key invalid"}');
  87.     }
  88. }else{
  89. //Ошибка в auth_key
  90. exit('{"error":"1","desc":"auth key invalid"}');
  91. }
  92. ?>
  93.  

Просмотрите этот код внимательно, теперь мы приступаем к клиенту.
[шаг 4: пишем клиент]
Я не буду описывать весь процесс создания приложения поэтому приведу ниже код и расскажу о некоторых особенностях.
  1.  
  2. Файл [b]ServerAPI.as[/b] :
  3. package  {
  4.     import flash.net.*;
  5.     import flash.events.*;
  6.     import vk.api.MD5;
  7.     import vk.api.serialization.json.JSON;
  8.    
  9.     public class ServerAPI {
  10.             private var server:String = null;
  11.             private var uid:String = null;
  12.             private var auth:String = null;
  13.            
  14.         public function ServerAPI(parms:Object) {
  15.             server = parms.url;
  16.             uid    = parms.uid;
  17.             auth   = parms.auth;
  18.         }
  19.         public function Executure(parms:Object){
  20.             /*
  21.                 Код представленный ниже взят из основы написанной в данной теме : topic165.html
  22.             */
  23.             var core_loader:URLLoader = new URLLoader();
  24.             var core_request:URLRequest=new URLRequest(server + "core.php");
  25.             core_request.method=URLRequestMethod.POST;
  26.             switch (parms.act)
  27.             {
  28.                 case "AUTH_USR":
  29.                     core_vars['viewer_id'] = uid;
  30.                     core_vars['auth_key'] = auth;
  31.                     core_vars['act'] = parms.act;
  32.                     core_vars['key'] = MD5.encrypt(MD5.encrypt(parms.act) + MD5.encrypt(auth) + MD5.encrypt(uid));
  33.                     //Больше параметров нет
  34.                 break;
  35.                 case "GET_USER_INFO":
  36.                     core_vars['viewer_id'] = uid;
  37.                     core_vars['auth_key'] = auth;
  38.                     core_vars['act'] = parms.act;
  39.                     core_vars['key'] = MD5.encrypt(MD5.encrypt(parms.act) + MD5.encrypt(auth) + MD5.encrypt(uid));
  40.                     //Больше параметров нет
  41.                 break;
  42.                 case "UPDATE_USER_INFO":
  43.                     core_vars['viewer_id'] = uid;
  44.                     core_vars['auth_key'] = auth;
  45.                     core_vars['act'] = parms.act;
  46.                     core_vars['key'] = MD5.encrypt(MD5.encrypt(parms.act) + MD5.encrypt(auth) + MD5.encrypt(uid));
  47.                     //Прочие параметры
  48.                     core_vars['m_add'] = parms.money;
  49.                     core_vars['r_add'] = parms.rait;
  50.                     core_vars['c_add'] = MD5.encrypt(parms.money + auth + parms.rait);
  51.                 break;
  52.                 default:
  53.                     throw new Error("Act is not valid");
  54.                 break;
  55.             }
  56.             var core_vars:URLVariables = new URLVariables();
  57.             core_request.data=core_vars;
  58.             core_loader.addEventListener(Event.COMPLETE, function(e:Event){
  59.                 trace(e.target.data);       //Для отладки
  60.                 var data:Object = JSON.decode(e.target.data);
  61.                 if (data['error'] == '0'){
  62.                     //Ошибок нет
  63.                     parms.onComplete(data);
  64.                 }else{
  65.                     parms.onError(data);
  66.                 }
  67.             });
  68.             core_loader.addEventListener(IOErrorEvent.IO_ERROR, function(e:Event){
  69.                 //Если ошибка из - за отсуствия доступа к скрипту
  70.                 parms.onError({error:1,desc:"IO_SCRIPT_ERROR"});        //Даём понять что ошибка не в скрипте
  71.             });
  72.             core_loader.load(core_request);
  73.         }
  74.     }
  75. }
  76.  

Особенности :
    1. Только 1 аргумент во всех функциях (включая конструктор)
    2. Обработка всех основных событий
    3. Дополняемость класса (вы можете дописать класс или переделать его)
Представленный выше код это класс помогающий работать с нашей серверной частью. Вот пример его использования (авторизация юзера) :
  1.  
  2.             var net:ServerAPI = new ServerAPI({url:"http://myserver.ru/",uid:"ваш ID",auth:"ваш auth key"});
  3.             net.Executure({onComplete: function(data:Object){
  4.                 //Успешный запрос  
  5.             }, onError: function(data:Object){
  6.                 //Ошибка
  7.             } });
  8.  

Ну вот и всё. Для тех кто знает азы и изучал уроки на этом форуме будет не сложно разобраться с кодом.
От себя : Поздравляю всех с днём знаний ;)
P.S. В данном уроке я не рассмотрел проверку времени последнего захода (защита от частых заходов). Это будет ваша домашняя работа :ugeek:
AlexProger

 
Автор темы
Сообщения: 6
Зарегистрирован: 01 сен 2012, 16:42
Откуда: Россия
Благодарил (а): 1 раз.
Поблагодарили: 0 раз.

Чтобы убрать блок с рекламой, зарегистрируйтесь на форуме или войдите.

Google
 



Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение gpv123 » 01 сен 2012, 19:41

AlexProger писал(а):Данный акт требует дополнительные данные (кол-во добавляемых едениц) и нам надо защитить эти данные. Поэтому мы будем принимать к этому методу не 2 параметра, а 3.
Параметры :
1. money - это сколько надо добавить денег
2. rait - это сколько надо добавить рейтинга
3. checker - это хэш данных (защищает только money и rait и привязан к auth_key)
Формула составления хэша :
md5(money + auth + rait);

Это все можно подменить.


Ну зачем? В чем смысл? $uid и $auth уже проверены чуть выше, а $act проверять нет смысла.
gpv123

 
Сообщения: 346
Зарегистрирован: 29 янв 2012, 20:57
Благодарил (а): 17 раз.
Поблагодарили: 73 раз.

Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение AlexProger » 01 сен 2012, 20:05

Это все можно подменить.

if (md5(md5($act).md5($auth).md5($uid)) == $key)

Ну зачем? В чем смысл? $uid и $auth уже проверены чуть выше, а $act проверять нет смысла.

Если знаешь как этот хэш составлялся то можно подменить. Я не призываю использовать именно мой код, я лишь показал как это можно реализовать. :)
Ну зачем? В чем смысл? $uid и $auth уже проверены чуть выше, а $act проверять нет смысла.

Смысл проверять есть всегда! :!:

Тем более что представленный код это отформатированные отрывки кода из моего проекта. Хотел поделиться с людьми ;)
AlexProger

 
Автор темы
Сообщения: 6
Зарегистрирован: 01 сен 2012, 16:42
Откуда: Россия
Благодарил (а): 1 раз.
Поблагодарили: 0 раз.

Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение gpv123 » 01 сен 2012, 20:08

AlexProger писал(а):Если знаешь как этот хэш составлялся то можно подменить.

Это можно узнать при декомпиляции флешки. Наверняка найдется тот, кто не пожалеет времени и взломает приложение.

За это сообщение автора gpv123 поблагодарил:
AlexProger
gpv123

 
Сообщения: 346
Зарегистрирован: 29 янв 2012, 20:57
Благодарил (а): 17 раз.
Поблагодарили: 73 раз.

Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение AlexProger » 01 сен 2012, 20:21

Это можно узнать при декомпиляции флешки. Наверняка найдется тот, кто не пожалеет времени и взломает приложение.
Если обфуцировать флешку то это будет сделать не так легко (я тестировал на своей флешке тремя декомпиляторами и не один из них не выдал правильный код) Может об этом тоже напишу статейку ;)
AlexProger

 
Автор темы
Сообщения: 6
Зарегистрирован: 01 сен 2012, 16:42
Откуда: Россия
Благодарил (а): 1 раз.
Поблагодарили: 0 раз.

Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение gpv123 » 01 сен 2012, 20:35

AlexProger писал(а):Если обфуцировать флешку то это будет сделать не так легко

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

По теме: topic5391.html
gpv123

 
Сообщения: 346
Зарегистрирован: 29 янв 2012, 20:57
Благодарил (а): 17 раз.
Поблагодарили: 73 раз.

Re: Создаём универсальный класс взаимодействия с вашим сервером

Сообщение AlexProger » 01 сен 2012, 21:13

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

Ну я не поднимал вопрос о безопасности. Я хотел показать как решить такую задачу т.к. некоторым будет полезно.
P.S. спс за ссылку на тему, почитаю на досуге. :mrgreen:
AlexProger

 
Автор темы
Сообщения: 6
Зарегистрирован: 01 сен 2012, 16:42
Откуда: Россия
Благодарил (а): 1 раз.
Поблагодарили: 0 раз.


Вернуться в Уроки на другие темы



Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 0