MySQLユーザのためのMySQLプロトコル入門#3
みなさん連休はどうでしたか?私はというものずーっと家に引きこもってcloudbitでsshアクセスできるようにしたりして遊んでいました。
今日の記事ではMySQL Serverに対してコマンドを発行して結果を取得していきます。
基本的にコマンドの実行の流れを最小で追ってみよう、という趣旨なので細かい分岐やエラーハンドリングなどは省いていますのであしからず。
Select Queryを投げてみよう
Queryの実行はMySQLサーバーに対してCOM_QUERYコマンドを送信すると結果が帰ってきます。
大まかなシーケンスとしてはdev.mysql.comに記載の通りとなっています。図でいうと一番左の分岐です。Select Queryを実行するだけですので、ProtocolText::Resultsetが帰ってくることが期待できそうですね。
簡単な図にするとこんな感じの順番でデータがやってきます。
説明はほどほどにしておいて、まずはMySQLに適当なデータセットをつっこんでおきます。
1 2 3 |
mysql -uroot -e 'create database ex;' mysql -uroot -e 'create table ex.ex1(id int unsigned primary key auto_increment, name varchar(255));' mysql -uroot -e 'insert into ex.ex1(name) values("petrucci"), ("adam"), ("belushi");' |
ngrepでパケットを確認しつつ、テーブルの中身をしてみましょう。
1 2 3 |
chobie% sudo ngrep -x -q -d lo0 '' 'port 3306' interface: lo0 (127.0.0.0/255.0.0.0) filter: (ip or ip6) and ( port 3306 ) |
selectを実行してみます
1 2 3 4 5 6 7 8 9 |
chobie% mysql -uchobie -p -e 'select * from ex.ex1' Enter password: +----+----------+ | id | name | +----+----------+ | 1 | petrucci | | 2 | adam | | 3 | belushi | +----+----------+ |
ngrepの結果は全て乗せると長くなってしまうので今回の興味がある部分だけ抜粋してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
~~~~~割愛 T 127.0.0.1:49688 -> 127.0.0.1:3306 [AP] 15 00 00 00 03 73 65 6c 65 63 74 20 2a 20 66 72 .....select * fr 6f 6d 20 65 78 2e 65 78 31 om ex.ex1 T 127.0.0.1:3306 -> 127.0.0.1:49688 [AP] 01 00 00 01 02 22 00 00 02 03 64 65 66 02 65 78 ....."....def.ex 03 65 78 31 03 65 78 31 02 69 64 02 69 64 0c 3f .ex1.ex1.id.id.? 00 0a 00 00 00 03 23 42 00 00 00 26 00 00 03 03 ......#B...&.... 64 65 66 02 65 78 03 65 78 31 03 65 78 31 04 6e def.ex.ex1.ex1.n 61 6d 65 04 6e 61 6d 65 0c 08 00 ff 00 00 00 fd ame.name........ 00 00 00 00 00 05 00 00 04 fe 00 00 22 00 0b 00 ............"... 00 05 01 31 08 70 65 74 72 75 63 63 69 07 00 00 ...1.petrucci... 06 01 32 04 61 64 61 6d 0a 00 00 07 01 33 07 62 ..2.adam.....3.b 65 6c 75 73 68 69 05 00 00 08 fe 00 00 22 00 elushi.......". T 127.0.0.1:49688 -> 127.0.0.1:3306 [AP] 01 00 00 00 01 ..... |
そのままでもなんとなく読めちゃいそうな結果ですね。
COM_QUERY
それではまずはClientから投げるMySQL Packetを分解してみます。
1 2 3 |
T 127.0.0.1:49688 -> 127.0.0.1:3306 [AP] 15 00 00 00 03 73 65 6c 65 63 74 20 2a 20 66 72 .....select * fr 6f 6d 20 65 78 2e 65 78 31 |
先頭4byteはMySQL Packetのheaderなのでpayloadと分割すると下記のようになります。
1 2 |
MySQL Packet header: 15 00 00 00 Payload(COM_QUERY): 03 73 65 6c 65 63 74 20 2a 20 66 72 6f 6d 20 65 78 2e 65 78 31 |
dev.mysql.comのCOM_QUERYの解説によるとCOM_QUERYは
1byteの0x03が識別子で残りはstring[EOF]のqueryとなります。string[EOF]はpayloadの残り全部が文字列であるという意味になります。
それでは早速COM_QUERYのMySQL Packetをつくる関数を作ってみましょう。
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 |
package main import ( "encoding/hex" "fmt" ) func com_query(query string) []byte { payload_size := len(query) + 1 buffer := make([]byte, payload_size+4) buffer[0] = byte(payload_size) buffer[1] = byte(payload_size >> 8) buffer[2] = byte(payload_size >> 16) buffer[3] = 0x00 // Sequence ID buffer[4] = 0x03 // COM_QUERY copy(buffer[5:], query) return buffer } // create COM_QUERY MySQL packet func main() { result := com_query("select * from ex.ex1") fmt.Printf("[result]:\n%s\n", hex.Dump(result)) } //[result]: //00000000 15 00 00 00 03 73 65 6c 65 63 74 20 2a 20 66 72 |.....select * fr| //00000010 6f 6d 20 65 78 2e 65 78 31 |om ex.ex1| |
このコードはPlayGroundで試せます、があまりにもシンプル過ぎるので特にいうことないですね。
それでは、引き続き結果セットをParseしていきましょう
COM_QUERY Response
COM_QUERYのResponseでは結果として取りうる値が数パターンあります。
MySQL Packetの1byte目によって下記の通りに分岐します。SELECTの場合はLength Encoded Integerの値が帰ります。
1 2 3 4 |
0xff => Error Packet 0x00 => OK Packet(UPDATEとかINSERTの時に帰ってくる) 0xfb => GET_MORE_CLIENT_DATA (確かload data local infileの場合) それ以外 => Length Encoded Integerで結果セットのカラム数が入る |
はて、Length Encoded Integerとは何でしょうか?
Length Encoded Integer
説明していませんでしたがMySQL protocolでの値表現はLittle Endianを使っています。
固定長の値表現だけでは時に効率的ではない事もありますので、効率性をあげたり、表現力を上げる為にMySQL ProtocolではLength Encoded Integerという表現がよく使われます。
Length Encoded integerの1から9byteに可変し、1byte目の値に応じでbyte, int8 int16, int24, int64の表現ができます。
ざっくり言うと250以下の値は1byteだけで表現でき、それ以上の場合は値の範囲に応じたサイズ分のデータがくっつく、というシンプルな構造です。
[1byte prefix]([n bytes if larger > 250])?
1 2 3 4 5 |
251: null とか、場合によって特別な意味を持たせたりします 252: uint16 253: int24 254: int64 255: error packet |
興味深い点としては1byte目をうまくerror packetに合わせてるところですね。それではlength encoded integer(lenenc 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 25 26 27 |
func read_lenenc_int(data []byte) (int64, int, error) { var result int64 var read int = 1 var err error switch data[0] { case 0xfb: return 0, read, fmt.Errorf("NULL") case 0xfc: result = int64(binary.LittleEndian.Uint16(data[1:3])) read = 3 case 0xfd: result = int64(data[1]) result += int64(data[2] << 8) result += int64(data[3] << 16) read = 4 case 0xfe: result = int64(binary.LittleEndian.Uint64(data[1:9])) read = 9 case 0xff: err = fmt.Errorf("Err packet") default: result = int64(data[0]) } return result, read, err } |
基本的にはprefix + 実データ形式となるのでそのままparseできます。GoのbinaryパッケージにはUint24というのはないので
そこだけ自前で読み込んでいますがparseは簡単ですね。
ついでに、文字列を表現するLength Encoded Stringというのもあるのでこの関数も作っときます。
1 2 3 4 5 6 7 8 9 10 |
func read_lenenc_str(data []byte) (string, int, error) { var result [] length, offset, err := read_lenenc_int(data) if err != nil { return result, 0, err } result = data[offset:offset+length] return string(result), offset+length, nil } |
見て分かる通りデータ部分のprefixにLength Encoded Integerが付いているだけです。
これらの関数はPlayGroundで確認できるようにしてありますのでいじってみると理解しやすいかと思います。
Length Encoded Integerの解説はこれくらいにしてクエリ結果の続きの解説です。
COM_QUERY Responseの続き
Select Queryの結果の場合はだいたいLength Encoded Integerでカラム数が帰ってくる、というところまで説明しました。
今度はカラム定義から結果セット部分をparseしていきます。
1 2 3 4 5 6 7 8 9 10 |
T 127.0.0.1:3306 -> 127.0.0.1:49688 [AP] 01 00 00 01 02 22 00 00 02 03 64 65 66 02 65 78 ....."....def.ex 03 65 78 31 03 65 78 31 02 69 64 02 69 64 0c 3f .ex1.ex1.id.id.? 00 0a 00 00 00 03 23 42 00 00 00 26 00 00 03 03 ......#B...&.... 64 65 66 02 65 78 03 65 78 31 03 65 78 31 04 6e def.ex.ex1.ex1.n 61 6d 65 04 6e 61 6d 65 0c 08 00 ff 00 00 00 fd ame.name........ 00 00 00 00 00 05 00 00 04 fe 00 00 22 00 0b 00 ............"... 00 05 01 31 08 70 65 74 72 75 63 63 69 07 00 00 ...1.petrucci... 06 01 32 04 61 64 61 6d 0a 00 00 07 01 33 07 62 ..2.adam.....3.b 65 6c 75 73 68 69 05 00 00 08 fe 00 00 22 00 elushi.......". |
これを手で分割していくと下記の通りとなります。少し長いですが、カラム定義と結果セットの返却とEOFパケットの3パターンだけなのでたんたんといけます
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 |
# 結果セットのカラム数 MySQL Packet Header: 01 00 00 01 (payload 1byte, sequence id = 1) Length Encoded Integer: 02 (結果セットはのrowは2カラム持っている) # 結果セットのカラム定義 MYSQL Packet Header: 22 00 00 02 (payload 34byte, sequence id = 2) Payload: 03 64 65 66 02 65 78 03 65 78 31 03 65 78 31 02 69 64 02 69 64 0c 3f 00 0a 00 00 00 03 23 42 00 00 00 # 結果セットのカラム定義 MySQL Packet Header: 26 00 00 03 (payload 40byte, sequence id = 3) Payload: 03 64 65 66 02 65 78 03 65 78 31 03 65 78 31 04 6e 61 6d 65 04 6e 61 6d 65 0c 08 00 ff 00 00 00 fd 00 00 00 00 00 05 # カラム定義の送信終了 MySQL Packet Header: 05 00 00 04 (payload 5byte, sequence id = 4) EOF Packet: fe 00 00 22 00 # 結果セット(1行目) MySQL Packet Header: 0b 00 00 05 (payload 11byte, sequence id = 5) Payload: 01 31 08 70 65 74 72 75 63 63 69 # 結果セット(2行目) MySQL Packet Header: 07 00 00 06 (payload 7byte, sequence id = 6) Payload: 01 32 04 61 64 61 6d # 結果セット(3行目) MySQL Packet Header: 0a 00 00 07 (payload 10byte, sequence id = 7) Payload: 01 33 07 62 65 6c 75 73 68 69 # 結果セットの送信終了 MySQL Packet Header: 05 00 00 08 (Payload 5byte, sequence id = 8) EOF Packet: fe 00 00 22 00 |
それではカラム定義部分から解説していきましょう。
結果セットのカラム定義
カラム定義のパケットはColumnDefinitionといって、データ定義部分を引用すると:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
lenenc_str catalog lenenc_str schema lenenc_str table lenenc_str org_table lenenc_str name lenenc_str org_name lenenc_int length of fixed-length fields [0c] 2 character set 4 column length 1 type 2 flags 1 decimals 2 filler [00] [00] if command was COM_FIELD_LIST { lenenc_int length of default-values string[$len] default values } |
とあるようにLength Encoded Stringでテーブル名やカラム名と型定義、そして型の詳細についてが入っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
MYSQL Packet Header: 22 00 00 02 (payload 34byte, sequence id = 2) Payload: 03 64 65 66 02 65 78 03 65 78 31 03 65 78 31 02 69 64 02 69 64 0c 3f 00 0a 00 00 00 03 23 42 00 00 00 [Column Definition] catalog (03): 64 65 66 (def) schema (02): 65 78 (ex) table (03): 65 78 31 (ex1) org_table (03): 65 78 31 (ex1) name (02): 69 64 (id) org_name (02): 69 64 (id) length of fixed_length fields: 0c character set: 3f 00 (binary) column length: 0a 00 00 00 (10 byte) type: 03 (Protocol::MYSQL_TYPE_LONG) flags: 23 42 decimals: 00 filter: 00 00 |
CharacterSetとColumnTypeの定義はこのリンクを確認して下さい。
EOF Packetが来たらColumn Definition部分は終了です。続いて結果セットに行きましょう。
結果セット
結果セットのパケットはResultsetRowといい、結果セットのカラムの数だけLength Encoded Stringが帰ってきます。
今回、2つのカラムがあるというのがCOM_QUERYの最初のResponseで帰ってきているので1行には2つのカラムが入っています。
とりあえず1行だけ手でparseしてみましょう。
1 2 3 4 |
MySQL Packet Header: 0b 00 00 05 (payload 11byte, sequence id = 5) Payload: 01 31 08 70 65 74 72 75 63 63 69 column0(01): 31 (1) column1(08): 70 65 74 72 75 63 63 69 (petrucci) |
Length Encoded Stringがカラム数分連続しているだけなのでParseは単純ですね。文字列の1とpetrucciという値が送らてきていました。
ResultsetRowもColmunDefinitionと同様にEOF Packetがかえってくるまで繰り返して終了です。
通しでCOM_QUERYをプログラムから実行してみる
前回の記事のプログラムに今日の部分を追加してみたのがこんな感じです。
MySQL Packetは読み飛ばしやすいのでクライアントの実装にトライアンドエラーでやれると思います。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
package main import ( "bufio" "bytes" "crypto/sha1" "encoding/binary" "encoding/hex" "fmt" "io" "net" ) var username = "chobie" var password = "chobie" type CapabilityFlag int var ( CLIENT_LONG_PASSWORD CapabilityFlag = 0x0001 CLIENT_FOUND_ROWS CapabilityFlag = 0x0002 CLIENT_LONG_FLAG CapabilityFlag = 0x0004 CLIENT_CONNECT_WITH_DB CapabilityFlag = 0x0008 CLIENT_NO_SCHEMA CapabilityFlag = 0x0010 CLIENT_COMPRESS CapabilityFlag = 0x0020 CLIENT_ODBC CapabilityFlag = 0x0040 CLIENT_LOCAL_FILES CapabilityFlag = 0x0080 CLIENT_IGNORE_SPACE CapabilityFlag = 0x0100 CLIENT_PROTOCOL_41 CapabilityFlag = 0x0200 CLIENT_INTERACTIVE CapabilityFlag = 0x0400 CLIENT_SSL CapabilityFlag = 0x0800 CLIENT_IGNORE_SIGPIPE CapabilityFlag = 0x1000 CLIENT_TRANSACTIONS CapabilityFlag = 0x2000 CLIENT_RESERVED CapabilityFlag = 0x4000 CLIENT_SECURE_CONNECTION CapabilityFlag = 0x8000 CLIENT_PLUGIN_AUTH CapabilityFlag = 0x00080000 CLIENT_SESSION_TRACK CapabilityFlag = 0x00800000 ) func read_packet(reader io.Reader) (byte, byte) { header := make([]byte, 4) reader.Read(header) payload_size := uint32(header[0]) payload_size += uint32(header[1] >> 8) payload_size += uint32(header[2] >> 16) payload := make([]byte, payload_size) reader.Read(payload) return header, payload } func sha1_sum(data byte) byte { h := sha1.New() io.Copy(h, bytes.NewReader(data)) return h.Sum(nil) } func read_lenenc_int(data []byte) (int64, int, error) { var result int64 var read int = 1 var err error switch data[0] { case 0xfb: return 0, read, fmt.Errorf("NULL") case 0xfc: result = int64(binary.LittleEndian.Uint16(data[1:3])) read = 3 case 0xfd: result = int64(data[1]) result += int64(data[2] << 8) result += int64(data[3] << 16) read = 4 case 0xfe: result = int64(binary.LittleEndian.Uint64(data[1:9])) read = 9 case 0xff: err = fmt.Errorf("Err packet") default: result = int64(data[0]) } return result, read, err } func read_lenenc_str(data []byte) (string, int, error) { var result []byte length, offset, err := read_lenenc_int(data) if err != nil { return "", 0, err } result = data[offset : int64(offset)+length] return string(result), (int)(int64(offset) + length), nil } func com_query(query string) []byte { payload_size := len(query) + 1 buffer := make([]byte, payload_size+4) buffer[0] = byte(payload_size) buffer[1] = byte(payload_size >> 8) buffer[2] = byte(payload_size >> 16) buffer[3] = 0x00 buffer[4] = 0x03 // COM_QUERY copy(buffer[5:], query) return buffer } func main() { //READ INITIAL PACKET conn, err := net.Dial("tcp", "localhost:3306") reader := bufio.NewReader(conn) if err != nil { panic(err) } header, payload := read_packet(reader) fmt.Printf("[header]:\n%s\n[payload]:\n%s\n", hex.Dump(header), hex.Dump(payload)) // INITIAL Packetのparse。前回と内容同じなのでよまなくて大丈夫 offset := 0 protocol_version := int(payload[offset]) fmt.Printf(" [protocol_version]: %d\n", protocol_version) offset++ fmt.Printf(" [Initial Handshake Packet]:\n") idx := bytes.IndexByte(payload[offset:], 0x00) version := payload[offset : offset+idx] fmt.Printf(" [version]: %s\n", version) offset += idx + 1 connectionId := binary.LittleEndian.Uint32(payload[offset : offset+4]) fmt.Printf(" [connectionId]: %d\n", connectionId) offset += 4 auth_plugin_data_part1 := payload[offset : offset+8] fmt.Printf(" [auth_plugin_data_part1]: \n") fmt.Printf(" %s", hex.Dump(auth_plugin_data_part1)) offset += 8 filter1 := payload[offset : offset+1] fmt.Printf(" [filter1]: %s\n", filter1) offset += 1 capability_lower := payload[offset : offset+2] offset += 2 charset := uint8(payload[offset]) fmt.Printf(" [charset]: %d\n", charset) offset++ status := binary.LittleEndian.Uint16(payload[offset : offset+2]) fmt.Printf(" [status]: %d\n", status) offset += 2 capability_upper := payload[offset : offset+2] offset += 2 capabilities := binary.LittleEndian.Uint32(append(capability_lower, capability_upper...)) var auth_plugin_data_part2 []byte var auth_plugin_name []byte if capabilities&0x00080000 > 0 { //CLIENT_PLUGIN_AUTH length_of_plugin_auth_data := int(payload[offset]) offset++ // skips reserved 10bytes offset += 10 if capabilities&0x8000 > 0 { //CLIENT_SECURE_CONNECTION auth_plugin_data_part2 = payload[offset : offset+(length_of_plugin_auth_data-8)] offset += length_of_plugin_auth_data - 8 fmt.Printf(" [auth_plugin_data_part2]: \n") fmt.Printf(" %s", hex.Dump(auth_plugin_data_part2)) } idx = bytes.IndexByte(payload[offset:], 0x00) auth_plugin_name = payload[offset : offset+idx] fmt.Printf(" [auth_plugin_name]: %s\n", auth_plugin_name) } else { panic("not supported") } // 前回の範囲のINITIAL RESPONSE PACKET // // とりあえず先にscrambled passwordを作っておく // SHA1( password ) XOR SHA1( challenge + SHA1( SHA1( password ) ) ) auth_response := sha1_sum([]byte(password)) // XOR用のデータを作る key2 := sha1_sum(auth_response) challenge := append(auth_plugin_data_part1, auth_plugin_data_part2[0:12]...) // SHA1(challenge + SHA1(SHA1(password)))の部分 challenge = append(challenge, key2...) challenge_key := sha1_sum(challenge) for i := 0; i < 20; i++ { // XORしてく auth_response[i] ^= challenge_key[i] } // initial response packetのbuffer sizeを確保する buffer_size := 4 + 4 + 4 + 1 + 23 buffer_size += len(username) + 1 buffer_size += 1 buffer_size += len(auth_response) buffer_size += len(auth_plugin_name) + 1 buffer := make([]byte, buffer_size) offset = 0 // headerに記載するのはpayloadのサイズなので-4しとく size := buffer_size - 4 buffer[0] = byte(size) buffer[1] = byte(size >> 8) buffer[2] = byte(size >> 16) // initial response packetはinitial packetの返答なのでsequence idを1にしとく buffer[3] = 0x01 offset = 4 // 必要なデータをどんどん積んでいく capability_flags := uint32(CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_LONG_PASSWORD | CLIENT_TRANSACTIONS | CLIENT_LONG_FLAG) charset = 0x21 buffer[offset] = byte(capability_flags) buffer[offset+1] = byte(capability_flags >> 8) buffer[offset+2] = byte(capability_flags >> 16) buffer[offset+3] = byte(capability_flags >> 24) offset += 4 // max packet sizeは0でいいのでskipする offset += 4 buffer[offset] = byte(charset) offset += 1 //reserved 23 bytes文をskipする offset += 23 // username (null terminated string) copy(buffer[offset:], username) offset += len(username) + 1 buffer[offset] = byte(len(auth_response)) offset += 1 // さっき作ったauth responseのデータをコピー copy(buffer[offset:], auth_response) offset += len(auth_response) // 最後にauth plugin nameをコピーしておしまい copy(buffer[offset:], auth_plugin_name) fmt.Printf("\n[write_packet:%d bytes]:\n%s", len(buffer), hex.Dump(buffer)) conn.Write(buffer) // payloadの先頭が0x00なら認証成功になる。 header, payload = read_packet(conn) fmt.Printf("[header]:\n%s\n[payload]:\n%s\n", hex.Dump(header), hex.Dump(payload)) // 今回の範囲です // // ほいじゃ、COM_QUERYなげます q := com_query("select * from ex.ex1") conn.Write(q) header, payload = read_packet(conn) fmt.Printf("[header]:\n%s\n[Column Count(Length Encoded Integer)]:\n%s", hex.Dump(header), hex.Dump(payload)) column_count, _, _ := read_lenenc_int(payload) fmt.Printf("Column Count: %d\n", column_count) // カラム定義 for { header, payload = read_packet(conn) if payload[0] == 0xfe { // EOF Packet break } else if payload[0] == 0x00 { panic("Unsupported query result") } else { fmt.Println() offset := 0 catalog, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("Catalog: %s\n", catalog) offset += length schema, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("Schema: %s\n", schema) offset += length table, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("Table: %s\n", table) offset += length org_table, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("OrgTable: %s\n", org_table) offset += length name, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("Name: %s\n", name) offset += length org_name, length, _ := read_lenenc_str(payload[offset:]) fmt.Printf("OrgName: %s\n", org_name) offset += length offset += 1 // length of fixed length fields character_set := binary.LittleEndian.Uint16(payload[offset : offset+2]) fmt.Printf("CharacterSet: %d\n", character_set) offset += 2 column_length := binary.LittleEndian.Uint32(payload[offset : offset+4]) fmt.Printf("ColumnLength: %d\n", column_length) offset += 4 column_type := uint8(payload[offset]) fmt.Printf("ColumnType: %d\n", column_type) offset += 1 flags := binary.LittleEndian.Uint16(payload[offset : offset+2]) fmt.Printf("Flags: %d\n", flags) offset += 2 decimals := uint8(payload[offset]) fmt.Printf("Decimals: %d\n", decimals) offset += 1 } } // 結果セット fmt.Println() for { header, payload = read_packet(conn) if payload[0] == 0xfe { // EOF Packet break } else { offset := 0 fmt.Println() for i := 0; i < int(column_count); i++ { value, length, _ := read_lenenc_str(payload[offset:]) offset += length fmt.Printf("Value: %s\n", value) } } } } |
ちょっと雑ですがSelect Queryの実行から結果セットの取得までひと通り出来ましたね!
次回に向けて
この三回の連載でMySQLプロトコルの基礎的な部分は終わったのであとは興味があれば自分で実装ができると思います。
MySQLプロトコルを理解していればMySQLのネットワーク的な振る舞いが想像しやすくなりますし、色々なhackもできるので食わず嫌いせずに是非一度試してみてください。
次回のMySQLプロトコル入門の記事ではプロトコルから一旦離れBinlog Formatの解説に入ります。
それでは Happy Hacking