Создаём собственный API-6

Кирилл Евсеев, April 5, 2011

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

Начнём с API Handler, чтобы изменения в Gateway были более очевидны. Забегая вперёд, заметим, что все изменения не касаются удалённого хоста. Т.о. драйвер для работы с API на удалённом хосте остаётся таким же. На API хосте мы всё так же принимаем запрос и всё так же возвращаем ответ, но, в данном случае, меняется способ обработки этого запроса и формат ответа в случае ошибки. Единственные изменения, которые должны произойти на удалённом хосте – это информация об авторизации (имя пользователя и пароль), которую необходимо добавить к запросу.

class api_handler extends api
{

//три объекта, которыми мы будем манипулировать.    
    protected $api_gateway = null;
    protected $api_login = null;
    protected $api_access = null;

//запрос от удалённого хоста
    protected $request_data = array();

//данные об удалённом хосте (авторизация)
    protected $user_data = array();
    

//в конструкторе создаём необходимые объекты
    public function __construct($db_object)
    {
        parent::__construct($db_object);

//этот класс мы рассмотрим чуть ниже
        $this->api_gateway = new api_gateway_smart($this->db_handler);
        $this->api_login = new api_login($this->db_handler);
        $this->api_access = new api_access($this->db_handler);
    }


//исполняемая функция array main(array) принимает запрос 
//от удалённого хоста,
//возвращает массив с ответом 
    public function main($request)
    {

//получили данные по установленному протоколу и привели их в рабочий вид
        $this->request_data = $this->api_gateway->get_request($request);

//если данные совпадают с ожидаемыми
        if($this->request_data)
        {
//проходим авторизацию.
            $this->user_data = $this->api_login->autorize(
$this->request_data['username'], $this->request_data['password']);

            
//если авторизация пройдена
            if($this->user_data)
            {

//и уровня доступа хватает для вызова запрошенной функции                
                if($this->api_access->chk_access(
$this->user_data['access'], $this->request_data['f_name']))
                {
//вызываем API функцию через объект шлюза
                    $this->api_gateway->process_data();

//и возвращаем результат её работы
                    return $this->api_gateway->send_response();
                }
                else
                {
//если не хватает уровня доступа для вызова API функции, 
//возвращаем ошибку
                    return $this->api_gateway->send_response(false, 3);
                }
            }
            else
            {
//если не пройдена авторизация, возвращаем ошибку
                return $this->api_gateway->send_response(false, 2);
            }
        }
        else
        {
//если запрос не соответствует протоколу, возвращаем ошибку
            return $this->api_gateway->send_response(false, 1);
        }
    }
}

Теперь изменим поведение класса Gateway для лучшей работы Api_Handler.

interface api_gateway_smart_itrfc
{

// array send_response([bool] [, integer]]) возвращаем 
// ответ удалённому хосту.
// в качестве необязательных параметров получает флаг в случае ошибки 
// и код ошибки
   public function send_response($flag=true, $int=0);

//единственные изменения, которые произошли в этом методе - тип доступа.
//был protected стал public   
   public function process_data();
}



class api_gateway_smart extends api_gateway 
  implements  api_gateway_itrfc, api_gateway_smart_itrfc
{
//массив с ответом
    protected $response_data = array();

//в api_gateway я использовал эту переменную без объявления. 
//синтаксис это позволяет, но 
//правила хорошего тона - нет. так что прошу прощения за эту оплошность. 
    protected $request_data = array();

//теперь мы будем жёстко проверять формат запроса и это будет ещё одной 
//дополнительной мерой безопасности
    protected $request_data_format = array();
    
    public function __construct($db_object)
    {
        parent::__construct($db_object);

// инициализируем переменную, отвечающую за формат запроса.
// теперь мы точно знаем все ключи и типы значений в запросе. 
// если значений больше или меньше или если их типы
// не совпадают с тем, что мы ожидаем, 
// то мы такой запрос будем игнорировать. 
        $this->request_data_format['username'] = '';
        $this->request_data_format['password'] = '';
        $this->request_data_format['f_name'] = '';
        $this->request_data_format['args'] = array();
        
    }

    public function get_request($str)
    {
//протокол
        parent::prepare_data($str);
        
        if($this->chk_request())
        {
//если проверка формата запроса успешно пройдена, работаем дальше
            return $this->request_data;
        }
        else
        {
//если нет, то возвращаем ошибку
            return false;
        }
    }

    public function process_data()
    {
        return parent::process_data();
    }


    public function send_response($bool=true, $int=0)
    {
        if($bool)
        {

//если метод вызван с параметрами по умолчанию, то возвращаем 
//родительский send_response, а именно
//отправляем заголовки удалённому хосту методом echo 
            return parent::send_response();
        }
        else
        {
//в противном случае произошла какая-то ошибка. 
//первым делом, в целях безопасности, обнулим 
//переменную $this->response_data, 
//которая содержит информацию для удалённого хоста 
            unset($this->response_data);

//теперь сформируем массив с инфомацией об ошибке с учётом кода, 
//от вызывающего приложения
//эту часть можно спрятать в базу данных или в конфигурационный файл 
            switch($int)
            {
                case '1':
                    $this->response_data = array(
  'error'=>'unsupported request format', 'errno'=>1);
                    break; 
                
                case '2':
                    $this->response_data = array(
  'error'=>'autorization failed', 'errno'=>2);
                    break;
                    
                case '3':
                    $this->response_data = array(
  'error'=>'access denied', 'errno'=>3);
                    break;

                default:
                    break;
                    $this->response_data = array(
  'error'=>'unknown', 'errno'=>0);
            }

//и вернём удалённому хосту
            return parent::send_response();
        }
    }
    


// bool chk_request(void) - один из ключевых методов, 
// отвечающих за безопасность.
// возвращает true только в том случае, если запрос сформирован 
// действительно правильно и не содержит 
// потенциально опасных данных
    protected function chk_request()
    {
        $temp = array(); //временная переменная, 
                               //аналог $this->request_data 

//сначала убедися, что запрос - действительно массив
        if(is_array($this->request_data))
        {

//затем в цикле пройдёмся по эталонному формату запроса. 
//эта переменная определяется в конструкторе.
            foreach($this->request_data_format as $key => $val)
            {
//сравним каждый элемент массива, полученный от удалённого хоста, 
//с элементом эталонного массива
//на предмет совпадения ключа и типа
                if(array_key_exists($key, $this->request_data) && 
  (gettype($val)===gettype($this->request_data[$key])))
                {
//если всё ок, то сохраним текущее значение во временной переменной
                    $temp[$key] = $this->request_data[$key];
                }
                else
                {
//в противном случае, запрос полученный от удалённого хоста 
//не содержит обязательных данных
//или содержит данные другого типа
                    return false;
                }
            }

//обнуляем $this->request_data и присваиваем ей значение 
//временной переменной - это гарантия того, что
//в запросе от пользователя не пришло каких-то дополнительных 
//неожиданных данных, которые могут потенциально
//угрожать безопасности
            unset($this->request_data);
            $this->request_data = $temp;

            
//если мы добрались до этой точки, то 
//$this->request_data на 100% имеет правильный формат и 
//содержит все необходимые данные для правильной работы приложения
            return true;
        }
        else
        {
            return false;
        }
    }
}

Таким образом, мы получили новый класс, наследник первой версии шлюза, который проверяет пользовательский запрос и гарантирует правильный формат данных для нормальной работы всего приложения. Теперь api_handler может свободно манипулировать всеми высокоуровневыми методами, оставляя внизу вопросы низкоуровневых проверок, выполнения запроса, реализацию протокола и т.п. По сути, api_handler стал удобной обёрткой. Его структура отличается от рассмотренной в первой статье. В первой статье предполагалось, что шлюз будет чем-то внешним, но сейчас api_handler реализован так, что он скрывает шлюз в себе. Хотя, логическая суть шлюза от этого не изменилась. Шлюз по прежнему принимает запрос и отправляет ответ удалённому хосту.

В следующей части статьи мы отфильтруем запрос удалённого хоста от потенциально опасного мусора и предотвратим различные неприятности в духе SQL-инъекций. Конечно, для этого можно было бы использовать какой-нибудь мощный класс типа AdoDB, в котором механизм защиты уже реализован, но, во-первых, этот пример API является исключительно демонстрационным, во-вторых, возможно, нам понадобится фильтровать запрос не только на уколы. Так что ждём следующей статьи, чтобы посмотреть на новые изменения :)

Как всегда, полная версия кода доступна в аттачменте, а я жду ваших критических замечаний и комментариев.

Всем удачи!

В тему:

0комментариев
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: