ET 框架的一个常用通信通道KService,用来游戏和服务器进行rpc通信
本质是一个或多个kcp包拼接起来,并再追加一个会话识别编号
抓包并自动提取protobuf rpc内容
ET 框架的一个常用通信通道KService,用来游戏和服务器进行rpc通信
本质是一个或多个kcp包拼接起来,并再追加一个会话识别编号
抓包并自动提取protobuf rpc内容
| if NOT [%1]==[] GOTO single | |
| start boot.bat 11002 | |
| start boot.bat 11003 | |
| start boot.bat 11004 | |
| exit | |
| :single | |
| php proxy.php %1 | cat |
| <?php | |
| chdir(__DIR__); | |
| require 'UnityBundle.php'; | |
| use Workerman\Connection\UdpConnection; | |
| use Workerman\Connection\AsyncUdpConnection; | |
| use Workerman\Worker; | |
| use Workerman\Timer; | |
| require_once __DIR__ . '/vendor/autoload.php'; | |
| $linkPool = []; | |
| $port = 11004; | |
| if (isset($argv[1])) { | |
| $port = $argv[1]; | |
| } | |
| define('PORT', $port); | |
| define('ID_MAP', json_decode(file_get_contents('../idMap.json'), true)); | |
| $proxy = new Worker('udp://0.0.0.0:'.PORT); | |
| $proxy->onWorkerStart = function ($worker) { | |
| }; | |
| $prevConnection = null; | |
| $upstream = new AsyncUdpConnection('udp://alpha.anxiens.com:'.PORT); | |
| $upstream->onMessage = function ($c, $data) { | |
| global $prevConnection; | |
| if (!$prevConnection) return; | |
| $data = str_replace(['106.75.223.112:11003','106.75.223.112:11004'], ['192.168.2.198:11003 ','192.168.2.198:11004 '], $data); | |
| $prevConnection->send($data); | |
| echo PORT." s->c\t" . strlen($data) . "\n"; | |
| try { | |
| readKServiceBody($data); | |
| } catch (Exception $_) {} | |
| }; | |
| $proxy->onMessage = function ($connection, $data) { | |
| echo PORT." c->s\t" . strlen($data) . "\n"; | |
| global $prevConnection; | |
| global $upstream; | |
| // if ($prevConnection) { | |
| // $prevConnection->close(); | |
| // } | |
| $prevConnection = $connection; | |
| $upstream->connect(); | |
| $upstream->send($data); | |
| try { | |
| readKServiceBody($data); | |
| } catch (Exception $_) {} | |
| }; | |
| $fragments = []; | |
| function getFullDataStart($conv, $sn, $frg) { | |
| global $fragments; | |
| $convFrag = &$fragments[$conv]; | |
| if ($frg === 0) { | |
| if ($sn === 0) return 0; | |
| // 往前找到上一个frg为0的分片 | |
| for ($i=$sn-1; $i>=0; $i--) { | |
| if (!isset($convFrag[$i])) return -1; | |
| if ($convFrag[$i] === true) return $i+1; | |
| if ($convFrag[$i]['frg'] === 0) return $i+1; | |
| } | |
| return -1; | |
| } | |
| if (isset($convFrag[$sn]) && $convFrag[$sn] === true) return -1; | |
| if (!isset($convFrag[$sn + $frg]) || $convFrag[$sn + $frg] === true) return -1; | |
| return getFullDataStart($conv, $sn + $frg, 0); | |
| } | |
| function receivedBody($info, $body) { | |
| global $fragments; | |
| $conv = $info['conv']; | |
| $sn = $info['sn']; | |
| $frg = $info['frg']; | |
| if (!isset($fragments[$conv])) $fragments[$conv] = []; | |
| $fragments[$conv][$sn] = ['body' => $body, 'frg' => $frg]; | |
| if (($start = getFullDataStart($conv, $sn, $frg)) !== -1) { | |
| $count = $fragments[$conv][$start]['frg']; | |
| $body = ''; | |
| for ($i=$start; $i<=$start+$count; $i++) { | |
| $body .= $fragments[$conv][$i]['body']; | |
| $fragments[$conv][$i] = true; | |
| } | |
| $size = strlen($body); | |
| echo "$sn\t$frg\tfound\t$start count $count\tsize $size\n"; | |
| return $body; | |
| } | |
| return null; | |
| } | |
| function readKServiceBody($data) { | |
| $type = ord($data[0]); | |
| switch ($type) { | |
| case 1: { | |
| echo "\t\t\tConnect\n"; | |
| break; | |
| } | |
| case 2: { | |
| echo "\t\t\tConnect Ack\n"; | |
| break; | |
| } | |
| case 3: { | |
| echo "\t\t\tKService Recv Fin\n"; | |
| break; | |
| } | |
| case 4: { | |
| // echo "\t\t\tKService Data\n"; | |
| $kcpStream = new MemoryStream($data); | |
| $kcpStream->littleEndian = true; | |
| $kcpStream->position = 5; | |
| while ($kcpStream->position < $kcpStream->size) { | |
| // echo "kcp\n"; | |
| $info = [ | |
| 'conv' => $kcpStream->long, | |
| 'cmd' => ord($kcpStream->byte), | |
| 'frg' => ord($kcpStream->byte), | |
| 'wnd' => $kcpStream->short, | |
| 'ts' => $kcpStream->long, | |
| 'sn' => $kcpStream->long, | |
| 'una' => $kcpStream->long, | |
| 'len' => $kcpStream->long, | |
| ]; | |
| $len = $info['len']; | |
| if ($len <= 2) { | |
| $kcpStream->position += $len; | |
| continue; | |
| } | |
| $body = $kcpStream->readData($len); | |
| $body = receivedBody($info, $body); | |
| if ($body === null) continue; | |
| $payload = new MemoryStream($body); | |
| $payload->littleEndian = true; | |
| $messageId = $payload->short; | |
| if (!isset(ID_MAP[$messageId])) { | |
| echo "Unknown message id: $messageId\n"; | |
| continue; | |
| } | |
| $message = ID_MAP[$messageId]; | |
| if (in_array($message, ['C2G_Ping', 'G2C_Ping'])) { | |
| // skip | |
| continue; | |
| } | |
| echo "$message: "; | |
| $proto = parseProto('../'.$message.'_gen.proto'); | |
| $data = parseProtoBuf($message, $payload->size - 2, $payload, $proto); | |
| // print_r($data); | |
| processDict($data); | |
| $output = prettifyJSON($data); | |
| echo $output."\n"; | |
| @mkdir("log/".PORT, 0777, true); | |
| file_put_contents("log/".PORT."/".date('Ymd-His').'-'.$info['sn'].'-'.$message.".json", $output); | |
| } | |
| echo "\n"; | |
| break; | |
| } | |
| } | |
| } | |
| function parseProto($file) { | |
| $proto = file_get_contents($file); | |
| $proto = preg_replace('(//.+\n)', "\n", $proto); | |
| preg_match_all('(message (\w+) \{([\w\W]+?)\})', $proto, $messages); | |
| $output = []; | |
| for ($i=0; $i<count($messages[0]); $i++) { | |
| $messageName = $messages[1][$i]; | |
| $lines = explode(';', trim($messages[2][$i], "; \n")); | |
| $output[$messageName] = []; | |
| foreach ($lines as $line) { | |
| $fields = explode('=', trim($line)); | |
| $tag = trim($fields[1]); | |
| $fields = explode(' ', trim($fields[0])); | |
| $output[$messageName][$tag] = array( | |
| 'repeated' => $fields[0] == 'repeated', | |
| 'type' => $fields[1], | |
| 'name' => $fields[2] | |
| ); | |
| } | |
| } | |
| return $output; | |
| } | |
| function readVarInt32($pb) { | |
| $b = ord($pb->readData(1)); | |
| $result = $b & 0x7f; | |
| $shift = 0; | |
| while ($b & 0x80) { | |
| $shift += 7; | |
| if ($shift > 64) throw new Exception('Too many bytes for varint'); | |
| $b=ord($pb->readData(1)); | |
| $result |= ($b & 0x7f) << $shift; | |
| } | |
| return $result; | |
| } | |
| function setValue(&$arr, $key, &$val, $isArr) { | |
| if ($isArr) { | |
| if (!isset($arr[$key])) $arr[$key] = []; | |
| $arr[$key][] = $val; | |
| } else { | |
| $arr[$key] = $val; | |
| } | |
| } | |
| function parseProtoBuf($messageName, $end, &$pb, &$proto, $messageCallback = null, $level = 1) { | |
| $message = []; | |
| while ($pb->position() < $end){ | |
| $id = readVarInt32($pb); | |
| $type = $id & 7; | |
| $id = $id >> 3; | |
| if (isset($proto[$messageName][$id])) { | |
| $def = $proto[$messageName][$id]; | |
| } else { | |
| $def = array('type'=>'byte', 'name'=>'UNKNOWN_'.$id, 'repeated'=>false); | |
| } | |
| $typeName = $def['type']; | |
| $name = $def['name']; | |
| $isRepeated = $def['repeated']; | |
| switch($type){ | |
| case 0: | |
| $data = readVarInt32($pb); | |
| setValue($message, $name, $data, $isRepeated); | |
| break; | |
| case 1: | |
| $data = $pb->readData(8); | |
| if ($typeName == 'double') { | |
| $data = unpack('d', $data)[1]; | |
| } else { | |
| $data = '<fix64> '.bin2hex($data); | |
| } | |
| setValue($message, $name, $data, $isRepeated); | |
| break; | |
| case 2: | |
| $length = readVarInt32($pb); | |
| if (isset($proto[$typeName])) { | |
| $sub = parseProtoBuf($typeName, $pb->position()+$length, $pb, $proto, $messageCallback, $level + 1); | |
| if ($messageCallback === null || call_user_func($messageCallback, $name, $sub, $level)) { | |
| setValue($message, $name, $sub, $isRepeated); | |
| } | |
| } else if ($length > 0) { | |
| $data = $pb->readData($length); | |
| if ($typeName == 'string') { | |
| } else { | |
| if (mb_detect_encoding($data, 'UTF-8', true)) { | |
| $data = '<guessed string> '.$data; | |
| } else { | |
| $data = '<byte> '.bin2hex($data); | |
| } | |
| } | |
| setValue($message, $name, $data, $isRepeated); | |
| } | |
| break; | |
| case 5: | |
| $data = $pb->readData(4); | |
| if ($typeName == 'float') { | |
| $data = unpack('f', $data)[1]; | |
| } else { | |
| $data = '<fix32> '.bin2hex($data); | |
| } | |
| setValue($message, $name, $data, $isRepeated); | |
| break; | |
| default: | |
| throw new Exception('unsupported type '.$type); | |
| } | |
| } | |
| return $message; | |
| } | |
| function processDict(&$arr) { | |
| foreach ($arr as $key=>&$val) { | |
| if (gettype($val) == 'array') { | |
| if (isset($val[0]) && gettype($val[0]) == 'array') { | |
| $keys = array_keys($val[0]); | |
| if (count($keys) == 2 | |
| && substr($keys[0], -4, 4) == '_key' | |
| && substr($keys[1], -6, 6) == '_value') { | |
| $newDict = []; | |
| foreach ($val as &$item) { | |
| $newDict[ $item[$keys[0]] ] = $item[$keys[1]]; | |
| } | |
| unset($arr[$key]); | |
| $arr[$key] = $newDict; | |
| $val = &$arr[$key]; | |
| } | |
| } | |
| processDict($val); | |
| $keys = array_keys($val); | |
| if (count($keys) == 1 && $keys[0] === 'entries') { | |
| $arr[$key] = $val['entries']; | |
| } | |
| } | |
| } | |
| } | |
| function prettifyJSON($in, Stream $out = NULL, $returnData = true) { | |
| if ($out == NULL) $out = new MemoryStream(''); | |
| $a = $in; | |
| if (gettype($a) == 'string') $a = json_decode($a); | |
| $a = json_encode($a, JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT); | |
| $a = preg_replace("/([^\]\}]),\n +/", "$1, ", $a); | |
| $a = preg_replace('/("[^"]+?":) /', '$1', $a); | |
| $a = preg_replace_callback("/\n +/", function ($m) { return "\n".str_repeat(' ', (strlen($m[0])-1) / 2); }, $a); | |
| $out->write($a); | |
| if (!$returnData) return; | |
| $out->seek(0); | |
| $output = $out->readData($out->size); | |
| unset($out); | |
| return $output; | |
| } | |
| Worker::runAll(); |