package navicat import ( "fmt" "strings" ) // Header is EchoHeader from ntunnel_mysql.php (16 bytes). type Header struct { Magic uint32 Version uint16 Errno uint32 Message string UncertainVersion bool } func (h Header) OK() bool { return h.Errno == 0 } // ConnInfo is actn=C payload after a successful header. type ConnInfo struct { HostInfo string ProtoInfo string ServerInfo string } // Field is one EchoFieldsHeader column. Type/flags/length are mysqli numeric values. type Field struct { Name string Table string Type uint32 Flags uint32 Length uint32 } // Cell is one row field. Bytes are kept as the PHP string; never parsed as int64/float64. type Cell struct { Null bool Bytes []byte } func (c Cell) Text() string { if c.Null { return "" } return string(c.Bytes) } // Result is one query in an actn=Q response. type Result struct { Errno uint32 Message string AffectedRows uint32 InsertID uint32 // protocol is 32-bit; do not invent high bits NumFields uint32 NumRows uint32 Info string Fields []Field Rows [][]Cell } func (r Result) IsError() bool { return r.Errno > 0 } func (r Result) IsResultSet() bool { return r.Errno == 0 && r.NumFields > 0 } // ConnectResponse is a parsed actn=C reply. type ConnectResponse struct { Header Header Conn ConnInfo } // QueryResponse is a parsed actn=Q reply. type QueryResponse struct { Header Header Results []Result } func ParseHeader(body []byte) (Header, *reader, error) { var h Header if len(body) < HeaderSize { if looksHTML(body) { return h, nil, &Error{Kind: "http", Message: "tunnel returned HTML, not Navicat binary"} } return h, nil, fmt.Errorf("response too short (%d bytes)", len(body)) } r := newReader(body) magic, err := r.u32() if err != nil { return h, nil, err } if magic != Magic { if looksHTML(body) { return h, nil, &Error{Kind: "http", Message: "tunnel returned HTML, not Navicat binary"} } return h, nil, fmt.Errorf("invalid Navicat magic %d (want %d)", magic, Magic) } ver, err := r.u16() if err != nil { return h, nil, err } errno, err := r.u32() if err != nil { return h, nil, err } if _, err := r.readN(6); err != nil { return h, nil, err } h.Magic = magic h.Version = ver h.Errno = errno h.UncertainVersion = !knownVersion(ver) if errno != 0 { msg, err := r.blockString() if err != nil { return h, nil, fmt.Errorf("read error message: %w", err) } h.Message = msg } return h, r, nil } func ParseConnect(body []byte) (*ConnectResponse, error) { h, r, err := ParseHeader(body) if err != nil { return nil, err } out := &ConnectResponse{Header: h} if !h.OK() { return out, nil } host, err := r.blockString() if err != nil { return nil, fmt.Errorf("conn host_info: %w", err) } proto, err := r.blockString() if err != nil { return nil, fmt.Errorf("conn proto_info: %w", err) } server, err := r.blockString() if err != nil { return nil, fmt.Errorf("conn server_info: %w", err) } out.Conn = ConnInfo{HostInfo: host, ProtoInfo: proto, ServerInfo: server} return out, nil } func ParseQuery(body []byte) (*QueryResponse, error) { h, r, err := ParseHeader(body) if err != nil { return nil, err } out := &QueryResponse{Header: h} if !h.OK() { return out, nil } for { if r.remaining() == 0 { break } res, err := parseOneResult(r) if err != nil { return nil, err } out.Results = append(out.Results, res) if r.remaining() == 0 { break } sep, err := r.readByte() if err != nil { return nil, err } if sep == sepLast { break } if sep != sepMore { return nil, fmt.Errorf("invalid query separator 0x%02x", sep) } } return out, nil } func parseOneResult(r *reader) (Result, error) { var res Result var err error if res.Errno, err = r.u32(); err != nil { return res, err } if res.AffectedRows, err = r.u32(); err != nil { return res, err } if res.InsertID, err = r.u32(); err != nil { return res, err } if res.NumFields, err = r.u32(); err != nil { return res, err } if res.NumRows, err = r.u32(); err != nil { return res, err } if _, err = r.readN(12); err != nil { return res, err } if res.Errno > 0 { res.Message, err = r.blockString() return res, err } if res.NumFields == 0 { res.Info, err = r.blockString() return res, err } res.Fields = make([]Field, 0, res.NumFields) for i := uint32(0); i < res.NumFields; i++ { f, err := parseField(r) if err != nil { return res, fmt.Errorf("field %d: %w", i, err) } res.Fields = append(res.Fields, f) } res.Rows = make([][]Cell, 0, res.NumRows) for i := uint32(0); i < res.NumRows; i++ { row := make([]Cell, res.NumFields) for j := uint32(0); j < res.NumFields; j++ { val, isNull, err := r.block(true) if err != nil { return res, fmt.Errorf("row %d col %d: %w", i, j, err) } row[j] = Cell{Null: isNull, Bytes: val} } res.Rows = append(res.Rows, row) } return res, nil } func parseField(r *reader) (Field, error) { var f Field var err error if f.Name, err = r.blockString(); err != nil { return f, err } if f.Table, err = r.blockString(); err != nil { return f, err } if f.Type, err = r.u32(); err != nil { return f, err } if f.Flags, err = r.u32(); err != nil { return f, err } if f.Length, err = r.u32(); err != nil { return f, err } return f, nil } func looksHTML(body []byte) bool { s := strings.TrimSpace(string(body)) if len(s) < 5 { return false } n := 64 if len(s) < n { n = len(s) } ls := strings.ToLower(s[:n]) return strings.HasPrefix(ls, "