From 5acb5690fa62d58bdfd470336ec7df7c88990dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Eeden?= Date: Mon, 28 Oct 2024 08:53:59 +0100 Subject: [PATCH] server: Improve example (#936) * server: Improve example 1. Use the example from the `README.md` and turn it into a `go-mysqlserver` binary that users can run. 2. Add more logging to make it easier to understand what it is doing. This is done both in `go-mysqlserver` as well as the `EmptyHandler` 3. Remove the `server/example` as we already have a example now. 4. Support the minimal set of queries that MySQL Shell `mysqlsh` needs to be able to connect. (tested with MySQL Shell 9.1.0) 5. Change the default version from 5.7.0 to 8.0.11 (first 8.0 GA version) * Update server/command.go Co-authored-by: lance6716 * Update based on review --------- Co-authored-by: lance6716 --- Makefile | 1 + README.md | 1 - cmd/go-mysqlserver/main.go | 42 ++++++++++++++++++++++ server/command.go | 60 ++++++++++++++++++++++++++++++++ server/example/server_example.go | 55 ----------------------------- server/server_conf.go | 2 +- 6 files changed, 104 insertions(+), 57 deletions(-) create mode 100644 cmd/go-mysqlserver/main.go delete mode 100644 server/example/server_example.go diff --git a/Makefile b/Makefile index 90930f3b2..87795751f 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ build: ${GO} build -o bin/go-mysqldump cmd/go-mysqldump/main.go ${GO} build -o bin/go-canal cmd/go-canal/main.go ${GO} build -o bin/go-binlogparser cmd/go-binlogparser/main.go + ${GO} build -o bin/go-mysqlserver cmd/go-mysqlserver/main.go test: ${GO} test --race -timeout 2m ./... diff --git a/README.md b/README.md index 8d9dc1777..7790c88b2 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,6 @@ func main() { } } } - ``` Another shell diff --git a/cmd/go-mysqlserver/main.go b/cmd/go-mysqlserver/main.go new file mode 100644 index 000000000..bf663976f --- /dev/null +++ b/cmd/go-mysqlserver/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "log" + "net" + + "github.com/go-mysql-org/go-mysql/server" +) + +func main() { + // Listen for connections on localhost port 4000 + l, err := net.Listen("tcp", "127.0.0.1:4000") + if err != nil { + log.Fatal(err) + } + + log.Println("Listening on port 4000, connect with 'mysql -h 127.0.0.1 -P 4000 -u root'") + + // Accept a new connection once + c, err := l.Accept() + if err != nil { + log.Fatal(err) + } + + log.Println("Accepted connection") + + // Create a connection with user root and an empty password. + // You can use your own handler to handle command here. + conn, err := server.NewConn(c, "root", "", server.EmptyHandler{}) + if err != nil { + log.Fatal(err) + } + + log.Println("Registered the connection with the server") + + // as long as the client keeps sending commands, keep handling them + for { + if err := conn.HandleCommand(); err != nil { + log.Fatal(err) + } + } +} diff --git a/server/command.go b/server/command.go index 78a0ea03d..3c73697ae 100644 --- a/server/command.go +++ b/server/command.go @@ -3,12 +3,15 @@ package server import ( "bytes" "fmt" + "log" + "github.com/go-mysql-org/go-mysql/mysql" . "github.com/go-mysql-org/go-mysql/mysql" "github.com/go-mysql-org/go-mysql/replication" "github.com/siddontang/go/hack" ) +// Handler is what a server needs to implement the client-server protocol type Handler interface { //handle COM_INIT_DB command, you can check whether the dbName is valid, or other. UseDB(dbName string) error @@ -31,6 +34,7 @@ type Handler interface { HandleOtherCommand(cmd byte, data []byte) error } +// ReplicationHandler is for handlers that want to implement the replication protocol type ReplicationHandler interface { // handle Replication command HandleRegisterSlave(data []byte) error @@ -38,6 +42,8 @@ type ReplicationHandler interface { HandleBinlogDumpGTID(gtidSet *MysqlGTIDSet) (*replication.BinlogStreamer, error) } +// HandleCommand is handling commands received by the server +// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_command_phase.html func (c *Conn) HandleCommand() error { if c.Conn == nil { return fmt.Errorf("connection closed") @@ -178,47 +184,101 @@ func (c *Conn) dispatch(data []byte) interface{} { } } +// EmptyHandler is a mostly empty implementation for demonstration purposes type EmptyHandler struct { } +// EmptyReplicationHandler is a empty handler that implements the replication protocol type EmptyReplicationHandler struct { EmptyHandler } +// UseDB is called for COM_INIT_DB func (h EmptyHandler) UseDB(dbName string) error { + log.Printf("Received: UseDB %s", dbName) return nil } + +// HandleQuery is called for COM_QUERY func (h EmptyHandler) HandleQuery(query string) (*Result, error) { + log.Printf("Received: Query: %s", query) + + // These two queries are implemented for minimal support for MySQL Shell + if query == `SET NAMES 'utf8mb4';` { + return nil, nil + } + if query == `select concat(@@version, ' ', @@version_comment)` { + r, err := mysql.BuildSimpleResultset([]string{"concat(@@version, ' ', @@version_comment)"}, [][]interface{}{ + {"8.0.11"}, + }, false) + if err != nil { + return nil, err + } + return &mysql.Result{ + Status: 0, + Warnings: 0, + InsertId: 0, + AffectedRows: 0, + Resultset: r, + }, nil + } + return nil, fmt.Errorf("not supported now") } +// HandleFieldList is called for COM_FIELD_LIST packets +// Note that COM_FIELD_LIST has been deprecated since MySQL 5.7.11 +// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_field_list.html func (h EmptyHandler) HandleFieldList(table string, fieldWildcard string) ([]*Field, error) { + log.Printf("Received: FieldList: table=%s, fieldWildcard:%s", table, fieldWildcard) return nil, fmt.Errorf("not supported now") } + +// HandleStmtPrepare is called for COM_STMT_PREPARE func (h EmptyHandler) HandleStmtPrepare(query string) (int, int, interface{}, error) { + log.Printf("Received: StmtPrepare: %s", query) return 0, 0, nil, fmt.Errorf("not supported now") } + +// 'context' isn't used but replacing it with `_` would remove important information for who +// wants to extend this later. +//revive:disable:unused-parameter + +// HandleStmtExecute is called for COM_STMT_EXECUTE func (h EmptyHandler) HandleStmtExecute(context interface{}, query string, args []interface{}) (*Result, error) { + log.Printf("Received: StmtExecute: %s (args: %v)", query, args) return nil, fmt.Errorf("not supported now") } +// HandleStmtClose is called for COM_STMT_CLOSE func (h EmptyHandler) HandleStmtClose(context interface{}) error { + log.Println("Received: StmtClose") return nil } +//revive:enable:unused-parameter + +// HandleRegisterSlave is called for COM_REGISTER_SLAVE func (h EmptyReplicationHandler) HandleRegisterSlave(data []byte) error { + log.Printf("Received: RegisterSlave: %x", data) return fmt.Errorf("not supported now") } +// HandleBinlogDump is called for COM_BINLOG_DUMP (non-GTID) func (h EmptyReplicationHandler) HandleBinlogDump(pos Position) (*replication.BinlogStreamer, error) { + log.Printf("Received: BinlogDump: pos=%s", pos.String()) return nil, fmt.Errorf("not supported now") } +// HandleBinlogDumpGTID is called for COM_BINLOG_DUMP_GTID func (h EmptyReplicationHandler) HandleBinlogDumpGTID(gtidSet *MysqlGTIDSet) (*replication.BinlogStreamer, error) { + log.Printf("Received: BinlogDumpGTID: gtidSet=%s", gtidSet.String()) return nil, fmt.Errorf("not supported now") } +// HandleOtherCommand is called for commands not handled elsewhere func (h EmptyHandler) HandleOtherCommand(cmd byte, data []byte) error { + log.Printf("Received: OtherCommand: cmd=%x, data=%x", cmd, data) return NewError( ER_UNKNOWN_ERROR, fmt.Sprintf("command %d is not supported now", cmd), diff --git a/server/example/server_example.go b/server/example/server_example.go deleted file mode 100644 index 2af545566..000000000 --- a/server/example/server_example.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "net" - - "github.com/go-mysql-org/go-mysql/mysql" - "github.com/go-mysql-org/go-mysql/server" - "github.com/go-mysql-org/go-mysql/test_util/test_keys" - "github.com/siddontang/go-log/log" - - "crypto/tls" - "time" -) - -type RemoteThrottleProvider struct { - *server.InMemoryProvider - delay int // in milliseconds -} - -func (m *RemoteThrottleProvider) GetCredential(username string) (password string, found bool, err error) { - time.Sleep(time.Millisecond * time.Duration(m.delay)) - return m.InMemoryProvider.GetCredential(username) -} - -func main() { - l, _ := net.Listen("tcp", "127.0.0.1:3306") - // user either the in-memory credential provider or the remote credential provider (you can implement your own) - //inMemProvider := server.NewInMemoryProvider() - //inMemProvider.AddUser("root", "123") - remoteProvider := &RemoteThrottleProvider{server.NewInMemoryProvider(), 10 + 50} - remoteProvider.AddUser("root", "123") - var tlsConf = server.NewServerTLSConfig(test_keys.CaPem, test_keys.CertPem, test_keys.KeyPem, tls.VerifyClientCertIfGiven) - for { - c, _ := l.Accept() - go func() { - // Create a connection with user root and an empty password. - // You can use your own handler to handle command here. - svr := server.NewServer("8.0.12", mysql.DEFAULT_COLLATION_ID, mysql.AUTH_CACHING_SHA2_PASSWORD, test_keys.PubPem, tlsConf) - conn, err := server.NewCustomizedConn(c, svr, remoteProvider, server.EmptyHandler{}) - - if err != nil { - log.Errorf("Connection error: %v", err) - return - } - - for { - err = conn.HandleCommand() - if err != nil { - log.Errorf(`Could not handle command: %v`, err) - return - } - } - }() - } -} diff --git a/server/server_conf.go b/server/server_conf.go index 0328bbe95..bff1967c9 100644 --- a/server/server_conf.go +++ b/server/server_conf.go @@ -48,7 +48,7 @@ func NewDefaultServer() *Server { certPem, keyPem := generateAndSignRSACerts(caPem, caKey) tlsConf := NewServerTLSConfig(caPem, certPem, keyPem, tls.VerifyClientCertIfGiven) return &Server{ - serverVersion: "5.7.0", + serverVersion: "8.0.11", protocolVersion: 10, capability: CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_CONNECT_WITH_DB | CLIENT_PROTOCOL_41 | CLIENT_TRANSACTIONS | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH | CLIENT_SSL | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA,