├── requirements.txt ├── test.py ├── README.md ├── web_server.py ├── LICENSE ├── .gitignore ├── back_socket_server.py └── client.js /requirements.txt: -------------------------------------------------------------------------------- 1 | loguru 2 | sanic 3 | websockets 4 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2021/12/19 21:41 3 | # @Author : zp 4 | # @Python3.7 5 | 6 | import requests 7 | url = "http://127.0.0.1:5000/send?group=ws-group&action=clientTime" 8 | import time 9 | st_time = time.time() 10 | print(requests.get(url).json()) 11 | print("共花费", time.time()-st_time) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-jsrpc 2 | python实现一套轻量、协程异步、websocket远程调用服务,js逆向、混淆加密一把梭,再也不用扣js了。 3 | 4 | - [x] 将back_socket_server合并到web_server一起启动 5 | 6 | 安装依赖: 7 | 8 | > pip install -r requirements.txt 9 | 10 | 启动步骤: 11 | 12 | 1. python web_server.py 13 | 2. 浏览器注入client.js 14 | 2. 调用接口,获取js执行结果(eg. http://127.0.0.1:5000/send?group=ws-group&action=clientTime) 15 | 16 | 实战:blog地址,https://paker.net.cn/blog/33-%E5%AE%9E%E6%88%98%EF%BC%9Apython%E5%BC%80%E5%8F%91jsrpc%E6%9C%8D%E5%8A%A1%E4%B8%8E%E6%BC%94%E7%A4%BA/article.html 17 | 18 | 19 | 20 | 更成熟的方案,大家可以去看看virjar大佬的 [sekiro](https://github.com/virjar/sekiro) 21 | 22 | -------------------------------------------------------------------------------- /web_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 2021/7/13 21:18 3 | # @Author : zp 4 | # @Python3.7 5 | import asyncio 6 | import websockets 7 | import json 8 | from loguru import logger 9 | from sanic import Sanic, response 10 | import threading 11 | from back_socket_server import ws_run 12 | 13 | app = Sanic(__name__) 14 | 15 | 16 | @app.route('/send', methods=['GET']) 17 | async def get_request(request): 18 | # http://127.0.0.1:5000/send?group=ws-group&action=clientTime 19 | args_dic = {} 20 | for params in request.query_args: 21 | args_dic[params[0]] = str(params[1]) 22 | if "action" not in args_dic: 23 | return response.json({"error": "need action"}) 24 | group = args_dic.pop('group') 25 | async with websockets.connect(f'ws://localhost:6789/invoke?group={group}') as websocket: 26 | await websocket.send(json.dumps(args_dic)) 27 | res = await websocket.recv() 28 | logger.info(res) 29 | return response.json(json.loads(res)) 30 | 31 | 32 | if __name__ == '__main__': 33 | app.add_task(ws_run()) 34 | app.run(debug=False, host="127.0.0.1", port=5000) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 睡七个小时怎么了 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /back_socket_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2021/7/13 17:40 3 | # @Author : zp 4 | 5 | import random 6 | import asyncio 7 | import json 8 | import websockets 9 | import re 10 | import uuid 11 | from loguru import logger 12 | 13 | # 实现一对多连接 14 | USERS = {} 15 | APIUSERS = {} 16 | 17 | 18 | async def register(websocket, path): 19 | # web clientjs 连接注册 20 | if path and path.startswith('/register'): 21 | group = re.findall(r'\?group=(.*?)&', path)[0] 22 | clientId = re.findall('&clientId=(.*)', path)[0] 23 | if group not in USERS: 24 | USERS[group] = {} 25 | USERS[group][clientId] = websocket 26 | logger.info("USERS register success") 27 | logger.info(USERS) 28 | return "webclient", group, clientId 29 | if path and path.startswith('/invoke'): 30 | group = re.findall(r'\?group=(.*)', path)[0] 31 | clientId = str(uuid.uuid4()) 32 | if group not in APIUSERS: 33 | APIUSERS[group] = {} 34 | APIUSERS[group][clientId] = websocket 35 | logger.info("APIUSERS register success") 36 | logger.info(USERS) 37 | return "apiclient", group, clientId 38 | return None, None, None 39 | 40 | 41 | async def unregister(role, group, clientId): 42 | if role == 'webclient': 43 | USERS[group].pop(clientId) 44 | logger.info("USERS unregister over") 45 | logger.info(USERS) 46 | if role == 'apiclient': 47 | APIUSERS[group].pop(clientId) 48 | logger.info("APIUSERS unregister over") 49 | logger.info(APIUSERS) 50 | 51 | 52 | async def counter(websocket, path): 53 | logger.info(path) 54 | role, group, clientId = await register(websocket, path) 55 | try: 56 | # 这样写会一直保持长连接 57 | async for orimessage in websocket: 58 | logger.info(orimessage) 59 | message = json.loads(orimessage) 60 | # 接收到api接口服务send来的消息时,action为必带的参数 61 | if "action" in message and role == 'apiclient': 62 | # 由于可能是分布式的,此处直接随机返回一个client执行rpc方法就行了 63 | message['__uuid_seq__'] = clientId 64 | await USERS[group][random.choice(list(USERS[group]))].send(json.dumps(message)) 65 | 66 | # 接收到web js client发来的消息时,需要由server将message send到api接口client 67 | if "status" in message and role == 'webclient': 68 | apiclient = message['__uuid_seq__'] 69 | message.pop("__uuid_seq__", None) 70 | await APIUSERS[group][apiclient].send(json.dumps(message)) 71 | 72 | finally: 73 | await unregister(role, group, clientId) 74 | 75 | 76 | async def ws_run(): 77 | logger.info("ws run") 78 | await websockets.serve(counter, 'localhost', 6789) 79 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | function JsClient(wsURL) { 2 | this.wsURL = wsURL; 3 | this.handlers = {}; 4 | this.socket = {}; 5 | if (!wsURL) { 6 | throw new Error('wsURL can not be empty!!') 7 | } 8 | this.webSocketFactory = this.resolveWebSocketFactory(); 9 | this.connect() 10 | } 11 | 12 | JsClient.prototype.resolveWebSocketFactory = function () { 13 | if (typeof window === 'object') { 14 | var theWebSocket = window.WebSocket ? window.WebSocket : window.MozWebSocket; 15 | return function (wsURL) { 16 | 17 | function WindowWebSocketWrapper(wsURL) { 18 | this.mSocket = new theWebSocket(wsURL); 19 | } 20 | 21 | WindowWebSocketWrapper.prototype.close = function () { 22 | this.mSocket.close(); 23 | }; 24 | 25 | WindowWebSocketWrapper.prototype.onmessage = function (onMessageFunction) { 26 | this.mSocket.onmessage = onMessageFunction; 27 | }; 28 | 29 | WindowWebSocketWrapper.prototype.onopen = function (onOpenFunction) { 30 | this.mSocket.onopen = onOpenFunction; 31 | }; 32 | WindowWebSocketWrapper.prototype.onclose = function (onCloseFunction) { 33 | this.mSocket.onclose = onCloseFunction; 34 | }; 35 | 36 | WindowWebSocketWrapper.prototype.send = function (message) { 37 | this.mSocket.send(message); 38 | }; 39 | 40 | return new WindowWebSocketWrapper(wsURL); 41 | } 42 | } 43 | if (typeof weex === 'object') { 44 | try { 45 | console.log("test webSocket for weex"); 46 | var ws = weex.requireModule('webSocket'); 47 | console.log("find webSocket for weex:" + ws); 48 | return function (wsURL) { 49 | try { 50 | ws.close(); 51 | } catch (e) { 52 | } 53 | ws.WebSocket(wsURL, ''); 54 | return ws; 55 | } 56 | } catch (e) { 57 | console.log(e); 58 | } 59 | } 60 | if (typeof WebSocket === 'object') { 61 | return function (wsURL) { 62 | return new theWebSocket(wsURL); 63 | } 64 | } 65 | throw new Error("the js environment do not support websocket"); 66 | }; 67 | 68 | JsClient.prototype.connect = function () { 69 | console.log('jsclient begin of connect to wsURL: ' + this.wsURL); 70 | var _this = this; 71 | try { 72 | this.socket = this.webSocketFactory(this.wsURL); 73 | } catch (e) { 74 | console.log("jsclient create connection failed,reconnect after 2s"); 75 | setTimeout(function () { 76 | _this.connect() 77 | }, 2000) 78 | } 79 | 80 | this.socket.onmessage(function (event) { 81 | _this.handleJsClientRequest(event.data) 82 | }); 83 | 84 | this.socket.onopen(function (event) { 85 | console.log('jsclient open a connection') 86 | }); 87 | 88 | this.socket.onclose(function (event) { 89 | console.log('jsclient disconnected ,reconnection after 2s'); 90 | setTimeout(function () { 91 | _this.connect() 92 | }, 2000) 93 | }); 94 | }; 95 | 96 | JsClient.prototype.handleJsClientRequest = function (requestJson) { 97 | console.log("receive request: " + requestJson); 98 | var request = JSON.parse(requestJson); 99 | var seq = request['__uuid_seq__']; 100 | 101 | if (!request['action']) { 102 | this.sendFailed(seq, 'need request param {action}'); 103 | return 104 | } 105 | var action = request['action']; 106 | if (!this.handlers[action]) { 107 | this.sendFailed(seq, 'no action handler: ' + action + ' defined'); 108 | return 109 | } 110 | 111 | var theHandler = this.handlers[action]; 112 | var _this = this; 113 | try { 114 | theHandler(request, function (response) { 115 | try { 116 | _this.sendSuccess(seq, response) 117 | } catch (e) { 118 | _this.sendFailed(seq, "e:" + e); 119 | } 120 | }, function (errorMessage) { 121 | _this.sendFailed(seq, errorMessage) 122 | }) 123 | } catch (e) { 124 | console.log("error: " + e); 125 | _this.sendFailed(seq, ":" + e); 126 | } 127 | }; 128 | 129 | JsClient.prototype.sendSuccess = function (seq, response) { 130 | var responseJson; 131 | if (typeof response == 'string') { 132 | try { 133 | responseJson = JSON.parse(response); 134 | } catch (e) { 135 | responseJson = {}; 136 | responseJson['data'] = response; 137 | } 138 | } else if (typeof response == 'object') { 139 | responseJson = response; 140 | } else { 141 | responseJson = {}; 142 | responseJson['data'] = response; 143 | } 144 | 145 | if (Array.isArray(responseJson)) { 146 | responseJson = { 147 | data: responseJson, 148 | code: 0 149 | } 150 | } 151 | 152 | if (responseJson['code']) { 153 | responseJson['code'] = 0; 154 | } else if (responseJson['status']) { 155 | responseJson['status'] = 0; 156 | } else { 157 | responseJson['status'] = 0; 158 | } 159 | responseJson['__uuid_seq__'] = seq; 160 | var responseText = JSON.stringify(responseJson); 161 | console.log("response :" + responseText); 162 | this.socket.send(responseText); 163 | }; 164 | 165 | JsClient.prototype.sendFailed = function (seq, errorMessage) { 166 | if (typeof errorMessage != 'string') { 167 | errorMessage = JSON.stringify(errorMessage); 168 | } 169 | var responseJson = {}; 170 | responseJson['message'] = errorMessage; 171 | responseJson['status'] = -1; 172 | responseJson['__uuid_seq__'] = seq; 173 | var responseText = JSON.stringify(responseJson); 174 | console.log("jsclient response :" + responseText); 175 | this.socket.send(responseText) 176 | }; 177 | 178 | JsClient.prototype.registerAction = function (action, handler) { 179 | if (typeof action !== 'string') { 180 | throw new Error("an action must be string"); 181 | } 182 | if (typeof handler !== 'function') { 183 | throw new Error("a handler must be function"); 184 | } 185 | console.log("jsclient register action: " + action); 186 | this.handlers[action] = handler; 187 | return this; 188 | }; 189 | function guid() { 190 | function S4() { 191 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 192 | } 193 | return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); 194 | } 195 | 196 | //demo 197 | var client = new JsClient("ws://localhost:6789/register?group=ws-group&clientId=" + guid()); 198 | //获取服务器时间 199 | client.registerAction("clientTime", function (request, resolve, reject) { 200 | resolve("" + new Date()); 201 | }) 202 | --------------------------------------------------------------------------------