From 1354a7d2f7fcc29fd85cd85158536ab1cc8d16fd Mon Sep 17 00:00:00 2001 From: nullcat Date: Sat, 24 Feb 2024 01:32:39 +0800 Subject: [PATCH 1/3] Sync changes to main branch (#2) Co-authored-by: Linwenxuan <116782992+Linwenxuan05@users.noreply.github.com> Co-authored-by: Linwenxuan Co-authored-by: RF-Tar-Railt <3165388245@qq.com> --- .gitignore | 268 +++++++++++++++ lagrange/client/base.py | 306 +++++++++++++++--- lagrange/client/client.py | 120 +++++++ lagrange/client/event.py | 43 +++ lagrange/client/message/__init__.py | 0 lagrange/client/message/decoder.py | 168 ++++++++++ lagrange/client/message/elems.py | 71 ++++ lagrange/client/message/encoder.py | 46 +++ lagrange/client/network.py | 102 ++++++ lagrange/client/ntlogin.py | 84 +++++ lagrange/client/oicq.py | 149 --------- lagrange/client/packet.py | 41 +++ lagrange/client/server_push/__init__.py | 4 + lagrange/client/server_push/binder.py | 30 ++ .../client/server_push/events/__init__.py | 0 lagrange/client/server_push/events/group.py | 37 +++ lagrange/client/server_push/log.py | 3 + lagrange/client/server_push/msg.py | 53 +++ lagrange/client/wtlogin/enum.py | 32 ++ lagrange/client/wtlogin/exchange.py | 51 +++ lagrange/client/wtlogin/oicq.py | 186 +++++++++++ lagrange/client/wtlogin/sso.py | 97 ++++++ lagrange/client/wtlogin/status_service.py | 35 ++ lagrange/client/wtlogin/tlv/common.py | 58 ++-- lagrange/client/wtlogin/tlv/qrcode.py | 17 +- lagrange/info/app.py | 2 +- lagrange/info/device.py | 2 +- lagrange/info/serialize.py | 94 ++---- lagrange/info/sig.py | 8 +- lagrange/utils/binary/builder.py | 27 +- .../{protobuf/__init__.py => protobuf.py} | 42 ++- lagrange/utils/binary/reader.py | 64 +++- lagrange/utils/crypto/aes.py | 19 ++ lagrange/utils/crypto/ecdh/__init__.py | 4 +- lagrange/utils/crypto/ecdh/curve.py | 2 +- lagrange/utils/crypto/ecdh/ecdh.py | 4 +- lagrange/utils/crypto/ecdh/impl.py | 55 ++-- lagrange/utils/httpcat.py | 200 ++++++++++++ lagrange/utils/log.py | 43 +++ lagrange/utils/network.py | 6 +- lagrange/utils/operator.py | 28 ++ lagrange/utils/sign.py | 78 +++++ main.py | 104 +++++- pdm.lock | 149 +++++++++ pyproject.toml | 6 +- 45 files changed, 2560 insertions(+), 378 deletions(-) create mode 100644 .gitignore create mode 100644 lagrange/client/client.py create mode 100644 lagrange/client/event.py create mode 100644 lagrange/client/message/__init__.py create mode 100644 lagrange/client/message/decoder.py create mode 100644 lagrange/client/message/elems.py create mode 100644 lagrange/client/message/encoder.py create mode 100644 lagrange/client/network.py create mode 100644 lagrange/client/ntlogin.py delete mode 100644 lagrange/client/oicq.py create mode 100644 lagrange/client/packet.py create mode 100644 lagrange/client/server_push/__init__.py create mode 100644 lagrange/client/server_push/binder.py create mode 100644 lagrange/client/server_push/events/__init__.py create mode 100644 lagrange/client/server_push/events/group.py create mode 100644 lagrange/client/server_push/log.py create mode 100644 lagrange/client/server_push/msg.py create mode 100644 lagrange/client/wtlogin/exchange.py create mode 100644 lagrange/client/wtlogin/oicq.py create mode 100644 lagrange/client/wtlogin/sso.py create mode 100644 lagrange/client/wtlogin/status_service.py rename lagrange/utils/binary/{protobuf/__init__.py => protobuf.py} (74%) create mode 100644 lagrange/utils/crypto/aes.py create mode 100644 lagrange/utils/httpcat.py create mode 100644 lagrange/utils/log.py create mode 100644 lagrange/utils/operator.py create mode 100644 lagrange/utils/sign.py create mode 100644 pdm.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a3a074 --- /dev/null +++ b/.gitignore @@ -0,0 +1,268 @@ +### venv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml +.pdm-python + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + diff --git a/lagrange/client/base.py b/lagrange/client/base.py index fd8e0e7..cd3544f 100644 --- a/lagrange/client/base.py +++ b/lagrange/client/base.py @@ -1,37 +1,21 @@ import asyncio -from typing import Optional +import hashlib +import time +from typing import Optional, Tuple, Union, overload, Coroutine, Callable, Dict +from typing_extensions import Literal -from lagrange.utils.binary.builder import Builder -from lagrange.utils.network import Connection +from lagrange.utils.log import logger +from lagrange.utils.binary.reader import Reader from lagrange.info import AppInfo, DeviceInfo, SigInfo +from .wtlogin.oicq import build_code2d_packet, build_uni_packet, build_login_packet, decode_login_response from .wtlogin.tlv import CommonTlvBuilder, QrCodeTlvBuilder -from .oicq import build_code2d_packet - - -class ClientNetwork(Connection): - default_upstream = ("msfwifi.3g.qq.com", 8080) - - def __init__(self, host: str = "", port: int = 0): - if not (host and port): - host, port = self.default_upstream - super().__init__(host, port) - self.conn_event = asyncio.Event() - - async def write(self, buf: bytes): - await self.conn_event.wait() - self.writer.write(buf) - await self.writer.drain() - - async def on_connected(self): - self.conn_event.set() - print("connected") - - async def on_disconnect(self): - self.conn_event.clear() - print("disconnected") - - async def on_message(self, msg_len: int): - print(self.reader.read(msg_len), 11) +from .wtlogin.exchange import build_key_exchange_request, parse_key_exchange_response +from .wtlogin.enum import QrCodeResult, LoginErrorCode +from .wtlogin.sso import SSOPacket +from .wtlogin.status_service import build_register_request, build_sso_heartbeat_request, parse_register_response +from .packet import PacketBuilder +from .network import ClientNetwork +from .ntlogin import build_ntlogin_request, parse_ntlogin_response class BaseClient: @@ -45,32 +29,63 @@ def __init__( uin: int, app_info: AppInfo, device_info: DeviceInfo, - sig_info: Optional[SigInfo] = None + sig_info: Optional[SigInfo] = None, + sign_provider: Callable[[str, int, bytes], Coroutine[None, None, dict]] = None ): self._uin = uin self._sig = sig_info self._app_info = app_info self._device_info = device_info - self._network = ClientNetwork() - self._loop_task: Optional[asyncio.Task] = None - self._online = False + self._server_push_queue: asyncio.Queue[SSOPacket] = asyncio.Queue() + self._tasks: Dict[str, Optional[asyncio.Task]] = { + "loop": None, + "push_handle": None + } + self._network = ClientNetwork(sig_info, self._server_push_queue, self.register, self._disconnect_cb) + self._sign_provider = sign_provider + + self._t106 = bytes() + self._t16a = bytes() + + self._online = asyncio.Event() + + async def _disconnect_cb(self): + self._online.clear() def get_seq(self) -> int: try: return self._sig.sequence finally: + if self._sig.sequence >= 0x8000: + self._sig.sequence = 0 self._sig.sequence += 1 def connect(self) -> None: - if not self._loop_task: - self._loop_task = asyncio.create_task(self._network.loop()) + if not self._tasks["loop"]: + self._tasks["loop"] = asyncio.create_task(self._network.loop()) + self._tasks["push_handle"] = asyncio.create_task(self._push_handle_loop()) else: raise RuntimeError("connect call twice") - async def stop(self): + async def disconnect(self): + self._online.clear() await self._network.stop() + async def stop(self): + await self.disconnect() + for _, task in self._tasks.items(): + if task: + task.cancel() + + async def _push_handle_loop(self): + while True: + sso = await self._server_push_queue.get() + try: + await self.push_handler(sso) + except: + logger.root.exception("Unhandled exception on push handler") + async def wait_closed(self) -> None: await self._network.wait_closed() @@ -91,27 +106,61 @@ def uin(self) -> int: return self._uin @property - def online(self) -> bool: + def uid(self) -> str: + return self._sig.uid + + @property + def online(self) -> asyncio.Event: return self._online - async def fetch_qrcode(self): + @overload + async def send_uni_packet(self, cmd: str, buf: bytes, send_only=False) -> SSOPacket: + ... + + @overload + async def send_uni_packet(self, cmd: str, buf: bytes, send_only: Literal[False]) -> SSOPacket: + ... + + @overload + async def send_uni_packet(self, cmd: str, buf: bytes, send_only: Literal[True]) -> None: + ... + + async def send_uni_packet(self, cmd, buf, send_only=False): + seq = self.get_seq() + sign = None + if self._sign_provider: + sign = await self._sign_provider(cmd, seq, buf) + packet = build_uni_packet( + uin=self.uin, + seq=seq, + cmd=cmd, + sign=sign, + app_info=self.app_info, + device_info=self.device_info, + sig_info=self._sig, + body=buf + ) + + return await self._network.send(packet, wait_seq=-1 if send_only else seq) + + async def fetch_qrcode(self) -> Union[int, Tuple[bytes, str]]: tlv = QrCodeTlvBuilder() body = ( - Builder() + PacketBuilder() .write_u16(0) .write_u64(0) .write_u8(0) .write_tlv( tlv.t16( self.app_info.app_id, - self.app_info.app_id_qrcode, - self.device_info.guid.encode(), + self.app_info.sub_app_id, + bytes.fromhex(self.device_info.guid), self.app_info.pt_version, self.app_info.package_name ), tlv.t1b(), tlv.t1d(self.app_info.misc_bitmap), - tlv.t33(self.device_info.guid.encode()), + tlv.t33(bytes.fromhex(self.device_info.guid)), tlv.t35(self.app_info.pt_os_version), tlv.t66(self.app_info.pt_os_version), tlv.td1(self.app_info.os, self.device_info.device_name) @@ -120,14 +169,173 @@ async def fetch_qrcode(self): packet = build_code2d_packet( self.uin, - self.get_seq(), 0x31, - self.app_info, - self.device_info, - self._sig, + self._app_info, body ) - await self._network.write(packet) + response = await self.send_uni_packet("wtlogin.trans_emp", packet) + + decrypted = Reader(response.data) + decrypted.read_bytes(54) + ret_code = decrypted.read_u8() + qrsig = decrypted.read_bytes_with_length("u16", False) + tlvs = decrypted.read_tlv() + + if not ret_code and tlvs[0x17]: + self._sig.qrsig = qrsig + return tlvs[0x17], Reader(tlvs[209]).read_bytes_with_length("u16").decode() + + return ret_code + + async def get_qrcode_result(self) -> QrCodeResult: + if not self._sig.qrsig: + raise AssertionError("No QrSig found, execute fetch_qrcode first") + + body = ( + PacketBuilder() + .write_bytes(self._sig.qrsig, "u16", False) + .write_u64(0) + .write_u32(0) + .write_u8(0) + .write_u8(0x03) + ).pack() + + response = await self.send_uni_packet( + "wtlogin.trans_emp", + build_code2d_packet( + 0, # self.uin + 0x12, + self.app_info, + body + ) + ) + + reader = Reader(response.data) + # length = reader.read_u32() + reader.read_bytes(8) # 4 + 4 + reader.read_u16() # cmd, 0x12 + reader.read_bytes(40) + _app_id = reader.read_u32() + ret_code = QrCodeResult(reader.read_u8()) + + if ret_code == 0: + reader.read_bytes(4) + self._uin = reader.read_u32() + reader.read_bytes(4) + t = reader.read_tlv() + self._t106 = t[0x18] + self._t16a = t[0x19] + self._sig.tgtgt = t[0x1e] + + return ret_code + + async def _key_exchange(self): + packet = await self.send_uni_packet( + "trpc.login.ecdh.EcdhService.SsoKeyExchange", + build_key_exchange_request( + self.uin, + self.device_info.guid + ) + ) + parse_key_exchange_response(packet.data, self._sig) + + async def password_login(self, password: str) -> LoginErrorCode: + md5_passwd = hashlib.md5(password.encode()).digest() + + cr = CommonTlvBuilder().t106( + self.app_info.app_id, + self.app_info.app_client_version, + self.uin, + md5_passwd, + self.device_info.guid, + self._sig.tgtgt + )[4:] + packet = await self.send_uni_packet( + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLogin", + build_ntlogin_request(self.uin, self.app_info, self.device_info, self._sig, cr) + ) + + return parse_ntlogin_response(packet.data, self._sig) + + async def token_login(self, token: bytes) -> LoginErrorCode: + packet = await self.send_uni_packet( + "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLogin", + build_ntlogin_request(self.uin, self.app_info, self.device_info, self._sig, token) + ) + + return parse_ntlogin_response(packet.data, self._sig) + + async def qrcode_login(self, refresh_interval=5) -> bool: + if not self._sig.qrsig: + raise AssertionError("No QrSig found, fetch qrcode first") + + while not self._network.closed: + await asyncio.sleep(refresh_interval) + ret_code = await self.get_qrcode_result() + if not ret_code.waitable: + if not ret_code.success: + raise AssertionError(ret_code.name) + else: + break + + tlv = CommonTlvBuilder() + app = self.app_info + device = self.device_info + body = ( + PacketBuilder() + .write_u16(0x09) + .write_tlv( + PacketBuilder().write_bytes(self._t106).pack(0x106), + tlv.t144(self._sig.tgtgt, app, device), + tlv.t116(app.sub_sigmap), + tlv.t142(app.package_name), + tlv.t145(bytes.fromhex(device.guid)), + tlv.t18( + 0, + app.app_client_version, + self.uin + ), + tlv.t141(b"Unknown"), + tlv.t177(app.wtlogin_sdk), + tlv.t191(), + tlv.t100(5, app.app_id, app.sub_app_id, 8001, app.main_sigmap), + tlv.t107(), + tlv.t318(), + PacketBuilder().write_bytes(self._t16a).pack(0x16a), + tlv.t166(5), + tlv.t521(), + ) + ).pack() + + response = await self.send_uni_packet( + "wtlogin.login", + build_login_packet(self.uin, "wtlogin.login", app, body) + ) + + return decode_login_response(response.data, self._sig) + + async def register(self) -> bool: + response = await self.send_uni_packet( + "trpc.qq_new_tech.status_svc.StatusService.Register", + build_register_request(self.app_info, self.device_info) + ) + if parse_register_response(response.data): + self._online.set() + logger.login.info("Register successful") + return True + logger.login.error("Register failure") + return False + + async def sso_heartbeat(self, calc_latency=False) -> float: + start_time = time.time() + await self.send_uni_packet( + "trpc.qq_new_tech.status_svc.StatusService.SsoHeartBeat", + build_sso_heartbeat_request() + ) + if calc_latency: + return time.time() - start_time + return 0 - print("done") + async def push_handler(self, sso: SSOPacket): + pass diff --git a/lagrange/client/client.py b/lagrange/client/client.py new file mode 100644 index 0000000..c862082 --- /dev/null +++ b/lagrange/client/client.py @@ -0,0 +1,120 @@ +import os +from typing import Coroutine, Callable, Optional, List + +from lagrange.utils.log import logger +from lagrange.utils.operator import timestamp +from lagrange.utils.binary.protobuf import proto_encode, proto_decode +from lagrange.info import DeviceInfo, AppInfo, SigInfo +from .base import BaseClient +from .event import Events +from .message.elems import T +from .message.encoder import build_message +from .wtlogin.sso import SSOPacket +from .server_push import push_handler + + +class Client(BaseClient): + def __init__( + self, + uin: int, + app_info: AppInfo, + device_info: DeviceInfo, + sig_info: Optional[SigInfo] = None, + sign_provider: Callable[[str, int, bytes], Coroutine[None, None, dict]] = None + ): + super().__init__(uin, app_info, device_info, sig_info, sign_provider) + + self._events = Events() + + @property + def events(self) -> Events: + return self._events + + async def login(self, password="", qrcode_path="./qrcode.png") -> bool: + try: + if self._sig.temp_pwd: # EasyLogin + await self._key_exchange() + + ret = await self.token_login(self._sig.temp_pwd) + if ret.successful: + return await self.register() + except: + logger.login.exception("EasyLogin fail") + + if password: # PasswordLogin, WIP + await self._key_exchange() + + while True: + ret = await self.password_login(password) + if ret.successful: + return await self.register() + elif ret.captcha_verify: + logger.root.warning("captcha verification required") + self._sig.captcha_info[0] = input("ticket?->") + self._sig.captcha_info[1] = input("rand_str?->") + else: + logger.root.error(f"Unhandled exception raised: {ret.name}") + else: # QrcodeLogin + png, _link = await self.fetch_qrcode() + logger.root.info(f"save qrcode to '{qrcode_path}'") + with open(qrcode_path, "wb") as f: + f.write(png) + if await self.qrcode_login(3): + return await self.register() + return False + + async def send_oidb_svc(self, cmd: int, sub_cmd: int, buf: bytes, is_uid=False) -> SSOPacket: + body = { + 1: cmd, + 2: sub_cmd, + 4: buf, + 12: is_uid + } + return await self.send_uni_packet( + "OidbSvcTrpcTcp.0x{:0>2x}_{}".format(cmd, sub_cmd), + proto_encode(body) + ) + + async def push_handler(self, sso: SSOPacket): + ret = await push_handler.execute(sso.cmd, sso) + if ret: + self._events.emit(ret, self) + + async def _send_msg_raw(self, pb: dict, *, uin=0, grp_id=0, uid="") -> dict: + assert uin or grp_id, "uin and grp_id" + seq = self.seq + 1 + sendto = {} + if not grp_id: # friend + assert uin and uid, "uin and uid must be filled" + sendto[1] = {1: uin, 2: uid} + elif grp_id: # grp + sendto[2] = {1: grp_id} + elif uin and grp_id: # temp msg + sendto[3] = {1: grp_id, 2: uin} + else: + assert False + body = { + 1: sendto, + 2: { + 1: 1, + 2: 0, + 3: 0 + }, + 3: pb, + 4: seq, + 5: int.from_bytes(os.urandom(4), byteorder="big", signed=False) + } + if not grp_id: + body[12] = {1: timestamp()} + + packet = await self.send_uni_packet( + "MessageSvc.PbSendMsg", + proto_encode(body) + ) + return proto_decode(packet.data) + + async def send_grp_msg(self, msg_chain: List[T], grp_id: int) -> None: + await self._send_msg_raw( + build_message(msg_chain), + grp_id=grp_id + ) \ No newline at end of file diff --git a/lagrange/client/event.py b/lagrange/client/event.py new file mode 100644 index 0000000..7302c32 --- /dev/null +++ b/lagrange/client/event.py @@ -0,0 +1,43 @@ +import asyncio +from typing import Dict, Callable, TypeVar, Type, Coroutine, TYPE_CHECKING, List + +from lagrange.utils.log import logger + +if TYPE_CHECKING: + from .client import Client + +T = TypeVar('T') +EVENT_HANDLER = Callable[["Client", T], Coroutine[None, None, None]] + + +class Events: + def __init__(self): + self._task_group: List[asyncio.Task] = [] + self._handle_map: Dict[Type[T], EVENT_HANDLER] = {} + + def subscribe(self, event: Type[T], handler: EVENT_HANDLER): + if event not in self._handle_map: + self._handle_map[event] = handler + else: + raise AssertionError("Event already subscribed to {}".format(self._handle_map[event])) + + def unsubscribe(self, event: Type[T]): + return self._handle_map.pop(event) + + async def _task_exec(self, client: "Client", event: T, handler: EVENT_HANDLER): + try: + await handler(client, event) + except: + logger.root.exception("Unhandled exception on task {}".format(event)) + + def emit(self, event: T, client: "Client"): + typ = type(event) + if typ not in self._handle_map: + logger.root.debug(f"Unhandled event: {event}") + return + + t = asyncio.create_task( + self._task_exec(client, event, self._handle_map[typ]) + ) + self._task_group.append(t) + t.add_done_callback(lambda _: self._task_group.remove(typ) if typ in self._task_group else None) diff --git a/lagrange/client/message/__init__.py b/lagrange/client/message/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lagrange/client/message/decoder.py b/lagrange/client/message/decoder.py new file mode 100644 index 0000000..98e7ff0 --- /dev/null +++ b/lagrange/client/message/decoder.py @@ -0,0 +1,168 @@ +import zlib +from typing import Tuple, List, Dict, Union + +from lagrange.utils.binary.protobuf import proto_encode +from lagrange.utils.operator import unpack_dict + +from . import elems +from ..server_push.events.group import GroupMessage + + +def parse_msg_info(pb: dict) -> Tuple[int, str, int, int, int]: + info, head, body = pb[1], pb[2], pb[3] + user_id = info[1] + uid = info[2] + seq = head[5] + time = head[6] + rand = unpack_dict(pb, "3.1.1.3", head.get(7, -1)) + + return user_id, uid, seq, time, rand + + +def parse_msg(rich: List[Dict[int, dict]]) -> List[Dict[str, Union[int, str]]]: + msg_chain = [] + ignore_next = False + for raw in rich: + if not raw or ignore_next: + ignore_next = False + continue + if 1 in raw: # msg + msg = raw[1] + if 1 in msg and 3 in msg: # At + buf3 = msg[3] if isinstance(msg[3], bytes) else msg[3].encode() + if buf3[6]: # AtAll + msg_chain.append({"type": "atall", "text": msg[1]}) + else: # At + msg_chain.append({ + "type": "at", + "text": msg[1], + "uin": int.from_bytes(buf3[7:11], "big"), + "uid": msg[12][9] + }) + else: # Text + msg_chain.append({ + "type": "text", + "text": msg[1] + }) + elif 2 in raw: # q emoji + emo = raw[2] + msg_chain.append({ + "type": "emoji", + "id": emo[1] + }) + elif 6 in raw: # qq大表情 + pass + elif 8 in raw: # gpic + img = raw[8] + msg_chain.append({ + "type": "image", + "text": unpack_dict(img, "34.9", "") or "[图片]", + "url": "https://gchat.qpic.cn" + img[16], + "name": unpack_dict(img, "2", "undefined"), + "is_emoji": bool(unpack_dict(img, "34.1", 0)) + }) + elif 9 in raw: # unknown + pass + elif 12 in raw: + service = raw[12] + if service[1]: + jr = service[1] + sid = service[2] + if jr[0]: + content = zlib.decompress(jr[1:]) + else: + content = jr[1:] + msg_chain.append({ + "type": "service", + "text": f"[service:{sid}]", + "raw": content, + "id": sid + }) + elif 16 in raw: # extra + # nickname = unpack_dict(raw, "16.2", "") + pass + elif 19 in raw: # video + pass + elif 37 in raw: # unknown + pass + elif 45 in raw: # msg source info + src = raw[45] + msg_text = "" + for v in src[5] if isinstance(src[5], list) else [src[5]]: + msg_text += unpack_dict(v, "1.1", "") + # src[10]: optional[grp_id] + msg_chain.append({ + "type": "quote", + "text": f"[quote:{msg_text}]", + "seq": src[1], + "uin": src[2], + "timestamp": src[3], + "uid": src[8][6] + }) + ignore_next = True + elif 51 in raw: # qq mini app or others + service = raw[51] + if service[1]: + jr = service[1] + if jr[0]: + content = zlib.decompress(jr[1:]) + else: + content = jr[1:] + msg_chain.append({ + "type": "json", + "text": f"[json:{len(content)}]", + "raw": content + }) + else: + print("unknown msg", raw) + return msg_chain + + +def parse_grp_msg(pb: dict): + user_id, uid, seq, time, rand = parse_msg_info(pb) + + grp_id = unpack_dict(pb, "1.8.1") + grp_name = unpack_dict(pb, "1.8.7") + sub_id = unpack_dict(pb, "1.4", 0) # some client may not report it, old pcqq? + sender = unpack_dict(pb, "1.8.4") + parsed_msg = parse_msg(unpack_dict(pb, "3.1.2")) + if isinstance(sender, dict): # admin or + sender_name = unpack_dict(sender, "1.-1.2") + else: + sender_name = sender + + if isinstance(grp_name, bytes): # unexpected end of data + grp_name = grp_name.decode("utf-8", errors="ignore") + + display_msg = "" + msg_chain: List[elems.T] = [] + for m in parsed_msg: + if "text" in m: + try: + display_msg += m["text"] + except TypeError: + # dec proto err, fallback + m["text"] = proto_encode(m["text"]) # noqa + display_msg += m["text"].decode() # noqa + + obj_name = m.pop("type").capitalize() + if hasattr(elems, obj_name): + msg_chain.append( + getattr(elems, obj_name)(**m) + ) + + msg = GroupMessage( + uin=user_id, + uid=uid, + nickname=sender_name, + seq=seq, + time=time, + rand=rand, + grp_id=grp_id, + grp_name=grp_name, + sub_id=sub_id, + msg=display_msg, + msg_chain=msg_chain + ) + + return msg diff --git a/lagrange/client/message/elems.py b/lagrange/client/message/elems.py new file mode 100644 index 0000000..8eba1f6 --- /dev/null +++ b/lagrange/client/message/elems.py @@ -0,0 +1,71 @@ +import json +from typing import TypeVar +from dataclasses import dataclass + +from lagrange.info.serialize import JsonSerializer + +T = TypeVar('T', "Text", "AtAll", "At", "Image", "Emoji", "Json") + + +@dataclass +class BaseElem(JsonSerializer): + @property + def display(self) -> str: + return "" + + @property + def type(self) -> str: + return self.__class__.__name__.lower() + + +@dataclass +class Text(BaseElem): + text: str + + @property + def display(self) -> str: + return self.text + + +@dataclass +class Quote(Text): + seq: int + uin: int + uid: str + timestamp: int + + +@dataclass +class Json(Text): + raw: bytes + + def to_dict(self) -> dict: + return json.loads(self.raw) + + +@dataclass +class Service(Json): + id: int + + +@dataclass +class AtAll(Text): + ... + + +@dataclass +class At(Text): + uin: int + uid: str + + +@dataclass +class Image(Text): + url: str + name: str + is_emoji: bool + + +@dataclass +class Emoji(BaseElem): + id: int diff --git a/lagrange/client/message/encoder.py b/lagrange/client/message/encoder.py new file mode 100644 index 0000000..ad6c3cc --- /dev/null +++ b/lagrange/client/message/encoder.py @@ -0,0 +1,46 @@ +import struct +from typing import List, Dict, Any +from .elems import ( + T, + At, + AtAll, + Image, + Json, + Text, + Emoji, +) + + +def build_message(msg_chain: List[T], compatible=True) -> dict: + msg_pb: List[Dict[int, Any]] = [] + for msg in msg_chain: + if isinstance(msg, Text): + msg_pb.append({1: {1: msg.text}}) + elif isinstance(msg, AtAll): + msg_pb.append({ + 1: { + 1: msg.text, + 6: b"\x00\x01\x00\x00\x00\x05\x01\x00\x00\x00\x00\x00\x00" + } + }) + elif isinstance(msg, At): + msg_pb.append({ + 1: { + 1: msg.text, + 6: struct.pack("!xb3xbbI2x", 1, len(msg.text), 0, msg.uin) + } + }) + elif isinstance(msg, Emoji): + msg_pb.append({ + 2: { + 1: msg.id + } + }) + else: + raise NotImplementedError + + return { + 1: { + 2: msg_pb + } + } \ No newline at end of file diff --git a/lagrange/client/network.py b/lagrange/client/network.py new file mode 100644 index 0000000..1dcf3c6 --- /dev/null +++ b/lagrange/client/network.py @@ -0,0 +1,102 @@ +""" +ClientNetwork Implement +""" +import asyncio +from typing import Dict, overload, Callable, Coroutine +from typing_extensions import Literal + +from lagrange.info import SigInfo +from lagrange.utils.network import Connection +from lagrange.utils.log import logger + +from .wtlogin.sso import parse_sso_header, parse_sso_frame, SSOPacket + + +class ClientNetwork(Connection): + default_upstream = ("msfwifi.3g.qq.com", 8080) + + def __init__( + self, + sig_info: SigInfo, + push_store: asyncio.Queue[SSOPacket], + reconnect_cb: Callable[[], Coroutine], + disconnect_cb: Callable[[], Coroutine], + host: str = "", + port: int = 0 + ): + if not (host and port): + host, port = self.default_upstream + super().__init__(host, port) + + self.conn_event = asyncio.Event() + self._push_store = push_store + self._reconnect_cb = reconnect_cb + self._disconnect_cb = disconnect_cb + self._wait_fut_map: Dict[int, asyncio.Future[SSOPacket]] = {} + self._connected = False + self._sig = sig_info + + async def write(self, buf: bytes): + await self.conn_event.wait() + self.writer.write(buf) + await self.writer.drain() + + @overload + async def send(self, buf: bytes, wait_seq: Literal["-1"] = -1, timeout=5) -> None: + ... + + @overload + async def send(self, buf: bytes, wait_seq=-1, timeout=5) -> SSOPacket: + ... + + async def send(self, buf: bytes, wait_seq=-1, timeout=5): + await self.write(buf) + if wait_seq != -1: + fut: asyncio.Future[SSOPacket] = asyncio.Future() + self._wait_fut_map[wait_seq] = fut + try: + await asyncio.wait_for(fut, timeout=timeout) + return fut.result() + finally: + self._wait_fut_map.pop(wait_seq) # noqa + + async def on_connected(self): + self.conn_event.set() + host, port = self.writer.get_extra_info('peername') + logger.network.info(f"Connected to {host}:{port}") + if self._connected and not self._stop_flag: + t = asyncio.create_task(self._reconnect_cb(), name="reconnect_cb") + else: + self._connected = True + + async def on_disconnect(self): + self.conn_event.clear() + logger.network.warning("Connection lost") + t = asyncio.create_task(self._disconnect_cb(), name="disconnect_cb") + + async def on_error(self) -> bool: + logger.network.exception("Connection got an unexpected error:") + return True + + async def on_message(self, msg_len: int): + raw = await self.reader.readexactly(msg_len) + enc_flag, uin, sso_body = parse_sso_header(raw, self._sig.d2_key) + + packet = parse_sso_frame(sso_body, enc_flag == 2) + + if packet.seq > 0: # uni rsp + logger.network.debug(f"{packet.seq}({packet.ret_code})-> {packet.cmd or packet.extra}") + if packet.ret_code != 0 and packet.seq in self._wait_fut_map: + return self._wait_fut_map[packet.seq].set_exception( + AssertionError(packet.ret_code, packet.extra) + ) + elif packet.ret_code != 0: + return logger.network.error(f"Unexpected error on sso layer: {packet.ret_code}: {packet.extra}") + + if packet.seq not in self._wait_fut_map: + logger.network.warning(f"Unknown packet: {packet.cmd}({packet.seq}), ignore") + else: + self._wait_fut_map[packet.seq].set_result(packet) + else: # server pushed + logger.network.debug(f"{packet.seq}({packet.ret_code})<- {packet.cmd or packet.extra}") + await self._push_store.put(packet) diff --git a/lagrange/client/ntlogin.py b/lagrange/client/ntlogin.py new file mode 100644 index 0000000..db58d7f --- /dev/null +++ b/lagrange/client/ntlogin.py @@ -0,0 +1,84 @@ +from lagrange.utils.log import logger +from lagrange.info import AppInfo, DeviceInfo, SigInfo +from lagrange.utils.binary.protobuf import proto_encode, proto_decode +from lagrange.utils.crypto.aes import aes_gcm_encrypt, aes_gcm_decrypt +from .wtlogin.enum import LoginErrorCode + + +def build_ntlogin_captcha_submit(ticket: str, rand_str: str, aid: str): + return { + 1: ticket, + 2: rand_str, + 3: aid + } + + +def build_ntlogin_request(uin: int, app: AppInfo, device: DeviceInfo, sig: SigInfo, credential: bytes) -> bytes: + body = { + 1: { + 1: { + 1: str(uin) + }, + 2: { + 1: app.os, + 2: device.device_name, + 3: app.nt_login_type, + 4: bytes.fromhex(device.guid) + }, + 3: { + 1: device.kernel_version, + 2: app.app_id, + 3: app.package_name + } + }, + 2: { + 1: credential + } + } + + if sig.cookies: + body[1][5] = {1: sig.cookies} + if all(sig.captcha_info): + logger.login.debug("login with captcha info") + body[2][2] = build_ntlogin_captcha_submit(*sig.captcha_info) + + return proto_encode({ + 1: sig.key_sig, + 3: aes_gcm_encrypt(proto_encode(body), sig.exchange_key), + 4: 1 + }) + + +def parse_ntlogin_response(response: bytes, sig: SigInfo) -> LoginErrorCode: + frame = proto_decode(response, 0) + body = proto_decode( + aes_gcm_decrypt(frame[3], sig.exchange_key), 2 + ) + + if 1 in body.get(2, {}): + cr = body[2][1] + sig.tgt = cr[4] + sig.d2 = cr[5] + sig.d2_key = cr[6] + sig.temp_pwd = cr[3] + + logger.login.debug("SigInfo got") + + return LoginErrorCode.success + else: + ret = LoginErrorCode(body[1][4][1]) + if ret == LoginErrorCode.captcha_verify: + sig.cookies = body[1][5][1] + verify_url: str = body[2][2][3] + aid = verify_url.split("&sid=")[1].split("&")[0] + sig.captcha_info[2] = aid + logger.login.waring("need captcha verify: " + verify_url) + elif 2 in body[1][4]: + stat = body[1][4] + title = stat[2] + content = stat[3] + logger.login.error(f"Login fail on ntlogin({ret.name}): [{title}]>{content}") + else: + logger.login.error(f"Login fail: {ret.name}") + + return ret diff --git a/lagrange/client/oicq.py b/lagrange/client/oicq.py deleted file mode 100644 index 6dc7d15..0000000 --- a/lagrange/client/oicq.py +++ /dev/null @@ -1,149 +0,0 @@ -import os -import time - -from lagrange.info import DeviceInfo, AppInfo, SigInfo -from lagrange.utils.binary.builder import Builder -from lagrange.utils.binary.protobuf import proto_encode -from lagrange.utils.crypto.ecdh import ecdh -from lagrange.utils.crypto.tea import qqtea_encrypt - - -def build_code2d_packet( - uin: int, - seq: int, - cmd_id: int, - app_info: AppInfo, - device_info: DeviceInfo, - sig_info: SigInfo, - body: bytes -) -> bytes: - return build_login_packet( - uin, - seq, - "wtlogin.trans_emp", - app_info, - device_info, - sig_info, - ( - Builder() - .write_u8(0) - .write_u16(len(body) + 53) - .write_u32(app_info.app_id) - .write_u32(0x72) - .write_bytes(bytes(3)) - .write_u32(int(time.time())) - .write_u8(2) - - .write_u16(len(body) + 49) - .write_u16(cmd_id) - .write_bytes(bytes(21)) - .write_u8(3) - .write_u32(50) - .write_bytes(bytes(14)) - .write_u32(app_info.app_id) - .write_bytes(body) - ).pack() - ) - - -def build_login_packet( - uin: int, - seq: int, - cmd: str, - app_info: AppInfo, - device_info: DeviceInfo, - sig_info: SigInfo, - body: bytes -) -> bytes: - enc_body = qqtea_encrypt(body, ecdh["secp192k1"].share_key) - - frame_body = ( - Builder() - .write_u16(8001) - .write_u16(2066 if cmd == "wtlogin.login" else 2064) - .write_u16(0) - .write_u32(uin) - .write_u8(3) - .write_u8(135) - .write_u32(0) - .write_u8(19) - .write_u16(0) - .write_u16(app_info.app_client_version) - .write_u32(0) - .write_u8(1) - .write_u8(1) - .write_bytes(bytes(16)) - .write_u16(0x102) - .write_u16(len(ecdh["secp192k1"].public_key)) - .write_bytes(ecdh["secp192k1"].public_key) - .write_bytes(enc_body) - .write_u8(3) - ).pack() - - frame = ( - Builder() - .write_u8(2) - .write_u16(len(frame_body) + 3) # + 2 + 1 - .write_bytes(frame_body) - ).pack() - - return build_uni_packet( - uin, - seq, - cmd, - app_info, - device_info, - sig_info, - frame - ) - - -def build_uni_packet( - uin: int, - seq: int, - cmd: str, - app_info: AppInfo, - device_info: DeviceInfo, - sig_info: SigInfo, - body: bytes -) -> bytes: - trace = f"00-{os.urandom(16).hex()}-{os.urandom(8).hex()}-01" - head = proto_encode({ - 15: trace, - 16: uin - }) - - sso_header = ( - Builder() - .write_u32(seq) - .write_u32(app_info.sub_app_id) - .write_u32(2052) # locale id - .write_bytes(bytes.fromhex("020000000000000000000000")) - .write_bytes(sig_info.tgt, "u32", True) - .write_string(cmd, "u32", True) - .write_bytes(b"", "u32", True) - .write_string(device_info.guid, "u32", True) - .write_bytes(b"", "u32", True) - .write_bytes(app_info.current_version, "u16", True) - .write_bytes(head, "u32", True) - ).pack() - - sso_packet = ( - Builder() - .write_bytes(sso_header, "u32", True) - .write_bytes(body, "u32", True) - ).pack() - - encrypted = qqtea_encrypt(sso_packet, sig_info.d2_key) - - service = ( - Builder() - .write_u32(12) - .write_u8(2 if sig_info.d2 else 1) - .write_bytes(sig_info.d2, "u8", True) - .write_u8(0) - .write_string(str(uin)) - .write_bytes(encrypted) - ).pack() - - return Builder().write_bytes(service, "u32", True).pack() diff --git a/lagrange/client/packet.py b/lagrange/client/packet.py new file mode 100644 index 0000000..5154f08 --- /dev/null +++ b/lagrange/client/packet.py @@ -0,0 +1,41 @@ +from typing_extensions import Literal, Self + +from lagrange.utils.binary.builder import Builder, BYTES_LIKE + +LENGTH_PREFIX = Literal["none", "u8", "u16", "u32", "u64"] + + +class PacketBuilder(Builder): + def write_bytes(self, v: BYTES_LIKE, prefix: LENGTH_PREFIX = "none", with_prefix: bool = True) -> Self: + if with_prefix: + if prefix == "none": + pass + elif prefix == "u8": + self.write_u8(len(v) + 1) + elif prefix == "u16": + self.write_u16(len(v) + 2) + elif prefix == "u32": + self.write_u32(len(v) + 4) + elif prefix == "u64": + self.write_u64(len(v) + 8) + else: + raise ArithmeticError("Invaild prefix") + else: + if prefix == "none": + pass + elif prefix == "u8": + self.write_u8(len(v)) + elif prefix == "u16": + self.write_u16(len(v)) + elif prefix == "u32": + self.write_u32(len(v)) + elif prefix == "u64": + self.write_u64(len(v)) + else: + raise ArithmeticError("Invaild prefix") + + self._buffer += v + return self + + def write_string(self, s: str, prefix: LENGTH_PREFIX = "u32", with_prefix: bool = True) -> Self: + return self.write_bytes(s.encode(), prefix, with_prefix) diff --git a/lagrange/client/server_push/__init__.py b/lagrange/client/server_push/__init__.py new file mode 100644 index 0000000..89ae36f --- /dev/null +++ b/lagrange/client/server_push/__init__.py @@ -0,0 +1,4 @@ +from . import msg +from .binder import push_handler + +__all__ = ["push_handler"] diff --git a/lagrange/client/server_push/binder.py b/lagrange/client/server_push/binder.py new file mode 100644 index 0000000..85c4263 --- /dev/null +++ b/lagrange/client/server_push/binder.py @@ -0,0 +1,30 @@ +import functools +from typing import Coroutine, Dict, Callable, Any + +from lagrange.client.wtlogin.sso import SSOPacket + +from .log import logger + + +class PushDeliver: + def __init__(self): + self._handle_map: Dict[str, Callable[[SSOPacket], Coroutine[None, None, Any]]] = {} + + def subscribe(self, cmd: str): + def _decorator(func: Callable[[SSOPacket], Coroutine[None, None, Any]]): + @functools.wraps(func) + async def _wrapper(packet: SSOPacket): + return await func(packet) + + self._handle_map[cmd] = _wrapper # noqa + + return _decorator + + async def execute(self, cmd: str, sso: SSOPacket): + if cmd not in self._handle_map: + logger.warning("unsupported command: {}".format(cmd)) + else: + return await self._handle_map[cmd](sso) + + +push_handler = PushDeliver() diff --git a/lagrange/client/server_push/events/__init__.py b/lagrange/client/server_push/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lagrange/client/server_push/events/group.py b/lagrange/client/server_push/events/group.py new file mode 100644 index 0000000..1f18b88 --- /dev/null +++ b/lagrange/client/server_push/events/group.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass, field +from typing import List +from client.message.elems import T + + +@dataclass +class MessageInfo: + uid: str + seq: int + time: int + rand: int + + +@dataclass +class GroupMessage(MessageInfo): + uin: int + grp_id: int + grp_name: str + nickname: str + sub_id: int = field(repr=False) # client ver identify + msg: str + msg_chain: List[T] + + +@dataclass +class GroupRecall(MessageInfo): + grp_id: int + suffix: str + + +@dataclass +class GroupMuteMember: + """when target_uid is empty, mute all member""" + operator_uid: str + target_uid: str + grp_id: int + duration: int diff --git a/lagrange/client/server_push/log.py b/lagrange/client/server_push/log.py new file mode 100644 index 0000000..24d9943 --- /dev/null +++ b/lagrange/client/server_push/log.py @@ -0,0 +1,3 @@ +from lagrange.utils.log import logger as _logger + +logger = _logger.fork("server_push") diff --git a/lagrange/client/server_push/msg.py b/lagrange/client/server_push/msg.py new file mode 100644 index 0000000..5e874b3 --- /dev/null +++ b/lagrange/client/server_push/msg.py @@ -0,0 +1,53 @@ +from lagrange.client.message.decoder import parse_grp_msg +from lagrange.utils.binary.reader import Reader +from lagrange.utils.binary.protobuf import proto_decode +from lagrange.utils.operator import unpack_dict + +from .log import logger +from .binder import push_handler +from .events.group import GroupRecall, GroupMuteMember +from ..wtlogin.sso import SSOPacket + + +@push_handler.subscribe("trpc.msg.olpush.OlPushService.MsgPush") +async def msg_push_handler(sso: SSOPacket): + pb = proto_decode(sso.data)[1] + typ = unpack_dict(pb, "2.1") + + logger.debug("msg_push received, type:{}".format(typ)) + if typ == 82: # grp msg + return parse_grp_msg(pb) + elif typ == 166: # frd msg + pass + elif typ == 0x210: + print(210, pb) + elif typ == 0x2dc: # grp event, 732 + sub_typ = unpack_dict(pb, "2.2") + if sub_typ == 20: # nudget(grp_id only) + pass + if sub_typ == 17: # recall + reader = Reader(unpack_dict(pb, "3.2")) + grp_id = reader.read_u32() + reader.read_u8() # reserve + in_pb = unpack_dict( + proto_decode(reader.read_bytes_with_length("u16", False)), + "11" + ) + + info = in_pb[3] + return GroupRecall( + uid=info[6], + seq=info[1], + time=info[2], + rand=info[3], + grp_id=grp_id, + suffix=unpack_dict(in_pb, "9.2", "").strip() + ) + elif sub_typ == 12: # mute + info = unpack_dict(pb, "3.2") + return GroupMuteMember( + grp_id=info[1], + operator_uid=info[4], + target_uid=unpack_dict(info, "5.3.1", ""), + duration=unpack_dict(info, "5.3.2") + ) diff --git a/lagrange/client/wtlogin/enum.py b/lagrange/client/wtlogin/enum.py index 16f8b5d..f7a7c7e 100644 --- a/lagrange/client/wtlogin/enum.py +++ b/lagrange/client/wtlogin/enum.py @@ -7,3 +7,35 @@ class QrCodeResult(IntEnum): waiting_for_scan = 48 waiting_for_confirm = 53 canceled = 54 + + @property + def waitable(self) -> bool: + if self in (self.waiting_for_scan, self.waiting_for_confirm): + return True + return False + + @property + def success(self) -> bool: + return self == self.confirmed + + +class LoginErrorCode(IntEnum): + token_expired = 140022015 + unusual_verify = 140022011 + login_failure = 140022013 + user_token_expired = 140022016 + server_failure = 140022002 # unknown reason + wrong_captcha = 140022007 + wrong_argument = 140022001 + new_device_verify = 140022010 + captcha_verify = 140022008 + unknown_error = -1 + success = 0 + + @classmethod + def _missing_(cls, value): + return cls.unknown_error + + @property + def successful(self) -> bool: + return self == self.success diff --git a/lagrange/client/wtlogin/exchange.py b/lagrange/client/wtlogin/exchange.py new file mode 100644 index 0000000..ea4a1d3 --- /dev/null +++ b/lagrange/client/wtlogin/exchange.py @@ -0,0 +1,51 @@ +import time +import hashlib + +from lagrange.info import SigInfo +from lagrange.utils.operator import timestamp +from lagrange.utils.binary.builder import Builder +from lagrange.utils.binary.protobuf import proto_encode, proto_decode +from lagrange.utils.crypto.aes import aes_gcm_encrypt, aes_gcm_decrypt +from lagrange.utils.crypto.ecdh import ecdh + +_enc_key = bytes.fromhex("e2733bf403149913cbf80c7a95168bd4ca6935ee53cd39764beebe2e007e3aee") + + +def build_key_exchange_request(uin: int, guid: str) -> bytes: + p1 = proto_encode({ + 1: uin, + 2: guid + }) + + enc1 = aes_gcm_encrypt(p1, ecdh["prime256v1"].share_key) + + p2 = ( + Builder() + .write_bytes(ecdh["prime256v1"].public_key) + .write_u32(1) + .write_bytes(enc1) + .write_u32(0) + .write_u32(timestamp()) + ).pack() + p2_hash = hashlib.sha256(p2).digest() + enc_p2_hash = aes_gcm_encrypt(p2_hash, _enc_key) + + return proto_encode({ + 1: ecdh["prime256v1"].public_key, + 2: 1, + 3: enc1, + 4: timestamp, + 5: enc_p2_hash + }) + + +def parse_key_exchange_response(response: bytes, sig: SigInfo): + p = proto_decode(response, 0) + + share_key = ecdh["prime256v1"].exchange(p[3]) + dec_pb = proto_decode( + aes_gcm_decrypt(p[1], share_key), 0 + ) + + sig.exchange_key = dec_pb[1] + sig.key_sig = dec_pb[2] diff --git a/lagrange/client/wtlogin/oicq.py b/lagrange/client/wtlogin/oicq.py new file mode 100644 index 0000000..9bbed7e --- /dev/null +++ b/lagrange/client/wtlogin/oicq.py @@ -0,0 +1,186 @@ +import hashlib +import os + +from lagrange.info import DeviceInfo, AppInfo, SigInfo +from lagrange.utils.log import logger +from lagrange.utils.operator import timestamp +from lagrange.utils.binary.protobuf import proto_encode, proto_decode +from lagrange.utils.crypto.ecdh import ecdh +from lagrange.utils.crypto.tea import qqtea_encrypt, qqtea_decrypt +from lagrange.client.packet import PacketBuilder +from lagrange.utils.binary.reader import Reader + + +def build_code2d_packet( + uin: int, + cmd_id: int, + app_info: AppInfo, + body: bytes +) -> bytes: + """need build_uni_packet function to wrapper""" + return build_login_packet( + uin, + "wtlogin.trans_emp", + app_info, + ( + PacketBuilder() + .write_u8(0) + .write_u16(len(body) + 53) + .write_u32(app_info.app_id) + .write_u32(0x72) + .write_bytes(bytes(3)) + .write_u32(timestamp()) + .write_u8(2) + + .write_u16(len(body) + 49) + .write_u16(cmd_id) + .write_bytes(bytes(21)) + .write_u8(3) + .write_u32(50) + .write_bytes(bytes(14)) + .write_u32(app_info.app_id) + .write_bytes(body) + ).pack() + ) + + +def build_login_packet( + uin: int, + cmd: str, + app_info: AppInfo, + body: bytes +) -> bytes: + enc_body = qqtea_encrypt(body, ecdh["secp192k1"].share_key) + + frame_body = ( + PacketBuilder() + .write_u16(8001) + .write_u16(2064 if cmd == "wtlogin.login" else 2066) + .write_u16(0) + .write_u32(uin) + .write_u8(3) + .write_u8(135) + .write_u32(0) + .write_u8(19) + .write_u16(0) + .write_u16(app_info.app_client_version) + .write_u32(0) + .write_u8(1) + .write_u8(1) + .write_bytes(bytes(16)) + .write_u16(0x102) + .write_u16(len(ecdh["secp192k1"].public_key)) + .write_bytes(ecdh["secp192k1"].public_key) + .write_bytes(enc_body) + .write_u8(3) + ).pack() + + frame = ( + PacketBuilder() + .write_u8(2) + .write_u16(len(frame_body) + 3) # + 2 + 1 + .write_bytes(frame_body) + ).pack() + + return frame + + +def build_uni_packet( + uin: int, + seq: int, + cmd: str, + sign: dict, + app_info: AppInfo, + device_info: DeviceInfo, + sig_info: SigInfo, + body: bytes +) -> bytes: + trace = f"00-{os.urandom(16).hex()}-{os.urandom(8).hex()}-01" + + head = { + 15: trace, + 16: sig_info.uid + } + if sign: + head[24] = { + 1: bytes.fromhex(sign["sign"]), + 2: bytes.fromhex(sign["token"]), + 3: bytes.fromhex(sign["extra"]) + } + + sso_header = ( + PacketBuilder() + .write_u32(seq) + .write_u32(app_info.sub_app_id) + .write_u32(2052) # locale id + .write_bytes(bytes.fromhex("020000000000000000000000")) + .write_bytes(sig_info.tgt, "u32") + .write_string(cmd, "u32") + .write_bytes(b"", "u32") + .write_bytes(bytes.fromhex(device_info.guid), "u32") + .write_bytes(b"", "u32") + .write_string(app_info.current_version, "u16") + .write_bytes(proto_encode(head), "u32") + ).pack() + + sso_packet = ( + PacketBuilder() + .write_bytes(sso_header, "u32") + .write_bytes(body, "u32") + ).pack() + + encrypted = qqtea_encrypt(sso_packet, sig_info.d2_key) + + service = ( + PacketBuilder() + .write_u32(12) + .write_u8(2 if len(sig_info.d2) == 0 else 1) + .write_bytes(sig_info.d2, "u32") + .write_u8(0) + .write_string(str(uin), "u32") + .write_bytes(encrypted) + ).pack() + + return PacketBuilder().write_bytes(service, "u32").pack() + + +def decode_login_response(buf: bytes, sig: SigInfo): + reader = Reader(buf) + reader.read_bytes(2) + typ = reader.read_u8() + tlv = reader.read_tlv() + + if typ == 0: + reader = Reader(qqtea_decrypt(tlv[0x119], sig.tgtgt)) + tlv = reader.read_tlv() + sig.tgt = tlv.get(0x10a) or sig.tgt + sig.d2 = tlv.get(0x143) or sig.d2 + sig.d2_key = tlv.get(0x305) or sig.d2_key + sig.tgtgt = hashlib.md5(sig.d2_key).digest() + sig.temp_pwd = tlv[0x106] + sig.uid = proto_decode(tlv[0x543])[9][11][1] + + logger.login.debug("SigInfo got") + + print("info:", tlv[0x11a]) + + return True + elif 0x146 in tlv: + err_buf = Reader(tlv[0x146]) + err_buf.read_bytes(4) + title = err_buf.read_string(err_buf.read_u16()) + content = err_buf.read_string(err_buf.read_u16()) + elif 0x149 in tlv: + err_buf = Reader(tlv[0x149]) + err_buf.read_bytes(2) + title = err_buf.read_string(err_buf.read_u16()) + content = err_buf.read_string(err_buf.read_u16()) + else: + title = "未知错误" + content = "无法解析错误原因,请将完整日志提交给开发者" + + logger.login.error( + f"Login fail on oicq({hex(typ)}): [{title}]>{content}" + ) + + return False diff --git a/lagrange/client/wtlogin/sso.py b/lagrange/client/wtlogin/sso.py new file mode 100644 index 0000000..c1b1771 --- /dev/null +++ b/lagrange/client/wtlogin/sso.py @@ -0,0 +1,97 @@ +import struct +import zlib +from io import BytesIO +from dataclasses import dataclass, field +from typing import Tuple + +from lagrange.utils.binary.reader import Reader +from lagrange.utils.crypto.tea import qqtea_decrypt +from lagrange.utils.crypto.ecdh import ecdh + + +@dataclass +class SSOPacket: + seq: int + ret_code: int + extra: str + session_id: bytes + cmd: str = field(default="") + data: bytes = field(default=b"") + + +def parse_lv(buffer: BytesIO): # u32 len only + length = struct.unpack('>I', buffer.read(4))[0] + return buffer.read(length - 4) + + +def parse_sso_header(raw: bytes, d2_key: bytes) -> Tuple[int, str, bytes]: + buf = BytesIO(raw) + # parse sso header + buf.read(4) + flag, _ = struct.unpack("!BB", buf.read(2)) + uin = parse_lv(buf).decode() + + if flag == 0: # no encrypted + dec = buf.read() + elif flag == 1: # enc with d2key + dec = qqtea_decrypt(buf.read(), d2_key) + elif flag == 2: # enc with \x00*16 + dec = qqtea_decrypt(buf.read(), bytes(16)) + else: + raise TypeError(f"invalid encrypt flag: {flag}") + return flag, uin, dec + + +def parse_sso_frame( + buffer: bytes, + is_oicq_body=False +) -> SSOPacket: + reader = Reader(buffer) + head_len, seq, ret_code = reader.read_struct("!I2i") + extra = reader.read_string_with_length("u32") # extra + cmd = reader.read_string_with_length("u32") + session_id = reader.read_bytes_with_length("u32") + + if ret_code != 0: + return SSOPacket(seq=seq, ret_code=ret_code, session_id=session_id, extra=extra) + + compress_type = reader.read_u32() + reader.read_bytes_with_length("u32", False) + + data = reader.read_bytes_with_length("u32", False) + if data: + if compress_type == 0: + pass + elif compress_type == 1: + data = zlib.decompress(data) + elif compress_type == 8: + data = data[4:] + else: + raise TypeError(f"Unsupported compress type {compress_type}") + + if is_oicq_body and cmd.find("wtlogin") == 0: + data = parse_oicq_body(data) + + return SSOPacket( + seq=seq, + ret_code=ret_code, + session_id=session_id, + extra=extra, + cmd=cmd, + data=data + ) + + +def parse_oicq_body( + buffer: bytes +) -> bytes: + flag, enc_type = struct.unpack("!B12xHx", buffer[:16]) + + if flag != 2: + raise ValueError(f"Invalid OICQ response flag. Expected 2, got {flag}.") + + body = buffer[16:-1] + if enc_type == 0: + return qqtea_decrypt(body, ecdh["secp192k1"].share_key) + else: + raise ValueError(f"Unknown encrypt type: {enc_type}") diff --git a/lagrange/client/wtlogin/status_service.py b/lagrange/client/wtlogin/status_service.py new file mode 100644 index 0000000..0956533 --- /dev/null +++ b/lagrange/client/wtlogin/status_service.py @@ -0,0 +1,35 @@ +from lagrange.utils.binary.protobuf import proto_encode, proto_decode +from lagrange.info import AppInfo, DeviceInfo + + +# trpc.qq_new_tech.status_svc.StatusService.Register +def build_register_request(app: AppInfo, device: DeviceInfo) -> bytes: + return proto_encode({ + 1: device.guid.upper(), + 2: 0, + 3: app.current_version, + 4: 0, + 5: 2052, # locale id + 6: { + 1: device.device_name, + 2: app.vendor_os.capitalize(), + 3: device.system_kernel, + 4: "", + 5: app.vendor_os + }, + 7: False, # set_mute + 8: False, # register_vendor_type + 9: True # regtype + }) + + +# trpc.qq_new_tech.status_svc.StatusService.SsoHeartBeat +def build_sso_heartbeat_request() -> bytes: + return proto_encode({1: 1}) + + +def parse_register_response(response: bytes) -> bool: + pb = proto_decode(response, 0) + if pb[2] == "register success": + return True + return False diff --git a/lagrange/client/wtlogin/tlv/common.py b/lagrange/client/wtlogin/tlv/common.py index e184aec..9b2aa48 100644 --- a/lagrange/client/wtlogin/tlv/common.py +++ b/lagrange/client/wtlogin/tlv/common.py @@ -1,13 +1,13 @@ import hashlib import random -import time +from lagrange.utils.operator import timestamp from lagrange.info import AppInfo, DeviceInfo +from lagrange.client.packet import PacketBuilder from lagrange.utils.crypto.tea import qqtea_encrypt -from lagrange.utils.binary.builder import Builder -class CommonTlvBuilder(Builder): +class CommonTlvBuilder(PacketBuilder): @classmethod def _rand_u32(cls) -> int: return random.randint(0x0, 0xffffffff) @@ -59,40 +59,40 @@ def t106( app_id: int, app_client_version: int, uin: int, - salt: int, password_md5: bytes, - guid: bytes, + guid: str, tgtgt_key: bytes, ip: bytes = bytes(4), save_password: bool = True, ) -> bytes: - key = hashlib.md5(password_md5 + bytes(4) + cls().write_u32(salt or uin).pack()).digest() + key = hashlib.md5(password_md5 + bytes(4) + cls().write_u32(uin).pack()).digest() body = ( cls().write_struct( - ">HIIIIIQ", + "HIIIIQ", 4, # tgtgt version cls._rand_u32(), 0, # sso_version, depreciated app_id, app_client_version, - uin or salt, + uin, ) - .write_u32(int(time.time())) + .write_u32(timestamp() & 0xffffffff) .write_bytes(ip) .write_bool(save_password) .write_bytes(password_md5) .write_bytes(tgtgt_key) .write_u32(0) - .write_bool(bool(guid)) - .write_u32(1) + .write_bool(True) + .write_bytes(bytes.fromhex(guid)) + .write_u32(0) .write_u32(1) - .write_string(str(uin)) + .write_string(str(uin), "u16", False) ).pack() return cls().write_bytes( qqtea_encrypt(body, key), - with_length=True + "u32" ).pack(0x106) @classmethod @@ -126,7 +126,7 @@ def t124(cls) -> bytes: return cls().write_bytes(bytes(12)).pack(0x124) @classmethod - def t128(cls, app_info_os: str, device_guid: str) -> bytes: + def t128(cls, app_info_os: str, device_guid: bytes) -> bytes: return ( cls() .write_u16(0) @@ -134,25 +134,21 @@ def t128(cls, app_info_os: str, device_guid: str) -> bytes: .write_u8(1) .write_u8(0) .write_u32(0) - .write_string(app_info_os) - .write_string(device_guid) - .write_string("") + .write_string(app_info_os, "u16", False) + .write_bytes(device_guid, "u16", False) + .write_string("", "u16", False) ).pack(0x128) @classmethod def t141( cls, sim_info: bytes, - network_type: int = 0, apn: bytes = bytes(0), - _version: int = 0 ) -> bytes: return ( cls() - .write_u16(_version) - .write_bytes(sim_info, with_length=True) - .write_u16(network_type) - .write_bytes(apn, with_length=True) + .write_bytes(sim_info, "u32", False) + .write_bytes(apn, "u32", False) ).pack(0x141) @classmethod @@ -160,7 +156,7 @@ def t142(cls, apk_id: str, _version: int = 0) -> bytes: return ( cls() .write_u16(_version) - .write_string(apk_id[:32]) + .write_string(apk_id[:32], "u16", False) ).pack(0x142) @classmethod @@ -170,7 +166,7 @@ def t144(cls, tgtgt_key: bytes, app_info: AppInfo, device: DeviceInfo) -> bytes: .write_tlv( cls.t16e(device.device_name), cls.t147(app_info.app_id, app_info.pt_version, app_info.package_name), - cls.t128(app_info.os, device.guid), + cls.t128(app_info.os, bytes.fromhex(device.guid)), cls.t124() ) ).pack(0x144) @@ -186,14 +182,14 @@ def t147(cls, app_id: int, pt_version: str, package_name: str) -> bytes: return ( cls() .write_u32(app_id) - .write_string(pt_version) - .write_string(package_name) + .write_string(pt_version, "u16", False) + .write_string(package_name, "u16", False) ).pack(0x147) @classmethod - def t166(cls, image_type: bytes) -> bytes: + def t166(cls, image_type: int) -> bytes: return ( - cls().write_byte(image_type[0]) + cls().write_byte(image_type) ).pack(0x166) @classmethod @@ -213,7 +209,7 @@ def t177(cls, sdk_version: str, build_time: int = 0) -> bytes: return ( cls() .write_struct("BI", 1, build_time) - .write_string(sdk_version) + .write_string(sdk_version, "u16", False) ).pack(0x177) @classmethod @@ -234,5 +230,5 @@ def t521(cls, product_type: int = 0x13, product_desc: str = "basicim") -> bytes: return ( cls() .write_u32(product_type) - .write_string(product_desc) + .write_string(product_desc, "u16", False) ).pack(0x521) diff --git a/lagrange/client/wtlogin/tlv/qrcode.py b/lagrange/client/wtlogin/tlv/qrcode.py index c9160b7..57c68e6 100644 --- a/lagrange/client/wtlogin/tlv/qrcode.py +++ b/lagrange/client/wtlogin/tlv/qrcode.py @@ -1,7 +1,8 @@ -from lagrange.utils.binary.builder import Builder from lagrange.utils.binary.protobuf import proto_encode +from lagrange.client.packet import PacketBuilder -class QrCodeTlvBuilder(Builder): + +class QrCodeTlvBuilder(PacketBuilder): @classmethod def t11(cls, unusual_sign: bytes) -> bytes: return ( @@ -10,21 +11,21 @@ def t11(cls, unusual_sign: bytes) -> bytes: ).pack(0x11) @classmethod - def t16(cls, sub_appid: int, appid_qrcode: int, guid: bytes, pt_version: str, package_name: str) -> bytes: + def t16(cls, appid: int, sub_appid: int, guid: bytes, pt_version: str, package_name: str) -> bytes: return ( cls() .write_u32(0) + .write_u32(appid) .write_u32(sub_appid) - .write_u32(appid_qrcode) .write_bytes(guid) - .write_string(package_name) - .write_string(pt_version) - .write_string(package_name) + .write_string(package_name, "u16", False) + .write_string(pt_version, "u16", False) + .write_string(package_name, "u16", False) ).pack(0x16) @classmethod def t1b(cls) -> bytes: - return cls().write_struct("8I", 0, 0, 3, 4, 72, 2, 2, 0).pack(0x18) + return cls().write_struct("7IH", 0, 0, 3, 4, 72, 2, 2, 0).pack(0x1B) @classmethod def t1d(cls, misc_bitmap: int) -> bytes: diff --git a/lagrange/info/app.py b/lagrange/info/app.py index e8c70c5..8e72a67 100644 --- a/lagrange/info/app.py +++ b/lagrange/info/app.py @@ -71,4 +71,4 @@ class AppInfo(JsonSerializer): sub_sigmap=0, nt_login_type=5 ) -} \ No newline at end of file +} diff --git a/lagrange/info/device.py b/lagrange/info/device.py index cfee5e1..e468cf7 100644 --- a/lagrange/info/device.py +++ b/lagrange/info/device.py @@ -23,4 +23,4 @@ def generate(cls, uin: Union[str, int]) -> 'DeviceInfo': device_name=f"Lagrange-{md5(uin.encode()).digest()[:4].hex().upper()}", system_kernel=f"{platform.system()} {platform.version()}", kernel_version=platform.version() - ) \ No newline at end of file + ) diff --git a/lagrange/info/serialize.py b/lagrange/info/serialize.py index 554f535..2808c01 100644 --- a/lagrange/info/serialize.py +++ b/lagrange/info/serialize.py @@ -1,13 +1,12 @@ import json -import struct +import pickle import hashlib -from io import BytesIO from abc import ABC from dataclasses import dataclass, asdict -from types import NoneType -from typing_extensions import Self, List +from typing_extensions import Self +from lagrange.utils.binary.reader import Reader from lagrange.utils.binary.builder import Builder @@ -25,7 +24,7 @@ class JsonSerializer(BaseSerializer): @classmethod def load(cls, buffer: bytes) -> Self: return cls( - **json.loads(string) # noqa + **json.loads(buffer) # noqa ) def dump(self) -> bytes: @@ -36,83 +35,30 @@ def dump(self) -> bytes: @dataclass class BinarySerializer(BaseSerializer): - type_map = (None, int, float, str, bytes, bytearray) + def _encode(self) -> bytes: + data = pickle.dumps(self) + data_hash = hashlib.sha256(data).digest() - @classmethod - def _type_to_int(cls, typ) -> int: - if typ not in cls.type_map: - raise TypeError(f"Unsupported type {typ}") - for c, v in enumerate(cls.type_map): - if v == typ: - return c + return ( + Builder() + .write_bytes(data_hash, True) + .write_bytes(data, True) + ).pack() @classmethod - def _parse_data(cls, typ: int, data: bytes): - data_type = cls.type_map[typ] - if data_type == int: - return int.from_bytes(data, byteorder="big") - elif data_type == float: - return struct.unpack("!f", data) - elif data_type == str: - return data.decode() - elif data_type == bytes: - return data - elif data_type == bytearray: - return bytearray(data) - elif data_type is None: - return None - else: - raise NotImplementedError + def _decode(cls, buffer: bytes, verify=True) -> Self: + reader = Reader(buffer) + data_hash = reader.read_bytes_with_length("u16", False) + data = reader.read_bytes_with_length("u16", False) + if verify and data_hash != hashlib.sha256(data).digest(): + raise AssertionError("Data hash does not match") - def _encode(self) -> bytes: - tlvs: List[bytes] = [] - uid = hashlib.sha256() - for k, v in asdict(self).items(): - if isinstance(v, int): - iv = v.to_bytes(8, byteorder="big") - elif isinstance(v, float): - iv = struct.pack("!f", v) - elif isinstance(v, str): - iv = v.encode() - elif isinstance(v, (bytes, bytearray)): - iv = bytes(v) - elif isinstance(v, NoneType): - iv = None - else: - raise NotImplementedError - - tlv = ( - Builder() - .write_bytes(iv) - ).pack(self._type_to_int(type(v))) - - uid.update(tlv) - tlvs.append(tlv) - - return Builder().write_bytes(uid.digest(), with_length=True).write_tlv(*tlvs).pack() + return pickle.loads(data) - @classmethod - def _decode(cls, buffer: bytes, *, strict=True) -> list: - uid_l = int.from_bytes(buffer[0:2], "big") + 2 - uid = buffer[2:uid_l] - data = BytesIO(buffer[uid_l:]) - if strict and hashlib.sha256(data.getbuffer()[2:]).digest() != uid: - raise AssertionError("Invalid UID, digest mismatch") - - pkg_len = struct.unpack(">H", data.read(2))[0] - result = [] - for _ in range(pkg_len): - typ, length = struct.unpack(">HH", data.read(4)) - result.append( - cls._parse_data(typ, data.read(length)) - ) - return result @classmethod def load(cls, buffer: bytes) -> Self: - return cls( - *cls._decode(buffer) # noqa - ) + return cls._decode(buffer) def dump(self) -> bytes: return self._encode() diff --git a/lagrange/info/sig.py b/lagrange/info/sig.py index 96e7211..32b44fe 100644 --- a/lagrange/info/sig.py +++ b/lagrange/info/sig.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import List from .serialize import BinarySerializer @@ -17,6 +18,8 @@ class SigInfo(BinarySerializer): cookies: str unusual_sig: bytes temp_pwd: bytes + uid: str + captcha_info: List[str] = field(default_factory=lambda: ["", "", ""]) @classmethod def new(cls, seq: int) -> "SigInfo": @@ -31,5 +34,6 @@ def new(cls, seq: int) -> "SigInfo": key_sig=bytes(), cookies="", unusual_sig=bytes(), - temp_pwd=bytes() + temp_pwd=bytes(), + uid="" ) diff --git a/lagrange/utils/binary/builder.py b/lagrange/utils/binary/builder.py index 7c122d0..6392945 100644 --- a/lagrange/utils/binary/builder.py +++ b/lagrange/utils/binary/builder.py @@ -5,7 +5,6 @@ from lagrange.utils.crypto.tea import qqtea_encrypt BYTES_LIKE = NewType("BYTES_LIKE", Union[bytes, bytearray, memoryview]) -LENGTH_PREFIX = Literal["none", "u8", "u16", "u32", "u64"] class Builder: @@ -47,24 +46,16 @@ def write_bool(self, v: bool) -> Self: return self._pack("?", v) def write_byte(self, v: int) -> Self: - return self._pack("c", v) - - def write_bytes(self, v: BYTES_LIKE, prefix: LENGTH_PREFIX = "none", with_length: bool = False) -> Self: - if prefix == "none": - return self - elif prefix == "u8": - return self.write_u8(len(v) + 1 if with_length else len(v)) - elif prefix == "u16": - return self.write_u16(len(v) + 2 if with_length else len(v)) - elif prefix == "u32": - return self.write_u32(len(v) + 4 if with_length else len(v)) - elif prefix == "u64": - return self.write_u32(len(v) + 8 if with_length else len(v)) - else: - raise ArithmeticError("Invaild prefix") + return self._pack("b", v) + + def write_bytes(self, v: BYTES_LIKE, with_length=False) -> Self: + if with_length: + self.write_u16(len(v)) + self._buffer += v + return self - def write_string(self, s: str, prefix: LENGTH_PREFIX = "none", with_length: bool = False) -> Self: - return self.write_bytes(s.encode(), prefix, with_length) + def write_string(self, s: str) -> Self: + return self.write_bytes(s.encode(), True) def write_struct(self, struct_fmt: str, *args) -> Self: return self._pack(struct_fmt, *args) diff --git a/lagrange/utils/binary/protobuf/__init__.py b/lagrange/utils/binary/protobuf.py similarity index 74% rename from lagrange/utils/binary/protobuf/__init__.py rename to lagrange/utils/binary/protobuf.py index 86839bb..4b1eaa7 100644 --- a/lagrange/utils/binary/protobuf/__init__.py +++ b/lagrange/utils/binary/protobuf.py @@ -18,10 +18,10 @@ def write_varint(self, v: int) -> Self: while v > 127: buffer[length] = (v & 127) | 128 v >>= 7 - v += 1 + length += 1 buffer[length] = v - self.write_bytes(buffer) + self.write_bytes(buffer[:length+1]) else: self.write_u8(v) @@ -54,7 +54,10 @@ def read_varint(self) -> int: def read_length_delimited(self) -> bytes: length = self.read_varint() - return self.read_bytes(length) + data = self.read_bytes(length) + if len(data) != length: + raise ValueError("length of data does not match") + return data def _encode(builder: ProtoBuilder, tag: int, value: ProtoEncodable): @@ -72,14 +75,14 @@ def _encode(builder: ProtoBuilder, tag: int, value: ProtoEncodable): else: raise Exception("Unsupported wire type in protobuf") - head = tag << 3 | wire_type + head = int(tag) << 3 | wire_type builder.write_varint(head) if wire_type == 0: if isinstance(value, bool): value = 1 if value else 0 - if value > 0: + if value >= 0: builder.write_varint(value) else: raise NotImplemented @@ -93,7 +96,7 @@ def _encode(builder: ProtoBuilder, tag: int, value: ProtoEncodable): raise AssertionError -def proto_decode(data: bytes) -> Proto: +def proto_decode(data: bytes, max_layer=-1) -> Proto: reader = ProtoReader(data) proto = {} @@ -102,14 +105,33 @@ def proto_decode(data: bytes) -> Proto: tag = leaf >> 3 wire_type = leaf & 0b111 + if not tag: + raise AssertionError("Invalid tag") + if wire_type == 0: proto[tag] = reader.read_varint() elif wire_type == 2: value = reader.read_length_delimited() - try: # serialize nested - proto[tag] = proto_decode(value) - except: - proto[tag] = value + + val = None + if max_layer > 0 or max_layer < 0 and len(value) > 1: + try: # serialize nested + val = proto_decode(value, max_layer - 1) + except: + pass + + if not val: + try: + val = value.decode() + except UnicodeDecodeError: + val = value + + if tag in proto: # repeated elem + if not isinstance(proto[tag], list): + proto[tag] = [proto[tag]] + proto[tag].append(val) + else: + proto[tag] = val else: raise AssertionError diff --git a/lagrange/utils/binary/reader.py b/lagrange/utils/binary/reader.py index b6a7c87..2ba1145 100644 --- a/lagrange/utils/binary/reader.py +++ b/lagrange/utils/binary/reader.py @@ -1,7 +1,9 @@ -from typing import Union +import struct +from typing import Union, Tuple, Any, Literal, Dict from typing_extensions import NewType +LENGTH_PREFIX = Literal["u8", "u16", "u32", "u64"] BYTES_LIKE = NewType("BYTES_LIKE", Union[bytes, bytearray, memoryview]) @@ -23,7 +25,67 @@ def read_u8(self) -> int: self._pos += 1 return v + def read_u16(self) -> int: + v = self._buffer[self._pos:self._pos+2] + self._pos += 2 + return struct.unpack(">H", v)[0] + + def read_u32(self) -> int: + v = self._buffer[self._pos:self._pos+4] + self._pos += 4 + return struct.unpack(">I", v)[0] + + def read_u64(self) -> int: + v = self._buffer[self._pos:self._pos+8] + self._pos += 8 + return struct.unpack(">Q", v)[0] + + def read_struct(self, format: str) -> Tuple[Any, ...]: + size = struct.calcsize(format) + v = self._buffer[self._pos:self._pos+size] + self._pos += size + return struct.unpack(format, v) + def read_bytes(self, length: int) -> bytes: v = self._buffer[self._pos:self._pos+length] self._pos += length return v + + def read_string(self, length: int) -> str: + return self.read_bytes(length).decode("utf-8") + + def read_bytes_with_length(self, prefix: LENGTH_PREFIX, with_prefix=True) -> bytes: + if with_prefix: + if prefix == "u8": + length = self.read_u8() - 1 + elif prefix == "u16": + length = self.read_u16() - 2 + elif prefix == "u32": + length = self.read_u32() - 4 + else: + length = self.read_u64() - 8 + else: + if prefix == "u8": + length = self.read_u8() + elif prefix == "u16": + length = self.read_u16() + elif prefix == "u32": + length = self.read_u32() + else: + length = self.read_u64() + v = self._buffer[self._pos:self._pos+length] + self._pos += length + return v + + def read_string_with_length(self, prefix: LENGTH_PREFIX, with_prefix=True) -> str: + return self.read_bytes_with_length(prefix, with_prefix).decode("utf-8") + + def read_tlv(self) -> Dict[int, bytes]: + result = {} + count = self.read_u16() + + for i in range(count): + tag = self.read_u16() + result[tag] = self.read_bytes(self.read_u16()) + + return result diff --git a/lagrange/utils/crypto/aes.py b/lagrange/utils/crypto/aes.py new file mode 100644 index 0000000..98c8009 --- /dev/null +++ b/lagrange/utils/crypto/aes.py @@ -0,0 +1,19 @@ +import secrets + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + +def aes_gcm_encrypt(data: bytes, key: bytes) -> bytes: + nonce = secrets.token_bytes(12) + + cipher = AESGCM(key) + return nonce + cipher.encrypt(nonce, data, None) + + +def aes_gcm_decrypt(data: bytes, key: bytes) -> bytes: + nonce = data[:12] + cipher = AESGCM(key) + return cipher.decrypt(nonce, data[12:], None) + + +__all__ = ["aes_gcm_encrypt", "aes_gcm_decrypt"] diff --git a/lagrange/utils/crypto/ecdh/__init__.py b/lagrange/utils/crypto/ecdh/__init__.py index 1a8ef27..bf10676 100644 --- a/lagrange/utils/crypto/ecdh/__init__.py +++ b/lagrange/utils/crypto/ecdh/__init__.py @@ -1,4 +1,6 @@ from .point import EllipticPoint from .curve import EllipticCurve, CURVE from .ecdh import ECDHProvider -from .impl import * \ No newline at end of file +from .impl import ecdh + +__all__ = ["ecdh"] diff --git a/lagrange/utils/crypto/ecdh/curve.py b/lagrange/utils/crypto/ecdh/curve.py index 16f5fb6..29d61e4 100644 --- a/lagrange/utils/crypto/ecdh/curve.py +++ b/lagrange/utils/crypto/ecdh/curve.py @@ -1,4 +1,4 @@ -from lagrange.utils.crypto.ecdh import EllipticPoint +from .point import EllipticPoint class EllipticCurve: diff --git a/lagrange/utils/crypto/ecdh/ecdh.py b/lagrange/utils/crypto/ecdh/ecdh.py index cd59c29..99ebdaf 100644 --- a/lagrange/utils/crypto/ecdh/ecdh.py +++ b/lagrange/utils/crypto/ecdh/ecdh.py @@ -2,7 +2,7 @@ import hashlib import random -from lagrange.utils.crypto.ecdh import * +from .curve import EllipticCurve, EllipticPoint class ECDHProvider: @@ -52,7 +52,7 @@ def pack_public(self, compress: bool) -> bytes: y = self._public.y.to_bytes(self._curve.size, "big") result = bytearray(1) + x + y result[0] = 0x04 - return result + return bytes(result) def _pack_shared(self, shared: EllipticPoint, hashed: bool) -> bytes: x = shared.x.to_bytes(self._curve.size, "big") diff --git a/lagrange/utils/crypto/ecdh/impl.py b/lagrange/utils/crypto/ecdh/impl.py index bda3778..42250a4 100644 --- a/lagrange/utils/crypto/ecdh/impl.py +++ b/lagrange/utils/crypto/ecdh/impl.py @@ -1,14 +1,23 @@ -from lagrange.utils.crypto.ecdh import ECDHProvider, CURVE - -ECDH_PRIME_PUBLIC = bytes.fromhex("049D1423332735980EDABE7E9EA451B3395B6F35250DB8FC56F25889F628CBAE3E8E73077914071EEEBC108F4E0170057792BB17AA303AF652313D17C1AC815E79") -ECDH_SECP_PUBLIC = bytes.fromhex("04928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8") - - -class ECDHPrime: - def __init__(self): - self._provider = ECDHProvider(CURVE["prime256v1"]) - self._public_key = self._provider.pack_public(False) - self._share_key = self._provider.key_exchange(ECDH_PRIME_PUBLIC, False) +from .ecdh import ECDHProvider +from .curve import CURVE + +ECDH_PRIME_PUBLIC = bytes.fromhex( + "04" + "9D1423332735980EDABE7E9EA451B3395B6F35250DB8FC56F25889F628CBAE3E" + "8E73077914071EEEBC108F4E0170057792BB17AA303AF652313D17C1AC815E79" +) +ECDH_SECP_PUBLIC = bytes.fromhex( + "04" + "928D8850673088B343264E0C6BACB8496D697799F37211DE" + "B25BB73906CB089FEA9639B4E0260498B51A992D50813DA8" +) + + +class BaseECDH: + _provider: ECDHProvider + _public_key: bytes + _share_key: bytes + _compress_key: bool @property def public_key(self) -> bytes: @@ -18,20 +27,24 @@ def public_key(self) -> bytes: def share_key(self) -> bytes: return self._share_key + def exchange(self, new_key: bytes): + return self._provider.key_exchange(new_key, self._compress_key) + -class ECDHSecp: +class ECDHPrime(BaseECDH): # exchange key def __init__(self): - self._provider = ECDHProvider(CURVE["secp192k1"]) # FIXME: Incorrect public key - self._public_key = self._provider.pack_public(True) - self._share_key = self._provider.key_exchange(ECDH_SECP_PUBLIC, True) + self._provider = ECDHProvider(CURVE["prime256v1"]) + self._public_key = self._provider.pack_public(False) + self._share_key = self._provider.key_exchange(ECDH_PRIME_PUBLIC, False) + self._compress_key = False - @property - def public_key(self) -> bytes: - return self._public_key - @property - def share_key(self) -> bytes: - return self._share_key +class ECDHSecp(BaseECDH): # login and others + def __init__(self): + self._provider = ECDHProvider(CURVE["secp192k1"]) + self._public_key = self._provider.pack_public(True) + self._share_key = self._provider.key_exchange(ECDH_SECP_PUBLIC, True) + self._compress_key = True ecdh = { diff --git a/lagrange/utils/httpcat.py b/lagrange/utils/httpcat.py new file mode 100644 index 0000000..df062ec --- /dev/null +++ b/lagrange/utils/httpcat.py @@ -0,0 +1,200 @@ +import gzip +import zlib +import json +import asyncio +import logging +from dataclasses import dataclass +from typing import Dict, Tuple, Union +from urllib import parse + +_logger = logging.getLogger("lagrange.utils.httpcat") + + +@dataclass +class HttpResponse: + code: int + status: str + header: Dict[str, str] + body: bytes + cookies: Dict[str, str] + + @property + def decompressed_body(self) -> bytes: + if "Content-Encoding" in self.header: + if self.header["Content-Encoding"] == "gzip": + return gzip.decompress(self.body) + elif self.header["Content-Encoding"] == "deflate": + return zlib.decompress(self.body) + else: + raise TypeError("Unsuppoted compress type:", self.header["Content-Encoding"]) + else: + return self.body + + def json(self, verify_type=True) -> Union[dict, list]: + if ( + "Content-Type" in self.header and + self.header["Content-Type"].find("application/json") == -1 and + verify_type + ): + raise TypeError(self.header.get("Content-Type", "NotSet")) + return json.loads(self.decompressed_body) + + def text(self, encoding="utf-8", errors="strict") -> str: + return self.decompressed_body.decode(encoding, errors) + + +class HttpCat: + @staticmethod + def _encode_header( + method: str, + path: str, + header: Dict[str, str], + *, + protocol="HTTP/1.1" + ) -> bytearray: + ret = bytearray() + ret += f"{method.upper()} {path} {protocol}\r\n".encode() + for k, v in header.items(): + ret += f"{k}: {v}\r\n".encode() + ret += b"\r\n" + return ret + + @staticmethod + async def _read_line(reader: asyncio.StreamReader) -> str: + return (await reader.readline()).rstrip(b"\r\n").decode() + + @staticmethod + def _parse_url(url: str) -> Tuple[Tuple[str, int], str, bool]: + purl = parse.urlparse(url) + if purl.scheme not in ("http", "https"): + raise ValueError("unsupported scheme:", purl.scheme) + if purl.netloc.find(":") != -1: + host, port = purl.netloc.split(":") + else: + host = purl.netloc + if purl.scheme == "https": + port = 443 + else: + port = 80 + return ( + (host, int(port)), + parse.quote(purl.path) + ("?" + purl.query if purl.query else ""), + purl.scheme == "https" + ) + + @classmethod + async def _read_all(cls, header: dict, reader: asyncio.StreamReader) -> bytes: + if header.get("Transfer-Encoding") == "chunked": + bs = bytearray() + while True: + length = int(await cls._read_line(reader) or "0", 16) + if length: + bs += await reader.readexactly(length) + else: + break + return bytes(bs) + elif "Content-Length" in header: + return await reader.readexactly(int(header["Content-Length"])) + else: + return await reader.read() + + @classmethod + async def _parse_response(cls, reader: asyncio.StreamReader) -> HttpResponse: + stat = await cls._read_line(reader) + if not stat: + raise ConnectionResetError + _, code, status = stat.split(" ", 2) + header = {} + cookies = {} + while True: + head_block = await cls._read_line(reader) + if head_block: + k, v = head_block.split(": ") # type: str + if k.title() == "Set-Cookie": + name, value = v[:v.find(";")].split("=", 1) + cookies[name] = value + else: + header[k.title()] = v + else: + break + return HttpResponse( + int(code), + status, + header, + await cls._read_all(header, reader), + cookies + ) + + @classmethod + async def _request( + cls, + address: Tuple[str, int], + method: str, + path: str, + header: Dict[str, str] = None, + body: bytes = None, + cookies: Dict[str, str] = None, + ssl: bool = False, + loop: asyncio.AbstractEventLoop = None + ) -> HttpResponse: + if not loop: + loop = asyncio.get_running_loop() + header = { + "Host": address[0], + "Connection": "close", + "User-Agent": "HttpCat/1.0", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "0" if not body else str(len(body)), + **(header if header else {}) + } + if cookies: + header["Cookie"] = "; ".join([f"{k}={v}" for k, v in cookies.items()]) + + reader, writer = await asyncio.open_connection(*address, ssl=ssl) + writer.write(cls._encode_header( + method, + path, + header + )) + if body: + writer.write(body) + await writer.drain() + + try: + return await cls._parse_response(reader) + finally: + loop.call_soon(writer.close) + + @classmethod + async def request( + cls, + method: str, + url: str, + header: Dict[str, str] = None, + body: bytes = None, + cookies: Dict[str, str] = None, + follow_redirect=True, + loop: asyncio.AbstractEventLoop = None + ) -> HttpResponse: + address, path, ssl = cls._parse_url(url) + resp = await cls._request( + address, + method, + path, + header, + body, + cookies, + ssl, + loop + ) + _logger.debug(f"request: {method} {url} {resp.code}") + if resp.code // 100 == 3 and follow_redirect: + return await cls.request( + method, + resp.header["Location"], + header, + body, + cookies + ) + else: + return resp diff --git a/lagrange/utils/log.py b/lagrange/utils/log.py new file mode 100644 index 0000000..f4d9980 --- /dev/null +++ b/lagrange/utils/log.py @@ -0,0 +1,43 @@ +import logging +from typing import Optional + +__all__ = ["logger"] + + +class LoggerProvider: + def __init__(self, root: Optional[logging.Logger] = None): + self._root = root or logging.getLogger("lagrange") + self._init() + + def _init(self): + self._login = self._root.getChild("login") + self._network = self._root.getChild("network") + self._utils = self._root.getChild("utils") + + def switch(self, _logger): + if not hasattr(_logger, "getChild"): + raise NotImplementedError("Logger must have getChild method") + self._root = _logger + self._init() + + def fork(self, child_name: str) -> logging.Logger: + return self._root.getChild(child_name) + + @property + def root(self) -> logging.Logger: + return self._root + + @property + def network(self) -> logging.Logger: + return self._network + + @property + def utils(self) -> logging.Logger: + return self._utils + + @property + def login(self) -> logging.Logger: + return self._login + + +logger = LoggerProvider() diff --git a/lagrange/utils/network.py b/lagrange/utils/network.py index 154e12c..775e9a4 100644 --- a/lagrange/utils/network.py +++ b/lagrange/utils/network.py @@ -76,7 +76,7 @@ async def stop(self): async def _read_loop(self): try: while not self.closed: - length = int.from_bytes(await self.reader.readexactly(4), byteorder="big") + length = int.from_bytes(await self.reader.readexactly(4), byteorder="big") - 4 if length: await self.on_message(length) else: @@ -84,9 +84,9 @@ async def _read_loop(self): except asyncio.CancelledError: await self.on_error() await self.stop() - except: + except Exception as e: if not await self.on_error(): - raise + raise e async def loop(self): while not self._stop_flag: diff --git a/lagrange/utils/operator.py b/lagrange/utils/operator.py new file mode 100644 index 0000000..e487a50 --- /dev/null +++ b/lagrange/utils/operator.py @@ -0,0 +1,28 @@ +import time +from typing import TypeVar, overload, Any, Union + +T = TypeVar('T') + + +@overload +def unpack_dict(pd: dict, rule: str) -> Any: ... + + +@overload +def unpack_dict(pd: dict, rule: str, default: T) -> Union[Any, T]: ... + + +def unpack_dict(pd: dict, rule: str, default: Union[T, None] = None) -> Union[Any, T]: + _pd: Any = pd + for r in rule.split("."): + if isinstance(_pd, list) or (isinstance(_pd, dict) and int(r) in _pd): + _pd = _pd[int(r)] + elif default is not None: + return default + else: + raise KeyError(r) + return _pd + + +def timestamp() -> int: + return int(time.time()) diff --git a/lagrange/utils/sign.py b/lagrange/utils/sign.py new file mode 100644 index 0000000..7b805f2 --- /dev/null +++ b/lagrange/utils/sign.py @@ -0,0 +1,78 @@ +import time +from typing import Optional + +from .httpcat import HttpCat +from .log import logger + +_logger = logger.fork("sign_provider") + +SIGN_PKG_LIST = [ + "trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey", + "trpc.o3.ecdh_access.EcdhAccess.SsoSecureAccess", + "trpc.o3.report.Report.SsoReport", + "MessageSvc.PbSendMsg", + # "wtlogin.trans_emp", + "wtlogin.login", + # "trpc.login.ecdh.EcdhService.SsoKeyExchange", + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLogin", + "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLogin", + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginNewDevice", + "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLoginUnusualDevice", + "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginUnusualDevice", + "OidbSvcTrpcTcp.0x11ec_1", + "OidbSvcTrpcTcp.0x758_1", + "OidbSvcTrpcTcp.0x7c2_5", + "OidbSvcTrpcTcp.0x10db_1", + "OidbSvcTrpcTcp.0x8a1_7", + "OidbSvcTrpcTcp.0x89a_0", + "OidbSvcTrpcTcp.0x89a_15", + "OidbSvcTrpcTcp.0x88d_0", + "OidbSvcTrpcTcp.0x88d_14", + "OidbSvcTrpcTcp.0x112a_1", + "OidbSvcTrpcTcp.0x587_74", + "OidbSvcTrpcTcp.0x1100_1", + "OidbSvcTrpcTcp.0x1102_1", + "OidbSvcTrpcTcp.0x1103_1", + "OidbSvcTrpcTcp.0x1107_1", + "OidbSvcTrpcTcp.0x1105_1", + "OidbSvcTrpcTcp.0xf88_1", + "OidbSvcTrpcTcp.0xf89_1", + "OidbSvcTrpcTcp.0xf57_1", + "OidbSvcTrpcTcp.0xf57_106", + "OidbSvcTrpcTcp.0xf57_9", + "OidbSvcTrpcTcp.0xf55_1", + "OidbSvcTrpcTcp.0xf67_1", + "OidbSvcTrpcTcp.0xf67_5", +] + + +def _pack_params(d: dict) -> str: + r = "?" + for k, v in d.items(): + r += "%s=%s&" % (k, v) + return r[:-1] + + +def sign_provider(upstream_url: str): + async def get_sign(cmd: str, seq: int, buf: bytes) -> Optional[dict]: + if cmd not in SIGN_PKG_LIST: + return + + params = { + "cmd": cmd, + "seq": seq, + "src": buf.hex() + } + + start_time = time.time() + ret = await HttpCat.request( + "get", + upstream_url + _pack_params(params) + ) + _logger.debug(f"signed for [{cmd}:{seq}]({round((time.time() - start_time) * 1000, 2)}ms)") + if ret.code != 200: + raise ConnectionError(ret.code, ret.body) + + return ret.json()["value"] + + return get_sign diff --git a/main.py b/main.py index dcb09b4..7155771 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,109 @@ +import os +import logging import asyncio -from lagrange.client.base import BaseClient +from lagrange.utils.sign import sign_provider +from lagrange.client.client import Client from lagrange.info.app import app_list from lagrange.info.device import DeviceInfo from lagrange.info.sig import SigInfo +from lagrange.client.server_push.events.group import GroupMessage +from lagrange.client.message.elems import Text + +DEVICE_INFO_PATH = "./device.json" +SIGINFO_PATH = "./sig.bin" + + +class InfoManager: + def __init__(self, uin: int, device_info_path: str, sig_info_path: str): + self.uin: int = uin + self._device_info_path: str = device_info_path + self._sig_info_path: str = sig_info_path + self._device = None + self._sig_info = None + + @property + def device(self) -> DeviceInfo: + assert self._device, "Device not initialized" + return self._device + + @property + def sig_info(self) -> SigInfo: + assert self._sig_info, "SigInfo not initialized" + return self._sig_info + + def save_all(self): + with open(self._sig_info_path, "wb") as f: + f.write(self._sig_info.dump()) + + with open(self._device_info_path, "wb") as f: + f.write(self._device.dump()) + + print("device info saved") + + def __enter__(self): + if os.path.isfile(self._device_info_path): + with open(self._device_info_path, "rb") as f: + self._device = DeviceInfo.load(f.read()) + else: + print(f"{self._device_info_path} not found, generating...") + self._device = DeviceInfo.generate(self.uin) + + if os.path.isfile(self._sig_info_path): + with open(self._sig_info_path, "rb") as f: + self._sig_info = SigInfo.load(f.read()) + else: + print(f"{self._sig_info_path} not found, generating...") + self._sig_info = SigInfo.new(8848) + return self + + def __exit__(self, *_): + pass + + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(name)s[%(levelname)s]: %(message)s" +) + + +async def heartbeat_task(client: Client): + while True: + await client.online.wait() + await asyncio.sleep(120) + print(f"{round(await client.sso_heartbeat(True) * 1000, 2)}ms to server") + + +async def msg_handler(client: Client, event: GroupMessage): + print(event) + if event.msg.startswith("114514"): + await client.send_grp_msg([Text("1919810")], event.grp_id) + print(f"{event.nickname}({event.grp_name}): {event.msg}") async def main(): - client = BaseClient(114514, app_list['linux'], DeviceInfo.generate(114514), SigInfo.new(500000)) - client.connect() - await client.fetch_qrcode() - await client.wait_closed() + uin = int(os.environ.get("LAGRANGE_UIN", "0")) + sign_url = os.environ.get("LAGRANGE_SIGN_URL", "") + + app = app_list["linux"] + + with InfoManager(uin, DEVICE_INFO_PATH, SIGINFO_PATH) as im: + client = Client( + uin, + app, + im.device, + im.sig_info, + sign_provider(sign_url) if sign_url else None + ) + client.events.subscribe(GroupMessage, msg_handler) + client.connect() + asyncio.create_task(heartbeat_task(client)) + if im.sig_info.d2: + if not await client.register(): + await client.login() + else: + await client.login() + im.save_all() + await client.wait_closed() if __name__ == '__main__': diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..0ae106d --- /dev/null +++ b/pdm.lock @@ -0,0 +1,149 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:f97c64ff0cbf024c770af7c9d6de7aeba092ea8a696c9b0a8d093a7477013ca1" + +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[[package]] +name = "cryptography" +version = "42.0.4" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["default"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, + {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, + {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, + {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, + {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, + {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, + {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, + {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, + {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, + {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, + {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, + {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, + {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, + {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, +] + +[[package]] +name = "crytography" +version = "1.0.0" +summary = "" +groups = ["default"] +files = [ + {file = "crytography-1.0.0.tar.gz", hash = "sha256:557311bf558b3be38dae79de7cebe27f6217ccdbd654085e44c688bb72339766"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "C parser in Python" +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] diff --git a/pyproject.toml b/pyproject.toml index f1859fa..9da2272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,17 @@ authors = [ {name="wyapx"}, ] dependencies = [ - "typing-extensions" + "typing-extensions", + "cryptography" ] requires-python = ">=3.8" [tool.pdm] distribution = true +[tool.pdm.build] +includes = ["lagrange"] + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" From 1bff7ce901c52eef39a064f85a1166b39376d407 Mon Sep 17 00:00:00 2001 From: nullcat Date: Sun, 17 Mar 2024 20:13:27 +0800 Subject: [PATCH 2/3] Create README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a645e2 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# WIP + +main分支是broken的 +所以真正的main分支在? From 682a73cd6a05dfa6717e28482af49b2a4e8fc68c Mon Sep 17 00:00:00 2001 From: nullcat Date: Mon, 18 Mar 2024 00:36:51 +0800 Subject: [PATCH 3/3] Create LICENSE --- LICENSE | 504 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8000a6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it!