app-init
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
func nativePasswordToken(password string, scramble []byte) []byte {
|
||||
if password == "" {
|
||||
return []byte{}
|
||||
}
|
||||
if len(scramble) > 20 {
|
||||
scramble = scramble[:20]
|
||||
}
|
||||
hash1 := sha1.Sum([]byte(password))
|
||||
hash2 := sha1.Sum(hash1[:])
|
||||
h := sha1.New()
|
||||
h.Write(scramble)
|
||||
h.Write(hash2[:])
|
||||
hash3 := h.Sum(nil)
|
||||
out := make([]byte, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
out[i] = hash1[i] ^ hash3[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func verifyNativePassword(password string, scramble, auth []byte) bool {
|
||||
if password == "" {
|
||||
return len(auth) == 0
|
||||
}
|
||||
want := nativePasswordToken(password, scramble)
|
||||
if len(auth) != len(want) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(auth, want) == 1
|
||||
}
|
||||
|
||||
func usernamesEqual(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type packetConn struct {
|
||||
c net.Conn
|
||||
seq uint8
|
||||
max int
|
||||
readTO time.Duration
|
||||
writeTO time.Duration
|
||||
}
|
||||
|
||||
func newPacketConn(c net.Conn, timeout time.Duration) *packetConn {
|
||||
return &packetConn{c: c, max: MaxPacketSize, readTO: timeout, writeTO: timeout}
|
||||
}
|
||||
|
||||
func (p *packetConn) resetSeq() { p.seq = 0 }
|
||||
|
||||
func (p *packetConn) readPacket() ([]byte, error) {
|
||||
var payload []byte
|
||||
for {
|
||||
if p.readTO > 0 {
|
||||
_ = p.c.SetReadDeadline(time.Now().Add(p.readTO))
|
||||
}
|
||||
var hdr [4]byte
|
||||
if _, err := io.ReadFull(p.c, hdr[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := int(readU24(hdr[:3]))
|
||||
seq := hdr[3]
|
||||
if seq != p.seq {
|
||||
return nil, fmt.Errorf("sequence mismatch: got %d want %d", seq, p.seq)
|
||||
}
|
||||
p.seq++
|
||||
if n == 0 {
|
||||
return payload, nil
|
||||
}
|
||||
if n > p.max {
|
||||
return nil, fmt.Errorf("packet too large: %d", n)
|
||||
}
|
||||
chunk := make([]byte, n)
|
||||
if _, err := io.ReadFull(p.c, chunk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload = append(payload, chunk...)
|
||||
if n < 0xffffff {
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetConn) writePacket(payload []byte) error {
|
||||
off := 0
|
||||
for {
|
||||
n := len(payload) - off
|
||||
if n > 0xffffff {
|
||||
n = 0xffffff
|
||||
}
|
||||
var hdr [4]byte
|
||||
hdr[0] = byte(n)
|
||||
hdr[1] = byte(n >> 8)
|
||||
hdr[2] = byte(n >> 16)
|
||||
hdr[3] = p.seq
|
||||
p.seq++
|
||||
if p.writeTO > 0 {
|
||||
_ = p.c.SetWriteDeadline(time.Now().Add(p.writeTO))
|
||||
}
|
||||
if _, err := p.c.Write(hdr[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
if _, err := p.c.Write(payload[off : off+n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
off += n
|
||||
if n < 0xffffff {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *packetConn) writeOK(affected, lastInsertID uint64, status, warnings uint16) error {
|
||||
return p.writePacket(encodeOK(affected, lastInsertID, status, warnings))
|
||||
}
|
||||
|
||||
func (p *packetConn) writeERR(errno uint16, msg string) error {
|
||||
return p.writePacket(encodeERR(errno, msg))
|
||||
}
|
||||
|
||||
func (p *packetConn) remote() string {
|
||||
if p.c.RemoteAddr() == nil {
|
||||
return ""
|
||||
}
|
||||
return p.c.RemoteAddr().String()
|
||||
}
|
||||
|
||||
func (p *packetConn) close() error {
|
||||
return p.c.Close()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package mysqlserver implements a loopback MySQL protocol server.
|
||||
// Yingdao connects here; COM_QUERY is forwarded through the Navicat HTTP tunnel.
|
||||
package mysqlserver
|
||||
@@ -0,0 +1,57 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func TestGoSQLDriverCompat(t *testing.T) {
|
||||
addr, _ := startServer(t, stubBackend{}, "root", "123456")
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dsn := fmt.Sprintf("root:123456@tcp(%s:%s)/app?timeout=5s&readTimeout=5s&writeTimeout=5s&charset=utf8mb4&parseTime=false", host, port)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
var v string
|
||||
if err := db.QueryRow("SELECT 1").Scan(&v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v != "1" {
|
||||
t.Fatalf("got %q", v)
|
||||
}
|
||||
|
||||
var n sql.NullString
|
||||
if err := db.QueryRow("SELECT NULL AS n").Scan(&n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n.Valid {
|
||||
t.Fatal("expected NULL")
|
||||
}
|
||||
|
||||
var big string
|
||||
if err := db.QueryRow("SELECT bigint_val").Scan(&big); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if big != "18446744073709551615" {
|
||||
t.Fatalf("bigint %q", big)
|
||||
}
|
||||
|
||||
var zh string
|
||||
if err := db.QueryRow("SELECT zh_val").Scan(&zh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if zh != "你好世界" {
|
||||
t.Fatalf("zh %q", zh)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package mysqlserver
|
||||
|
||||
import "crypto/rand"
|
||||
|
||||
func buildHandshake(connID uint32, scramble []byte) []byte {
|
||||
if len(scramble) < 20 {
|
||||
s := make([]byte, 20)
|
||||
copy(s, scramble)
|
||||
scramble = s
|
||||
}
|
||||
b := []byte{ProtocolVersion}
|
||||
b = appendNUL(b, ServerVersion)
|
||||
b = appendU32(b, connID)
|
||||
b = append(b, scramble[:8]...)
|
||||
b = append(b, 0x00)
|
||||
b = appendU16(b, uint16(serverCapabilities))
|
||||
b = append(b, charsetUTF8MB4)
|
||||
b = appendU16(b, serverStatusAutocommit)
|
||||
b = appendU16(b, uint16(serverCapabilities>>16))
|
||||
b = append(b, 21) // auth plugin data len: 20 bytes + NUL
|
||||
b = append(b, make([]byte, 10)...)
|
||||
b = append(b, scramble[8:20]...)
|
||||
b = append(b, 0x00)
|
||||
b = appendNUL(b, AuthPluginNative)
|
||||
return b
|
||||
}
|
||||
|
||||
func randomScramble() ([]byte, error) {
|
||||
s := make([]byte, 20)
|
||||
if _, err := rand.Read(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, c := range s {
|
||||
if c == 0 {
|
||||
s[i] = 1
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type handshakeResponse struct {
|
||||
Capabilities uint32
|
||||
MaxPacket uint32
|
||||
Charset byte
|
||||
Username string
|
||||
Auth []byte
|
||||
Database string
|
||||
Plugin string
|
||||
SSL bool
|
||||
}
|
||||
|
||||
func parseHandshakeResponse(payload []byte) (handshakeResponse, error) {
|
||||
var hr handshakeResponse
|
||||
if len(payload) < 32 {
|
||||
return hr, errPacket("handshake response too short")
|
||||
}
|
||||
hr.Capabilities = uint32(payload[0]) | uint32(payload[1])<<8 | uint32(payload[2])<<16 | uint32(payload[3])<<24
|
||||
hr.MaxPacket = uint32(payload[4]) | uint32(payload[5])<<8 | uint32(payload[6])<<16 | uint32(payload[7])<<24
|
||||
hr.Charset = payload[8]
|
||||
hr.SSL = hr.Capabilities&clientSSL != 0
|
||||
if hr.SSL && len(payload) <= 32 {
|
||||
return hr, nil
|
||||
}
|
||||
if hr.Capabilities&clientProtocol41 == 0 {
|
||||
return hr, errPacket("CLIENT_PROTOCOL_41 required")
|
||||
}
|
||||
off := 32
|
||||
user, off, err := readNUL(payload, off)
|
||||
if err != nil {
|
||||
return hr, err
|
||||
}
|
||||
hr.Username = user
|
||||
|
||||
switch {
|
||||
case hr.Capabilities&clientPluginAuthLenencClientData != 0:
|
||||
n, nOff, err := readLenencInt(payload, off)
|
||||
if err != nil {
|
||||
return hr, err
|
||||
}
|
||||
off = nOff
|
||||
end := off + int(n)
|
||||
if end > len(payload) {
|
||||
return hr, errPacket("auth-response truncated")
|
||||
}
|
||||
hr.Auth = append([]byte(nil), payload[off:end]...)
|
||||
off = end
|
||||
case hr.Capabilities&clientSecureConnection != 0:
|
||||
if off >= len(payload) {
|
||||
return hr, errPacket("auth-response truncated")
|
||||
}
|
||||
n := int(payload[off])
|
||||
off++
|
||||
end := off + n
|
||||
if end > len(payload) {
|
||||
return hr, errPacket("auth-response truncated")
|
||||
}
|
||||
hr.Auth = append([]byte(nil), payload[off:end]...)
|
||||
off = end
|
||||
default:
|
||||
s, nOff, err := readNUL(payload, off)
|
||||
if err != nil {
|
||||
return hr, err
|
||||
}
|
||||
hr.Auth = []byte(s)
|
||||
off = nOff
|
||||
}
|
||||
|
||||
if hr.Capabilities&clientConnectWithDB != 0 && off < len(payload) {
|
||||
db, nOff, err := readNUL(payload, off)
|
||||
if err != nil {
|
||||
return hr, err
|
||||
}
|
||||
hr.Database = db
|
||||
off = nOff
|
||||
}
|
||||
if hr.Capabilities&clientPluginAuth != 0 && off < len(payload) {
|
||||
plugin, _, err := readNUL(payload, off)
|
||||
if err != nil {
|
||||
return hr, err
|
||||
}
|
||||
hr.Plugin = plugin
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
func encodeAuthSwitch(plugin string, scramble []byte) []byte {
|
||||
b := []byte{markerEOF} // 0xFE AuthSwitchRequest
|
||||
b = appendNUL(b, plugin)
|
||||
b = append(b, scramble...)
|
||||
b = append(b, 0x00)
|
||||
return b
|
||||
}
|
||||
|
||||
type protoError struct{ s string }
|
||||
|
||||
func (e protoError) Error() string { return e.s }
|
||||
|
||||
func errPacket(s string) error { return protoError{s} }
|
||||
@@ -0,0 +1,263 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
ProtocolVersion = 10
|
||||
ServerVersion = "8.0.0-mysql-tunnel"
|
||||
AuthPluginNative = "mysql_native_password"
|
||||
AuthPluginSHA2 = "caching_sha2_password"
|
||||
|
||||
MaxPacketSize = 16 << 20
|
||||
)
|
||||
|
||||
const (
|
||||
ComQuit byte = 0x01
|
||||
ComInitDB byte = 0x02
|
||||
ComQuery byte = 0x03
|
||||
ComFieldList byte = 0x04
|
||||
ComPing byte = 0x0e
|
||||
ComStmtPrepare byte = 0x16
|
||||
ComStmtExecute byte = 0x17
|
||||
ComStmtClose byte = 0x19
|
||||
ComResetConnection byte = 0x1f
|
||||
)
|
||||
|
||||
const (
|
||||
clientLongPassword uint32 = 1
|
||||
clientFoundRows uint32 = 1 << 1
|
||||
clientLongFlag uint32 = 1 << 2
|
||||
clientConnectWithDB uint32 = 1 << 3
|
||||
clientProtocol41 uint32 = 1 << 9
|
||||
clientSSL uint32 = 1 << 11
|
||||
clientTransactions uint32 = 1 << 13
|
||||
clientSecureConnection uint32 = 1 << 15
|
||||
clientMultiStatements uint32 = 1 << 16
|
||||
clientMultiResults uint32 = 1 << 17
|
||||
clientPSMultiResults uint32 = 1 << 18
|
||||
clientPluginAuth uint32 = 1 << 19
|
||||
clientConnectAttrs uint32 = 1 << 20
|
||||
clientPluginAuthLenencClientData uint32 = 1 << 21
|
||||
clientSessionTrack uint32 = 1 << 23
|
||||
clientDeprecateEOF uint32 = 1 << 24
|
||||
)
|
||||
|
||||
const (
|
||||
serverStatusAutocommit uint16 = 0x0002
|
||||
)
|
||||
|
||||
const (
|
||||
charsetUTF8MB4 uint8 = 45
|
||||
charsetBinary uint8 = 63
|
||||
)
|
||||
|
||||
const (
|
||||
typeDecimal byte = 0
|
||||
typeTiny byte = 1
|
||||
typeShort byte = 2
|
||||
typeLong byte = 3
|
||||
typeFloat byte = 4
|
||||
typeDouble byte = 5
|
||||
typeNull byte = 6
|
||||
typeTimestamp byte = 7
|
||||
typeLongLong byte = 8
|
||||
typeInt24 byte = 9
|
||||
typeDate byte = 10
|
||||
typeTime byte = 11
|
||||
typeDatetime byte = 12
|
||||
typeYear byte = 13
|
||||
typeJSON byte = 245
|
||||
typeNewDecimal byte = 246
|
||||
typeTinyBlob byte = 249
|
||||
typeMediumBlob byte = 250
|
||||
typeLongBlob byte = 251
|
||||
typeBlob byte = 252
|
||||
typeVarString byte = 253
|
||||
typeString byte = 254
|
||||
)
|
||||
|
||||
const (
|
||||
flagNotNull uint16 = 1
|
||||
flagPriKey uint16 = 2
|
||||
flagUniqueKey uint16 = 4
|
||||
flagMultipleKey uint16 = 8
|
||||
flagBlob uint16 = 16
|
||||
flagUnsigned uint16 = 32
|
||||
flagZerofill uint16 = 64
|
||||
flagBinary uint16 = 128
|
||||
flagEnum uint16 = 256
|
||||
flagAutoIncrement uint16 = 512
|
||||
flagTimestamp uint16 = 1024
|
||||
flagSet uint16 = 2048
|
||||
)
|
||||
|
||||
const (
|
||||
errAccessDenied uint16 = 1045
|
||||
errUnknownCommand uint16 = 1047
|
||||
errNoDB uint16 = 1046
|
||||
errParseError uint16 = 1064
|
||||
errUnknown uint16 = 1105
|
||||
errNotSupported uint16 = 1235
|
||||
)
|
||||
|
||||
const (
|
||||
markerOK byte = 0x00
|
||||
markerEOF byte = 0xfe
|
||||
markerERR byte = 0xff
|
||||
markerNull byte = 0xfb
|
||||
)
|
||||
|
||||
var serverCapabilities = clientLongPassword | clientFoundRows | clientLongFlag |
|
||||
clientConnectWithDB | clientProtocol41 | clientTransactions | clientSecureConnection |
|
||||
clientMultiResults | clientPluginAuth | clientPluginAuthLenencClientData |
|
||||
clientConnectAttrs | clientDeprecateEOF | clientSessionTrack
|
||||
|
||||
func appendU24(dst []byte, n uint32) []byte {
|
||||
return append(dst, byte(n), byte(n>>8), byte(n>>16))
|
||||
}
|
||||
|
||||
func appendU32(dst []byte, n uint32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, n)
|
||||
return append(dst, b...)
|
||||
}
|
||||
|
||||
func appendU16(dst []byte, n uint16) []byte {
|
||||
return append(dst, byte(n), byte(n>>8))
|
||||
}
|
||||
|
||||
func appendNUL(dst []byte, s string) []byte {
|
||||
dst = append(dst, s...)
|
||||
return append(dst, 0)
|
||||
}
|
||||
|
||||
func appendLenencInt(dst []byte, n uint64) []byte {
|
||||
switch {
|
||||
case n < 251:
|
||||
return append(dst, byte(n))
|
||||
case n < 1<<16:
|
||||
return append(dst, 0xfc, byte(n), byte(n>>8))
|
||||
case n < 1<<24:
|
||||
return append(dst, 0xfd, byte(n), byte(n>>8), byte(n>>16))
|
||||
default:
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, n)
|
||||
return append(append(dst, 0xfe), b...)
|
||||
}
|
||||
}
|
||||
|
||||
func appendLenencString(dst []byte, s []byte) []byte {
|
||||
dst = appendLenencInt(dst, uint64(len(s)))
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
func readU24(b []byte) uint32 {
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16
|
||||
}
|
||||
|
||||
func readLenencInt(b []byte, off int) (uint64, int, error) {
|
||||
if off >= len(b) {
|
||||
return 0, off, io.ErrUnexpectedEOF
|
||||
}
|
||||
switch b[off] {
|
||||
case 0xfb:
|
||||
return 0, off + 1, fmt.Errorf("NULL lenenc")
|
||||
case 0xfc:
|
||||
if off+3 > len(b) {
|
||||
return 0, off, io.ErrUnexpectedEOF
|
||||
}
|
||||
return uint64(b[off+1]) | uint64(b[off+2])<<8, off + 3, nil
|
||||
case 0xfd:
|
||||
if off+4 > len(b) {
|
||||
return 0, off, io.ErrUnexpectedEOF
|
||||
}
|
||||
return uint64(b[off+1]) | uint64(b[off+2])<<8 | uint64(b[off+3])<<16, off + 4, nil
|
||||
case 0xfe:
|
||||
if off+9 > len(b) {
|
||||
return 0, off, io.ErrUnexpectedEOF
|
||||
}
|
||||
return binary.LittleEndian.Uint64(b[off+1 : off+9]), off + 9, nil
|
||||
default:
|
||||
return uint64(b[off]), off + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readLenencString(b []byte, off int) ([]byte, int, error) {
|
||||
n, off, err := readLenencInt(b, off)
|
||||
if err != nil {
|
||||
return nil, off, err
|
||||
}
|
||||
end := off + int(n)
|
||||
if end > len(b) {
|
||||
return nil, off, io.ErrUnexpectedEOF
|
||||
}
|
||||
return b[off:end], end, nil
|
||||
}
|
||||
|
||||
func readNUL(b []byte, off int) (string, int, error) {
|
||||
for i := off; i < len(b); i++ {
|
||||
if b[i] == 0 {
|
||||
return string(b[off:i]), i + 1, nil
|
||||
}
|
||||
}
|
||||
return string(b[off:]), len(b), nil
|
||||
}
|
||||
|
||||
func sqlState(errno uint16) string {
|
||||
switch errno {
|
||||
case errAccessDenied:
|
||||
return "28000"
|
||||
case errParseError:
|
||||
return "42000"
|
||||
case errNoDB:
|
||||
return "3D000"
|
||||
case errUnknownCommand, errNotSupported:
|
||||
return "HY000"
|
||||
default:
|
||||
return "HY000"
|
||||
}
|
||||
}
|
||||
|
||||
func encodeOK(affected, lastInsertID uint64, status, warnings uint16) []byte {
|
||||
b := []byte{markerOK}
|
||||
b = appendLenencInt(b, affected)
|
||||
b = appendLenencInt(b, lastInsertID)
|
||||
b = appendU16(b, status)
|
||||
b = appendU16(b, warnings)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeERR(errno uint16, msg string) []byte {
|
||||
b := []byte{markerERR}
|
||||
b = appendU16(b, errno)
|
||||
b = append(b, '#')
|
||||
state := sqlState(errno)
|
||||
if len(state) != 5 {
|
||||
state = "HY000"
|
||||
}
|
||||
b = append(b, state...)
|
||||
b = append(b, msg...)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeEOF(warnings, status uint16) []byte {
|
||||
b := []byte{markerEOF}
|
||||
b = appendU16(b, warnings)
|
||||
b = appendU16(b, status)
|
||||
return b
|
||||
}
|
||||
|
||||
func isNumericType(t byte) bool {
|
||||
switch t {
|
||||
case typeDecimal, typeTiny, typeShort, typeLong, typeFloat, typeDouble,
|
||||
typeNull, typeLongLong, typeInt24, typeYear, typeNewDecimal, typeBit:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const typeBit byte = 16
|
||||
@@ -0,0 +1,122 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"mysql-tunnel/internal/navicat"
|
||||
)
|
||||
|
||||
func TestLenencInt(t *testing.T) {
|
||||
cases := []uint64{0, 1, 250, 251, 0xffff, 0x10000, 1 << 32}
|
||||
for _, n := range cases {
|
||||
enc := appendLenencInt(nil, n)
|
||||
got, _, err := readLenencInt(enc, 0)
|
||||
if err != nil || got != n {
|
||||
t.Fatalf("n=%d got=%d err=%v", n, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOKERRPackets(t *testing.T) {
|
||||
ok := encodeOK(3, 9, serverStatusAutocommit, 0)
|
||||
if ok[0] != markerOK {
|
||||
t.Fatal("ok header")
|
||||
}
|
||||
n, off, err := readLenencInt(ok, 1)
|
||||
if err != nil || n != 3 {
|
||||
t.Fatal(n, err)
|
||||
}
|
||||
id, _, err := readLenencInt(ok, off)
|
||||
if err != nil || id != 9 {
|
||||
t.Fatal(id, err)
|
||||
}
|
||||
|
||||
er := encodeERR(1064, "syntax")
|
||||
if er[0] != markerERR || er[1] != 0x28 || er[2] != 0x04 {
|
||||
t.Fatalf("%x", er)
|
||||
}
|
||||
if !bytes.Contains(er, []byte("syntax")) {
|
||||
t.Fatal(string(er))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativePasswordRoundtrip(t *testing.T) {
|
||||
scramble := bytes.Repeat([]byte{3}, 20)
|
||||
tok := nativePasswordToken("123456", scramble)
|
||||
if !verifyNativePassword("123456", scramble, tok) {
|
||||
t.Fatal("should match")
|
||||
}
|
||||
if verifyNativePassword("wrong", scramble, tok) {
|
||||
t.Fatal("should fail")
|
||||
}
|
||||
if !verifyNativePassword("", scramble, nil) {
|
||||
t.Fatal("empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshakeBuildParseResponse(t *testing.T) {
|
||||
scramble := bytes.Repeat([]byte{7}, 20)
|
||||
hs := buildHandshake(42, scramble)
|
||||
if hs[0] != ProtocolVersion {
|
||||
t.Fatal("proto")
|
||||
}
|
||||
if !bytes.Contains(hs, []byte(ServerVersion)) {
|
||||
t.Fatal("version")
|
||||
}
|
||||
if !bytes.Contains(hs, []byte(AuthPluginNative)) {
|
||||
t.Fatal("plugin")
|
||||
}
|
||||
|
||||
token := nativePasswordToken("p", scramble)
|
||||
resp := buildClientAuth("root", token, "app", clientProtocol41|clientSecureConnection|clientPluginAuth|clientConnectWithDB|clientPluginAuthLenencClientData)
|
||||
hr, err := parseHandshakeResponse(resp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hr.Username != "root" || hr.Database != "app" || !bytes.Equal(hr.Auth, token) {
|
||||
t.Fatalf("%+v", hr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextRowNULLAndBigint(t *testing.T) {
|
||||
row := encodeTextRow([]navicat.Cell{
|
||||
{Null: true},
|
||||
{Bytes: []byte("18446744073709551615")},
|
||||
{Bytes: []byte("1.50")},
|
||||
{Bytes: []byte("你好")},
|
||||
})
|
||||
if row[0] != markerNull {
|
||||
t.Fatal("null marker")
|
||||
}
|
||||
off := 1
|
||||
b, off, err := readLenencString(row, off)
|
||||
if err != nil || string(b) != "18446744073709551615" {
|
||||
t.Fatal(string(b), err)
|
||||
}
|
||||
b, off, err = readLenencString(row, off)
|
||||
if string(b) != "1.50" {
|
||||
t.Fatal(string(b))
|
||||
}
|
||||
b, _, err = readLenencString(row, off)
|
||||
if string(b) != "你好" {
|
||||
t.Fatal(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func buildClientAuth(user string, token []byte, db string, caps uint32) []byte {
|
||||
b := appendU32(nil, caps)
|
||||
b = appendU32(b, MaxPacketSize)
|
||||
b = append(b, charsetUTF8MB4)
|
||||
b = append(b, make([]byte, 23)...)
|
||||
b = appendNUL(b, user)
|
||||
b = appendLenencInt(b, uint64(len(token)))
|
||||
b = append(b, token...)
|
||||
if caps&clientConnectWithDB != 0 {
|
||||
b = appendNUL(b, db)
|
||||
}
|
||||
if caps&clientPluginAuth != 0 {
|
||||
b = appendNUL(b, AuthPluginNative)
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"mysql-tunnel/internal/navicat"
|
||||
)
|
||||
|
||||
func encodeColumnCount(n int) []byte {
|
||||
return appendLenencInt(nil, uint64(n))
|
||||
}
|
||||
|
||||
func encodeColumn(f navicat.Field) []byte {
|
||||
typ := byte(f.Type)
|
||||
charset := uint16(charsetUTF8MB4)
|
||||
if isNumericType(typ) || f.Flags&uint32(flagBinary) != 0 && isBlobType(typ) {
|
||||
charset = uint16(charsetBinary)
|
||||
}
|
||||
if isNumericType(typ) {
|
||||
charset = uint16(charsetBinary)
|
||||
}
|
||||
length := f.Length
|
||||
if length == 0 {
|
||||
length = uint32(len(f.Name))
|
||||
if length == 0 {
|
||||
length = 64
|
||||
}
|
||||
}
|
||||
flags := uint16(f.Flags)
|
||||
decimals := byte(0)
|
||||
if typ == typeFloat || typ == typeDouble || typ == typeNewDecimal || typ == typeDecimal {
|
||||
decimals = 31
|
||||
}
|
||||
|
||||
b := appendLenencString(nil, []byte("def"))
|
||||
b = appendLenencString(b, nil) // schema
|
||||
b = appendLenencString(b, []byte(f.Table))
|
||||
b = appendLenencString(b, []byte(f.Table))
|
||||
b = appendLenencString(b, []byte(f.Name))
|
||||
b = appendLenencString(b, []byte(f.Name))
|
||||
b = appendLenencInt(b, 0x0c)
|
||||
b = appendU16(b, charset)
|
||||
b = appendU32(b, length)
|
||||
b = append(b, typ)
|
||||
b = appendU16(b, flags)
|
||||
b = append(b, decimals, 0x00, 0x00)
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeTextRow(row []navicat.Cell) []byte {
|
||||
var b []byte
|
||||
for _, c := range row {
|
||||
if c.Null {
|
||||
b = append(b, markerNull)
|
||||
continue
|
||||
}
|
||||
b = appendLenencString(b, c.Bytes)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isBlobType(t byte) bool {
|
||||
switch t {
|
||||
case typeTinyBlob, typeMediumBlob, typeLongBlob, typeBlob:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *packetConn) writeResultSet(res navicat.Result, caps uint32, status uint16) error {
|
||||
if err := p.writePacket(encodeColumnCount(len(res.Fields))); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range res.Fields {
|
||||
if err := p.writePacket(encodeColumn(f)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if caps&clientDeprecateEOF != 0 {
|
||||
if err := p.writeOK(0, 0, status, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := p.writePacket(encodeEOF(0, status)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, row := range res.Rows {
|
||||
if err := p.writePacket(encodeTextRow(row)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if caps&clientDeprecateEOF != 0 {
|
||||
return p.writeOK(uint64(res.AffectedRows), uint64(res.InsertID), status, 0)
|
||||
}
|
||||
return p.writePacket(encodeEOF(0, status))
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"mysql-tunnel/internal/logger"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
DefaultDB string
|
||||
MaxConnections int
|
||||
Timeout time.Duration
|
||||
LogSQL bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
opt Options
|
||||
backend Backend
|
||||
log *logger.Logger
|
||||
|
||||
ln net.Listener
|
||||
connID atomic.Uint32
|
||||
|
||||
sem chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(opt Options, backend Backend, log *logger.Logger) *Server {
|
||||
if opt.Host == "" {
|
||||
opt.Host = "127.0.0.1"
|
||||
}
|
||||
if opt.Port == 0 {
|
||||
opt.Port = 13306
|
||||
}
|
||||
if opt.MaxConnections < 1 {
|
||||
opt.MaxConnections = 5
|
||||
}
|
||||
if opt.Timeout <= 0 {
|
||||
opt.Timeout = 30 * time.Second
|
||||
}
|
||||
return &Server{
|
||||
opt: opt,
|
||||
backend: backend,
|
||||
log: log,
|
||||
sem: make(chan struct{}, opt.MaxConnections),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Addr() string {
|
||||
if s.ln != nil {
|
||||
return s.ln.Addr().String()
|
||||
}
|
||||
return net.JoinHostPort(s.opt.Host, strconv.Itoa(s.opt.Port))
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort(s.opt.Host, strconv.Itoa(s.opt.Port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(ctx, ln)
|
||||
}
|
||||
|
||||
func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
|
||||
s.ln = ln
|
||||
if s.log != nil {
|
||||
s.log.Info("server started", ln.Addr().String())
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
}()
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.wg.Wait()
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
select {
|
||||
case s.sem <- struct{}{}:
|
||||
default:
|
||||
_ = c.Close()
|
||||
if s.log != nil {
|
||||
s.log.Warn("max connections reached, rejected", c.RemoteAddr().String())
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go func(c net.Conn) {
|
||||
defer s.wg.Done()
|
||||
defer func() { <-s.sem }()
|
||||
s.handle(ctx, c)
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handle(ctx context.Context, c net.Conn) {
|
||||
pc := newPacketConn(c, s.opt.Timeout)
|
||||
remote := pc.remote()
|
||||
if s.log != nil {
|
||||
s.log.Info("client connected", remote)
|
||||
}
|
||||
defer func() {
|
||||
_ = pc.close()
|
||||
if s.log != nil {
|
||||
s.log.Info("client disconnected", remote)
|
||||
}
|
||||
}()
|
||||
|
||||
scramble, err := randomScramble()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := s.connID.Add(1)
|
||||
if err := pc.writePacket(buildHandshake(id, scramble)); err != nil {
|
||||
return
|
||||
}
|
||||
payload, err := pc.readPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hr, err := parseHandshakeResponse(payload)
|
||||
if err != nil {
|
||||
_ = pc.writeERR(errUnknown, "bad handshake")
|
||||
return
|
||||
}
|
||||
if hr.SSL {
|
||||
_ = pc.writeERR(errNotSupported, "SSL is not supported on the local tunnel")
|
||||
return
|
||||
}
|
||||
auth := hr.Auth
|
||||
plugin := hr.Plugin
|
||||
if plugin == "" {
|
||||
plugin = AuthPluginNative
|
||||
}
|
||||
if plugin != AuthPluginNative {
|
||||
if err := pc.writePacket(encodeAuthSwitch(AuthPluginNative, scramble)); err != nil {
|
||||
return
|
||||
}
|
||||
auth, err = pc.readPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(auth) > 0 && auth[len(auth)-1] == 0 && len(auth) != 20 {
|
||||
auth = auth[:len(auth)-1]
|
||||
}
|
||||
}
|
||||
db := hr.Database
|
||||
if db == "" {
|
||||
db = s.opt.DefaultDB
|
||||
}
|
||||
if len(auth) == 21 && auth[20] == 0 {
|
||||
auth = auth[:20]
|
||||
}
|
||||
if !usernamesEqual(hr.Username, s.opt.Username) || !verifyNativePassword(s.opt.Password, scramble, auth) {
|
||||
_ = pc.writeERR(errAccessDenied, fmt.Sprintf("Access denied for user '%s'@'localhost'", hr.Username))
|
||||
return
|
||||
}
|
||||
if err := pc.writeOK(0, 0, serverStatusAutocommit, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sess := &session{
|
||||
pc: pc,
|
||||
backend: s.backend,
|
||||
log: s.log,
|
||||
logSQL: s.opt.LogSQL,
|
||||
timeout: func() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, s.opt.Timeout)
|
||||
},
|
||||
user: hr.Username,
|
||||
db: db,
|
||||
defaultDB: s.opt.DefaultDB,
|
||||
caps: hr.Capabilities,
|
||||
status: serverStatusAutocommit,
|
||||
scramble: scramble,
|
||||
}
|
||||
sess.serve(ctx)
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mysql-tunnel/internal/navicat"
|
||||
"mysql-tunnel/internal/tunnel"
|
||||
)
|
||||
|
||||
type stubBackend struct{}
|
||||
|
||||
func (stubBackend) Query(_ context.Context, _ string, sqls ...string) (*navicat.QueryResponse, error) {
|
||||
var results []navicat.Result
|
||||
for _, sql := range sqls {
|
||||
s := strings.TrimSpace(sql)
|
||||
u := strings.ToUpper(s)
|
||||
switch {
|
||||
case strings.HasPrefix(u, "SET NAMES"), strings.HasPrefix(u, "USE "):
|
||||
results = append(results, navicat.Result{})
|
||||
case u == "SELECT 1" || strings.HasPrefix(u, "SELECT 1 "):
|
||||
results = append(results, navicat.Result{
|
||||
NumFields: 1, NumRows: 1,
|
||||
Fields: []navicat.Field{{Name: "1", Type: 8, Length: 20}},
|
||||
Rows: [][]navicat.Cell{{{Bytes: []byte("1")}}},
|
||||
})
|
||||
case strings.Contains(u, "SELECT NULL"):
|
||||
results = append(results, navicat.Result{
|
||||
NumFields: 1, NumRows: 1,
|
||||
Fields: []navicat.Field{{Name: "n", Type: 6}},
|
||||
Rows: [][]navicat.Cell{{{Null: true}}},
|
||||
})
|
||||
case strings.Contains(strings.ToLower(s), "bigint_val"):
|
||||
results = append(results, navicat.Result{
|
||||
NumFields: 1, NumRows: 1,
|
||||
Fields: []navicat.Field{{Name: "bigint_val", Type: 8, Flags: 32, Length: 20}},
|
||||
Rows: [][]navicat.Cell{{{Bytes: []byte("18446744073709551615")}}},
|
||||
})
|
||||
case strings.Contains(strings.ToLower(s), "dec_val"):
|
||||
results = append(results, navicat.Result{
|
||||
NumFields: 1, NumRows: 1,
|
||||
Fields: []navicat.Field{{Name: "dec_val", Type: 246, Length: 32}},
|
||||
Rows: [][]navicat.Cell{{{Bytes: []byte("1234567890.123456789")}}},
|
||||
})
|
||||
case strings.Contains(s, "你好") || strings.Contains(strings.ToLower(s), "zh_val"):
|
||||
results = append(results, navicat.Result{
|
||||
NumFields: 1, NumRows: 1,
|
||||
Fields: []navicat.Field{{Name: "zh_val", Type: 253, Length: 12}},
|
||||
Rows: [][]navicat.Cell{{{Bytes: []byte("你好世界")}}},
|
||||
})
|
||||
case strings.HasPrefix(u, "UPDATE"), strings.HasPrefix(u, "INSERT"):
|
||||
results = append(results, navicat.Result{AffectedRows: 2, InsertID: 7})
|
||||
case strings.Contains(u, "SYNTAX"):
|
||||
results = append(results, navicat.Result{Errno: 1064, Message: "You have an error in your SQL syntax"})
|
||||
default:
|
||||
results = append(results, navicat.Result{Errno: 1064, Message: "stub: unhandled SQL"})
|
||||
}
|
||||
}
|
||||
return &navicat.QueryResponse{Results: results}, nil
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, backend Backend, user, pass string) (addr string, cancel context.CancelFunc) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(Options{
|
||||
Host: "127.0.0.1", Port: 1, Username: user, Password: pass,
|
||||
DefaultDB: "app", MaxConnections: 5, Timeout: 5 * time.Second,
|
||||
}, backend, nil)
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- s.Serve(ctx, ln) }()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = ln.Close()
|
||||
})
|
||||
return ln.Addr().String(), cancel
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
pc *packetConn
|
||||
caps uint32
|
||||
}
|
||||
|
||||
func dialTest(t *testing.T, addr, user, pass, db string) *testClient {
|
||||
t.Helper()
|
||||
c, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc := newPacketConn(c, 5*time.Second)
|
||||
hs, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scramble, err := scrambleFromHandshake(hs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
caps := clientProtocol41 | clientSecureConnection | clientPluginAuth |
|
||||
clientConnectWithDB | clientPluginAuthLenencClientData | clientDeprecateEOF | clientMultiResults
|
||||
token := nativePasswordToken(pass, scramble)
|
||||
if err := pc.writePacket(buildClientAuth(user, token, db, caps)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ok) == 0 || ok[0] != markerOK {
|
||||
t.Fatalf("auth failed: %x %q", ok, ok)
|
||||
}
|
||||
return &testClient{pc: pc, caps: caps}
|
||||
}
|
||||
|
||||
func scrambleFromHandshake(payload []byte) ([]byte, error) {
|
||||
if len(payload) < 20 || payload[0] != ProtocolVersion {
|
||||
return nil, fmt.Errorf("bad handshake")
|
||||
}
|
||||
off := 1
|
||||
_, off, err := readNUL(payload, off)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
off += 4 // conn id
|
||||
part1 := payload[off : off+8]
|
||||
off += 8 + 1 + 2 + 1 + 2 + 2 + 1 + 10
|
||||
if off+13 > len(payload) {
|
||||
return nil, fmt.Errorf("short scramble")
|
||||
}
|
||||
part2 := payload[off : off+12]
|
||||
s := append(append([]byte{}, part1...), part2...)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *testClient) query(t *testing.T, sql string) (cols []string, rows [][]*string, affected, lastID uint64, errno int, errMsg string) {
|
||||
t.Helper()
|
||||
c.pc.resetSeq()
|
||||
payload := append([]byte{ComQuery}, []byte(sql)...)
|
||||
if err := c.pc.writePacket(payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkt, err := c.pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pkt) == 0 {
|
||||
t.Fatal("empty")
|
||||
}
|
||||
if pkt[0] == markerERR {
|
||||
errno = int(pkt[1]) | int(pkt[2])<<8
|
||||
if len(pkt) > 9 {
|
||||
errMsg = string(pkt[9:])
|
||||
}
|
||||
return
|
||||
}
|
||||
if pkt[0] == markerOK {
|
||||
n, off, _ := readLenencInt(pkt, 1)
|
||||
id, _, _ := readLenencInt(pkt, off)
|
||||
return nil, nil, n, id, 0, ""
|
||||
}
|
||||
nCols, _, err := readLenencInt(pkt, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := uint64(0); i < nCols; i++ {
|
||||
colPkt, err := c.pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := columnName(colPkt)
|
||||
cols = append(cols, name)
|
||||
}
|
||||
// metadata end OK/EOF
|
||||
if _, err := c.pc.readPacket(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
rowPkt, err := c.pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rowPkt) == 0 {
|
||||
break
|
||||
}
|
||||
if rowPkt[0] == markerERR {
|
||||
errno = int(rowPkt[1]) | int(rowPkt[2])<<8
|
||||
return
|
||||
}
|
||||
if rowPkt[0] == markerEOF && len(rowPkt) < 9 {
|
||||
break
|
||||
}
|
||||
if rowPkt[0] == markerOK && len(rowPkt) >= 7 {
|
||||
break
|
||||
}
|
||||
off := 0
|
||||
row := make([]*string, nCols)
|
||||
for i := uint64(0); i < nCols; i++ {
|
||||
if off >= len(rowPkt) {
|
||||
t.Fatal("short row")
|
||||
}
|
||||
if rowPkt[off] == markerNull {
|
||||
row[i] = nil
|
||||
off++
|
||||
continue
|
||||
}
|
||||
b, nOff, err := readLenencString(rowPkt, off)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(b)
|
||||
row[i] = &s
|
||||
off = nOff
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *testClient) ping(t *testing.T) {
|
||||
t.Helper()
|
||||
c.pc.resetSeq()
|
||||
if err := c.pc.writePacket([]byte{ComPing}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkt, err := c.pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pkt[0] != markerOK {
|
||||
t.Fatalf("ping %x", pkt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testClient) quit() {
|
||||
c.pc.resetSeq()
|
||||
_ = c.pc.writePacket([]byte{ComQuit})
|
||||
_ = c.pc.close()
|
||||
}
|
||||
|
||||
func columnName(pkt []byte) string {
|
||||
off := 0
|
||||
skip := func() {
|
||||
_, n, err := readLenencString(pkt, off)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
off = n
|
||||
}
|
||||
skip() // catalog
|
||||
skip() // schema
|
||||
skip() // table
|
||||
skip() // org table
|
||||
b, _, err := readLenencString(pkt, off)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestServerAuthPingQueryQuit(t *testing.T) {
|
||||
addr, _ := startServer(t, stubBackend{}, "root", "123456")
|
||||
cl := dialTest(t, addr, "root", "123456", "app")
|
||||
cl.ping(t)
|
||||
_, rows, _, _, errno, msg := cl.query(t, "SELECT 1")
|
||||
if errno != 0 {
|
||||
t.Fatalf("errno %d %s", errno, msg)
|
||||
}
|
||||
if len(rows) != 1 || rows[0][0] == nil || *rows[0][0] != "1" {
|
||||
t.Fatalf("rows=%v", rows)
|
||||
}
|
||||
cl.quit()
|
||||
}
|
||||
|
||||
func TestServerBadPassword(t *testing.T) {
|
||||
addr, _ := startServer(t, stubBackend{}, "root", "123456")
|
||||
c, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
pc := newPacketConn(c, 5*time.Second)
|
||||
hs, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scramble, err := scrambleFromHandshake(hs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
caps := clientProtocol41 | clientSecureConnection | clientPluginAuth | clientPluginAuthLenencClientData
|
||||
tok := nativePasswordToken("wrong", scramble)
|
||||
if err := pc.writePacket(buildClientAuth("root", tok, "", caps)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkt, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pkt[0] != markerERR {
|
||||
t.Fatalf("want ERR got %x", pkt)
|
||||
}
|
||||
errno := int(pkt[1]) | int(pkt[2])<<8
|
||||
if errno != int(errAccessDenied) {
|
||||
t.Fatalf("errno %d", errno)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerResultTypes(t *testing.T) {
|
||||
addr, _ := startServer(t, stubBackend{}, "root", "p")
|
||||
cl := dialTest(t, addr, "root", "p", "app")
|
||||
defer cl.quit()
|
||||
|
||||
_, rows, _, _, errno, _ := cl.query(t, "SELECT NULL AS n")
|
||||
if errno != 0 || rows[0][0] != nil {
|
||||
t.Fatalf("null errno=%d row=%v", errno, rows)
|
||||
}
|
||||
|
||||
_, rows, _, _, errno, _ = cl.query(t, "SELECT bigint_val")
|
||||
if errno != 0 || *rows[0][0] != "18446744073709551615" {
|
||||
t.Fatalf("bigint %v errno=%d", rows, errno)
|
||||
}
|
||||
|
||||
_, rows, _, _, errno, _ = cl.query(t, "SELECT dec_val")
|
||||
if errno != 0 || *rows[0][0] != "1234567890.123456789" {
|
||||
t.Fatalf("dec %v", rows)
|
||||
}
|
||||
|
||||
_, rows, _, _, errno, _ = cl.query(t, "SELECT zh_val")
|
||||
if errno != 0 || *rows[0][0] != "你好世界" {
|
||||
t.Fatalf("zh %v", rows)
|
||||
}
|
||||
|
||||
_, _, affected, lastID, errno, msg := cl.query(t, "UPDATE t SET x=1")
|
||||
if errno != 0 {
|
||||
t.Fatal(msg)
|
||||
}
|
||||
if affected != 2 || lastID != 7 {
|
||||
t.Fatalf("affected=%d id=%d", affected, lastID)
|
||||
}
|
||||
|
||||
_, _, _, _, errno, msg = cl.query(t, "SELECT SYNTAX")
|
||||
if errno != 1064 || !strings.Contains(msg, "syntax") {
|
||||
t.Fatalf("errno=%d msg=%s", errno, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerAuthSwitch(t *testing.T) {
|
||||
addr, _ := startServer(t, stubBackend{}, "root", "p")
|
||||
c, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
pc := newPacketConn(c, 5*time.Second)
|
||||
hs, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scramble, err := scrambleFromHandshake(hs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
caps := clientProtocol41 | clientSecureConnection | clientPluginAuth | clientPluginAuthLenencClientData
|
||||
b := appendU32(nil, caps)
|
||||
b = appendU32(b, MaxPacketSize)
|
||||
b = append(b, charsetUTF8MB4)
|
||||
b = append(b, make([]byte, 23)...)
|
||||
b = appendNUL(b, "root")
|
||||
b = appendLenencInt(b, 0) // empty, will switch
|
||||
b = appendNUL(b, AuthPluginSHA2)
|
||||
if err := pc.writePacket(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sw, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sw[0] != markerEOF {
|
||||
t.Fatalf("want auth switch %x", sw)
|
||||
}
|
||||
tok := nativePasswordToken("p", scramble)
|
||||
if err := pc.writePacket(tok); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, err := pc.readPacket()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok[0] != markerOK {
|
||||
t.Fatalf("auth after switch %x", ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhase4NavicatStack(t *testing.T) {
|
||||
mock := httptest.NewServer(navicat.NewMockHandler(navicat.WithToken("t")))
|
||||
t.Cleanup(mock.Close)
|
||||
httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second})
|
||||
n := navicat.NewClient(httpClient, navicat.ClientOptions{
|
||||
URL: mock.URL + "?token=t",
|
||||
Target: navicat.Target{
|
||||
Host: "mysql", Port: 3306, User: "remote", Password: "rp",
|
||||
Database: "app", Charset: "utf8mb4",
|
||||
},
|
||||
})
|
||||
addr, _ := startServer(t, n, "root", "local")
|
||||
cl := dialTest(t, addr, "root", "local", "app")
|
||||
defer cl.quit()
|
||||
_, rows, _, _, errno, msg := cl.query(t, "SELECT 1")
|
||||
if errno != 0 {
|
||||
t.Fatal(msg)
|
||||
}
|
||||
if *rows[0][0] != "1" {
|
||||
t.Fatalf("%v", rows)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package mysqlserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"mysql-tunnel/internal/logger"
|
||||
"mysql-tunnel/internal/navicat"
|
||||
)
|
||||
|
||||
var useDBRe = regexp.MustCompile(`(?is)^\s*USE\s+(?:` + "`" + `([^` + "`" + `]+)` + "`" + `|([A-Za-z0-9_]+))\s*;?\s*$`)
|
||||
|
||||
type Backend interface {
|
||||
Query(ctx context.Context, database string, sqls ...string) (*navicat.QueryResponse, error)
|
||||
}
|
||||
|
||||
type session struct {
|
||||
pc *packetConn
|
||||
backend Backend
|
||||
log *logger.Logger
|
||||
logSQL bool
|
||||
timeout ctxTimeout
|
||||
user string
|
||||
db string
|
||||
defaultDB string
|
||||
caps uint32
|
||||
status uint16
|
||||
scramble []byte
|
||||
}
|
||||
|
||||
type ctxTimeout func() (context.Context, context.CancelFunc)
|
||||
|
||||
func (s *session) serve(ctx context.Context) {
|
||||
defer s.pc.close()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
s.pc.resetSeq()
|
||||
payload, err := s.pc.readPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
_ = s.pc.writeERR(errUnknownCommand, "empty command")
|
||||
continue
|
||||
}
|
||||
if err := s.dispatch(ctx, payload[0], payload[1:]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) dispatch(parent context.Context, cmd byte, body []byte) error {
|
||||
switch cmd {
|
||||
case ComQuit:
|
||||
return fmt.Errorf("quit")
|
||||
case ComPing:
|
||||
return s.pc.writeOK(0, 0, s.status, 0)
|
||||
case ComInitDB:
|
||||
db := strings.TrimSpace(string(body))
|
||||
return s.changeDB(parent, db)
|
||||
case ComQuery:
|
||||
return s.query(parent, string(body))
|
||||
case ComResetConnection:
|
||||
s.db = s.defaultDB
|
||||
return s.pc.writeOK(0, 0, s.status, 0)
|
||||
case ComFieldList, ComStmtPrepare, ComStmtExecute, ComStmtClose:
|
||||
return s.pc.writeERR(errNotSupported, "command not implemented yet")
|
||||
default:
|
||||
return s.pc.writeERR(errUnknownCommand, "unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) changeDB(parent context.Context, db string) error {
|
||||
if db == "" {
|
||||
return s.pc.writeERR(errNoDB, "no database selected")
|
||||
}
|
||||
s.db = db
|
||||
return s.pc.writeOK(0, 0, s.status, 0)
|
||||
}
|
||||
|
||||
func (s *session) query(parent context.Context, sql string) error {
|
||||
sql = strings.TrimRight(sql, "\x00")
|
||||
s.logQuery(sql)
|
||||
pendingDB := ""
|
||||
if m := useDBRe.FindStringSubmatch(sql); m != nil {
|
||||
pendingDB = m[1]
|
||||
if pendingDB == "" {
|
||||
pendingDB = m[2]
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := s.timeout()
|
||||
defer cancel()
|
||||
if ctx == nil {
|
||||
ctx = parent
|
||||
}
|
||||
|
||||
resp, err := s.backend.Query(ctx, s.db, sql)
|
||||
if err != nil {
|
||||
return s.writeBackendErr(err)
|
||||
}
|
||||
for _, r := range resp.Results {
|
||||
if r.IsError() {
|
||||
return s.pc.writeERR(uint16(r.Errno), r.Message)
|
||||
}
|
||||
}
|
||||
if pendingDB != "" {
|
||||
s.db = pendingDB
|
||||
}
|
||||
res, ok := navicat.LastResultSet(resp)
|
||||
if !ok {
|
||||
return s.pc.writeOK(0, 0, s.status, 0)
|
||||
}
|
||||
if res.IsError() {
|
||||
return s.pc.writeERR(uint16(res.Errno), res.Message)
|
||||
}
|
||||
if res.IsResultSet() {
|
||||
if s.log != nil {
|
||||
s.log.Info("query completed", fmt.Sprintf("rows=%d", len(res.Rows)))
|
||||
}
|
||||
return s.pc.writeResultSet(res, s.caps, s.status)
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Info("query completed", fmt.Sprintf("affected=%d", res.AffectedRows))
|
||||
}
|
||||
return s.pc.writeOK(uint64(res.AffectedRows), uint64(res.InsertID), s.status, 0)
|
||||
}
|
||||
|
||||
func (s *session) writeBackendErr(err error) error {
|
||||
msg := logger.RedactError(err)
|
||||
if ne, ok := err.(*navicat.Error); ok {
|
||||
errno := uint16(ne.Errno)
|
||||
if errno == 0 {
|
||||
errno = errUnknown
|
||||
}
|
||||
if ne.Kind == "http" || ne.Kind == "tunnel" {
|
||||
errno = errUnknown
|
||||
}
|
||||
return s.pc.writeERR(errno, msg)
|
||||
}
|
||||
return s.pc.writeERR(errUnknown, msg)
|
||||
}
|
||||
|
||||
func (s *session) logQuery(sql string) {
|
||||
if s.log == nil {
|
||||
return
|
||||
}
|
||||
if !s.logSQL {
|
||||
s.log.Info("query")
|
||||
return
|
||||
}
|
||||
s.log.Info("query", trimSQL(sql))
|
||||
}
|
||||
|
||||
func trimSQL(sql string) string {
|
||||
s := logger.RedactText(sql)
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
if len(s) > 240 {
|
||||
return s[:240] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user