原文链接:https://www.turing.com/blog/understanding-mysql-client-server-protocol-using-python-and-wireshark-part-2/ 
在上一篇文章中我们使用 Wireshark 工具研究了 MySQL 客户端 / 服务端协议内容。现在我们开始使用 Python 语言编码来开发一个模拟 MySQL 原生客户端的工具。最终的代码在这里:Github repo 
首先我们需要创建一个 MYSQL_PACKAGE 类。MYSQL_PACKAGE 类是其他 package 类(例如:HANDSHAKE_PACKAGE, LOGIN_PACKAGE, OK_PACKAGE 等等)的父类。
1 2 3 4 5 6 7 class  MYSQL_PACKAGE :    """Data between client and server is exchanged in packages of max 16MByte size."""        def  __init__ (self, resp = b''  ):         self.resp = resp         self.start = 0          self.end = 0  
它在初始化时接受 resp 参数。 Resp 是从服务器接收到的字节数组类型的二进制响应。这个类的一个重要而有趣的方法是 next 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def  next (self, length = None , type =int , byteorder='little' , signed=False , freeze=False  ):    if  not  freeze:         if  length:             self.end += length             portion = self.resp[self.start:self.end]             self.start = self.end         else :             portion = self.resp[self.start:]             self.start = self.end = 0      else :         if  length:             portion = self.resp[self.start:self.start + length]         else :             portion = self.resp[self.start:]     if  type  is  int :         return  int .from_bytes(portion, byteorder=byteorder, signed=signed)     elif  type  is  str :         return  portion.decode('utf-8' )     elif  type  is  hex :         return  portion.hex ()     else :         return  portion 
next 方法从二进制响应数据中读取了一部分字节的数据。当我们调用此方法时,它会读取部分字节并将指针指向读取结束的最后位置(更改 self.start 和 self.end 属性的值)。当我们再次调用这个方法时,它会从上次停止的位置开始读取字节数据。
next 方法接收五个参数:length、type、byteorder、signed 和 freeze。如果 freeze 参数是 True,它将会从二进制响应中读取一部分字节数据,但是并不会改变指针的位置;否则的话,它将读取指定长度的一部分字节数据,并同时修改指针的位置。如果 length 参数为指定,则方法读取字节,直到响应字节数组结束。type 参数可以是 int、str 和 hex 数据类型。方法 next 根据类型参数的值将一部分字节转换为适当的数据类型。
参数 byteorder 确定字节到整数类型的转换,这取决于您计算机的体系结构,如果你的机器是 big-endian,那么它会在内存中存储从大地址到小地址的字节。如果您的机器是 little-endian,那么它会将字节存储在内存中,从小地址到大地址。这就是为什么我们必须知道我们架构的确切类型才能正确地将字节转换为整数。在我的例子中,它是 little-endian,这就是为什么我将 byteorder 参数的默认值设置为“little”。
参数 signed 同样用于字节到整数类型的转换,我们告诉函数将每个整数视为无符号整数或有符号整数。
这个类中第二个有意思的方法是:encrypt_password。这个方法使用指定的算法来加密密码。
1 2 3 4 5 6 7 8 9 10 11 from  hashlib import  sha1    def  encrypt_password (self, salt, password ):         bytes1 = sha1(password.encode("utf-8" )).digest()         concat1 = salt.encode('utf-8' )         concat2 = sha1(sha1(password.encode("utf-8" )).digest()).digest()         bytes2 = bytearray ()         bytes2.extend(concat1)         bytes2.extend(concat2)         bytes2 = sha1(bytes2).digest()         hash  = bytearray (x ^ y for  x, y in  zip (bytes1, bytes2))         return  hash  
这个方法接收两个参数:salt 和 password。参数 salt 是从服务器接收到的握手数据中的两个 salt1 和 salt2 字符串的串联。参数 password 是 MySQL 用户的密码字符串。
在官方文档 中密码的加密算法是:
1 SHA1( password ) XOR SHA1( "20-bytes random data from server" < concat>  SHA1( SHA1( password ) ) ) 
在这里 ”20-bytes random data from server“  是从服务器接收到的握手数据中的两个 salt1 和 salt2 字符串的串联。要想知道握手包是什么内容,请看上一篇文章。
接下来我将会逐行解释一下 encrypt_password 方法的内容。
1 bytes1 = sha1(password.encode(“utf-8”)).digest() 
我们将密码字符串转换为字节,然后使用 sha1 函数进行加密,并将结果赋值给 bytes1 变量。它等价与算法的这一部分:
接下来我们将 salt 字符串转换为字节,并赋值给 concat1 变量:
1 concat1 = salt.encode(‘utf-8’) 
方法的第三行是:
1 concat2 = sha1(sha1(password.encode(“utf-8”)).digest()).digest() 
在这里我们使用 sha1 函数对密码字符串进行两次加密,并将结果赋值给 concat2 字符串变量。
现在我们有了两个变量:concat1 和 concat2。我们需要使用一个字节数组将他们连接到一起:
1 2 3 bytes2 = bytearray() bytes2.extend(concat1) bytes2.extend(concat2) 
接下来我们需要使用 sha1 函数对连接后字节进行加密,并将结果赋值给 bytes2 变量:
1 bytes2 = sha1(bytes2).digest() 
现在我们已经有了两个加密的字节变量:bytes1 和 bytes2。现在我们必须在这些变量之间进行按位异或运算并返回获得的哈希值。
1 2 hash=bytearray(x ^ y for x, y in zip(bytes1, bytes2)) return hash 
数据类型 Class 在前面的文章中我们学习了 MySQL 客户端 / 服务端协议中的有关 Int 和 String 数据类型内容。现在我们需要一些 Class 来从接收到的数据包中读取字段。
INT 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class  Int :    """see documentation: https://dev.mysql.com/doc/internals/en/integer.html"""      def  __init__ (self, package, length=-1 , type ='fix'  ):         self.package = package         self.length = length         self.type  = type                   def  next (self ):                  if  self.type  == 'fix'  and  self.length > 0 :             return  self.package.next (self.length)                  if  self.type  == 'lenenc' :             byte = self.package.next (1 )             if  byte < 0xfb :                 return  self.package.next (1 )             elif  byte == 0xfc :                 return  self.package.next (2 )             elif  byte == 0xfd :                 return  self.package.next (3 )             elif  byte == 0xfe :                 return  self.package.next (8 ) 
Int 类实现了 MySQL 客户端 / 服务端协议中的 INT 数据类型。它在初始化时接受数据包参数,参数包应该是继承自 MYSQL_PACKAGE 类的任何包类的实例。next 方法决定了 Integer 的类型(int<fix> 类型或者是 int<lenenc> 类型. 请参考上一篇文章内容)并且调用包对象中的 next 方法来读取接收到的响应的字节部分。
STR 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class  Str :    """see documentation: https://dev.mysql.com/doc/internals/en/string.html"""      def  __init__ (self, package, length = -1 , type ="fix"  ):         self.package = package         self.length = length         self.type  = type      def  next (self ):                  if  self.type  == 'fix'  and  self.length > 0 :             return  self.package.next (self.length, str )                  elif  self.type  == 'lenenc' :             length = self.package.next (1 )             if  length == 0x00 :                 return  ""              elif  length == 0xfb :                 return  "NULL"              elif  length == 0xff :                 return  "undefined"              return  self.package.next (length, str )                  elif  self.type  == 'var' :             length = Int(self.package, type ='lenenc' ).next ()             return  self.package.next (length, str )                  elif  self.type  == 'eof' :             return  self.package.next (type =str )                  elif  self.type  == 'null' :             strbytes = bytearray ()             byte = self.package.next (1 )             while  True :                 if  byte == 0x00 :                     break                  else :                     strbytes.append(byte)                     byte = self.package.next (1 )                          return  strbytes.decode('utf-8' ) 
Str 类实现了 MySQL 客户端 / 服务端协议中的 STRING 数据类型。它在初始化时接受数据包参数,参数包应该是继承自 MYSQL_PACKAGE 类的任何包类的实例。next 方法决定了 String 的类型(String<fix>、String<Var>、String<NULL>、String<EOF> 或者是 String<lenenc>。请参考上一篇文章内容)并且调用包对象中的 next 方法来读取接收到的响应的字节部分。
HANDSHAKE_PACKAGE 类 HANDSHAKE_PACKAGE 类用来解析从服务端接收到的握手数据包。它继承自 MYSQL_PACKAGE 类并且在初始化时接收 resp 参数。参数 resp 是从服务端接收的以字节为单位的握手包响应数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class  HANDSHAKE_PACKAGE (MYSQL_PACKAGE ):    def  __init__ (self, resp ):         super ().__init__(resp)          def  parse (self ):         return  {             "package_name" : "HANDSHAKE_PACKAGE" ,             "package_length" : Int(self, 3 ).next (),              "package_number" : Int(self, 1 ).next (),              "protocol" : Int(self, 1 ).next (),              "server_version" : Str(self, type ='null' ).next (),             "connection_id" : Int(self, 4 ).next (),              "salt1" : Str(self, type ='null' ).next (),             "server_capabilities" : self.get_server_capabilities(Int(self, 2 ).next ()),             "server_language" : self.get_character_set(Int(self, 1 ).next ()),             "server_status" : self.get_server_status(Int(self, 2 ).next ()),             "server_extended_capabilities" : self.get_server_extended_capabilities(Int(self, 2 ).next ()),             "authentication_plugin_length" : Int(self, 1 ).next (),             "unused" : Int(self, 10 ).next (),              "salt2" : Str(self, type ='null' ).next (),             "authentication_plugin" : Str(self, type ='eof' ).next ()         } 
方法 parse 使用 Int 和 Str 类从响应中读取属性内容,并将他们存入字典中做为结果返回。
LOGIN_PACKAGE 类 这个类用于创建登录请求数据包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class  LOGIN_PACKAGE (MYSQL_PACKAGE ):    def  __init__ (self, handshake ):         super ().__init__()         self.handshake_info = handshake.parse()          def  create_package (self, user, password, package_number ):         package = bytearray ()                  package.extend(self.capabilities_2_bytes(self.client_capabilities))                  package.extend(self.capabilities_2_bytes(self.extended_client_capabilities))                  max_package = (16777216 ).to_bytes(4 , byteorder='little' )         package.extend(max_package)                  package.append(33 )                  reserved = (0 ).to_bytes(23 , byteorder='little' )         package.extend(reserved)                  package.extend(user.encode('utf-8' ))         package.append(0 )                  salt = self.handshake_info['salt1' ] + self.handshake_info['salt2' ]         encrypted_password = self.encrypt_password(salt.strip(), password)         length = len (encrypted_password)         package.append(length)         package.extend(encrypted_password)                  plugin = self.handshake_info['authentication_plugin' ].encode('utf-8' )         package.extend(plugin)         finpack = bytearray ()         package_length = len (package)                  finpack.append(package_length)         finpack.extend((0 ).to_bytes(2 , byteorder='little' ))         finpack.append(package_number)         finpack.extend(package)         return  finpack 
OK 数据包和 ERR 数据包是在服务端认证之后或者向服务端发送查询命令阶段后服务端反馈的响应数据包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class  OK_PACKAGE (MYSQL_PACKAGE ):    def  __init__ (self, resp ):         super ().__init__(resp)     def  parse (self ):         return  {             "package_name" : "OK_PACKAGE" ,             "package_length" : Int(self, 3 ).next (),              "package_number" : Int(self, 1 ).next (),              "header" : hex (Int(self, 1 ).next ()),             "affected_rows" : Int(self, 1 ).next (),              "last_insert_id" : Int(self, 1 ).next (),              "server_status" : self.get_server_status(Int(self, 2 ).next ()),             "warnings" : Int(self, 2 ).next ()         } 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class  ERR_PACKAGE (MYSQL_PACKAGE ):    def  __init__ (self, resp ):         super ().__init__(resp)     def  parse (self ):         return  {             "package_name" : "ERR_PACKAGE" ,             "package_length" : Int(self, 3 ).next (),              "package_number" : Int(self, 1 ).next (),              "header" : hex (Int(self, 1 ).next ()),              "error_code" : Int(self, 2 ).next (),              "sql_state" : Str(self, 6 ).next (),             "error_message" : Str(self, type ='eof' ).next ()         } 
MYSQL 类 MYSQL 类是是创建服务端 TCP 连接的包装类,使用上述类从服务端发送和接收包。
1 2 3 4 5 6 7 8 9 10 11 12 13 def  __enter__ (self ):    self.client = socket(AF_INET, SOCK_STREAM)     ip = gethostbyname(self.host)     address=(ip,int (self.port))     self.client.connect(address)     return  self def  __exit__ (self, exc_type, exc_value, traceback ):    print ("Good Bye!" )     self.close() def  close (self ):    self.client.close() 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class  MySQL :     def  __init__ (self, host="" , port="" , user="" , password=""  ):         self.host = host         self.port = port         self.user = user         self.password = password          def  connect (self ):         resp = self.client.recv(65536 )         return  HANDSHAKE_PACKAGE(resp)     def  login (self, handshake_package, package_number ):         """Sending Authentication package"""          login_package = LOGIN_PACKAGE(handshake_package)         package = login_package.create_package(user=self.user, password=self.password, package_number=package_number)         self.client.sendall(package)         resp = self.client.recv(65536 )         package = self.detect_package(resp)         if  isinstance (package, ERR_PACKAGE):             info = package.parse()             raise  Exception(f"MySQL Server Error: {info['error_code' ]}  - {info['error_message' ]} " )         elif  isinstance (package, OK_PACKAGE):             return  package.parse()['package_number' ]         elif  isinstance (package, EOF_PACKAGE):             return  False  
我认为这个类一切都很清晰了。我已经定义了 __enter__ 和 __exit__ 以便能够使用带有“with”语句的这个类来自动关闭 TCP 连接。在 __enter__ 方法中我正在通过套接字创建 TCP 连接,在 __exit__ 方法中去关闭创建的连接。这个类在初始化中接收 host、port、user 和 password 参数。
在连接方法中我们从服务端接收了握手数据包:
1 2 resp = self.client.recv(65536) return HANDSHAKE_PACKAGE(resp) 
在登录方法中我们使用 LOGIN_PACKAGE 和 HANDSHAKE_PACKAGE 类创建登录请求数据包,并发送到服务端,同时从服务端获取 OK 或者 ERR 数据包。
就这样。我们已经实现了连接阶段,为了避免这篇文章太长,我不会解释命令阶段,因为命令阶段比连接阶段更容易,您可以使用从本文和以前的文章中积累的知识自行研究。
示例视频:
VIDEO