Files
mysql-tunnel/internal/navicat/response.go
T
2026-09-21 16:46:55 +08:00

267 lines
5.5 KiB
Go

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, "<!doctype") || strings.HasPrefix(ls, "<html")
}