├── .gitignore ├── LICENSE ├── README.md ├── README_ja.md ├── client.go ├── cmd └── mysqlproxy │ ├── .gitignore │ ├── example.php │ └── main.go ├── genkey └── main.go ├── parser ├── mysqlproxy.toml ├── parser.go └── parser_test.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | genkey/genkey 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, YAMASAKI Masahide 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of mysqlproxy nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysqlproxy 2 | 3 | MySQL Proxy server. 4 | 5 | ## 前準備 6 | 7 | ### ワークディレクトリの作成 8 | 9 | ``` 10 | sudo mkdir -p /path/to/workdir 11 | ``` 12 | 13 | ### 公開鍵の設置 14 | 15 | ワークディレクトリに設置する 16 | 17 | ### 設定ファイルの設置(必要な場合) 18 | 19 | ※ ルート(クライアントから接続するためのデーモン)で必要な場合のみ設置 20 | 21 | ``` 22 | vim /path/to/mysqlproxy.toml 23 | ["<接続先MySQLユーザー名1>"] 24 | username = "<接続先MySQLユーザー名1>" 25 | password = "<接続先MySQLパスワード1>" 26 | proxyserver = "<接続先プロキシサーバのホスト1>" 27 | ["<接続先MySQLユーザー名2>"] 28 | username = "<接続先MySQLユーザー名2>" 29 | password = "<接続先MySQLパスワード2>" 30 | proxyserver = "<接続先プロキシサーバのホスト2>" 31 | …(繰り返し) 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Connect to MySQL Server via MySQL proxy server 37 | 38 | MySQL Proxy サーバーを経由してのMySQL接続方法 39 | 40 | #### 設定ファイルを使用しない場合 41 | 42 | ``` 43 | mysql -S /path/to/mysqlproxy.sock -u (:)@<プロキシサーバーのホスト>:<プロキシサーバーのポート>;:(;) -p 44 | Enter password: 45 | 46 | ※ ポートは3306番であっても必須。 47 | ※ パスワード、DB名は省略可能。パスワードがない場合、-pオプションは省略可能。 48 | ※ mysqlコマンドからだとユーザー名に文字数制限があるため、rdsのように長いドメインの場合は、 49 | PHP等の各種プログラミング言語のMySQL接続アダプタを介せば接続可能です。 50 | ``` 51 | 52 | #### 設定ファイルを使用する場合 53 | 54 | ``` 55 | mysql -S /path/to/mysqlproxy.sock -u @(:) -p 56 | Enter password: 57 | 58 | ※ プロキシサーバのアドレスは設定ファイルに書いているため省略可能ですが、ユーザー名とパスワードはプロキシサーバ自体の認証に必要です。 59 | ※ ポートが3306番であれば省略可能。 60 | ※ パスワードがない場合、-pオプションは省略可能。 61 | ※ mysqlコマンドからだとユーザー名に文字数制限があるため、rdsのように長いドメインの場合は、 62 | PHP等の各種プログラミング言語のMySQL接続アダプタを介せば接続可能です。 63 | ``` 64 | 65 | 66 | ### Starting MySQL proxy server (root) 67 | 68 | クライアントから接続するためのデーモン 69 | 70 | ``` 71 | ./mysqlproxy -root -workdir ワークディレクトリのパス -config 設定ファイルのパス 72 | ``` 73 | 74 | ### Starting MySQL proxy server 75 | 76 | MySQLサーバーに中継するためのデーモン 77 | 78 | ``` 79 | ./mysqlproxy -workdir ワークディレクトリのパス 80 | ``` 81 | 82 | ### PHP Sample 83 | 84 | ```php 85 | $link = mysql_connect( 86 | '/path/to/mysqlproxy.sock', 87 | ':@:;:', 88 | '', 89 | ); 90 | 91 | // For example in following Data flow. 92 | 93 | // Connect to A 94 | $link = mysql_connect( 95 | ':/path/to/mysqlproxy.sock', 96 | 'user_a:******@192.168.1.1:9696;192.168.1.2:3306', 97 | '******', 98 | ); 99 | 100 | // Connect to B 101 | $link = mysql_connect( 102 | ':/path/to/mysqlproxy.sock', 103 | 'user_b:******@192.168.1.1:9696;192.168.1.3:3306', 104 | '******', 105 | ); 106 | 107 | // Connect to C 108 | $link = mysql_connect( 109 | ':/path/to/mysqlproxy.sock', 110 | 'user_c:******@192.168.2.1:9696;192.168.2.2:3306', 111 | '******', 112 | ); 113 | 114 | // Connect to D 115 | $link = mysql_connect( 116 | ':/path/to/mysqlproxy.sock', 117 | 'user_d:******@192.168.2.1:9696;192.168.2.3:3306', 118 | '******', 119 | ); 120 | ``` 121 | 122 | ### Data flow 123 | 124 | ``` 125 | Unix domain socket TLS TCP 126 | Connect Connect Connect 127 | +--------+ +-------------+ +------------------+ +------------------+ 128 | | mysql | ---> | mysql proxy | -+-> | mysql proxy | -+-> | mysql server | 129 | | client | | (root) | | | | | | (A) | 130 | | (PHP) | | localhost | | | 192.168.1.1:9696 | | | 192.168.1.2:3306 | 131 | +--------+ +-------------+ | +------------------+ | +------------------+ 132 | | | 133 | | | +------------------+ 134 | | +-> | mysql server | 135 | | | (B) | 136 | | | 192.168.1.3:3306 | 137 | | +------------------+ 138 | | 139 | | +------------------+ +------------------+ 140 | +-> | mysql proxy | -+-> | mysql server | 141 | | | | | (C) | 142 | | 192.168.2.1:9696 | | | 192.168.2.2:3306 | 143 | +------------------+ | +------------------+ 144 | | 145 | | +------------------+ 146 | +-> | mysql server | 147 | | (D) | 148 | | 192.168.2.3:3306 | 149 | +------------------+ 150 | ``` 151 | 152 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # mysqlproxy 2 | 3 | MySQL Proxy server. 4 | 5 | ## Usage 6 | 7 | 設定ファイルを読み込む方法での使い方 8 | 9 | ### 親プロキシ側 10 | 11 | ```sh 12 | ./mysqlproxy -root -configpath=/path/to/config 13 | ``` 14 | 15 | ### 子プロキシ側 16 | 17 | ```sh 18 | # 従来通り 19 | ./mysqlproxy 20 | ``` 21 | 22 | ``` 23 | 接続方法 24 | mysql -S /path/to/mysqlproxy.sock -u user1@192.168.1.1 25 | mysql -S /path/to/mysqlproxy.sock -u user2@192.168.2.1 26 | 27 | 28 | 設定ファイル 29 | ["user1"] 30 | username = "user1" 31 | password = "******" 32 | proxyserver = "192.168.1.1" 33 | 34 | ["user2"] 35 | username = "user2" 36 | password = "******" 37 | proxyserver = "192.168.2.1" 38 | ``` 39 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package mysqlproxy 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "strings" 11 | "time" 12 | 13 | "github.com/siddontang/mixer/mysql" 14 | ) 15 | 16 | //proxy <-> mysql server 17 | type Conn struct { 18 | client *ClientConn 19 | conn net.Conn 20 | pkg *mysql.PacketIO 21 | addr string 22 | user string 23 | password string 24 | db string 25 | capability uint32 26 | status uint16 27 | collation mysql.CollationId 28 | charset string 29 | salt []byte 30 | lastPing int64 31 | pkgErr error 32 | } 33 | 34 | func (c *Conn) Connect(addr string, user string, password string, db string) error { 35 | c.addr = addr 36 | c.user = user 37 | c.password = password 38 | c.db = db 39 | 40 | //use utf8 41 | c.collation = mysql.DEFAULT_COLLATION_ID 42 | c.charset = mysql.DEFAULT_CHARSET 43 | 44 | return c.ReConnect() 45 | } 46 | 47 | func (c *Conn) ReConnect() error { 48 | if c.conn != nil { 49 | c.conn.Close() 50 | } 51 | 52 | n := "tcp" 53 | if strings.Contains(c.addr, "/") { 54 | n = "unix" 55 | } 56 | 57 | var err error 58 | var netConn net.Conn 59 | if c.client.proxy.cfg.TlsClient { 60 | netConn, err = tls.Dial(n, c.addr, c.client.proxy.cfg.TlsClientConf) 61 | } else { 62 | netConn, err = net.Dial(n, c.addr) 63 | } 64 | if err != nil { 65 | return err 66 | } 67 | 68 | switch netConn.(type) { 69 | case *net.TCPConn: 70 | tcpConn := netConn.(*net.TCPConn) 71 | 72 | //SetNoDelay controls whether the operating system should delay packet transmission 73 | // in hopes of sending fewer packets (Nagle's algorithm). 74 | // The default is true (no delay), 75 | // meaning that data is sent as soon as possible after a Write. 76 | //I set this option false. 77 | tcpConn.SetNoDelay(false) 78 | c.conn = tcpConn 79 | default: 80 | c.conn = netConn 81 | } 82 | c.pkg = mysql.NewPacketIO(c.conn) 83 | 84 | if err := c.readInitialHandshake(); err != nil { 85 | c.conn.Close() 86 | return err 87 | } 88 | 89 | if err := c.writeAuthHandshake(); err != nil { 90 | c.conn.Close() 91 | 92 | return err 93 | } 94 | 95 | if _, err := c.readOK(); err != nil { 96 | c.conn.Close() 97 | 98 | return err 99 | } 100 | 101 | //we must always use autocommit 102 | if !c.IsAutoCommit() { 103 | if _, err := c.exec("set autocommit = 1"); err != nil { 104 | c.conn.Close() 105 | 106 | return err 107 | } 108 | } 109 | 110 | c.lastPing = time.Now().Unix() 111 | 112 | return nil 113 | } 114 | 115 | func (c *Conn) readInitialHandshake() error { 116 | data, err := c.readPacket() 117 | if err != nil { 118 | return err 119 | } 120 | 121 | if data[0] == mysql.ERR_HEADER { 122 | return errors.New("read initial handshake error") 123 | } 124 | 125 | if data[0] < mysql.MinProtocolVersion { 126 | return fmt.Errorf("invalid protocol version %d, must >= 10", data[0]) 127 | } 128 | 129 | //skip mysql version and connection id 130 | //mysql version end with 0x00 131 | //connection id length is 4 132 | pos := 1 + bytes.IndexByte(data[1:], 0x00) + 1 + 4 133 | 134 | c.salt = append(c.salt, data[pos:pos+8]...) 135 | 136 | //skip filter 137 | pos += 8 + 1 138 | 139 | //capability lower 2 bytes 140 | c.capability = uint32(binary.LittleEndian.Uint16(data[pos : pos+2])) 141 | 142 | pos += 2 143 | 144 | if len(data) > pos { 145 | //skip server charset 146 | //c.charset = data[pos] 147 | pos += 1 148 | 149 | c.status = binary.LittleEndian.Uint16(data[pos : pos+2]) 150 | pos += 2 151 | 152 | c.capability = uint32(binary.LittleEndian.Uint16(data[pos:pos+2]))<<16 | c.capability 153 | 154 | pos += 2 155 | 156 | //skip auth data len or [00] 157 | //skip reserved (all [00]) 158 | pos += 10 + 1 159 | 160 | // The documentation is ambiguous about the length. 161 | // The official Python library uses the fixed length 12 162 | // mysql-proxy also use 12 163 | // which is not documented but seems to work. 164 | c.salt = append(c.salt, data[pos:pos+12]...) 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (c *Conn) writeAuthHandshake() error { 171 | // Adjust client capability flags based on server support 172 | capability := mysql.CLIENT_PROTOCOL_41 | mysql.CLIENT_SECURE_CONNECTION | 173 | mysql.CLIENT_LONG_PASSWORD | mysql.CLIENT_TRANSACTIONS | mysql.CLIENT_LONG_FLAG 174 | 175 | capability &= c.capability 176 | 177 | //packet length 178 | //capbility 4 179 | //max-packet size 4 180 | //charset 1 181 | //reserved all[0] 23 182 | length := 4 + 4 + 1 + 23 183 | 184 | //username 185 | length += len(c.user) + 1 186 | 187 | //we only support secure connection 188 | auth := mysql.CalcPassword(c.salt, []byte(c.password)) 189 | 190 | length += 1 + len(auth) 191 | 192 | if len(c.db) > 0 { 193 | capability |= mysql.CLIENT_CONNECT_WITH_DB 194 | 195 | length += len(c.db) + 1 196 | } 197 | 198 | c.capability = capability 199 | 200 | data := make([]byte, length+4) 201 | 202 | //capability [32 bit] 203 | data[4] = byte(capability) 204 | data[5] = byte(capability >> 8) 205 | data[6] = byte(capability >> 16) 206 | data[7] = byte(capability >> 24) 207 | 208 | //MaxPacketSize [32 bit] (none) 209 | //data[8] = 0x00 210 | //data[9] = 0x00 211 | //data[10] = 0x00 212 | //data[11] = 0x00 213 | 214 | //Charset [1 byte] 215 | data[12] = byte(c.collation) 216 | 217 | //Filler [23 bytes] (all 0x00) 218 | pos := 13 + 23 219 | 220 | //User [null terminated string] 221 | if len(c.user) > 0 { 222 | pos += copy(data[pos:], c.user) 223 | } 224 | //data[pos] = 0x00 225 | pos++ 226 | 227 | // auth [length encoded integer] 228 | data[pos] = byte(len(auth)) 229 | pos += 1 + copy(data[pos+1:], auth) 230 | 231 | // db [null terminated string] 232 | if len(c.db) > 0 { 233 | pos += copy(data[pos:], c.db) 234 | //data[pos] = 0x00 235 | } 236 | 237 | return c.writePacket(data) 238 | } 239 | 240 | func (c *Conn) readOK() (*mysql.Result, error) { 241 | data, err := c.readPacket() 242 | if err != nil { 243 | return nil, err 244 | } 245 | 246 | if data[0] == mysql.OK_HEADER { 247 | return c.handleOKPacket(data) 248 | } else if data[0] == mysql.ERR_HEADER { 249 | return nil, c.handleErrorPacket(data) 250 | } else { 251 | return nil, errors.New("invalid ok packet") 252 | } 253 | } 254 | 255 | func (c *Conn) writePacket(data []byte) error { 256 | err := c.pkg.WritePacket(data) 257 | c.pkgErr = err 258 | return err 259 | } 260 | 261 | func (c *Conn) readPacket() ([]byte, error) { 262 | d, err := c.pkg.ReadPacket() 263 | c.pkgErr = err 264 | return d, err 265 | } 266 | 267 | func (c *Conn) handleOKPacket(data []byte) (*mysql.Result, error) { 268 | var n int 269 | var pos int = 1 270 | 271 | r := new(mysql.Result) 272 | 273 | r.AffectedRows, _, n = mysql.LengthEncodedInt(data[pos:]) 274 | pos += n 275 | r.InsertId, _, n = mysql.LengthEncodedInt(data[pos:]) 276 | pos += n 277 | 278 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 279 | r.Status = binary.LittleEndian.Uint16(data[pos:]) 280 | c.status = r.Status 281 | pos += 2 282 | 283 | //todo:strict_mode, check warnings as error 284 | //Warnings := binary.LittleEndian.Uint16(data[pos:]) 285 | //pos += 2 286 | } else if c.capability&mysql.CLIENT_TRANSACTIONS > 0 { 287 | r.Status = binary.LittleEndian.Uint16(data[pos:]) 288 | c.status = r.Status 289 | pos += 2 290 | } 291 | 292 | //info 293 | return r, nil 294 | } 295 | func (c *Conn) handleErrorPacket(data []byte) error { 296 | e := new(mysql.SqlError) 297 | 298 | var pos int = 1 299 | 300 | e.Code = binary.LittleEndian.Uint16(data[pos:]) 301 | pos += 2 302 | 303 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 304 | //skip '#' 305 | pos++ 306 | e.State = string(data[pos : pos+5]) 307 | pos += 5 308 | } 309 | 310 | e.Message = string(data[pos:]) 311 | 312 | return e 313 | } 314 | 315 | func (c *Conn) IsAutoCommit() bool { 316 | return c.status&mysql.SERVER_STATUS_AUTOCOMMIT > 0 317 | } 318 | func (c *Conn) exec(query string) (*mysql.Result, error) { 319 | if err := c.writeCommandStr(mysql.COM_QUERY, query); err != nil { 320 | return nil, err 321 | } 322 | 323 | return c.readResult(false) 324 | } 325 | func (c *Conn) writeCommandStr(command byte, arg string) error { 326 | c.pkg.Sequence = 0 327 | 328 | length := len(arg) + 1 329 | 330 | data := make([]byte, length+4) 331 | 332 | data[4] = command 333 | 334 | copy(data[5:], arg) 335 | 336 | return c.writePacket(data) 337 | } 338 | func (c *Conn) readResult(binary bool) (*mysql.Result, error) { 339 | data, err := c.readPacket() 340 | if err != nil { 341 | return nil, err 342 | } 343 | 344 | if data[0] == mysql.OK_HEADER { 345 | return c.handleOKPacket(data) 346 | } else if data[0] == mysql.ERR_HEADER { 347 | return nil, c.handleErrorPacket(data) 348 | } else if data[0] == mysql.LocalInFile_HEADER { 349 | return nil, mysql.ErrMalformPacket 350 | } 351 | 352 | return c.readResultset(data, binary) 353 | } 354 | func (c *Conn) readResultset(data []byte, binary bool) (*mysql.Result, error) { 355 | result := &mysql.Result{ 356 | Status: 0, 357 | InsertId: 0, 358 | AffectedRows: 0, 359 | 360 | Resultset: &mysql.Resultset{}, 361 | } 362 | 363 | // column count 364 | count, _, n := mysql.LengthEncodedInt(data) 365 | 366 | if n-len(data) != 0 { 367 | return nil, mysql.ErrMalformPacket 368 | } 369 | 370 | result.Fields = make([]*mysql.Field, count) 371 | result.FieldNames = make(map[string]int, count) 372 | 373 | if err := c.readResultColumns(result); err != nil { 374 | return nil, err 375 | } 376 | 377 | if err := c.readResultRows(result, binary); err != nil { 378 | return nil, err 379 | } 380 | 381 | return result, nil 382 | } 383 | func (c *Conn) readResultColumns(result *mysql.Result) (err error) { 384 | var i int = 0 385 | var data []byte 386 | 387 | for { 388 | data, err = c.readPacket() 389 | if err != nil { 390 | return 391 | } 392 | 393 | // EOF Packet 394 | if c.isEOFPacket(data) { 395 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 396 | //result.Warnings = binary.LittleEndian.Uint16(data[1:]) 397 | //todo add strict_mode, warning will be treat as error 398 | result.Status = binary.LittleEndian.Uint16(data[3:]) 399 | c.status = result.Status 400 | } 401 | 402 | if i != len(result.Fields) { 403 | err = mysql.ErrMalformPacket 404 | } 405 | 406 | return 407 | } 408 | 409 | result.Fields[i], err = mysql.FieldData(data).Parse() 410 | if err != nil { 411 | return 412 | } 413 | 414 | result.FieldNames[string(result.Fields[i].Name)] = i 415 | 416 | i++ 417 | } 418 | } 419 | func (c *Conn) readResultRows(result *mysql.Result, isBinary bool) (err error) { 420 | var data []byte 421 | 422 | for { 423 | data, err = c.readPacket() 424 | 425 | if err != nil { 426 | return 427 | } 428 | 429 | // EOF Packet 430 | if c.isEOFPacket(data) { 431 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 432 | //result.Warnings = binary.LittleEndian.Uint16(data[1:]) 433 | //todo add strict_mode, warning will be treat as error 434 | result.Status = binary.LittleEndian.Uint16(data[3:]) 435 | c.status = result.Status 436 | } 437 | 438 | break 439 | } 440 | 441 | result.RowDatas = append(result.RowDatas, data) 442 | } 443 | 444 | result.Values = make([][]interface{}, len(result.RowDatas)) 445 | 446 | for i := range result.Values { 447 | result.Values[i], err = result.RowDatas[i].Parse(result.Fields, isBinary) 448 | 449 | if err != nil { 450 | return err 451 | } 452 | } 453 | 454 | return nil 455 | } 456 | func (c *Conn) readUntilEOF() (err error) { 457 | var data []byte 458 | 459 | for { 460 | data, err = c.readPacket() 461 | 462 | if err != nil { 463 | return 464 | } 465 | 466 | // EOF Packet 467 | if c.isEOFPacket(data) { 468 | return 469 | } 470 | } 471 | return 472 | } 473 | 474 | func (c *Conn) isEOFPacket(data []byte) bool { 475 | return data[0] == mysql.EOF_HEADER && len(data) <= 5 476 | } 477 | -------------------------------------------------------------------------------- /cmd/mysqlproxy/.gitignore: -------------------------------------------------------------------------------- 1 | ca.key 2 | ca.pem 3 | client.key 4 | client.pem 5 | db.php 6 | mysqlproxy 7 | mysqlproxy.sock 8 | mysqlproxy.toml 9 | -------------------------------------------------------------------------------- /cmd/mysqlproxy/example.php: -------------------------------------------------------------------------------- 1 | proxy 200 | type ClientConn struct { 201 | pkg *mysql.PacketIO 202 | c net.Conn 203 | proxy *Server 204 | capability uint32 205 | connectionId uint32 206 | status uint16 207 | collation mysql.CollationId 208 | charset string 209 | user string 210 | db string 211 | salt []byte 212 | closed bool 213 | lastInsertId int64 214 | affectedRows int64 215 | node *NodeConfig 216 | } 217 | 218 | func (c *ClientConn) Close() error { 219 | if c.closed { 220 | return nil 221 | } 222 | 223 | c.c.Close() 224 | 225 | c.closed = true 226 | 227 | return nil 228 | } 229 | func (c *ClientConn) IsAllowConnect() bool { 230 | clientHost, _, err := net.SplitHostPort(c.c.RemoteAddr().String()) 231 | if err != nil { 232 | fmt.Println(err) 233 | } 234 | clientIP := net.ParseIP(clientHost) 235 | 236 | ipVec := c.proxy.allowips 237 | if ipVecLen := len(ipVec); ipVecLen == 0 { 238 | return true 239 | } 240 | for _, ip := range ipVec { 241 | if ip.Equal(clientIP) { 242 | return true 243 | } 244 | } 245 | 246 | log.Printf("Error server.IsAllowConnect [Access denied]. address:%s ", c.c.RemoteAddr().String()) 247 | return false 248 | } 249 | func (c *ClientConn) writeError(e error) error { 250 | var m *mysql.SqlError 251 | var ok bool 252 | if m, ok = e.(*mysql.SqlError); !ok { 253 | m = mysql.NewError(mysql.ER_UNKNOWN_ERROR, e.Error()) 254 | } 255 | 256 | data := make([]byte, 4, 16+len(m.Message)) 257 | 258 | data = append(data, mysql.ERR_HEADER) 259 | data = append(data, byte(m.Code), byte(m.Code>>8)) 260 | 261 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 262 | data = append(data, '#') 263 | data = append(data, m.State...) 264 | } 265 | 266 | data = append(data, m.Message...) 267 | 268 | return c.writePacket(data) 269 | } 270 | func (c *ClientConn) Handshake() error { 271 | if err := c.writeInitialHandshake(); err != nil { 272 | log.Printf("Error server.Handshake [%s] connectionId:%d", err.Error(), c.connectionId) 273 | return err 274 | } 275 | 276 | if err := c.readHandshakeResponse(); err != nil { 277 | log.Printf("Error server.readHandshakeResponse [%s] connectionId:%d", err.Error(), c.connectionId) 278 | 279 | c.writeError(err) 280 | 281 | return err 282 | } 283 | 284 | if err := c.writeOK(nil); err != nil { 285 | log.Printf("Error server.readHandshakeResponse [write ok fail] [%s] connectionId:%d", err.Error(), c.connectionId) 286 | return err 287 | } 288 | 289 | c.pkg.Sequence = 0 290 | 291 | return nil 292 | } 293 | func (c *ClientConn) writeOK(r *mysql.Result) error { 294 | if r == nil { 295 | r = &mysql.Result{Status: c.status} 296 | } 297 | data := make([]byte, 4, 32) 298 | 299 | data = append(data, mysql.OK_HEADER) 300 | 301 | data = append(data, mysql.PutLengthEncodedInt(r.AffectedRows)...) 302 | data = append(data, mysql.PutLengthEncodedInt(r.InsertId)...) 303 | 304 | if c.capability&mysql.CLIENT_PROTOCOL_41 > 0 { 305 | data = append(data, byte(r.Status), byte(r.Status>>8)) 306 | data = append(data, 0, 0) 307 | } 308 | 309 | return c.writePacket(data) 310 | } 311 | 312 | func (c *ClientConn) writeInitialHandshake() error { 313 | data := make([]byte, 4, 128) 314 | 315 | //min version 10 316 | data = append(data, 10) 317 | 318 | //server version[00] 319 | data = append(data, mysql.ServerVersion...) 320 | data = append(data, 0) 321 | 322 | //connection id 323 | data = append(data, byte(c.connectionId), byte(c.connectionId>>8), byte(c.connectionId>>16), byte(c.connectionId>>24)) 324 | 325 | //auth-plugin-data-part-1 326 | data = append(data, c.salt[0:8]...) 327 | 328 | //filter [00] 329 | data = append(data, 0) 330 | 331 | //capability flag lower 2 bytes, using default capability here 332 | data = append(data, byte(DEFAULT_CAPABILITY), byte(DEFAULT_CAPABILITY>>8)) 333 | 334 | //charset, utf-8 default 335 | data = append(data, uint8(mysql.DEFAULT_COLLATION_ID)) 336 | 337 | //status 338 | data = append(data, byte(c.status), byte(c.status>>8)) 339 | 340 | //below 13 byte may not be used 341 | //capability flag upper 2 bytes, using default capability here 342 | data = append(data, byte(DEFAULT_CAPABILITY>>16), byte(DEFAULT_CAPABILITY>>24)) 343 | 344 | //filter [0x15], for wireshark dump, value is 0x15 345 | data = append(data, 0x15) 346 | 347 | //reserved 10 [00] 348 | data = append(data, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 349 | 350 | //auth-plugin-data-part-2 351 | data = append(data, c.salt[8:]...) 352 | 353 | //filter [00] 354 | data = append(data, 0) 355 | 356 | return c.writePacket(data) 357 | } 358 | 359 | func (c *ClientConn) getNodeFromConfigFile() (*NodeConfig, error) { 360 | if c.proxy.cfg.ConfigPath == "" { 361 | return nil, nil 362 | } 363 | if strings.Contains(c.user, ";") { 364 | return nil, nil 365 | } 366 | p := parser.Parser{ 367 | ConfigPath: c.proxy.cfg.ConfigPath, 368 | } 369 | proxyUsers, err := p.Parse() 370 | if err != nil { 371 | return nil, err 372 | } 373 | substrings := strings.Split(c.user, "@") 374 | if len(substrings) != 2 { 375 | return nil, fmt.Errorf("Invalid user: %s", c.user) 376 | } 377 | proxyUser := proxyUsers[substrings[0]] 378 | proxyAddr := proxyUser.ProxyServer 379 | if !strings.Contains(proxyAddr, ":") { 380 | proxyAddr = fmt.Sprintf("%s:%s", proxyAddr, DefaultMySQLProxyPort) 381 | } 382 | dbAddr := substrings[1] 383 | if !strings.Contains(dbAddr, ":") { 384 | dbAddr = fmt.Sprintf("%s:%s", dbAddr, DefaultMySQLPort) 385 | } 386 | node := &NodeConfig{ 387 | User: fmt.Sprintf( 388 | "%s:%s@%s;%s", 389 | proxyUser.Username, 390 | proxyUser.Password, 391 | proxyAddr, 392 | dbAddr, 393 | ), 394 | Password: proxyUser.Password, 395 | Addr: proxyAddr, 396 | } 397 | return node, nil 398 | } 399 | 400 | var nodeRe = regexp.MustCompile(`^(.+):(.*)@(.+:\d+);(.+:\d+)(;(.+))?$`) 401 | 402 | // getNode parse from c.user 403 | // example: user:pass@proxy_host:proxy_port;db_host:db_port;db_name 404 | // pass and db_name is optional 405 | // example: user:@proxy_host:proxy_port;db_host:db_port 406 | func (c *ClientConn) getNode() error { 407 | var err error 408 | if c.node, err = c.getNodeFromConfigFile(); err != nil { 409 | log.Print(err) 410 | } 411 | if c.node != nil { 412 | return nil 413 | } 414 | matches := nodeRe.FindStringSubmatch(c.user) 415 | if len(matches) != 7 { 416 | return fmt.Errorf("Invalid user: %s", c.user) 417 | } 418 | if c.proxy.cfg.TlsClient { 419 | c.node = &NodeConfig{ 420 | User: c.user, 421 | Password: matches[2], 422 | Addr: matches[3], 423 | } 424 | return nil 425 | } 426 | c.node = &NodeConfig{ 427 | User: matches[1], 428 | Password: matches[2], 429 | Db: matches[6], 430 | Addr: matches[4], 431 | } 432 | return nil 433 | } 434 | func (c *ClientConn) readHandshakeResponse() error { 435 | data, err := c.readPacket() 436 | 437 | if err != nil { 438 | return err 439 | } 440 | 441 | pos := 0 442 | 443 | //capability 444 | c.capability = binary.LittleEndian.Uint32(data[:4]) 445 | pos += 4 446 | 447 | //skip max packet size 448 | pos += 4 449 | 450 | //charset, skip, if you want to use another charset, use set names 451 | //c.collation = CollationId(data[pos]) 452 | pos++ 453 | 454 | //skip reserved 23[00] 455 | pos += 23 456 | 457 | //user name 458 | c.user = string(data[pos : pos+bytes.IndexByte(data[pos:], 0)]) 459 | if err := c.getNode(); err != nil { 460 | return err 461 | } 462 | pos += len(c.user) + 1 463 | 464 | //auth length and auth 465 | authLen := int(data[pos]) 466 | pos++ 467 | auth := data[pos : pos+authLen] 468 | 469 | checkAuth := mysql.CalcPassword(c.salt, []byte(c.node.Password)) 470 | if !bytes.Equal(auth, checkAuth) { 471 | log.Printf("Error ClientConn.readHandshakeResponse. auth:%v, checkAuth:%v, Password:%v", auth, checkAuth, c.node.Password) 472 | return mysql.NewDefaultError(mysql.ER_ACCESS_DENIED_ERROR, c.c.RemoteAddr().String(), c.user, "Yes") 473 | } 474 | 475 | pos += authLen 476 | 477 | if c.capability&mysql.CLIENT_CONNECT_WITH_DB > 0 { 478 | if len(data[pos:]) == 0 { 479 | return nil 480 | } 481 | 482 | db := string(data[pos : pos+bytes.IndexByte(data[pos:], 0)]) 483 | pos += len(c.db) + 1 484 | 485 | if err := c.useDB(db); err != nil { 486 | return err 487 | } 488 | } 489 | 490 | return nil 491 | } 492 | func (c *ClientConn) useDB(db string) error { 493 | c.db = db 494 | c.node.Db = db 495 | return nil 496 | } 497 | 498 | func (c *ClientConn) readPacket() ([]byte, error) { 499 | return c.pkg.ReadPacket() 500 | } 501 | 502 | func (c *ClientConn) writePacket(data []byte) error { 503 | return c.pkg.WritePacket(data) 504 | } 505 | 506 | func (c *ClientConn) Run() { 507 | defer func() { 508 | r := recover() 509 | if err, ok := r.(error); ok { 510 | const size = 4096 511 | buf := make([]byte, size) 512 | buf = buf[:runtime.Stack(buf, false)] 513 | 514 | log.Printf("Error ClientConn.Run [%s] stak:%s", err.Error(), string(buf)) 515 | } 516 | 517 | c.Close() 518 | }() 519 | 520 | log.Printf("Success handshake. RemoteAddr:%s", c.c.RemoteAddr()) 521 | co := new(Conn) 522 | co.client = c 523 | db := c.node 524 | if err := co.Connect(db.Addr, db.User, db.Password, db.Db); err != nil { 525 | log.Fatal(err) 526 | } 527 | log.Printf("Success Connect. RemoteAddr:%s", co.conn.RemoteAddr()) 528 | 529 | done := make(chan bool) 530 | var once sync.Once 531 | onceDone := func() { 532 | log.Printf("done.") 533 | done <- true 534 | } 535 | go func() { 536 | io.Copy(c.c, co.conn) 537 | once.Do(onceDone) 538 | }() 539 | go func() { 540 | io.Copy(co.conn, c.c) 541 | once.Do(onceDone) 542 | }() 543 | <-done 544 | } 545 | --------------------------------------------------------------------------------