* @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Push; use Workerman\Connection\AsyncTcpConnection; use Workerman\Connection\TcpConnection; use Workerman\Protocols\Http\Request; use Workerman\Protocols\Http\Response; use Workerman\Timer; use Workerman\Worker; class Server { /** * 应用信息 * * @var array */ public $appInfo = []; /** * 心跳时间 * * @var int */ public $keepAliveTimeout = 60; /** * api监听的ip端口 * * @var string */ public $apiListen = 'http://0.0.0.0:1080'; /** * webhook 延迟设置 * * @var int */ public $webHookDelay = 3; /** * @var array */ protected $_globalDataSnapshot = []; /** * 事件对应的客户端链接 * * @var array */ protected $_eventClients = []; /** * 所有的客户端链接 * * @var array */ protected $_allClients = []; /** * array( * 'app_key1' => array( * 'channel1' => array( * 'users' => array( * 'uid1' => array('user_info'=>[], 'ref_count' => x), * 'uid2' => array('user_info'=>[], 'ref_count' => x), * ), * 'type' => 'presence', * 'subscription_count' => x * ), * 'channel2' => array( * 'users' => array( * 'uid3' => array('user_info'=>[], 'ref_count' => x) * ), * 'type' => 'presence', * 'subscription_count' => x * ), * ), * 'app_key2' => array( * 'channel1' => array( * 'type' => 'private', * 'subscription_count' => x * ), * 'channel2' => array( * 'type' => 'public', * 'subscription_count' => x * ), * ) * ) * @var array */ protected $_globalData = []; /** * 当前进程全局唯一订阅id * * @var string */ protected $_globalID = 1; /** * 构造函数 * * @param string $socket_name * @param array $context */ public function __construct($api_listen, $app_info) { $this->apiListen = $api_listen; $this->appInfo = $app_info; } /** * @param $worker * @return void * @throws \Exception */ public function onWorkerStart($worker) { $api_worker = new Worker($this->apiListen); $api_worker->onMessage = array($this, 'onApiClientMessage'); $api_worker->listen(); Timer::add($this->keepAliveTimeout/2, array($this, 'checkHeartbeat')); Timer::add($this->webHookDelay, array($this, 'webHookCheck')); } /** * 客户端连接后 * * @param $connection */ public function onConnect($connection) { // 客户端有多少次没在规定时间发送心跳 $connection->clientNotSendPingCount = 0; // 设置websocket握手事件回调 $connection->onWebSocketConnect = array($this, 'onWebSocketConnect'); } /** * 当websocket握手时 * @param $connection * @return mixed */ public function onWebSocketConnect(TcpConnection $connection, $header) { // /app/1234567890abcdefghig?protocol=7&client=js&version=3.2.4&flash=false if (!preg_match('/\/app\/([^\/^\?^ ]+)/', (string)$header, $match)) { echo "app_key not found\n$header\n"; $connection->pauseRecv(); return; } $app_key = $match[1]; if (!isset($this->appInfo[$app_key])) { echo "Invalid app_key $app_key\n"; $connection->pauseRecv(); return; } $socket_id = $this->createsocketID($connection); $connection->appKey = $app_key; $connection->socketID = $socket_id; $connection->channels = array(''=>''); $connection->channelUidMap = []; $connection->clientNotSendPingCount = 0; $this->_eventClients[$app_key][''][$socket_id] = $connection; $this->_allClients[$socket_id] = $connection; /* * 向客户端发送链接成功的消息 * {"event":"pusher:connection_established","data":"{\"socket_id\":\"208836.27464492\",\"activity_timeout\":120}"} */ $data = array( 'event' => 'pusher:connection_established', 'data' => json_encode(array( 'socket_id' => $socket_id, 'activity_timeout' => 55 )) ); $connection->send(json_encode($data)); } /** * 客户端关闭链接时 * * @param $connection */ public function onClose($connection) { if (!isset($connection->socketID)) { return; } $socket_id = $connection->socketID; $app_key = $connection->appKey; unset($this->_allClients[$socket_id]); unset($this->_eventClients[$app_key][''][$socket_id]); if (isset($connection->channels)) { $app_key = $connection->appKey; foreach ($connection->channels as $channel => $uid) { if ('' === $channel) { continue; } if ($uid === '') { $this->unsubscribePublicChannel($connection, $channel); } else { $this->unsubscribePresenceChannel($connection, $channel, $uid); } unset($this->_eventClients[$app_key][$channel][$socket_id]); } } } /** * 客户端发来消息时 * * @param $connection * @param $data * * @return void */ public function onMessage($connection, $data) { $connection->clientNotSendPingCount = 0; $data = json_decode($data, true); if (!$data) { return; } if (!isset($data['event'])) { $connection->send($this->error(null, 'Empty event')); return; } $event = $data['event']; switch ($event) { case 'pusher:ping': $connection->send('{"event":"pusher:pong","data":"{}"}'); return; // {"event":"pusher:subscribe","data":{"channel":"my-channel"}} case 'pusher:subscribe': $channel = $data['data']['channel']; // private- 和 presence- 开头的channel需要验证 $channel_type = $this->getChannelType($channel); if ($channel_type === 'presence') { // {"event":"pusher:subscribe","data":{"auth":"b054014693241bcd9c26:10e3b628cb78e8bc4d1f44d47c9294551b446ae6ec10ef113d3d7e84e99763e6","channel_data":"{\"user_id\":100,\"user_info\":{\"name\":\"123\"}}","channel":"presence-channel"}} $client_auth = $data['data']['auth']; if (!isset($data['data']['channel_data'])) { $connection->send($this->error(null, 'Empty channel_data')); return; } $auth = $connection->appKey.':'.hash_hmac('sha256', $connection->socketID.':'.$channel.':'.$data['data']['channel_data'], $this->appInfo[$connection->appKey]['app_secret'], false); // {"event":"pusher:error","data":{"code":null,"message":"Received invalid JSON"}} if ($client_auth !== $auth) { return $connection->send($this->error(null, 'Received invalid Auth '.$auth)); } $user_data = json_decode($data['data']['channel_data'], true); if (!$user_data || !isset($user_data['user_id']) || !isset($user_data['user_info'])) { $connection->send($this->error(null, 'Bad channel_data')); return; } $this->subscribePresence($connection, $channel, $user_data['user_id'], $user_data['user_info']); return; } elseif($channel_type === 'private') { // {"event":"pusher:subscribe","data":{"auth":"b054014693241bcd9c26:10e3b628cb78e8bc4d1f44d47c9294551b446ae6ec10ef113d3d7e84e99763e6","channel_data":"{\"user_id\":100,\"user_info\":{\"name\":\"123\"}}","channel":"presence-channel"}} $client_auth = $data['data']['auth']; $auth = $connection->appKey.':'.hash_hmac('sha256', $connection->socketID.':'.$channel, $this->appInfo[$connection->appKey]['app_secret'], false); // {"event":"pusher:error","data":{"code":null,"message":"Received invalid Auth"}} if ($client_auth !== $auth) { return $connection->send($this->error(null, 'Received invalid Auth '.$auth)); } $this->subscribePrivateChannel($connection, $channel); } else { $this->subscribePublicChannel($connection, $channel); } // {"event":"pusher_internal:subscription_succeeded","data":"{}","channel":"my-channel"} $connection->send(json_encode( array( 'event' => 'pusher_internal:subscription_succeeded', 'data' => '{}', 'channel' => $channel ), JSON_UNESCAPED_UNICODE )); return; // {"event":"pusher:unsubscribe","data":{"channel":"my-channel"}} case 'pusher:unsubscribe': $app_key = $connection->appKey; $channel = $data['data']['channel']; $channel_type = $this->getChannelType($channel); switch ($channel_type) { case 'public': $this->unsubscribePublicChannel($connection, $channel); break; case 'private': $this->unsubscribePrivateChannel($connection, $channel); break; case 'presence': $uid = $connection->channels[$channel]; $this->unsubscribePresenceChannel($connection, $channel, $uid); break; } return; // {"event":"client-event","data":{"your":"hi"},"channel":"presence-channel"} default: if (strpos($event, 'pusher:') === 0) { return $connection->send($this->error(null, 'Unknown event')); } if (!isset($data['channel'])) { $connection->send($this->error(null, 'Empty channel')); return; } $channel = $data['channel']; // 客户端触发事件必须是private 或者 presence的channel $channel_type = $this->getChannelType($channel); if ($channel_type !== 'private' && $channel_type !== 'presence') { // {"event":"pusher:error","data":{"code":null,"message":"Client event rejected - only supported on private and presence channels"}} return $connection->send($this->error(null, 'Client event rejected - only supported on private and presence channels')); } // 当前链接没有订阅这个channel if (!isset($connection->channels[$channel])) { return $connection->send($this->error(null, 'Client event rejected - you didn\'t subscribe this channel')); } // 事件必须以client-为前缀 if (strpos($event, 'client-') !== 0) { return $connection->send($this->error(null, 'Client event rejected - client events must be prefixed by \'client-\'')); } // @todo 检查是否设置了可前端发布事件 // {"event":"pusher:error","data":{"code":null,"message":"To send client events, you must enable this feature in the Settings page of your dashboard."}} // 全局发布事件 $this->publishToClients($connection->appKey, $channel, $event, json_encode($data['data'], JSON_UNESCAPED_UNICODE), $connection->socketID); } } /** * 获得channel类型 * * @param $channel * @return string */ protected function getChannelType($channel) { if (strpos($channel, 'private-') === 0) { return 'private'; } elseif (strpos($channel, 'presence-') === 0) { return 'presence'; } return 'public'; } /** * 组装失败信息 * * @param $code * @param $message * @return string */ protected function error($code, $message) { return json_encode(array('event'=>'pusher:error', 'data' => array('code' => $code, 'message' => $message)), JSON_UNESCAPED_UNICODE); } /** * 客户端订阅channel * * @param $connection * @param $channel * * @return void */ public function subscribePublicChannel($connection, $channel) { $app_key = $connection->appKey; $connection->channels[$channel] = ''; $this->_eventClients[$app_key][$channel][$connection->socketID] = $connection; if (!isset($this->_globalData[$app_key][$channel])) { $this->_globalData[$app_key][$channel] = array( 'type' => 'presence', 'subscription_count' => 0 ); } $this->_globalData[$app_key][$channel]['subscription_count'] += 1; } /** * 客户端订阅channel * * @param $connection * @param $channel * * @return void */ public function subscribePrivateChannel($connection, $channel) { $this->subscribePublicChannel($connection, $channel); } /** * 客户端订阅channel * * @param $connection * @param $channel * * @return void */ public function subscribePresence($connection, $channel, $uid, $user_info) { $app_key = $connection->appKey; $connection->channels[$channel] = $uid; $this->_eventClients[$app_key][$channel][$connection->socketID] = $connection; if (!isset($this->_globalData[$app_key][$channel])) { $this->_globalData[$app_key][$channel] = array( 'type' => 'presence', 'users' => [], 'subscription_count' => 0 ); } $this->_globalData[$app_key][$channel]['subscription_count'] += 1; $member_added = false; if (!isset($this->_globalData[$app_key][$channel]['users'][$uid]['user_info'])) { $this->_globalData[$app_key][$channel]['users'][$uid] = array('user_info'=>$user_info, 'ref_count' => 0); $member_added = true; } $this->_globalData[$app_key][$channel]['users'][$uid]['ref_count'] += 1; $presence_data = $this->getPresenceChannelDataForSubscribe($app_key, $channel); if ($member_added) { // {"event":"pusher_internal:member_added","data":"{\"user_id\":1488465780,\"user_info\":{\"name\":\"123\",\"sex\":\"1\"}}","channel":"presence-channel"} $this->publishToClients($app_key, $channel, 'pusher_internal:member_added', json_encode(array( 'user_id' => $uid, 'user_info' => $user_info ), JSON_UNESCAPED_UNICODE), $connection->socketID); } // {"event":"pusher_internal:subscription_succeeded","data":"{\"presence\":{\"count\":2,\"ids\":[\"1488465780\",\"14884657802\"],\"hash\":{\"1488465780\":{\"name\":\"123\",\"sex\":\"1\"},\"14884657802\":{\"name\":\"123\",\"sex\":\"1\"}}}}","channel":"presence-channel"} $connection->send(json_encode(array( 'event' => 'pusher_internal:subscription_succeeded', 'data' => json_encode($presence_data), 'channel' => $channel ),JSON_UNESCAPED_UNICODE )); } public function getPresenceChannelDataForSubscribe($app_key, $channel) { $hash = []; $count = 100; if (isset($this->_globalData[$app_key][$channel])) { foreach ($this->_globalData[$app_key][$channel]['users'] as $uid => $item) { $hash[$uid] = $item['user_info']; if ($count-- <= 0) { break; } } //$hash = array_slice($this->_globalData[$app_key][$channel]['users'], 0, 100, true); } return array( 'presence' => array( 'count' => count($this->_globalData[$app_key][$channel]['users']), 'ids' => array_keys($hash), 'hash' => $hash ) ); } /** * 客户端取消订阅channel * * @param $connection * @param $channel * * @return void */ public function unsubscribePublicChannel($connection, $channel) { $app_key = $connection->appKey; $this->_globalData[$app_key][$channel]['subscription_count']--; if ($this->_globalData[$app_key][$channel]['subscription_count'] <= 0) { unset($this->_globalData[$app_key][$channel]); } unset($connection->channels[$channel], $this->_eventClients[$connection->appKey][$channel][$connection->socketID]); } /** * 客户端取消订阅channel * * @param $connection * @param $channel * * @return void */ public function unsubscribePrivateChannel($connection, $channel) { $this->unsubscribePublicChannel($connection, $channel); } /** * 客户端取消订阅channel * * @param $connection * @param $channel * * @return void */ public function unsubscribePresenceChannel($connection, $channel, $uid) { $app_key = $connection->appKey; $member_removed = false; $this->_globalData[$app_key][$channel]['subscription_count']--; if ($this->_globalData[$app_key][$channel]['subscription_count'] <= 0) { unset($this->_globalData[$app_key][$channel]); $member_removed = true; } else { if (!isset($this->_globalData[$app_key][$channel]['users'][$uid]['ref_count'])) { error_log("\$this->_globalData[$app_key][$channel]['users'][$uid]['ref_count'] not exist\n"); return; } $this->_globalData[$app_key][$channel]['users'][$uid]['ref_count']--; $ref_count = $this->_globalData[$app_key][$channel]['users'][$uid]['ref_count']; if ($ref_count <= 0) { unset($this->_globalData[$app_key][$channel]['users'][$uid]); $member_removed = true; } } if ($member_removed) { // {"event":"pusher_internal:member_removed","data":"{\"user_id\":\"14884657801\"}","channel":"presence-channel"} $this->publishToClients($app_key, $channel, 'pusher_internal:member_removed', json_encode(array('user_id'=>$uid), JSON_UNESCAPED_UNICODE)); } unset($connection->channels[$channel], $this->_eventClients[$connection->appKey][$channel][$connection->socketID]); } /** * 发布事件 * * @param $data */ public function publishToClients($app_key, $channel, $event, $data, $socket_id = null) { if (!isset($this->_eventClients[$app_key][$channel])) { return; } $data = json_encode(array( 'event' => $event, 'data' => $data, 'channel' => $channel ), JSON_UNESCAPED_UNICODE); foreach ($this->_eventClients[$app_key][$channel] as $connection) { if ($connection->socketID === $socket_id) { continue; } $connection->clientNotSendPingCount = 0; // {"event":"my-event","data":"{\"message\":\"hello world\"}","channel":"my-channel"} $connection->send($data); } } /** * 检查心跳,将心跳超时的客户端关闭 * * @return void */ public function checkHeartbeat() { foreach ($this->_allClients as $connection) { if ($connection->clientNotSendPingCount > 1) { $connection->destroy(); } $connection->clientNotSendPingCount ++; } } /** * 创建一个全局的客户端id * * @param $connection * @return string */ protected function createsocketID($connection) { $socket_id = "{$this->_globalID}.{$connection->id}"; return $socket_id; } /** * 创建channel key,用于监听分发给该channel的事件 * * @param $app_key * @param $channel * @return string */ protected function createChannelKey($app_key, $channel) { return "$app_key:$channel"; } /** * POST /apps/1024/events?auth_key=b054014693241bcd9c26&auth_signature=ed7f5b604e6bbd21a888a861ed536a430a9d5e4df210937a241a811bd17fcf97&auth_timestamp=1487428415&auth_version=1.0&body_md5=15d251b35306a6da7efa515a0e971f80 HTTP/1.1 * {"name":"my-event","data":"{\"message\":\"hello world\"}","channels":["my-channel"]} * {"name":"my-event","data":"{\"message\":\"haha\"}","channels":["my-channel"],"socket_id":"123.456"} * * GET /apps/1024/channels/my-channel?auth_key=b054014693241bcd9c26&auth_signature=5226650be00a064b417d50d49229e42bbb918e969c42e63aaa63b9d1c6cf9803&auth_timestamp=1489898340&auth_version=1.0 * * GET /apps/1024/channels/presence-channel?auth_key=b054014693241bcd9c26&auth_signature=d46281bf69ccadfe9da270176c85daa88d4b9da55b1f3c2570d48fa1236f0b2c&auth_timestamp=1489903433&auth_version=1.0&info=subscription_count,user_count * * GET /apps/1024/channels/presence-channel/users?auth_key=b054014693241bcd9c26&auth_signature=2eee0ca6292e17b00484bdcb0bba686a47e8a7365a1b190248946182fc926309&auth_timestamp=1489904560&auth_version=1.0 */ public function onApiClientMessage($connection, Request $request) { if (!($app_key = $request->get('auth_key'))) { return $connection->send(new Response(400, [], 'Bad Request')); } if (!isset($this->appInfo[$app_key])) { return $connection->send(new Response(401, [], 'Invalid app_key')); } $path = $request->path(); $explode = explode('/', trim($path, '/')); if (count($explode) < 3) { return $connection->send(new Response(400, [], 'Bad Request')); } $auth_signature = $request->get('auth_signature'); $params = $request->get(); unset($params['auth_signature']); ksort($params); $string_to_sign = $request->method()."\n" . $path . "\n" . self::array_implode('=', '&', $params); $real_auth_signature = hash_hmac('sha256', $string_to_sign, $this->appInfo[$app_key]['app_secret'], false); if ($auth_signature !== $real_auth_signature) { return $connection->send(new Response(401, [], 'Invalid signature')); } $type = $explode[2]; switch ($type) { case 'batch_events': $packages = json_decode($request->rawBody(), true); if (!$packages || !isset($packages['batch'])) { return $connection->send(new Response(400, [], 'Bad request')); } $packages = $packages['batch']; foreach ($packages as $package) { $channel = $package['channel']; $event = $package['name']; $data = $package['data']; $socket_id = isset($package['socket_id']) ? isset($package['socket_id']) : null; $this->publishToClients($app_key, $channel, $event, $data, $socket_id); } return $connection->send('{}'); break; case 'events': $package = json_decode($request->rawBody(), true); if (!$package) { return $connection->send(new Response(401, [], 'Invalid signature')); } $channels = $package['channels']; $event = $package['name']; $data = $package['data']; foreach ($channels as $channel) { $socket_id = isset($package['socket_id']) ? isset($package['socket_id']) : null; $this->publishToClients($app_key, $channel, $event, $data, $socket_id); } return $connection->send('{}'); case 'channels': // info $request_info = explode(',', $request->get('info', '')); if (!isset($explode[3])) { $channels = []; $prefix = $request->get('filter_by_prefix'); $return_subscription_count = in_array('subscription_count', $request_info); foreach ($this->_globalData[$app_key] ?? [] as $channel => $item) { if ($prefix !== null) { if (strpos($channel, $prefix) !== 0) { continue; } } $channels[$channel] = []; if ($return_subscription_count) { $channels[$channel]['subscription_count'] = $item['subscription_count']; } } return $connection->send(json_encode(['channels' => $channels], JSON_UNESCAPED_UNICODE)); } $channel = $explode[3]; // users if (isset($explode[4])) { if ($explode[4] !== 'users') { return $connection->send(new Response(400, [], 'Bad Request')); } $id_array = isset($this->_globalData[$app_key][$channel]['users']) ? array_keys($this->_globalData[$app_key][$channel]['users']) : array(); $user_id_array = array(); foreach ($id_array as $id) { $user_id_array[] = array('id' => $id); } $connection->send(json_encode($user_id_array, JSON_UNESCAPED_UNICODE)); } $occupied = isset($this->_globalData[$app_key][$channel]); $user_count = isset($this->_globalData[$app_key][$channel]['users']) ? count($this->_globalData[$app_key][$channel]['users']) : 0; $subscription_count = $occupied ? $this->_globalData[$app_key][$channel]['subscription_count'] : 0; $channel_info = array( 'occupied' => $occupied ); foreach ($request_info as $name) { switch ($name) { case 'user_count': $channel_info['user_count'] = $user_count; break; case 'subscription_count': $channel_info['subscription_count'] = $subscription_count; break; } } $connection->send(json_encode($channel_info, JSON_UNESCAPED_UNICODE)); break; default: return $connection->send(new Response(400, [], 'Bad Request')); } } public function webHookCheck() { $channel_events = []; $user_events = []; $all_app_keys = array_unique(array_merge(array_keys($this->_globalData), array_keys($this->_globalDataSnapshot))); foreach($all_app_keys as $app_key) { if (empty($this->appInfo[$app_key])) { continue; } $snapshot_items = isset($this->_globalDataSnapshot[$app_key]) ? $this->_globalDataSnapshot[$app_key] : []; $items = isset($this->_globalData[$app_key]) ? $this->_globalData[$app_key] : []; $channels_added = array_diff_key($items, $snapshot_items); $channels_removed = array_diff_key($snapshot_items, $items); if ($channels_added) { $channel_events[$app_key]['channels_added'] = array_keys($channels_added); } if ($channels_removed) { $channel_events[$app_key]['channels_removed'] = array_keys($channels_removed); } $all_channels = []; foreach ($items as $channel => $foo) { if ($foo['type'] === 'presence') { $all_channels[$channel] = $channel; } } foreach ($snapshot_items as $channel => $foo) { if ($foo['type'] === 'presence' && !isset($all_channels[$channel])) { $all_channels[$channel] = $channel; } } foreach ($all_channels as $channel) { $user_array_snapshot = isset($snapshot_items[$channel]['users']) ? $snapshot_items[$channel]['users'] : []; $user_array = isset($items[$channel]['users']) ? $items[$channel]['users'] : []; $user_added = array_diff_key($user_array, $user_array_snapshot); $user_removed = array_diff_key($user_array_snapshot, $user_array); if ($user_added) { $user_events[$app_key][$channel]['user_added'] = array_keys($user_added); } if ($user_removed) { $user_events[$app_key][$channel]['user_removed'] = array_keys($user_removed); } } } $this->_globalDataSnapshot = $this->_globalData; $this->webHookSend(array('channel_events' => $channel_events, 'user_events' => $user_events)); } protected function webHookSend($data) { $channel_events = $data['channel_events']; $user_events = $data['user_events']; $time_ms = microtime(true); foreach ($user_events as $app_key => $items) { // 没设置user_event回调则忽略 if (empty($this->appInfo[$app_key]['user_hook'])) { continue; } // {"time_ms":1494300453609,"events":[{"channel":"presence-channel2","user_id":"59094971a","name":"member_added"}]} $http_events_body = array( 'time_ms' => $time_ms, 'events' => [] ); foreach ($items as $channel => $item) { if (isset($item['user_added'])) { foreach ($item['user_added'] as $user_id) { $http_events_body['events'][] = array( 'channel' => $channel, 'user_id' => $user_id, 'name' => 'user_added' ); } } } foreach ($items as $channel => $item) { if (isset($item['user_removed'])) { foreach ($item['user_removed'] as $user_id) { $http_events_body['events'][] = array( 'channel' => $channel, 'user_id' => $user_id, 'name' => 'user_removed' ); } } } if ($http_events_body['events']) { $this->sendHttpRequest($this->appInfo[$app_key]['user_hook'], $app_key, $this->appInfo[$app_key]['app_secret'], json_encode($http_events_body, JSON_UNESCAPED_UNICODE)); } } foreach ($channel_events as $app_key => $item) { // 没设置channel_event回调则忽略 if (empty($this->appInfo[$app_key]['channel_hook'])) { continue; } // {"time_ms":1494300446592,"events":[{"channel":"presence-channel2","name":"channel_added"}]} $http_events_body = array( 'time_ms' => $time_ms, 'events' => [] ); if (isset($item['channels_added'])) { foreach ($item['channels_added'] as $channel) { $http_events_body['events'][] = array( 'channel' => $channel, 'name' => 'channel_added' ); } } if (isset($item['channels_removed'])) { foreach ($item['channels_removed'] as $channel) { $http_events_body['events'][] = array( 'channel' => $channel, 'name' => 'channel_removed' ); } } if ($http_events_body['events']) { $this->sendHttpRequest($this->appInfo[$app_key]['channel_hook'], $app_key, $this->appInfo[$app_key]['app_secret'], json_encode($http_events_body, JSON_UNESCAPED_UNICODE)); } } } protected function sendHttpRequest($address, $app_key, $secret, $body, $redirect_count = 0) { $address_info = parse_url($address); if (!$address_info) { echo new \Exception('bad remote_address'); return false; } $scheme = isset($address_info['scheme']) && $address_info['scheme'] === 'https' ? 'ssl' : 'tcp'; if (!isset($address_info['port'])) { $address_info['port'] = $scheme == 'ssl' ? 443 : 80; } if (!isset($address_info['path'])) { $address_info['path'] = '/'; } if (!isset($address_info['query'])) { $address_info['query'] = ''; } else { $address_info['query'] = '?' . $address_info['query']; } $remote_address = "{$address_info['host']}:{$address_info['port']}"; $remote_host = $address_info['host']; $remote_URI = "{$address_info['path']}{$address_info['query']}"; $signature = hash_hmac('sha256', $body, $secret, false); $base_url = $scheme == 'ssl' ? "https://$remote_address/" : "http://$remote_address/"; $header = "POST $remote_URI HTTP/1.0\r\n"; $header .= "Host: $remote_host\r\n"; $header .= "Connection: close\r\n"; $header .= "X-Pusher-Key: $app_key\r\n"; $header .= "X-Pusher-Signature: $signature\r\n"; $header .= "Content-Type: application/json\r\n"; $header .= "Content-Length: " . strlen($body); $http_buffer = $header . "\r\n\r\n" . $body; $client = new AsyncTcpConnection('tcp://' . $remote_address); if ($scheme == 'ssl') { $client->transport = 'ssl'; } $client->onConnect = function ($client) use ($http_buffer) { $client->send($http_buffer); }; $client->onMessage = function ($client, $buffer) use ($address, $app_key, $secret, $body, $base_url, $redirect_count) { $client->close(); if (!preg_match("/HTTP\/1\.\d (\d*?) .*?\r\n/", $buffer, $match)) { echo "http code not found $buffer\n"; return; } $http_code = $match[1]; $base_code = intval($http_code / 100); if ($base_code == 3 && preg_match("/Location: (.*?)\r\n/", $buffer, $match)) { if (++$redirect_count > 3) { $msg = date('Y-m-d H:i:s') . "\nURL:$address\nAPP_KEY:$app_key ERR:too many redirect\n$buffer"; echo $msg; return; } $location = $match[1]; if (strpos($location, 'http://') === 0 || strpos($location, 'https://') === 0) { $this->sendHttpRequest($location, $app_key, $secret, $body, $redirect_count); } else { $this->sendHttpRequest($base_url . $location, $app_key, $secret, $body, $redirect_count); } } if ($base_code !== 2) { $msg = date('Y-m-d H:i:s') . "\nURL:$address\nAPP_KEY:$app_key\n$buffer"; echo $msg; } }; Timer::add(10, array($client, 'close'), null, false); $client->connect(); } /** * array_implode * * @param $glue * @param $separator * @param $array * @return string */ public static function array_implode($glue, $separator, $array) { if (!is_array($array)) { return $array; } $string = array(); foreach ($array as $key => $val) { if (is_array($val)) { $val = implode(',', $val); } $string[] = "{$key}{$glue}{$val}"; } return implode($separator, $string); } }