分享个几年前实现的grin代理充值的奇思妙想

当时我们团队要做一个grin币充值服务,因该服务无法修改介入,那么用户对此服务发起的充值,我们平台无法感知到,而且该服务没有钱包地址概念,也就实现不了充值功能,该问题一直困扰了团队挺久。
后经过我的不懈努力,以及大量的对该服务的通讯抓包分析,我产生了一个奇妙的主意,做一个代理充值服务,代码使用laravel结合workerman实现,部分贴出来和大家分享一下,欢迎大家尽情批判该代码,一晃3年过去了,想起3年前初出茅庐的自己能有这样的想法还是挺难得的。

服务启动命令:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Workerman\Worker;
use App\Workerman\Events;

/**
 * Grin钱包监听
 *
 * @author wen <756307849@qq.com>
 * @date 2019-03-20
 */
class Grin extends Command
{

    protected $signature = 'grin {action} {--d}';

    protected $description = 'Start a grin wallet listen server';

    public function handle()
    {
        global $argv;
        $action = $this->argument('action');

        $argv[0] = 'wk';
        $argv[1] = $action;
        $argv[2] = $this->option('d') ? '-d' : '';

        $this->start();
    }

    private function start()
    {
        $this->startWorker();
        Worker::runAll();
    }

    private function startWorker()
    {
        $socket_name = 'Grin://' . env('GRIN_PROXY_HOST', '127.0.0.1') . ':' . env('GRIN_PROXY_PORT', '9501');
        // 是否启用https
        if ('on' == env('HTTPS', 'off')) {
            // wss 证书最好是申请的证书
            $context = array(
                // 更多ssl选项请参考手册 http://php.net/manual/zh/context.ssl.php
                'ssl' => array(
                    // 请使用绝对路径
                    'local_cert'        => env('SSL_LOCAL_CERT'), // 也可以是crt文件
                    'local_pk'          => env('SSL_LOCAL_PK'),
                    'verify_peer'       => false
                )
            );
            $worker = new Worker($socket_name, $context);
            // 设置transport开启ssl,websocket+ssl即wss
            $worker->transport = 'ssl';
        } else {
            $worker = new Worker($socket_name);
        }

        $worker->name  = 'GrinWalletListen';
        $worker->count = 1;

        $worker->onWorkerStart = function ($worker) {
            Events::onWorkerStart($worker);
        };

        $worker->onConnect = function ($connection) {
            Events::onConnect($connection);
        };

        $worker->onMessage = function ($connection, $buffer) {
            Events::onMessage($connection, $buffer);
        };

        $worker->onClose = function ($connection) {
            Events::onClose($connection);
        };
    }

}

关键业务代码:

<?php

namespace App\Workerman;

use Workerman\Connection\AsyncTcpConnection;
use App\JsonRpc\Blockchain as BC;

/**
 * Grin钱包监听回调
 *
 * @author wen <756307849@qq.com>
 * @date 2019-04-29
 */
class Events
{
    /**
     * 充值状态
     */
    const STATUS = [
        'unconfirmed' => 1, // 未确认的充值
    ];

    public static function onWorkerStart($worker)
    {
    }

    public static function onConnect($connection)
    {
        echo "--onConnect\n";
    }

    /**
     * @param $connection
     * @param $buffer
     * @throws \Exception
     */
    public static function onMessage($connection, $buffer)
    {
        $data = decode($buffer);

        // 验证充值地址是否存在
        try {
            $result = self::verifyAddress($data['ADDRESS']);
        } catch (\Exception $e) {
            $connection->send('Account Address Error');
            custom_log('info', 'grin/info.log', '--充值地址验证失败:' . $e->getMessage());
            echo '--充值地址验证失败:' . $e->getMessage() . "\n";
            return;
        }
        echo "--充值地址验证通过\n";

        $body = json_decode($data['BODY'], true);

        // 写入充值记录
        $recharge_data = [
            'symbol' => 'GRIN',
            'amount' => bcdiv($body['amount'], 1000000000, 8),
            'address' => $data['ADDRESS'],
            'tx_hash' => $body['id'],
            'status' => self::STATUS['unconfirmed'],
            'confirms' => 0,
        ];

        try {
            $result = self::save($recharge_data);
        } catch (\Exception $e) {
            $connection->send('Create failure');
            custom_log('info', 'grin/info.log', '--写入充值记录失败:' . $e->getMessage());
            echo '--写入充值记录失败:' . $e->getMessage() . "\n";
            return;
        }
        echo "--写入充值记录成功\n";

        $remote_address = 'GrinServer://' . env('GRIN_HOST') . ':' . env('GRIN_PORT');
        $remote_connection = new AsyncTcpConnection($remote_address);

        // 将原始报文发送给服务端
        $remote_connection->send($data['BUFFER']);

        // Pipe.
        $remote_connection->pipe($connection);
        $connection->pipe($remote_connection);

        $remote_connection->connect();
    }

    public static function onClose($connection)
    {
        echo "--onClose\n";
    }

    /**
     * 保存交易数据
     *
     * @param $data
     * @return mixed
     * @throws \Exception
     */
    protected static function save($data)
    {
        try {
            $result = BC::post('/api/recharge/add', [$data]);
            if (200 != $result->code)
                throw new \Exception('blockchain error: ' . $result->msg, $result->code);

            return $result->data;
        } catch (\Exception $e) {
            throw new \Exception('blockchain error: ' . $e->getMessage());
        }
    }

    /**
     * 验证地址是否有效
     *
     * @param string $address
     * @return mixed
     * @throws \Exception
     */
    protected static function verifyAddress(string $address)
    {
        try {
            $result = BC::post('/api/address/check', [['symbol' => 'grin', 'address' => $address]]);
            if (200 != $result->code)
                throw new \Exception('blockchain error: ' . $result->msg, $result->code);

            return $result;
        } catch (\Exception $e) {
            throw new \Exception('blockchain error: ' . $e->getMessage());
        }
    }


}

助手函数:

<?php
/**
 * 自定义助手类
 *
 * 函数定义规范: 下划线法
 * 注释要写 author
 */

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

if (!function_exists('decode')) {
    /**
     * grin client to server data decode
     *
     * @author wen <756307849@qq.com>
     * @access public
     * @return array
     */
    function decode($buffer)
    {
        $server = [
            'REQUEST_METHOD'  => '',
            'REQUEST_URI'     => '',
            'SERVER_PROTOCOL' => '',
            'REQUEST_TIME'    => time(),
            'BUFFER'          => '',
            'ADDRESS'         => '',
            'BODY'            => '',
        ];

        // Parse headers.
        list($http_header, $http_body) = explode("\r\n\r\n", $buffer, 2);
        $header_data = explode("\r\n", $http_header);
        $server['BODY'] = $http_body;

        list($server['REQUEST_METHOD'], $server['REQUEST_URI'], $server['SERVER_PROTOCOL']) = explode(' ',
            $header_data[0]);

        $address = str_replace('/v1/wallet/foreign/receive_tx', '', $server['REQUEST_URI']);
        $server['ADDRESS'] = ltrim($address, '/');
        $server['BUFFER'] = str_replace($server['REQUEST_URI'], '/v1/wallet/foreign/receive_tx', $buffer);

        return $server;
    }
}

if (!function_exists('grin_date_format')) {
    /**
     * grin client to server data format
     *
     * @author wen <756307849@qq.com>
     * @access public
     * @return array
     */
    function grin_date_format($data)
    {
        // 用户充值地址
        list($method, $url) = explode(' ', $data);
        $addr = str_replace('/v1/wallet/foreign/receive_tx', '', $url);
        $addr = ltrim($addr, '/');

        // 合约内容
        $data = explode("\n", $data);
        $tx = end($data);
        $tx = json_decode($tx, true);

        $res['method'] = $method;
        $res['addr'] = $addr;
        $res['tx'] = $tx;
        $res['url'] = $url;
        return $res;
    }
}

if (!function_exists('graphql_post')) {
    /**
     * @param $query
     * @return mixed
     */
    function graphql_post($query)
    {
        $result = ['code' => 400, 'msg' => ''];

        $client = new Client(['base_uri' => env('BC_HOST')]);

        $options = [
            'form_params' => ['query' => $query],
            'stream' => true,
            'read_timeout' => 3
        ];
        try {
            $response = $client->request('POST', '', $options)->getBody();
        } catch (GuzzleException $e) {
            $result['msg'] = $e->getMessage();
            return $result;
        }
        $contents = json_decode($response->getContents(), true);
        if (!isset($contents['code']) || 200 != $contents['code']) {
            $result['code'] = $contents['code'];
            $result['msg'] = $contents['msg'];
            return $result;
        }

        $result['code'] = 200;
        $result['data'] = $contents['data'];
        return $result;
    }
}

if (!function_exists('custom_log')) {
    /**
     * 自定义日志
     *
     * @param string $level
     * @param string $path
     * @param $msg
     */
    function custom_log(string $level, string $path, $msg)
    {
        $log = new Logger('');
        try {
            $log->pushHandler(new StreamHandler(storage_path('logs/' . $path)));
        } catch (Exception $e) {

        }

        $msg = is_array($msg) ? json_encode($msg) : $msg;
        $log->log($level, $msg);
    }
}

两个自定义基于TCP的GRIN通讯协议:

<?php

namespace Protocols;

use Workerman\Connection\TcpConnection;

/**
 * 自定义Grin通讯协议
 *
 * Class Grin
 * @package Protocols
 * @author wen <756307849@qq.com>
 * @date 2019-03-30
 */
class Grin
{
    public static $methods = array('POST');

    /**
     * 检查包的完整性
     * 如果能够得到包长,则返回包的在buffer中的长度,否则返回0继续等待数据
     * 如果协议有问题,则可以返回false,当前客户端连接会因此断开
     * @param string $recv_buffer
     * @return int
     */
    public static function input($recv_buffer, TcpConnection $connection)
    {
        echo "原始报文:--------------------\n$recv_buffer\n--------------------------------\n";
        if (!strpos($recv_buffer, "\r\n\r\n")) {
            // Judge whether the package length exceeds the limit.
            if (strlen($recv_buffer) >= $connection->maxPackageSize) {
                $connection->close();
                return 0;
            }
            return 0;
        }

        list($header,) = explode("\r\n\r\n", $recv_buffer, 2);
        $method = substr($header, 0, strpos($header, ' '));

        if(in_array($method, static::$methods)) {
            return static::getRequestSize($header);
        }else{
            $connection->send("HTTP/1.1 400 Bad Request\r\n\r\n", true);
            return 0;
        }
    }

    /**
     * Get whole size of the request
     * includes the request headers and request body.
     * @param string $header The request headers
     * @return integer
     */
    protected static function getRequestSize($header)
    {
        $match = array();
        if (preg_match("/\r\nContent-Length: ?(\d+)/i", $header, $match)) {
            $content_length = isset($match[1]) ? $match[1] : 0;
            return $content_length + strlen($header) + 4;
        }
        return 0;
    }

    /**
     * 打包,当向客户端发送数据的时候会自动调用
     * @param string $buffer
     * @return string
     */
    public static function encode($buffer)
    {
        return $buffer;
    }

    /**
     * 解包,当接收到的数据字节数等于input返回的值(大于0的值)自动调用
     * 并传递给onMessage回调函数的$data参数
     * @param string $buffer
     * @return string
     */
    public static function decode($buffer)
    {
        return $buffer;
    }
}
<?php

namespace Protocols;

use Workerman\Connection\TcpConnection;

/**
 * 与服务端的通讯协议
 *
 * Class GrinServer
 * @package Protocols
 * @author wen <756307849@qq.com>
 * @date 2019-05-24
 */
class GrinServer
{
    /**
     * 检查包的完整性
     * 如果能够得到包长,则返回包的在buffer中的长度,否则返回0继续等待数据
     * 如果协议有问题,则可以返回false,当前客户端连接会因此断开
     * @param string $recv_buffer
     * @return int
     */
    public static function input($recv_buffer, TcpConnection $connection)
    {
        echo "服务端原始报文:--------------------\n$recv_buffer\n--------------------------------\n";
        if (!strpos($recv_buffer, "\r\n\r\n")) {
            // Judge whether the package length exceeds the limit.
            if (strlen($recv_buffer) >= $connection->maxPackageSize) {
                $connection->close();
                return 0;
            }
            return 0;
        }

        list($header,) = explode("\r\n\r\n", $recv_buffer, 2);

        return static::getRequestSize($header);
    }

    /**
     * Get whole size of the request
     * includes the request headers and request body.
     * @param string $header The request headers
     * @return integer
     */
    protected static function getRequestSize($header)
    {
        $match = array();
        if (preg_match("/\r\nContent-Length: ?(\d+)/i", $header, $match)) {
            $content_length = isset($match[1]) ? $match[1] : 0;
            return $content_length + strlen($header) + 4;
        }
        return 0;
    }

    /**
     * 打包,当向客户端发送数据的时候会自动调用
     * @param string $buffer
     * @return string
     */
    public static function encode($buffer)
    {
        return $buffer;
    }

    /**
     * 解包,当接收到的数据字节数等于input返回的值(大于0的值)自动调用
     * 并传递给onMessage回调函数的$data参数
     * @param string $recv_buffer
     * @return string
     */
    public static function decode($recv_buffer)
    {
        // Grin版本升级有bug,暂时处理自行处理
        list($http_header, $http_body) = explode("\r\n\r\n", $recv_buffer, 2);

        // 处理服务端报文可能出现反引号的问题
        $http_body = trim($http_body, '"');
        $http_body = str_replace("\\", '', $http_body);

        preg_match("/\r\n(Content-Length: ?(\d+))/i", $http_header, $match);

        $http_header = str_replace($match[1], 'content-length: ' . strlen($http_body), $http_header);

        $buffer = $http_header . "\r\n\r\n" . $http_body;
        echo "服务端处理后报文:--------------------\n$buffer\n--------------------------------\n";
        return $buffer;
    }
}
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!