github.com/XiaoMi/Gaea@v1.2.5/backend/direct_connection_cn.md (about)

     1  # backend 包的直连 direct_connection
     2  
     3  ## 代码说明
     4  
     5  ### 第一步 初始交握,传送讯息方向为 MariaDB 至 Gaea
     6  
     7  参考 [官方文档](https://mariadb.com/kb/en/connection/) ,有以下内容
     8  
     9  <img src="./assets/image-20220315221559157.png" alt="image-20220315221559157" style="zoom:100%;" /> 
    10  
    11  根据官方文档,使用范例说明
    12  
    13  | 内容                            | 演示范例                                                     |
    14  | ------------------------------- | ------------------------------------------------------------ |
    15  | int<1> protocol version         | 协定 Protocol 版本为 10                                      |
    16  | string<NUL> server version      | 数据库的版本号 version 为<br /><br />[]uint8{<br />53, 46, 53, 46, 53,<br />45, 49, 48, 46, 53,<br />46, 49, 50, 45, 77,<br />97, 114, 105, 97, 68,<br />66, 45, 108, 111, 103<br />}<br /><br />对照 ASCII 表为<br />5.5.5-10.5.12-MariaDB-log |
    17  | int<4> connection id            | 连接编号为<br /><br />[]uint8{16, 0, 0, 0}<br />先反向排列为 []uint8{0, 0, 0, 16}<br /><br />最后求得的连接编号为 []uint32{16} |
    18  | string<8> scramble 1st part     | 第一部份的 Scramble,Scramble 总共需要组成 20 bytes,<br />第一个部份共 8 bytes,其值为 []uint8{81, 64, 43, 85, 76, 90, 97, 91} |
    19  | string<1> reserved byte         | 数值为 0                                                     |
    20  | int<2> server capabilities      | 第一部份的功能标志 capability,数值为 []uint8{254, 247}      |
    21  | int<1> server default collation | 数据库编码 charset 为 33,经<br />以下 [文档](https://mariadb.com/kb/en/supported-character-sets-and-collations/) 查询<br />或者是 命令 SHOW CHARACTER SET LIKE 'utf8'; 查询,<br />charset 的数值为 utf8_general_ci |
    22  | int<2> status flags             | 服务器状态为 []uint8{2, 0}<br />进行反向排列为[]uint8{0, 2},再转成二进制为<br />[]uint{0b000000000, 0b00000010}.<br /><br />对照 Gaea/mysql/constants.go 后,得知目前服务器的状况为<br />Autocommit (ServerStatusAutocommit) |
    23  | int<2> server capabilities      | 延伸的功能标志 capability,数值为 []uint8{255, 129}.         |
    24  
    25  先对 功能标志 capability 进行计算
    26  
    27  ```
    28  先把所有的功能标志 capability 的数据收集起来,包含延伸部份
    29  
    30  数值分别为 []uint8{254, 247, 255, 129}
    31  并反向排列
    32  数值分别为 []uint8{129, 255, 247, 254}
    33  全部 十进制 转成 二进制,为 []uint8{10000001, 11111111, 11110111, 11111110} (转成十进制数值为 2181036030)
    34  
    35  再用 [文档](https://mariadb.com/kb/en/connection/) 进行对照
    36  比如,功能标志 capability 的第一个值为 0,意思为 CLIENT_MYSQL 值为 0,代表是由服务器发出的讯息
    37  ```
    38  
    39  接续上表
    40  
    41  | 项目 | 内容                                                         |
    42  | ---- | ------------------------------------------------------------ |
    43  | 公式 | if (server_capabilities & PLUGIN_AUTH)<br/>    int<1> plugin data length <br/>else<br/>    int<1> 0x00 |
    44  | 范例 | 跳过 1 个 byte                                               |
    45  
    46  接续上表
    47  
    48  | 项目 | 内容             |
    49  | ---- | ---------------- |
    50  | 公式 | string<6> filler |
    51  | 范例 | 跳过 6 个 bytes  |
    52  
    53  接续上表
    54  
    55  | 项目 | 内容                                                         |
    56  | ---- | ------------------------------------------------------------ |
    57  | 公式 | if (server_capabilities & CLIENT_MYSQL)<br/>    string<4> filler <br/>else<br/>    int<4> server capabilities 3rd part .<br />    MariaDB specific flags /* MariaDB 10.2 or later */ |
    58  | 范例 | 跳过 4 个 bytes                                              |
    59  
    60  接续上表
    61  
    62  | 项目 | 内容                                                         |
    63  | ---- | ------------------------------------------------------------ |
    64  | 公式 | if (server_capabilities & CLIENT_SECURE_CONNECTION)<br/>    string<n> scramble 2nd part . Length = max(12, plugin data length - 9)<br/>    string<1> reserved byte |
    65  | 范例 | scramble 一共要 20 个 bytes,第一部份共 8 bytes,所以第二部份共有 20 - 8 = 12 bytes,该数值为 []uint8{34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125} |
    66  
    67  接续上表
    68  
    69  | 项目 | 内容                                                         |
    70  | ---- | ------------------------------------------------------------ |
    71  | 公式 | if (server_capabilities & PLUGIN_AUTH)<br/>    string<NUL> authentication plugin name |
    72  | 范例 | 之后的资料都不使用                                           |
    73  
    74  合拼所有 Scramble 的资料
    75  
    76  ```
    77  第一部份 Scramble 为 []uint8{81, 64, 43, 85, 76, 90, 97, 91}
    78  第二部份 Scramble 为 []uint8{34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125}
    79  
    80  两部份 Scramble 合拼后为 []uint8{81, 64, 43, 85, 76, 90, 97, 91, 34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125}
    81  ```
    82  
    83  ### 第二步 计算用于验证密码的验证码 Auth
    84  
    85  参考 [官方文档](https://dev.mysql.com/doc/internals/en/secure-password-authentication.html) ,有整个完整验证码的计算公式说明
    86  
    87  验证码的公式如下
    88  
    89  ```
    90  SHA1( 密码 ) XOR SHA1( "服务器所提供的乱数" <接合> SHA1( SHA1( 密码 ) ) )
    91      其中
    92      stage1 = SHA1( 密码 )
    93      stage1Hash = SHA1( stage1 ) = SHA1( SHA1( 密码 ) )
    94      scramble = SHA1( scramble <接合> SHA1( stage1Hash ) ) // 第一次修改 scramble 的数值
    95      scramble = stage1 XOR scramble // 第二次修改 scramble 的数值
    96  ```
    97  
    98  假设
    99  
   100  - 输入密码参数 password 为 12345
   101  - 数据库服务器回传的乱数 scramble为 []uint8{81, 64, 43, 85, 76, 90, 97, 91, 34, 53, 36, 85, 93, 86, 117, 105, 49, 87, 65, 125},这是用十进位的方式来表示
   102    如果用十六进位来表示的数值为 []uint8{51, 40, 2B, 55, 4c, 5a, 61, 5b, 22, 35, 24, 55, 5d, 56,  75,  69, 31, 57, 41,  7d},显示的数值为 51402B554c5A615b223524555d5675693157417d
   103  
   104  计算 stage1 为 stage1 = SHA1( 密码 ),使用 bash 进行计算
   105  
   106  ```bash
   107  # 使用 Linux Bash 去计算和验证 stage1
   108  $ echo -n 12345 | sha1sum | head -c 40 # 把 密码 12345 转成 stage1
   109  8cb2237d0679ca88db6464eac60da96345513964 # 此为 stage1 的值
   110  ```
   111  
   112  计算 stage1Hash 的数值,公式为 stage1Hash = SHA1( stage1 ) = SHA1( SHA1( 密码 ) )
   113  
   114  ```bash
   115  # 使用 Linux Bash 去计算和验证 stage1Hash
   116  
   117  $ echo -n 12345 | sha1sum | xxd -r -p | sha1sum | head -c 40
   118  00a51f3f48415c7d4e8908980d443c29c69b60c9 # 此为 stage1hash 的值
   119  
   120  $ echo -n 8cb2237d0679ca88db6464eac60da96345513964 | xxd -r -p | sha1sum | head -c 40
   121  00a51f3f48415c7d4e8908980d443c29c69b60c9 # 此为 stage1hash 的值
   122  ```
   123  
   124  计算 "服务器所提供的乱数" 和 SHA1( SHA1( 密码 ) ) 的接合值
   125  
   126  ```bash
   127  # scramble 的值为 51402B554c5A615b223524555d5675693157417d,为连接的前半段
   128  # stage1Hash 的值为 00a51f3f48415c7d4e8908980d443c29c69b60c9,为连接的后半段
   129  
   130  # 计算 "服务器所提供的乱数" <接合> SHA1( SHA1( 密码 ) ) 的接合值
   131  $ echo -n 51402B554c5A615b223524555d5675693157417d 00a51f3f48415c7d4e8908980d443c29c69b60c9 |  sed "s/ //g"
   132  51402B554c5A615b223524555d5675693157417d00a51f3f48415c7d4e8908980d443c29c69b60c9 # 合拼后的接合值
   133  ```
   134  
   135  计算第一次重写的 scramble,公式为
   136  
   137  scramble = SHA1( 接合值) = SHA1( scramble <接合> SHA1( stage1Hash ) )
   138  
   139  ```bash
   140  # 使用 Linux Bash 去计算和验证 第一次重写 scramble
   141  $ echo -n 51402B554c5A615b223524555d5675693157417d00a51f3f48415c7d4e8908980d443c29c69b60c9 | xxd -r -p | sha1sum | head -c 40
   142  0ca0f764a59d1cdb10a87f0155d61aa54be1c71a # 此为第一次修改的 scramble
   143  ```
   144  
   145  计算第二次重写的 scramble,公式为 scramble = stage1 XOR scramble
   146  
   147  ```bash
   148  # 使用 Linux Bash 去计算和验证 第二次重写 scramble
   149  $ stage1=0x8cb2237d0679ca88db6464eac60da96345513964 # 之前计算出来的 stage1 的数值
   150  $ scramble=0x0ca0f764a59d1cdb10a87f0155d61aa54be1c71a # 第一次修改 scramble 的数值
   151  $ echo $(( $stage1^$scramble ))
   152  -7792437067003134338 # 错误答案,精度不足
   153  
   154  $ stage1=0x8cb2237d0679ca88db6464eac60da96345513964
   155  $ scramble=0x0ca0f764a59d1cdb10a87f0155d61aa54be1c71a
   156  $ printf "0x%X" $(( (($stage1>>40)) ^ (($scrambleFirst>>40)) ))
   157  0xFFFFFFFFFF93DBB3 # 错误答案,精度不足
   158  
   159  # stage1 和 第一次修改的 scramble 分成四段执行 XOR
   160  $ printf "0x%X" $(( ((0x8cb2237d06)) ^ ((0x0ca0f764a5)) ))
   161  $ printf "%X" $(( ((0x79ca88db64)) ^ ((0x9d1cdb10a8)) ))
   162  $ printf "%X" $(( ((0x64eac60da9)) ^ ((0x7f0155d61a)) ))
   163  $ printf "%X" $(( ((0x6345513964)) ^ ((0xa54be1c71a)) ))
   164  0x8012D419A3E4D653CBCC1BEB93DBB3C60EB0FE7E # 正确答案
   165  
   166  # scramble 为 []uint8{ 80, 12,  D4, 19,  A3,  E4,  D6, 53,  CB,  CC, 1B,  EB,  93,  DB,  B3,  C6, 0E,  B0,  FE,  7E} // 十六进位
   167  # 用十进位表示为
   168  # scramble 为 []uint8{128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126} // 十进位 (跟代码产出的答案相同)
   169  ```
   170  
   171  下图为代码执行的结果,结果和用 Bash 推算的相同
   172  
   173  <img src="./assets/image-20220318183833245.png" alt="image-20220318183833245" style="zoom:70%;" /> 
   174  
   175  ### 第三步 回应交握,传送讯息方向为 Gaea 至 MariaDB
   176  
   177  参考 [官方文档](https://mariadb.com/kb/en/connection/) ,有以下内容
   178  
   179  <img src="./assets/image-20220318083633693.png" alt="image-20220318083633693" style="zoom:100%;" /> 
   180  
   181  根据官方文档,使用范例说明,先对 Gaea 要处理的功能标志 capability 进行计算,
   182  
   183  | capability 项目              | 二进位             | 十进位 |
   184  | ---------------------------- | ------------------ | ------ |
   185  | mysql.ClientProtocol41       | 0b0000001000000000 | 512    |
   186  | mysql.ClientSecureConnection | 0b1000000000000000 | 32768  |
   187  | mysql.ClientLongPassword     | 0b0000000000000001 | 1      |
   188  | mysql.ClientTransactions     | 0b0010000000000000 | 8192   |
   189  | mysql.ClientLongFlag         | 0b0000000000000100 | 4      |
   190  |                              |                    |        |
   191  | 总合                         |                    |        |
   192  | Gaea 支持的 capability       | 0b1010001000000101 | 41477  |
   193  
   194  计算 Gaea 和 MariaDB 双方共同支持的 capability
   195  
   196  ```
   197  在前面第一步里,dc 对象的 capability 为 0b10000001111111111111011111111110 (转成十进制数值为 2181036030),很明显地,这个 capability 并不支持 mysql.ClientLongPassword
   198  
   199  进行 Gaea支持的capability 和 dc.capability 进行 AND 操作
   200  Gaea支持的capability & dc.capability = []uint32{41477} & []uint32{2181036030} = []uint32{41476}
   201  ```
   202  
   203  <img src="./assets/image-20220319002738908.png" alt="image-20220319002738908" style="zoom:70%;" /> 
   204  
   205  | 内容                              | 演示范例                                                     |
   206  | --------------------------------- | ------------------------------------------------------------ |
   207  | int<4> client capabilities        | 经由上述计算为 []uint32{41476},但是传输过程中,会反向排列,所以传送的资料为 []uint8{4, 162, 0, 0}<br /><img src="./assets/image-20220319113026919.png" alt="image-20220319113026919" style="zoom:50%;" /> |
   208  | int<4> max packet size            | 官方文档有提到写入的值都为 0,传送的数值为 []uint8{0, 0, 0, 0} |
   209  | int<1> client character collation | 在 [官方文档](https://mariadb.com/kb/en/supported-character-sets-and-collations/) 里有说明,以这个例子为 46 ,意思为 utf8mb4_bin |
   210  | string<19> reserved               | 全部写入为 0  的数值,传送的数值为 []uint8{<br />                                                                                    0, 0, 0, 0, 0,<br />                                                                                    0, 0, 0, 0, 0,<br />                                                                                    0, 0, 0, 0, 0,<br />                                                                                    0, 0, 0, 0,<br />                                                                               } |
   211  
   212  根据官方文档,使用范例说明
   213  
   214  | 项目 | 内容                                                         |
   215  | ---- | ------------------------------------------------------------ |
   216  | 公式 | if not (server_capabilities & CLIENT_MYSQL)<br/>    int<4> extended client capabilities <br/>else<br/>    string<4> reserved |
   217  | 范例 | **CLIENT_MYSQL** 意思为这封包是否为客户端的封包,目前为 True,<br />所以 **not (server_capabilities & CLIENT_MYSQL)** 的数值为 False,<br />全部写入为 0 的数值,传送的数值为 []uint8{<br />                                                                                    0, 0, 0, 0,<br />                                                                               } |
   218  
   219  接续上表
   220  
   221  | 项目 | 内容                                                         |
   222  | ---- | ------------------------------------------------------------ |
   223  | 公式 | string<NUL> username                                         |
   224  | 范例 | 写入登录数据库的用户的名称 xiaomi,但最后再多写一个 0 作为中断结尾,<br />写入的资料为 []uint8{120, 105, 97, 111, 109, 105, 0} |
   225  
   226  使用 Bash 进行验证
   227  
   228  ```bash
   229  $ echo -n xiaomi | od -td1
   230  0000000  120  105   97  111  109  105
   231  0000006
   232  ```
   233  
   234  接续上表
   235  
   236  | 项目 | 内容                                                         |
   237  | ---- | ------------------------------------------------------------ |
   238  | 公式 | if (server_capabilities & PLUGIN_AUTH_LENENC_CLIENT_DATA)<br/>    string<lenenc> authentication data <br/>else if (server_capabilities & CLIENT_SECURE_CONNECTION)<br/>    int<1> length of authentication response<br/>    string<fix> authentication response (length is indicated by previous field) <br/>else<br/>    string<NUL> authentication response null ended |
   239  | 范例 | 目前 Gaea 支持 CLIENT_SECURE_CONNECTION,<br /><br />前面已经计算出来,scramble 为 []uint8{128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126}<br /><br />但是要先告知 MariaDB 服务器 scramble 的长度为 20<br />所以回传的资料为 []uint8{20, 128, 18, 212, 25, 163, 228, 214, 83, 203, 204, 27, 235, 147, 219, 179, 198, 14, 176, 254, 126} |
   240  
   241  接续上表
   242  
   243  | 项目 | 内容                                                         |
   244  | ---- | ------------------------------------------------------------ |
   245  | 公式 | if (server_capabilities & CLIENT_CONNECT_WITH_DB)<br/>    string<NUL> default database name |
   246  | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 |
   247  
   248  接续上表
   249  
   250  | 项目 | 内容                                                         |
   251  | ---- | ------------------------------------------------------------ |
   252  | 公式 | if (server_capabilities & CLIENT_PLUGIN_AUTH)<br/>    string<NUL> authentication plugin name |
   253  | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 |
   254  
   255  接续上表
   256  
   257  | 项目 | 内容                                                         |
   258  | ---- | ------------------------------------------------------------ |
   259  | 公式 | if (server_capabilities & CLIENT_CONNECT_ATTRS)<br/>    int<lenenc> size of connection attributes<br/>    while packet has remaining data<br/>        string<lenenc> key<br/>        string<lenenc> value |
   260  | 范例 | 目前 Gaea 在直连 dc 要处理的 capabilities 如下<br />mysql.ClientProtocol41<br/>mysql.ClientSecureConnection<br/>mysql.ClientTransactions<br/>mysql.ClientLongFlag<br /><br />先略过此步骤 |
   261  
   262  ## 测试说明
   263  
   264  > 以下会说明在写测试时考量的点
   265  
   266  ### 匿名函数的考量
   267  
   268  在以下代码内有一个 测试函数 t.Run("测试数据库后端连线初始交握后的回应", func(t *testing.T)
   269  
   270  此测试函数内含 匿名函数 customFunc
   271  
   272  以下代码,匿名函数 customFunc 内的变量将会取 dc 对象的内存位置,考量后,觉得可以这样写
   273  
   274  主要是担心 错误的变数或者是错误的数值 会进入 匿名函数
   275  
   276  ```go
   277  	// 交握第二步 Step2
   278  	t.Run("测试数据库后端连线初始交握后的回应", func(t *testing.T) {
   279  		var connForSengingMsgToMariadb = mysql.NewConn(mockGaea.GetConnWrite())
   280  		dc.conn = connForSengingMsgToMariadb
   281  		dc.conn.StartWriterBuffering()
   282          
   283  		customFunc := func() {
   284  			err := dc.writeHandshakeResponse41()
   285  			require.Equal(t, err, nil)
   286  			err = dc.conn.Flush()
   287  			require.Equal(t, err, nil)
   288  			err = mockGaea.GetConnWrite().Close()
   289  			require.Equal(t, err, nil)
   290  		}
   291  
   292  		fmt.Println(mockGaea.CustomSend(customFunc).ArrivedMsg(mockMariaDB))
   293  	})
   294  ```
   295  
   296  ## 验证
   297  
   298  使用 Linux 命令 或者是 网站 去计算 Sha1sum 时,计算出来的结果为 16 进位,只不过 IDE 工具在取中断点时会显示为 10 进位,以下使用 mysql 包里的 CalcPassword 函数中的 stage1 变量为例
   299  
   300  ### 使用工具和网站把密码转换成验证码
   301  
   302  使用 Linux 命令 去产生 stage1 的 sha1sum 验证码
   303  
   304  <img src="./assets/image-20220314214316673.png" alt="image-20220314214316673" style="zoom:80%;" /> 
   305  
   306  使用 [网站](https://coding.tools/tw/sha1) 去计算 stage1 的 sha1sum 验证码
   307  
   308  <img src="./assets/image-20220314215924425.png" alt="image-20220314215924425" style="zoom:80%;" /> 
   309  
   310  ### 使用中断点去观察 stage1 变量
   311  
   312  使用中断点去取出相对应 stage1 的 sha1sum 验证码
   313  
   314  <img src="./assets/image-20220314220921338.png" alt="image-20220314220921338" style="zoom:100%;" /> 
   315  
   316  ### 确认 Sha1sum 验证码的数值
   317  
   318  使用下表去对照检查 "中断点" 和 "Linux 命令" 产生的 stage1  验证码,确认其值为正确的
   319  
   320  | 数组位置 |  二进位  | 十进位 | 十六进位 |
   321  | :------: | :------: | :----: | :------: |
   322  |    0     | 10001100 |  140   |    8c    |
   323  |    1     | 10110010 |  178   |    b2    |
   324  |    2     | 00100011 |   35   |    23    |
   325  |    3     | 01111101 |  125   |    7d    |
   326  |    4     | 00000110 |   6    |    06    |
   327  |    5     | 01111001 |  121   |    79    |
   328  |    6     | 11001010 |  202   |    ca    |
   329  |    7     | 10001000 |  136   |    88    |
   330  |    8     | 11011011 |  219   |    db    |
   331  |    9     | 01100100 |  100   |    64    |
   332  |    10    | 01100100 |  100   |    64    |
   333  |    11    | 11101010 |  234   |    ea    |
   334  |    12    | 11000110 |  198   |    c6    |
   335  |    13    | 00001101 |   13   |    0d    |
   336  |    14    | 10101001 |  169   |    a9    |
   337  |    15    | 01100011 |   99   |    63    |
   338  |    16    | 01000101 |   69   |    45    |
   339  |    17    | 01010001 |   81   |    51    |
   340  |    18    | 00111001 |   57   |    39    |
   341  |    19    | 01100100 |  100   |    64    |