HTTPリクエストするためのクラス書いてみた

HTTPリクエスト再び

Twitter API用のライブラリがどうとか言いながら、全然公開してませんな。

スピンアウト品による、HTTPリクエストについてメモしとくです。この後にOAuthかな。そしてTwitter, Flickr, Google, YahooあたりのAPIと遊ぶ方法とか。一通り試した後なので、暇を見つけながら、内容をまとめて自分メモ的にブログ書くつもり。a-blog cmsと連携したTwitter BOTぐらいはちゃんと作って公開すっか。

APIを叩く用に

<?php
$http = new SimpleHttp();

$http->get('http://havelog.ayumusato.com');

$res = $http->header;
$body = $http->body;

if ( !($http->error) ) {
     return true;
} else {
     return false;
}
?>

GET!POST!で適当にリクエスト投げれて、レスポンスヘッダーもレスポンスボディも、一通り取ってこれるようにしたかったのです。API相手しか考えてないので、POSTメソッドもクエリーのやり取りしか考えられてません。

file_get_contentsとかだとレスポンスコード200以外のときのレスポンスボディを端折りやがるので、ソケット接続から始めてリクエストを書き込んで受け取って、と地味な作業を繰り返してます。地味ですけど、HTTPの仕様の勉強になります。裏でこういうやり取りされてたんですね。

ハードウェアやソフトウェアの深淵を見るには、文系の自分には叶いません。でも、たまーに少し深いところを垣間見ると、なんとなく、してやったりという気分になれます。ああ現金。

覚え書きリスト

  • リクエスト時の改行コードはCR+LFで鉄板くさい
  • 末尾の改行コードが足りないとシカトされることすらある
  • ステータスコード204と304はレスポンスボディがない(仕様的に)
  • HTTP1.1のchunkedデータは、送ってきたり送ってこなかったりする
  • chunked受け取りたくなかったら1.0としてリクエスト
  • phpのparse_url関数はステキ(regex書き終わった頃に教えてもらった)
  • httpsはssl://に変換して、portも合わせて変える

おまけに普通なソースコード

<?php

class SimpleHttp
{
    private $_connection;
    private $_header;

    private $version    = '1.1';
    private $maxlen     = 2048;
    private $blocking   = true;
    private $timeout    = 60;
    private $eol        = "\r\n";

    public $header;
    public $body;

    public function __construct()
    {
        $this->_initialize();
    }

    public function __destruct()
    {
        $this->disconnect();
    }

    public function __toString()
    {
        return $this->host;
    }

    /**
     * Set Default Configuration
     */
    private function _initialize()
    {
        $this->scheme   = 'http';
        $this->host     = 'localhost';
        $this->port     = 80;
        $this->sslport  = 443;
        $this->user     = '';
        $this->pass     = '';
        $this->path     = '';
        $this->query    = '';
        $this->fragment = '';

        $this->_header  = array(
            'Accept'            => '',
            'Accept-Charset'    => '',
            'Accept-Language'   => '',
            'Accept-Encoding'   => 'gzip',
            'Allow'             => '',
            'Authorization'     => '',
            'Cache-Control'     => '',
            'Connection'        => 'close',
            'Content-Language'  => '',
            'Content-Length'    => '',
            'Content-Type'      => '',
            'Expect'            => '',
            'Host'              => '',
            'If-Modified-Since' => '',
            'Max-Forwards'      => '',
            'Range'             => '',
            'Referer'           => '',
            'User-Agent'        => 'PHP/'.phpversion(),
            'WWW-Authenticate'  => '',
        );

        $this->header   = null;
        $this->body     = null;
        $this->error    = true;
    }

    private function _parseUrl($url)
    {
        $parsed = parse_url($url);

        foreach ( $parsed as $key => $val ) {
            $this->$key = $val;
        }
    }

    private function _initConnection($url)
    {
        if ( !is_resource($this->_connection) )
        {
            $this->_initialize();
            $this->connect($url);
        }
        elseif ( $this->host != parse_url($url, PHP_URL_HOST) )
        {
            $this->_initialize();
            $this->connect($url);
        }
    }

    /**
     * HTTP Protocol Methods
     */
    public function get($url)
    {
        $this->method = 'GET';
        $this->_request($url);
    }

    public function post($url)
    {
        $this->method = 'POST';
        $this->_request($url);
    }
    
    private function _request($url)
    {
        $this->_initConnection($url);

        fwrite($this->_connection, $this->buildRequest());
        $this->getResponse();

        if ( 200 == $this->header['Status-Code']['code'] )
            $this->error    = false;
        else
            $this->error    = true;
    }

    /**
     * Response Resources Methods
     */
    public function getResponse()
    {
        $eol    = array("\r", "\n", '\r\n');
        $regex  = '/^\s?HTTP\/([0-9].[0-9x])\s+([0-9]{3})\s+([0-9a-zA-z\s]*)$/';

        while ( '' !== ($line = str_replace($eol, '', fgets($this->_connection))) ) {
            if ( strpos($line, ':') === false && preg_match($regex, $line, $match) )
            {
                $this->header['Status-Code'] = array(
                    'version'   => $match[1],
                    'code'      => $match[2],
                    'status'    => $match[3],
                );
            }
            else
            {
                list($key, $val) = explode(':', $line, 2);
                $this->header[$key] = ltrim($val);
            }
        }

        $code   = $this->header['Status-Code']['code'];

        if ( $code >= 200 && $code != 204 && $code != 304 )
        {
            $this->body = stream_get_contents($this->_connection);

            if ( @$this->header['Transfer-Encoding'] == 'chunked' )
            {
                $this->body = $this->_chunkdecode($this->body);
            }

            if ( @$this->header['Content-Encoding'] == 'gzip' )
            {
                $this->body = $this->_gzdecode($this->body);
            }
        }
    }

    public function getStatusCode()
    {
        return $this->header['Status-Code'];
    }

    private function _gzdecode($data)
    {
        $data   = "data:application/x-gzip;base64,".base64_encode($data); 
        $fp     = gzopen($data, "r");
        return gzread($fp, 524288); 
    }

    private function _chunkdecode ($str, $eol = "\r\n")
    {
        $tmp    = $str;
        $add    = strlen($eol);
        $str    = '';

        do {
            $tmp    = ltrim($tmp);
            $pos    = strpos($tmp, $eol);
            $len    = hexdec(substr($tmp, 0, $pos));

            $str   .= substr($tmp, ($pos + $add), $len);

            $tmp    = substr($tmp, ($len + $pos + $add));
            $check  = trim($tmp);
        } while ( !empty($check) );

        return $str;
    }

    /**
     * Scoket Connection Methods
     */
    public function connect($url)
    {
        $this->_parseUrl($url);

        $scheme = ($this->scheme == 'https') ? 'ssl://' : '';
        $port   = ($this->scheme == 'https') ? $this->sslport : $this->port;
        $this->_connection = fsockopen($scheme.$this->host, $port, $errno, $errstr, $this->timeout);
    }

    public function disconnect()
    {
        if ( is_resource($this->_connection) )
            @fclose($this->_connection);
        else
            $this->_connection = null;
    }

    /**
     * Header and Context Manipulating Methods
     */
    public function setHeader($key, $val)
    {
        $this->_header[$key] = $val;
    }

    public function buildRequest()
    {
        $eol    = $this->eol;
        $header = array_merge(array_diff($this->_header, array('')));

        // Host
        $header['Host']  = "{$this->host}";

        // Auth
        if ( !empty($this->user) && !empty($this->pass) )
        {
            $header['Authorization'] = 'Basic '.base64_encode("{$this->user}:{$this->pass}");
        }

        // Build
        switch ( $this->method )
        {
            case 'POST' :
                $request    = "{$this->method} {$this->path} HTTP/{$this->version}{$eol}";
                $header['Content-Type']   = 'application/x-www-form-urlencoded';
                $header['Content-Length'] = strlen($this->query);
                break;
            case 'GET'  :
                $request    = "{$this->method} {$this->path}?{$this->query} HTTP/{$this->version}{$eol}";
                break;
        }
            /* header */
            foreach ( $header as $key => $val ) {
                $request .= "{$key}: {$val}{$eol}";
            }
            /* body? */
            if ( $this->method == 'POST' && !empty($this->query) )
            {
                $data     = $this->query;
                $request .= "{$eol}{$data}";
            }
            /* important CRLF */
            $request .= $eol;

        return $request;
    }
}