├── .gitignore ├── Pipfile ├── README.md ├── handshake.py └── protocol ├── __init__.py ├── mcField.py ├── mcInt.py ├── mcString.py ├── packet.py ├── session.py └── varNum.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | ### JetBrains template 129 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 130 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 131 | 132 | # User-specific stuff 133 | .idea/**/workspace.xml 134 | .idea/**/tasks.xml 135 | .idea/**/usage.statistics.xml 136 | .idea/**/dictionaries 137 | .idea/**/shelf 138 | 139 | # Generated files 140 | .idea/**/contentModel.xml 141 | 142 | # Sensitive or high-churn files 143 | .idea/**/dataSources/ 144 | .idea/**/dataSources.ids 145 | .idea/**/dataSources.local.xml 146 | .idea/**/sqlDataSources.xml 147 | .idea/**/dynamic.xml 148 | .idea/**/uiDesigner.xml 149 | .idea/**/dbnavigator.xml 150 | 151 | # Gradle 152 | .idea/**/gradle.xml 153 | .idea/**/libraries 154 | 155 | # Gradle and Maven with auto-import 156 | # When using Gradle or Maven with auto-import, you should exclude module files, 157 | # since they will be recreated, and may cause churn. Uncomment if using 158 | # auto-import. 159 | # .idea/modules.xml 160 | # .idea/*.iml 161 | # .idea/modules 162 | # *.iml 163 | # *.ipr 164 | 165 | # CMake 166 | cmake-build-*/ 167 | 168 | # Mongo Explorer plugin 169 | .idea/**/mongoSettings.xml 170 | 171 | # File-based project format 172 | *.iws 173 | 174 | # IntelliJ 175 | out/ 176 | 177 | # mpeltonen/sbt-idea plugin 178 | .idea_modules/ 179 | 180 | # JIRA plugin 181 | atlassian-ide-plugin.xml 182 | 183 | # Cursive Clojure plugin 184 | .idea/replstate.xml 185 | 186 | # Crashlytics plugin (for Android Studio and IntelliJ) 187 | com_crashlytics_export_strings.xml 188 | crashlytics.properties 189 | crashlytics-build.properties 190 | fabric.properties 191 | 192 | # Editor-based Rest Client 193 | .idea/httpRequests 194 | 195 | # Android studio 3.1+ serialized cache file 196 | .idea/caches/build_file_checksums.ser 197 | 198 | .idea/.gitignore 199 | .idea/inspectionProfiles/ 200 | .idea/mc_server_list_ping.iml 201 | .idea/misc.xml 202 | .idea/modules.xml 203 | .idea/vcs.xml 204 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mcProtocol 2 | mc工具人 3 | 4 | 目标是实现一个mc无头客户端 5 | -------------------------------------------------------------------------------- /handshake.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : handshake.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : 模拟握手 7 | 8 | # 小写的类名有点难受 9 | from socket import socket as Socket 10 | from protocol import Session, Packet, MCString, VarInt, UnsignedShort 11 | 12 | 13 | def onPacketRecv(packet: Packet): 14 | if packet.id != 0: 15 | return 16 | mcString, = packet.parse([MCString]) 17 | print(mcString.get()) 18 | 19 | 20 | host = "127.0.0.1" 21 | port = 25565 22 | socket = Socket() 23 | socket.connect((host, port)) 24 | session = Session(socket, onPacketRecv) 25 | packet = Packet(id=0) 26 | # 协议号,-1表示获取协议号 27 | packet.addField(VarInt(-1)) 28 | # host 29 | packet.addField(MCString(host)) 30 | # port 31 | packet.addField(UnsignedShort(port)) 32 | # Next state 33 | packet.addField(VarInt(1)) 34 | session.sendPacket(packet) 35 | # 请求包,id=0,字段为空 36 | packet2 = Packet(0) 37 | session.sendPacket(packet2) 38 | -------------------------------------------------------------------------------- /protocol/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : __init__.py.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : import 常用的进来 7 | 8 | from protocol.session import Session 9 | from protocol.mcString import MCString 10 | from protocol.packet import Packet 11 | from protocol.varNum import VarInt, VarLong 12 | from protocol.mcInt import Byte, UnsignedByte, Short, UnsignedShort, Integer, UnsignedInteger, Long, UnsignedLong 13 | -------------------------------------------------------------------------------- /protocol/mcField.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : mcField.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : 字段基类 7 | 8 | 9 | # 这个程序写的很智障的一点在于:read接收的参数是一个bytes或者byteArray而不是一个流 10 | # 不过这个操蛋的点会在session中得到妥善处理 11 | # 主要是python好像处理流这种东西不太得劲 12 | 13 | class MCField: 14 | # 设置这个字段的值 15 | def set(self, value): 16 | raise NotImplementedError() 17 | 18 | # 得到这个字段的值 19 | def get(self): 20 | raise NotImplementedError() 21 | 22 | # 从一个bytes或者byteArray对象中取出这个字段,并返回长度 23 | # 如果数据不完整应该返回-1 24 | def read(self, data) -> int: 25 | raise NotImplementedError 26 | 27 | # 获得字节流格式的数据 28 | @property 29 | def data(self) -> bytearray: 30 | raise NotImplementedError 31 | -------------------------------------------------------------------------------- /protocol/mcInt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : mcInt.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : 不可边长度的整数 7 | 8 | from protocol.mcField import MCField 9 | 10 | 11 | class MCInt(MCField): 12 | def __init__(self, value, length, signed): 13 | self.value = value 14 | self.length = length 15 | self.signed = signed 16 | 17 | def set(self, value): 18 | self.value = value 19 | 20 | def get(self): 21 | return self.value 22 | 23 | def read(self, data) -> int: 24 | if len(data) < self.length: 25 | return -1 26 | data: bytearray = data[:self.length] 27 | self.value = int.from_bytes(data, 'big', signed=self.signed) 28 | return self.length 29 | 30 | @property 31 | def data(self) -> bytearray: 32 | return bytearray(self.value.to_bytes(self.length, 'big', signed=self.signed)) 33 | 34 | 35 | def Byte(value=0): 36 | return MCInt(value, 1, True) 37 | 38 | 39 | def Short(value=0): 40 | return MCInt(value, 2, True) 41 | 42 | 43 | def Integer(value=0): 44 | return MCInt(value, 4, True) 45 | 46 | 47 | def Long(value=0): 48 | return MCInt(value, 8, True) 49 | 50 | 51 | def UnsignedByte(value=0): 52 | return MCInt(value, 1, False) 53 | 54 | 55 | def UnsignedShort(value=0): 56 | return MCInt(value, 2, False) 57 | 58 | 59 | def UnsignedInteger(value=0): 60 | return MCInt(value, 4, False) 61 | 62 | 63 | def UnsignedLong(value=0): 64 | return MCInt(value, 8, False) 65 | -------------------------------------------------------------------------------- /protocol/mcString.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : mcString.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : mc string字段 7 | 8 | from protocol.mcField import MCField 9 | from protocol.varNum import VarInt 10 | 11 | 12 | class MCString(MCField): 13 | 14 | def __init__(self, string=None): 15 | self._data = None 16 | if string is not None: 17 | self.set(string) 18 | 19 | def set(self, string): 20 | encodeStr = string.encode("utf-8") 21 | self._data = VarInt(len(encodeStr)).data + encodeStr 22 | 23 | def get(self): 24 | varInt = VarInt() 25 | i = varInt.read(self.data) 26 | return self.data[i:].decode("utf-8") 27 | 28 | def read(self, data) -> int: 29 | varInt = VarInt() 30 | i = varInt.read(data) 31 | # -1表示数据不完整 32 | if i == -1: 33 | return -1 34 | length = i + varInt.get() 35 | if len(data) < length: 36 | return -1 37 | self._data = data[:length] 38 | 39 | return length 40 | 41 | @property 42 | def data(self) -> bytearray: 43 | return self._data 44 | 45 | 46 | # for test 47 | # mcString = MCString("fuck!艹") 48 | # mcString2 = MCString() 49 | # print(mcString2.read(mcString.data)) 50 | # print(mcString2.get()) 51 | # print(mcString2.read(mcString.data[0:1])) 52 | -------------------------------------------------------------------------------- /protocol/packet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : packet.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : 封装成包 7 | 8 | from protocol.varNum import VarNum, INT, VarInt 9 | from protocol.mcField import MCField 10 | from typing import List 11 | 12 | 13 | class Packet: 14 | def __init__(self, id=None, sendData=None): 15 | if sendData is None: 16 | sendData = bytearray() 17 | self.sendData = sendData 18 | self.id = id 19 | 20 | def addField(self, field: MCField): 21 | if self.sendData is None: 22 | self.sendData = bytearray() 23 | self.sendData += field.data 24 | 25 | @property 26 | def data(self): 27 | return Packet.buildBytes(self.id, self.sendData) 28 | 29 | @data.setter 30 | def data(self, value): 31 | varInt = VarInt() 32 | i = 0 33 | i += varInt.read(value) 34 | i += varInt.read(value[i:]) 35 | self.id = varInt.get() 36 | self.sendData = value[i:] 37 | 38 | @staticmethod 39 | def buildBytes(id_, sendData): 40 | if id_ is None or sendData is None: 41 | raise ValueError("id或data为空") 42 | bID = VarNum(INT).set(id_).data 43 | bLength = VarNum(INT).set(len(sendData) + len(bID)).data 44 | return bytearray(bLength + bID + sendData) 45 | 46 | # 将包中的内容解析成mcField 47 | def parse(self, fieldClassList: List[callable]): 48 | data = self.sendData 49 | out = [] 50 | i = 0 51 | for t in fieldClassList: 52 | field: MCField = t() 53 | i += field.read(data[i:]) 54 | out.append(field) 55 | return tuple(out) 56 | 57 | def read(self, data: bytearray): 58 | varInt = VarInt() 59 | i = varInt.read(data) 60 | if i == -1: 61 | return -1 62 | length = varInt.get() 63 | if len(data) < i + length: 64 | return -1 65 | self.data = data[:length + i] 66 | return length + i 67 | 68 | 69 | # for test 70 | # from protocol.mcString import MCString 71 | # from protocol.mcInt import UnsignedShort 72 | # 73 | # packet1 = Packet(1) 74 | # packet1.addField(MCString("艹")) 75 | # packet1.addField(MCString("fuck")) 76 | # packet1.addField(UnsignedShort(65535)) 77 | # packet2 = Packet() 78 | # print(len(packet1.data)) 79 | # print(packet2.read(packet1.data)) 80 | # fieldList = packet2.parse([MCString, MCString, UnsignedShort]) 81 | # print("id=%d,字段=%s" % (packet2.id, repr([field.get() for field in fieldList]))) 82 | -------------------------------------------------------------------------------- /protocol/session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : session.py 4 | # @Date : 2019-12-27 5 | # @Author : 王超逸 6 | # @Brief : 表示和服务器的会话 7 | 8 | 9 | from socket import socket, timeout 10 | from threading import Thread 11 | from protocol.packet import Packet 12 | 13 | BUFF_SIZE = 1024 14 | 15 | 16 | class Session: 17 | 18 | def __init__(self, socket_: socket, onPacketRecv=None, onClose=None): 19 | def doNothing(*x, **y): 20 | pass 21 | 22 | self.socket = socket_ 23 | if onPacketRecv is None: 24 | onPacketRecv = doNothing 25 | if onClose is None: 26 | onClose = doNothing 27 | self.receiveThread = Session.ReceiveThread(socket_, onPacketRecv, onClose) 28 | self.receiveThread.start() 29 | 30 | def sendPacket(self, packet: Packet): 31 | self.socket.sendall(packet.data) 32 | 33 | class ReceiveThread(Thread): 34 | def __init__(self, socket_: socket, onPacketRecv, onClose): 35 | super().__init__() 36 | self.socket = socket_ 37 | self.onClose = onClose 38 | self.onPacketRecv = onPacketRecv 39 | 40 | def run(self) -> None: 41 | self.socket.settimeout(1) 42 | data = bytearray() 43 | while True: 44 | packet = Packet() 45 | # 没读出完整的包之前一直读下去 46 | while packet.read(data) == -1: 47 | readBuff = None 48 | try: 49 | readBuff = self.socket.recv(BUFF_SIZE) 50 | except timeout as e: 51 | continue 52 | if not readBuff: 53 | self.onClose(data) 54 | return 55 | data += readBuff 56 | self.onPacketRecv(packet) 57 | data = data[len(packet.data):] 58 | -------------------------------------------------------------------------------- /protocol/varNum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # @File : varNum.py 4 | # @Date : 2019-12-26 5 | # @Author : 王超逸 6 | # @Brief : mine craft 协议 VarInt和VarLong实现 7 | # python位运算太操蛋了 8 | 9 | from protocol.mcField import MCField 10 | 11 | LONG = 64 12 | INT = 32 13 | 14 | 15 | class VarNum(MCField): 16 | 17 | def __init__(self, _type): 18 | self._data = None 19 | self.type = _type 20 | 21 | @property 22 | def data(self): 23 | return self._data 24 | 25 | def set(self, num): 26 | # 这一步相当于先转成无符号数 27 | if num < 0: 28 | num = 2 ** self.type + num 29 | data = bytearray() 30 | while True: 31 | temp = num & 0b01111111 32 | num = num >> 7 33 | if num != 0: 34 | temp |= 0b10000000 35 | data.append(temp) 36 | if num == 0: 37 | break 38 | self._data = data 39 | return self 40 | 41 | def get(self): 42 | data = self._data 43 | if data is None: 44 | return None 45 | numRead = 0 46 | result = 0 47 | while True: 48 | read = data[0] 49 | data = data[1:] 50 | value = read & 0b01111111 51 | result |= (value << (7 * numRead)) 52 | numRead += 1 53 | if read & 0b10000000 == 0: 54 | # 重新转成带符号数 55 | # 判断符号位 56 | if 1 << self.type - 1 & result != 0: 57 | return result - 2 ** self.type 58 | else: 59 | return result 60 | 61 | def read(self, data): 62 | i = 0 63 | while i != len(data): 64 | if data[i] < 128: 65 | break 66 | i += 1 67 | if i == len(data): 68 | return -1 69 | self._data = data[:i + 1] 70 | return i + 1 71 | 72 | 73 | def VarInt(num=None): 74 | if num is None: 75 | return VarNum(INT) 76 | return VarNum(INT).set(num) 77 | 78 | 79 | def VarLong(num=None): 80 | if num is None: 81 | return VarNum(LONG) 82 | return VarNum(LONG).set(num) 83 | --------------------------------------------------------------------------------