267 lines
5.5 KiB
Go
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")
|
|
}
|