一、前言
我們在寫爬蟲的時候,經常會遇到各種加密的引數,只能在瀏覽器打斷點然後一步一步的去追程式碼,想辦法把加密部分的js給扣出來在外部呼叫。這個過程漫長且費力,還需要一定的ast能力,容易把人勸退。所以現在有一種思想,直接在外部呼叫瀏覽器內已經生成的函式獲取結果,這就是所謂的
js-rpc
遠端呼叫。
在白度搜尋js-rpc
框架,有python和go實現的,所以我如法炮製,使用php基於websocket開發了一個,取名shermie
,我個人覺得使用起來更方便一些,先上效果圖:
服務端
瀏覽器
訪問
程式碼
二、使用方法
- 啟動服務
進入bin目錄,在命令列執行下面的命令
./cli.exe Websocket.php
- 瀏覽器執行
1.在瀏覽器建立Websocket連線(把Websocket.js裡面的複製出來貼上到瀏覽器命令列執行),會返回一個client物件
2.在client物件上註冊需要呼叫的js函式
# 假設我們需要通過http呼叫btoa這個函式,第一個引數隨便命名,第二個引數是函式執行的內容
client.registeCall("btoa",function(params){
return btoa(params);
});
# 會輸出一個訪問地址,比如這樣
[2022/4/24 18:16:01][info] 連線到伺服器成功
> client.registeCall("btoa",function(params){
return window.btoa(params);
});
[2022/4/24 18:16:52][info] 註冊函式btoa成功
[2022/4/24 18:16:52][info] 訪問地址:http://127.0.0.1:9501?group=ef8d3da2-dca4-4236-ba99-82f76a5e1901&action=btoa&input=
# 引數說明
group:客戶端分組ID(不用管)
action:註冊的需要呼叫的函式(不用管)
input:呼叫這個函式傳入的引數(需要輸入)
- 訪問地址獲取結果
訪問上面列印的地址,並傳入自定義引數:http://127.0.0.1:9501?group=df777a58-ff44-41bb-81ce-935b6bea9c25&action=btoa&input="hh"
最終返回的就是:window.btoa(“hh”)執行的結果
三、說明
github不能上傳超過100m的檔案,所以只能貼一個外部下載連結了
share.weiyun.com/xKKhm1MC
Websocket.js
const host = "ws://127.0.0.1:9501"; function WebsocketClient(host) { this.ws = null; this.uniqueId = null; this.registeCallMap = {}; this.timer = -1; // 入口函式 this.start = function () { this.logo() this.uniqueId = this.uuid(); this.ws = new WebSocket(host); this.ws.onopen = this.open(); this.ws.onmessage = this.message(); this.ws.onclose = this.close(); return this; } // 心跳函式 this.heartbeat = function () { let _this = this; let post = {"msg": "success", "data": "ping", "code": 200, "uuid": this.uniqueId}; if (this.ws.readyState === WebSocket.OPEN) { this.timer = setInterval(function () { _this.ws.send(JSON.stringify(post)); }, 60000); } return this; } // 列印logo this.logo = function () { let logo = `%c ______ __ __ ______ ______ __ __ __ ______ /\\ ___\\ /\\ \\_\\ \\ /\\ ___\\ /\\ == \\ /\\ "-./ \\ /\\ \\ /\\ ___\\ \\ \\___ \\ \\ \\ __ \\ \\ \\ __\\ \\ \\ __< \\ \\ \\-./\\ \\ \\ \\ \\ \\ \\ __\\ \\/\\_____\\ \\ \\_\\ \\_\\ \\ \\_____\\ \\ \\_\\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_____\\ \\/_____/ \\/_/\\/_/ \\/_____/ \\/_/ /_/ \\/_/ \\/_/ \\/_/ \\/_____/` console.log(logo, "color:blue;") } // 回撥函式 this.open = function () { let _this = this; return function (event) { _this.log("info", "連線到伺服器成功"); // 心跳定時器 _this.heartbeat(); // 向伺服器傳送訊息 _this.sendSuccess(null); } } // 回撥函式 this.message = function () { let _this = this; return function (event) { let receive = event.data; _this.log("info", "收到伺服器傳送訊息:" + receive); // 解析分組和函式引數 receive = JSON.parse(receive); const {group, action, input} = receive.data; if (_this.uniqueId !== group) { _this.sendError("客戶端分組不存在"); return; } if (!_this.registeCallMap[group][action]) { _this.sendError("呼叫函式未註冊"); return; } // 呼叫函式 try { let result = _this.registeCallMap[group][action](input); _this.log("info", `呼叫函式${action}返回值:${result}`); if (result === undefined) { result = null; } _this.sendSuccess({result: result, input: input}); } catch (e) { _this.log("error", "呼叫函式報錯:" + e.message); _this.sendError("呼叫函式報錯:" + e.message); } } } // 回撥函式 this.close = function () { let _this = this; return function (event) { clearInterval(_this.timer) _this.log("info", "客戶端斷開連線"); } } // 生成客戶端id this.uuid = function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 日誌函式 this.log = function (type, msg) { let datetime = (new Date()).toLocaleString() let color = type.toLowerCase() === "info" ? "green" : "red"; console.log(`[${datetime}][${type}] %c ${msg}`, `color:${color};`) } // 註冊呼叫函式 this.registeCall = function (callName, callback) { this.log("info", `註冊函式${callName}成功`); // 儲存傳入的函式 this.registeCallMap[this.uniqueId] = { [callName]: callback }; // 訪問地址 this.log("info", `訪問地址:${host.replace("ws", "http")}?group=${this.uniqueId}&action=${callName}&input=`) } this.sendError = function (msg) { let post = {"msg": msg, "data": msg, "code": 9999, "uuid": this.uniqueId}; this.ws.send(JSON.stringify(post)); } this.sendSuccess = function (data) { let post = {"msg": "success", "data": data, "code": 200, "uuid": this.uniqueId}; this.ws.send(JSON.stringify(post)); } } let client = (new WebsocketClient(host)).start();
Websocket.php
<?php use Swoole\Http\Request; use Swoole\Http\Response; use Swoole\Table; use Swoole\WebSocket\Frame; use Swoole\WebSocket\Server; /** * Class WebsocketServer */ class WebsocketServer { /** * @var int */ public int $port = 9501; /** * @var Server|null */ public ?Server $server = null; /** * @var Table|null */ public ?Table $group = null; /** * */ const OPEN_EVENT = "open"; /** * */ const REQU_EVENT = "request"; /** * */ const START_EVENT = "start"; /** * */ const MSG_EVENT = "message"; /** * */ const CLS_EVENT = "close"; /** * WebsocketServer constructor. */ public function __construct() { $this->server = new Swoole\WebSocket\Server("0.0.0.0", $this->port); $this->server->set([ "worker_num" => 2, "heartbeat_check_interval" => 60, "heartbeat_idle_time" => 600, ]); $table = new Table(1024); $table->column("fd", Table::TYPE_STRING, 1024); $table->column("data", Table::TYPE_STRING, 1024); $table->create(); $this->group = $table; } /** * */ public function registeEvent() { $this->server->on(self::START_EVENT, [$this, 'start']); $this->server->on(self::REQU_EVENT, [$this, 'request']); $this->server->on(self::OPEN_EVENT, [$this, 'open']); $this->server->on(self::MSG_EVENT, [$this, 'message']); $this->server->on(self::CLS_EVENT, [$this, 'close']); } /** * @param Server $server */ public function start(Server $server) { $log = <<<EOF ______ __ __ ______ ______ __ __ __ ______ /\ ___\ /\ \_\ \ /\ ___\ /\ == \ /\ "-./ \ /\ \ /\ ___\ \ \___ \ \ \ __ \ \ \ __\ \ \ __< \ \ \-./\ \ \ \ \ \ \ __\ \/\_____\ \ \_\ \_\ \ \_____\ \ \_\ \_\ \ \_\ \ \_\ \ \_\ \ \_____\ \/_____/ \/_/\/_/ \/_____/ \/_/ /_/ \/_/ \/_/ \/_/ \/_____/ EOF; $this->log(self::START_EVENT, $log); $this->log(self::START_EVENT, "server listen at 0.0.0.0:{$this->port}"); } /** * @param Server $server * @param Request $request */ public function open(Server $server, Request $request) { $this->log(self::OPEN_EVENT, "new websocket client connect {$request->fd}"); } /** * @param Server $server * @param Frame $frame */ public function message(Server $server, Frame $frame) { $this->log(self::MSG_EVENT, "receive data from websocket client {$frame->fd}:" . $frame->data); $data = json_decode($frame->data, true); $this->group->set($data["uuid"], ["fd" => $frame->fd, "data" => json_encode($data, 256)]); } /** * @param Server $server * @param int $fd */ public function close(Server $server, int $fd) { // TODO 刪除記憶體表中的客戶端的fd $this->log(self::CLS_EVENT, "server close websocket client {$fd}"); } /** * @param Request $request * @param Response $response */ public function request(Swoole\Http\Request $request, Swoole\Http\Response $response) { // 獲取請求引數 $server = $request->server; $method = strtoupper($server["request_method"]); $path = $server["request_uri"]; $remote = $server["remote_addr"]; $port = $server["remote_port"]; $params = $request->get; $this->log(self::REQU_EVENT, "$remote:$port $method $path " . json_encode($params, JSON_UNESCAPED_UNICODE)); $response->header("Content-Type", "application/json;charset=utf-8"); // 分組引數 $group = $params["group"] ?? ""; if (!$group) { $data = ["code" => 9999, "data" => "客戶端分組引數不能為空", "status" => "success"]; $response->end(json_encode($data, 256)); return; } // 呼叫函式式 $action = $params["action"] ?? ""; if (!$action) { $data = ["code" => 9999, "data" => "呼叫方法引數不能為空", "status" => "success"]; $response->end(json_encode($data, 256)); return; } // 傳入引數 $input = $params["input"] ?? ""; if (!$input) { $data = ["code" => 9999, "data" => "呼叫方法傳入引數不能為空", "status" => "success"]; $response->end(json_encode($data, 256)); return; } // 建立連線或者傳送資料 if (!($this->group->get($group) ?? null)) { $data = ["code" => 9999, "data" => "當前分組內不存在已連線的客戶端,傳送資料失敗", "status" => "success"]; $response->end(json_encode($data, 256)); return; } $fd = $this->group->get($group)["fd"]; // 判斷連線是否可用 if (!$this->server->exist($fd) || !$this->server->isEstablished($fd)) { $data = ["code" => 9999, "data" => "當前分組客戶端連線已超時斷開,傳送資料失敗", "status" => "success"]; $response->end(json_encode($data, 256)); return; } $data = ["code" => 9999, "data" => $params, "status" => "success"]; $this->server->push($fd, json_encode($data, 256), WEBSOCKET_OPCODE_TEXT, true); while (true) { $receiveData = json_decode($this->group->get($group)["data"], true); usleep(200); if ($receiveData["data"] ?? null) { $wsData = $receiveData["data"] ?? null; break; } } // 清空資料 $this->group->set($group, ["fd" => $fd, "data" => null]); $result = ["code" => 200, "data" => $wsData, "status" => "success"]; $response->end(json_encode($result, 256)); } /** * */ public function run() { $this->registeEvent(); $this->server->start(); } /** * @param string $event * @param string $msg */ private function log(string $event, string $msg) { $msg = sprintf("[%s][$event]:%s" . PHP_EOL, (new DateTime())->format("Y-m-d Hs"), $msg); fwrite(STDOUT, $msg); fflush(STDOUT); } } (new WebsocketServer())->run();
本作品採用《CC 協議》,轉載必須註明作者和本文連結