app-init
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func putU32(n uint32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, n)
|
||||
return b
|
||||
}
|
||||
|
||||
func putU16(n uint16) []byte {
|
||||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, n)
|
||||
return b
|
||||
}
|
||||
|
||||
// AppendBlock encodes GetBlock() from ntunnel_mysql.php.
|
||||
// NULL is not represented here; use AppendNull for row cells.
|
||||
func AppendBlock(dst []byte, val []byte) []byte {
|
||||
if val == nil {
|
||||
val = []byte{}
|
||||
}
|
||||
n := len(val)
|
||||
if n < 254 {
|
||||
dst = append(dst, byte(n))
|
||||
return append(dst, val...)
|
||||
}
|
||||
dst = append(dst, markLong)
|
||||
dst = append(dst, putU32(uint32(n))...)
|
||||
return append(dst, val...)
|
||||
}
|
||||
|
||||
func AppendNull(dst []byte) []byte {
|
||||
return append(dst, markNull)
|
||||
}
|
||||
|
||||
func AppendHeader(dst []byte, version uint16, errno uint32) []byte {
|
||||
dst = append(dst, putU32(Magic)...)
|
||||
dst = append(dst, putU16(version)...)
|
||||
dst = append(dst, putU32(errno)...)
|
||||
dst = append(dst, 0, 0, 0, 0, 0, 0)
|
||||
return dst
|
||||
}
|
||||
|
||||
func AppendResultSetHeader(dst []byte, errno, affected, insertID, numFields, numRows uint32) []byte {
|
||||
dst = append(dst, putU32(errno)...)
|
||||
dst = append(dst, putU32(affected)...)
|
||||
dst = append(dst, putU32(insertID)...)
|
||||
dst = append(dst, putU32(numFields)...)
|
||||
dst = append(dst, putU32(numRows)...)
|
||||
dst = append(dst, make([]byte, 12)...)
|
||||
return dst
|
||||
}
|
||||
|
||||
func AppendField(dst []byte, name, table string, typ, flags, length uint32) []byte {
|
||||
dst = AppendBlock(dst, []byte(name))
|
||||
dst = AppendBlock(dst, []byte(table))
|
||||
dst = append(dst, putU32(typ)...)
|
||||
dst = append(dst, putU32(flags)...)
|
||||
dst = append(dst, putU32(length)...)
|
||||
return dst
|
||||
}
|
||||
|
||||
func knownVersion(v uint16) bool {
|
||||
return v == Version202 || v == Version206
|
||||
}
|
||||
|
||||
func fmtErrno(errno uint32, msg string) string {
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("tunnel errno %d", errno)
|
||||
}
|
||||
return fmt.Sprintf("tunnel errno %d: %s", errno, msg)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"mysql-tunnel/internal/logger"
|
||||
"mysql-tunnel/internal/tunnel"
|
||||
)
|
||||
|
||||
var charsetIdent = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`)
|
||||
|
||||
// Target is the remote MySQL as seen by ntunnel_mysql.php.
|
||||
type Target struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
Charset string
|
||||
}
|
||||
|
||||
// Client talks Navicat HTTP Tunnel over the generic HTTP client.
|
||||
type Client struct {
|
||||
http *tunnel.HTTPClient
|
||||
url string
|
||||
target Target
|
||||
encodeBase64 bool
|
||||
retries int
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
URL string
|
||||
Target Target
|
||||
EncodeBase64 bool
|
||||
// Retries is extra attempts on transport failure only (not SQL errors). 0 means a single try.
|
||||
Retries int
|
||||
}
|
||||
|
||||
func NewClient(httpClient *tunnel.HTTPClient, opt ClientOptions) *Client {
|
||||
retries := opt.Retries
|
||||
if retries < 0 {
|
||||
retries = 0
|
||||
}
|
||||
return &Client{
|
||||
http: httpClient,
|
||||
url: opt.URL,
|
||||
target: opt.Target,
|
||||
encodeBase64: opt.EncodeBase64,
|
||||
retries: retries,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Connect(ctx context.Context) (*ConnectResponse, error) {
|
||||
body, status, err := c.post(ctx, Request{
|
||||
Action: ActionConnect,
|
||||
Host: c.target.Host,
|
||||
Port: c.target.Port,
|
||||
Login: c.target.User,
|
||||
Password: c.target.Password,
|
||||
Database: c.target.Database,
|
||||
EncodeBase64: c.encodeBase64,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := ParseConnect(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.Header.OK() {
|
||||
return resp, headerError(resp.Header, status)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Query(ctx context.Context, database string, sqls ...string) (*QueryResponse, error) {
|
||||
db := database
|
||||
if db == "" {
|
||||
db = c.target.Database
|
||||
}
|
||||
queries := make([]string, 0, len(sqls)+1)
|
||||
if s := charsetSQL(c.target.Charset); s != "" {
|
||||
queries = append(queries, s)
|
||||
}
|
||||
queries = append(queries, nonEmpty(sqls)...)
|
||||
if len(queries) == 0 {
|
||||
return nil, fmt.Errorf("no SQL to execute")
|
||||
}
|
||||
body, status, err := c.post(ctx, Request{
|
||||
Action: ActionQuery,
|
||||
Host: c.target.Host,
|
||||
Port: c.target.Port,
|
||||
Login: c.target.User,
|
||||
Password: c.target.Password,
|
||||
Database: db,
|
||||
Queries: queries,
|
||||
EncodeBase64: c.encodeBase64,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := ParseQuery(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.Header.OK() {
|
||||
return resp, headerError(resp.Header, status)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) post(ctx context.Context, req Request) (body []byte, status int, err error) {
|
||||
encoded := req.Encode()
|
||||
attempts := 1 + c.retries
|
||||
var last error
|
||||
for i := 0; i < attempts; i++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
httpResp, err := c.http.Do(ctx, http.MethodPost, c.url, "application/x-www-form-urlencoded", strings.NewReader(encoded))
|
||||
if err != nil {
|
||||
last = err
|
||||
continue
|
||||
}
|
||||
status = httpResp.StatusCode
|
||||
body = httpResp.Body
|
||||
if status == http.StatusForbidden {
|
||||
return body, status, &Error{Kind: "http", HTTPStatus: status, Errno: ErrnoInvalidToken, Message: "invalid or missing token"}
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return body, status, &Error{Kind: "http", HTTPStatus: status, Message: "unexpected status"}
|
||||
}
|
||||
if looksHTML(body) {
|
||||
return body, status, &Error{Kind: "http", HTTPStatus: status, Message: "tunnel returned HTML, not Navicat binary"}
|
||||
}
|
||||
return body, status, nil
|
||||
}
|
||||
return nil, 0, last
|
||||
}
|
||||
|
||||
func charsetSQL(cs string) string {
|
||||
cs = strings.TrimSpace(cs)
|
||||
if cs == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.EqualFold(cs, "utf8") || strings.EqualFold(cs, "utf8mb3") {
|
||||
return ""
|
||||
}
|
||||
if !charsetIdent.MatchString(cs) {
|
||||
return ""
|
||||
}
|
||||
return "SET NAMES " + cs
|
||||
}
|
||||
|
||||
// LastResultSet returns the last result that has columns (skipping SET NAMES OK packets).
|
||||
func LastResultSet(resp *QueryResponse) (Result, bool) {
|
||||
if resp == nil {
|
||||
return Result{}, false
|
||||
}
|
||||
for i := len(resp.Results) - 1; i >= 0; i-- {
|
||||
if resp.Results[i].IsResultSet() {
|
||||
return resp.Results[i], true
|
||||
}
|
||||
}
|
||||
if len(resp.Results) == 0 {
|
||||
return Result{}, false
|
||||
}
|
||||
return resp.Results[len(resp.Results)-1], true
|
||||
}
|
||||
|
||||
func RedactedURL(raw string) string {
|
||||
return logger.RedactURL(raw)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mysql-tunnel/internal/tunnel"
|
||||
)
|
||||
|
||||
func testClient(t *testing.T, opts ...MockOption) (*Client, *httptest.Server) {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(NewMockHandler(opts...))
|
||||
t.Cleanup(srv.Close)
|
||||
httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second})
|
||||
c := NewClient(httpClient, ClientOptions{
|
||||
URL: srv.URL + "?token=secret",
|
||||
Target: Target{
|
||||
Host: "mysql",
|
||||
Port: 3306,
|
||||
User: "root",
|
||||
Password: "remote-pass",
|
||||
Database: "app",
|
||||
Charset: "utf8mb4",
|
||||
},
|
||||
})
|
||||
return c, srv
|
||||
}
|
||||
|
||||
func TestClientConnectAndSelect1(t *testing.T) {
|
||||
var cap CapturedRequest
|
||||
c, _ := testClient(t, WithToken("secret"), WithCapture(&cap))
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := c.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conn.Conn.ServerInfo != "8.0.36-mock" {
|
||||
t.Fatalf("%+v", conn.Conn)
|
||||
}
|
||||
if cap.Action != ActionConnect || cap.Host != "mysql" || cap.Login != "root" {
|
||||
t.Fatalf("connect capture %+v", cap)
|
||||
}
|
||||
if cap.Port != "3306" {
|
||||
t.Fatalf("port %s", cap.Port)
|
||||
}
|
||||
if !cap.HasPassword {
|
||||
t.Fatal("password field missing")
|
||||
}
|
||||
|
||||
q, err := c.Query(ctx, "", "SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, ok := LastResultSet(q)
|
||||
if !ok || res.Rows[0][0].Text() != "1" {
|
||||
t.Fatalf("select 1: %+v", q)
|
||||
}
|
||||
if cap.Action != ActionQuery {
|
||||
t.Fatalf("query capture %+v", cap)
|
||||
}
|
||||
joined := strings.Join(cap.Queries, " | ")
|
||||
if !strings.Contains(joined, "SET NAMES utf8mb4") {
|
||||
t.Fatalf("expected SET NAMES in %q", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "SELECT 1") {
|
||||
t.Fatalf("expected SELECT 1 in %q", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientTypesOverHTTP(t *testing.T) {
|
||||
c, _ := testClient(t, WithToken("secret"))
|
||||
ctx := context.Background()
|
||||
|
||||
q, err := c.Query(ctx, "app", "SELECT NULL AS n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, _ := LastResultSet(q)
|
||||
if !res.Rows[0][0].Null {
|
||||
t.Fatal("NULL")
|
||||
}
|
||||
|
||||
q, err = c.Query(ctx, "", "SELECT bigint_val")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, _ = LastResultSet(q)
|
||||
got := res.Rows[0][0].Text()
|
||||
if got != "18446744073709551615" {
|
||||
t.Fatalf("bigint %q", got)
|
||||
}
|
||||
if _, err := strconv.ParseInt(got, 10, 64); err == nil {
|
||||
t.Fatal("test value should overflow int64")
|
||||
}
|
||||
|
||||
q, err = c.Query(ctx, "", "SELECT dec_val")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, _ = LastResultSet(q)
|
||||
if res.Fields[0].Type != 246 || res.Rows[0][0].Text() != "1234567890.123456789" {
|
||||
t.Fatalf("decimal %+v", res)
|
||||
}
|
||||
|
||||
q, err = c.Query(ctx, "", "SELECT zh_val")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, _ = LastResultSet(q)
|
||||
if res.Rows[0][0].Text() != "你好世界" {
|
||||
t.Fatalf("zh %q", res.Rows[0][0].Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSQLError(t *testing.T) {
|
||||
c, _ := testClient(t, WithToken("secret"))
|
||||
q, err := c.Query(context.Background(), "", "SELECT SYNTAX")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, ok := LastResultSet(q)
|
||||
if !ok || !res.IsError() || res.Errno != 1064 {
|
||||
t.Fatalf("%+v", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDML(t *testing.T) {
|
||||
c, _ := testClient(t, WithToken("secret"))
|
||||
q, err := c.Query(context.Background(), "", "UPDATE product SET price=1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res := q.Results[len(q.Results)-1]
|
||||
if res.IsResultSet() || res.AffectedRows != 1 || res.InsertID != 42 {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientBadTokenPOST(t *testing.T) {
|
||||
srv := httptest.NewServer(NewMockHandler(WithToken("right")))
|
||||
t.Cleanup(srv.Close)
|
||||
httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second})
|
||||
c := NewClient(httpClient, ClientOptions{
|
||||
URL: srv.URL + "?token=wrong",
|
||||
Target: Target{Host: "mysql", Port: 3306, User: "root"},
|
||||
})
|
||||
_, err := c.Connect(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected token error")
|
||||
}
|
||||
ne, ok := err.(*Error)
|
||||
if !ok || ne.Errno != ErrnoInvalidToken {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConnectDenied(t *testing.T) {
|
||||
c, _ := testClient(t, WithToken("secret"), WithConnectError(1045, "Access denied for user 'root'@'localhost'"))
|
||||
_, err := c.Connect(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected deny")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "1045") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "remote-pass") {
|
||||
t.Fatal("password leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeBase64Query(t *testing.T) {
|
||||
var cap CapturedRequest
|
||||
srv := httptest.NewServer(NewMockHandler(WithToken("secret"), WithCapture(&cap)))
|
||||
t.Cleanup(srv.Close)
|
||||
httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second})
|
||||
c := NewClient(httpClient, ClientOptions{
|
||||
URL: srv.URL + "?token=secret",
|
||||
Target: Target{Host: "mysql", Port: 3306, User: "root", Charset: "utf8"},
|
||||
EncodeBase64: true,
|
||||
})
|
||||
_, err := c.Query(context.Background(), "", "SELECT 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cap.EncodeBase64 != "1" {
|
||||
t.Fatalf("%+v", cap)
|
||||
}
|
||||
if strings.Contains(strings.Join(cap.Queries, ""), "SELECT 1") {
|
||||
t.Fatalf("wire should be base64, got %v", cap.Queries)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Package navicat implements the Navicat HTTP Tunnel protocol used by
|
||||
// ntunnel_mysql.php (script version 206 in this repository).
|
||||
//
|
||||
// Every request is stateless: the PHP script opens a new MySQL connection,
|
||||
// runs actn=C or actn=Q, and disconnects. See docs/PROTOCOL.md.
|
||||
package navicat
|
||||
|
||||
const (
|
||||
Magic uint32 = 1111
|
||||
Version202 uint16 = 202
|
||||
Version206 uint16 = 206
|
||||
HeaderSize = 16
|
||||
|
||||
ActionConnect = "C"
|
||||
ActionQuery = "Q"
|
||||
|
||||
ErrnoUnsupportedPHP uint32 = 201
|
||||
ErrnoInvalidParams uint32 = 202
|
||||
ErrnoMySQLMissing uint32 = 203
|
||||
ErrnoInvalidToken uint32 = 204
|
||||
|
||||
sepMore byte = 0x01
|
||||
sepLast byte = 0x00
|
||||
markLong byte = 0xFE
|
||||
markNull byte = 0xFF
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
package navicat
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error is a tunnel-layer or remote MySQL error. It never includes passwords or tokens.
|
||||
type Error struct {
|
||||
Kind string // http, tunnel, mysql
|
||||
HTTPStatus int
|
||||
Errno uint32
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
msg := e.Message
|
||||
if msg == "" {
|
||||
msg = "unknown error"
|
||||
}
|
||||
switch e.Kind {
|
||||
case "http":
|
||||
if e.HTTPStatus != 0 {
|
||||
return fmt.Sprintf("tunnel http %d: %s", e.HTTPStatus, msg)
|
||||
}
|
||||
return "tunnel http: " + msg
|
||||
case "mysql":
|
||||
return fmt.Sprintf("mysql errno %d: %s", e.Errno, msg)
|
||||
default:
|
||||
return fmtErrno(e.Errno, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func headerError(h Header, httpStatus int) *Error {
|
||||
kind := "tunnel"
|
||||
switch h.Errno {
|
||||
case ErrnoUnsupportedPHP, ErrnoInvalidParams, ErrnoMySQLMissing, ErrnoInvalidToken:
|
||||
kind = "tunnel"
|
||||
default:
|
||||
if h.Errno > 0 {
|
||||
kind = "mysql"
|
||||
}
|
||||
}
|
||||
return &Error{Kind: kind, HTTPStatus: httpStatus, Errno: h.Errno, Message: h.Message}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MockOption configures the in-process ntunnel_mysql.php stand-in.
|
||||
type MockOption func(*mockState)
|
||||
|
||||
type mockState struct {
|
||||
token string
|
||||
connectFail *Error
|
||||
queryFn func(sql string) Result
|
||||
capture *CapturedRequest
|
||||
}
|
||||
|
||||
// CapturedRequest is the last POST seen by the mock.
|
||||
type CapturedRequest struct {
|
||||
Action string
|
||||
Host string
|
||||
Port string
|
||||
Login string
|
||||
Database string
|
||||
Queries []string
|
||||
EncodeBase64 string
|
||||
HasPassword bool
|
||||
}
|
||||
|
||||
func WithToken(token string) MockOption {
|
||||
return func(m *mockState) { m.token = token }
|
||||
}
|
||||
|
||||
func WithConnectError(errno uint32, msg string) MockOption {
|
||||
return func(m *mockState) {
|
||||
m.connectFail = &Error{Kind: "mysql", Errno: errno, Message: msg}
|
||||
}
|
||||
}
|
||||
|
||||
func WithQueryHandler(fn func(sql string) Result) MockOption {
|
||||
return func(m *mockState) { m.queryFn = fn }
|
||||
}
|
||||
|
||||
func WithCapture(dst *CapturedRequest) MockOption {
|
||||
return func(m *mockState) { m.capture = dst }
|
||||
}
|
||||
|
||||
// NewMockHandler implements a subset of ntunnel_mysql.php for tests.
|
||||
func NewMockHandler(opts ...MockOption) http.Handler {
|
||||
st := &mockState{queryFn: defaultQuery}
|
||||
for _, o := range opts {
|
||||
o(st)
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
if st.token != "" && r.URL.Query().Get("token") != st.token {
|
||||
if r.PostForm.Get("actn") != "" {
|
||||
writeBytes(w, encodeHeaderError(ErrnoInvalidToken, "invalid access token"))
|
||||
return
|
||||
}
|
||||
http.Error(w, "403 Forbidden: invalid or missing token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
writeBytes(w, encodeHeaderError(ErrnoInvalidParams, "invalid parameters"))
|
||||
return
|
||||
}
|
||||
actn := r.PostForm.Get("actn")
|
||||
host := r.PostForm.Get("host")
|
||||
login := r.PostForm.Get("login")
|
||||
if actn == "" || host == "" || !r.PostForm.Has("port") || login == "" {
|
||||
writeBytes(w, encodeHeaderError(ErrnoInvalidParams, "invalid parameters"))
|
||||
return
|
||||
}
|
||||
if st.capture != nil {
|
||||
*st.capture = CapturedRequest{
|
||||
Action: actn,
|
||||
Host: host,
|
||||
Port: r.PostForm.Get("port"),
|
||||
Login: login,
|
||||
Database: r.PostForm.Get("db"),
|
||||
Queries: append([]string(nil), r.PostForm["q[]"]...),
|
||||
EncodeBase64: r.PostForm.Get("encodeBase64"),
|
||||
HasPassword: r.PostForm.Has("password"),
|
||||
}
|
||||
}
|
||||
if st.connectFail != nil {
|
||||
writeBytes(w, encodeHeaderError(st.connectFail.Errno, st.connectFail.Message))
|
||||
return
|
||||
}
|
||||
switch actn {
|
||||
case ActionConnect:
|
||||
writeBytes(w, encodeConnectOK("127.0.0.1 via TCP/IP", "10", "8.0.36-mock"))
|
||||
case ActionQuery:
|
||||
queries := append([]string(nil), r.PostForm["q[]"]...)
|
||||
if r.PostForm.Get("encodeBase64") == "1" {
|
||||
decoded := make([]string, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
b, err := base64.StdEncoding.DecodeString(q)
|
||||
if err != nil {
|
||||
writeBytes(w, encodeQueryResults([]Result{{Errno: 1064, Message: "bad base64 query"}}))
|
||||
return
|
||||
}
|
||||
decoded = append(decoded, string(b))
|
||||
}
|
||||
queries = decoded
|
||||
}
|
||||
var results []Result
|
||||
for _, q := range queries {
|
||||
if q == "" {
|
||||
continue
|
||||
}
|
||||
results = append(results, st.queryFn(q))
|
||||
}
|
||||
writeBytes(w, encodeQueryResults(results))
|
||||
default:
|
||||
writeBytes(w, AppendHeader(nil, Version206, 0))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func defaultQuery(sql string) Result {
|
||||
s := strings.TrimSpace(sql)
|
||||
u := strings.ToUpper(s)
|
||||
switch {
|
||||
case strings.HasPrefix(u, "SET NAMES"):
|
||||
return Result{}
|
||||
case u == "SELECT 1" || strings.HasPrefix(u, "SELECT 1 "):
|
||||
return selectLiteral("1", "1", 8, 20)
|
||||
case strings.Contains(u, "SELECT NULL"):
|
||||
return Result{
|
||||
NumFields: 1,
|
||||
NumRows: 1,
|
||||
Fields: []Field{{Name: "n", Type: 6, Length: 0}},
|
||||
Rows: [][]Cell{{{Null: true}}},
|
||||
}
|
||||
case strings.Contains(strings.ToLower(s), "bigint_val"):
|
||||
r := selectLiteral("bigint_val", "18446744073709551615", 8, 20)
|
||||
r.Fields[0].Flags = 32 // UNSIGNED
|
||||
return r
|
||||
case strings.Contains(strings.ToLower(s), "dec_val"):
|
||||
return selectLiteral("dec_val", "1234567890.123456789", 246, 32)
|
||||
case strings.Contains(s, "你好") || strings.Contains(strings.ToLower(s), "zh_val"):
|
||||
return selectLiteral("zh_val", "你好世界", 253, 12)
|
||||
case strings.HasPrefix(u, "INSERT") || strings.HasPrefix(u, "UPDATE") || strings.HasPrefix(u, "DELETE") || strings.HasPrefix(u, "REPLACE"):
|
||||
return Result{AffectedRows: 1, InsertID: 42, Info: "Rows matched: 1 Changed: 1 Warnings: 0"}
|
||||
case strings.Contains(u, "SYNTAX"):
|
||||
return Result{Errno: 1064, Message: "You have an error in your SQL syntax"}
|
||||
default:
|
||||
return Result{Errno: 1064, Message: "mock: unhandled SQL"}
|
||||
}
|
||||
}
|
||||
|
||||
func selectLiteral(name, value string, typ, length uint32) Result {
|
||||
return Result{
|
||||
NumFields: 1,
|
||||
NumRows: 1,
|
||||
Fields: []Field{{Name: name, Type: typ, Length: length}},
|
||||
Rows: [][]Cell{{{Bytes: []byte(value)}}},
|
||||
}
|
||||
}
|
||||
|
||||
func encodeHeaderError(errno uint32, msg string) []byte {
|
||||
b := AppendHeader(nil, Version206, errno)
|
||||
return AppendBlock(b, []byte(msg))
|
||||
}
|
||||
|
||||
func encodeConnectOK(host, proto, server string) []byte {
|
||||
b := AppendHeader(nil, Version206, 0)
|
||||
b = AppendBlock(b, []byte(host))
|
||||
b = AppendBlock(b, []byte(proto))
|
||||
b = AppendBlock(b, []byte(server))
|
||||
return b
|
||||
}
|
||||
|
||||
func encodeQueryResults(results []Result) []byte {
|
||||
b := AppendHeader(nil, Version206, 0)
|
||||
if len(results) == 0 {
|
||||
return b
|
||||
}
|
||||
for i, res := range results {
|
||||
b = appendQueryResult(b, res)
|
||||
if i < len(results)-1 {
|
||||
b = append(b, sepMore)
|
||||
} else {
|
||||
b = append(b, sepLast)
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func appendQueryResult(b []byte, res Result) []byte {
|
||||
nfields := res.NumFields
|
||||
nrows := res.NumRows
|
||||
if nfields == 0 {
|
||||
nfields = uint32(len(res.Fields))
|
||||
}
|
||||
if nrows == 0 && len(res.Rows) > 0 {
|
||||
nrows = uint32(len(res.Rows))
|
||||
}
|
||||
b = AppendResultSetHeader(b, res.Errno, res.AffectedRows, res.InsertID, nfields, nrows)
|
||||
if res.Errno > 0 {
|
||||
return AppendBlock(b, []byte(res.Message))
|
||||
}
|
||||
if nfields == 0 {
|
||||
return AppendBlock(b, []byte(res.Info))
|
||||
}
|
||||
for _, f := range res.Fields {
|
||||
b = AppendField(b, f.Name, f.Table, f.Type, f.Flags, f.Length)
|
||||
}
|
||||
for _, row := range res.Rows {
|
||||
for _, cell := range row {
|
||||
if cell.Null {
|
||||
b = AppendNull(b)
|
||||
} else {
|
||||
b = AppendBlock(b, cell.Bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func writeBytes(w http.ResponseWriter, body []byte) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=x-user-defined")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type reader struct {
|
||||
buf []byte
|
||||
off int
|
||||
}
|
||||
|
||||
func newReader(b []byte) *reader {
|
||||
return &reader{buf: b}
|
||||
}
|
||||
|
||||
func (r *reader) remaining() int {
|
||||
return len(r.buf) - r.off
|
||||
}
|
||||
|
||||
func (r *reader) peek() (byte, error) {
|
||||
if r.off >= len(r.buf) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return r.buf[r.off], nil
|
||||
}
|
||||
|
||||
func (r *reader) readByte() (byte, error) {
|
||||
if r.off >= len(r.buf) {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := r.buf[r.off]
|
||||
r.off++
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *reader) readN(n int) ([]byte, error) {
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("negative length")
|
||||
}
|
||||
if r.off+n > len(r.buf) {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
s := r.buf[r.off : r.off+n]
|
||||
r.off += n
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *reader) u16() (uint16, error) {
|
||||
b, err := r.readN(2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint16(b), nil
|
||||
}
|
||||
|
||||
func (r *reader) u32() (uint32, error) {
|
||||
b, err := r.readN(4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint32(b), nil
|
||||
}
|
||||
|
||||
// block reads GetBlock. nullable allows 0xFF as SQL NULL (EchoData only).
|
||||
func (r *reader) block(nullable bool) (val []byte, isNull bool, err error) {
|
||||
if r.off >= len(r.buf) {
|
||||
return nil, false, io.ErrUnexpectedEOF
|
||||
}
|
||||
first := r.buf[r.off]
|
||||
if nullable && first == markNull {
|
||||
r.off++
|
||||
return nil, true, nil
|
||||
}
|
||||
r.off++
|
||||
var n int
|
||||
if first < 254 {
|
||||
n = int(first)
|
||||
} else if first == markLong {
|
||||
ln, err := r.u32()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if ln > 64<<20 {
|
||||
return nil, false, fmt.Errorf("block length %d exceeds 64MiB limit", ln)
|
||||
}
|
||||
n = int(ln)
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("invalid block marker 0x%02x", first)
|
||||
}
|
||||
b, err := r.readN(n)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return b, false, nil
|
||||
}
|
||||
|
||||
func (r *reader) blockBytes() ([]byte, error) {
|
||||
b, isNull, err := r.block(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isNull {
|
||||
return nil, fmt.Errorf("unexpected NULL block")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *reader) blockString() (string, error) {
|
||||
b, err := r.blockBytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBlockRoundTripShortAndLong(t *testing.T) {
|
||||
cases := [][]byte{
|
||||
{},
|
||||
[]byte("a"),
|
||||
bytes.Repeat([]byte("x"), 253),
|
||||
bytes.Repeat([]byte("y"), 254),
|
||||
bytes.Repeat([]byte("z"), 1024),
|
||||
[]byte("你好世界"),
|
||||
}
|
||||
for _, in := range cases {
|
||||
enc := AppendBlock(nil, in)
|
||||
r := newReader(enc)
|
||||
got, isNull, err := r.block(false)
|
||||
if err != nil {
|
||||
t.Fatalf("len=%d: %v", len(in), err)
|
||||
}
|
||||
if isNull {
|
||||
t.Fatal("null")
|
||||
}
|
||||
if !bytes.Equal(got, in) {
|
||||
t.Fatalf("mismatch len=%d", len(in))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullCell(t *testing.T) {
|
||||
enc := AppendNull(nil)
|
||||
r := newReader(enc)
|
||||
_, isNull, err := r.block(true)
|
||||
if err != nil || !isNull {
|
||||
t.Fatalf("err=%v null=%v", err, isNull)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeader202And206(t *testing.T) {
|
||||
for _, ver := range []uint16{Version202, Version206} {
|
||||
body := AppendHeader(nil, ver, 0)
|
||||
h, _, err := ParseHeader(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Version != ver || !h.OK() {
|
||||
t.Fatalf("%+v", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderError204(t *testing.T) {
|
||||
body := encodeHeaderError(ErrnoInvalidToken, "invalid access token")
|
||||
h, _, err := ParseHeader(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Errno != ErrnoInvalidToken || h.Message != "invalid access token" {
|
||||
t.Fatalf("%+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectParse(t *testing.T) {
|
||||
body := encodeConnectOK("127.0.0.1 via TCP/IP", "10", "8.0.36")
|
||||
resp, err := ParseConnect(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Conn.ProtoInfo != "10" || resp.Conn.ServerInfo != "8.0.36" {
|
||||
t.Fatalf("%+v", resp.Conn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuerySelect1(t *testing.T) {
|
||||
body := encodeQueryResults([]Result{selectLiteral("1", "1", 8, 20)})
|
||||
resp, err := ParseQuery(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp.Results) != 1 {
|
||||
t.Fatalf("results=%d", len(resp.Results))
|
||||
}
|
||||
r := resp.Results[0]
|
||||
if !r.IsResultSet() || r.NumRows != 1 {
|
||||
t.Fatalf("%+v", r)
|
||||
}
|
||||
if r.Rows[0][0].Null || r.Rows[0][0].Text() != "1" {
|
||||
t.Fatalf("cell=%+v", r.Rows[0][0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullDecimalBigintUTF8(t *testing.T) {
|
||||
body := encodeQueryResults([]Result{
|
||||
{
|
||||
NumFields: 4,
|
||||
NumRows: 1,
|
||||
Fields: []Field{
|
||||
{Name: "n", Type: 6},
|
||||
{Name: "b", Type: 8, Length: 20},
|
||||
{Name: "d", Type: 246, Length: 32},
|
||||
{Name: "z", Type: 253, Length: 12},
|
||||
},
|
||||
Rows: [][]Cell{{
|
||||
{Null: true},
|
||||
{Bytes: []byte("18446744073709551615")},
|
||||
{Bytes: []byte("1234567890.123456789")},
|
||||
{Bytes: []byte("你好世界")},
|
||||
}},
|
||||
},
|
||||
})
|
||||
resp, err := ParseQuery(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
row := resp.Results[0].Rows[0]
|
||||
if !row[0].Null {
|
||||
t.Fatal("expected NULL")
|
||||
}
|
||||
if row[1].Text() != "18446744073709551615" {
|
||||
t.Fatalf("bigint parsed badly: %q", row[1].Text())
|
||||
}
|
||||
if row[2].Text() != "1234567890.123456789" {
|
||||
t.Fatalf("decimal parsed badly: %q", row[2].Text())
|
||||
}
|
||||
if row[3].Text() != "你好世界" {
|
||||
t.Fatalf("utf8: %q", row[3].Text())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLError(t *testing.T) {
|
||||
body := encodeQueryResults([]Result{{Errno: 1064, Message: "You have an error in your SQL syntax"}})
|
||||
resp, err := ParseQuery(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.Results[0].IsError() || !strings.Contains(resp.Results[0].Message, "syntax") {
|
||||
t.Fatalf("%+v", resp.Results[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDMLOk(t *testing.T) {
|
||||
body := encodeQueryResults([]Result{{AffectedRows: 3, InsertID: 9, Info: "Rows matched: 3"}})
|
||||
resp, err := ParseQuery(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := resp.Results[0]
|
||||
if r.IsResultSet() || r.AffectedRows != 3 || r.InsertID != 9 {
|
||||
t.Fatalf("%+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLRejected(t *testing.T) {
|
||||
_, err := ParseConnect([]byte("<html>nope</html>"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestOmitsEmptyQueriesAndEncodesArray(t *testing.T) {
|
||||
enc := Request{
|
||||
Action: ActionQuery,
|
||||
Host: "mysql",
|
||||
Port: 3306,
|
||||
Login: "root",
|
||||
Queries: []string{"SELECT 1", "", "SELECT 2"},
|
||||
}.Encode()
|
||||
if strings.Contains(enc, "password=secret") {
|
||||
t.Fatal("unexpected")
|
||||
}
|
||||
if !strings.Contains(enc, "q%5B%5D=SELECT+1") && !strings.Contains(enc, "q%5B%5D=SELECT%201") {
|
||||
t.Fatalf("q[] missing: %s", enc)
|
||||
}
|
||||
if strings.Count(enc, "q%5B%5D=") != 2 {
|
||||
t.Fatalf("empty query not skipped: %s", enc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBase64(t *testing.T) {
|
||||
enc := Request{
|
||||
Action: ActionQuery,
|
||||
Host: "mysql",
|
||||
Port: 3306,
|
||||
Login: "root",
|
||||
Queries: []string{"SELECT 1"},
|
||||
EncodeBase64: true,
|
||||
}.Encode()
|
||||
if !strings.Contains(enc, "encodeBase64=1") {
|
||||
t.Fatal(enc)
|
||||
}
|
||||
if strings.Contains(enc, "SELECT+1") {
|
||||
t.Fatalf("sql should be base64: %s", enc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorDoesNotEmbedPassword(t *testing.T) {
|
||||
e := &Error{Kind: "mysql", Errno: 1045, Message: "Access denied for user 'root'@'localhost'"}
|
||||
if strings.Contains(e.Error(), "password") && strings.Contains(e.Error(), "=") {
|
||||
t.Fatal(e.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package navicat
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Request is one Navicat HTTP Tunnel POST body.
|
||||
type Request struct {
|
||||
Action string
|
||||
Host string
|
||||
Port int
|
||||
Login string
|
||||
Password string
|
||||
Database string
|
||||
Queries []string
|
||||
EncodeBase64 bool
|
||||
}
|
||||
|
||||
// Form builds application/x-www-form-urlencoded fields matching $_POST in ntunnel_mysql.php.
|
||||
func (r Request) Form() url.Values {
|
||||
v := url.Values{}
|
||||
v.Set("actn", r.Action)
|
||||
v.Set("host", r.Host)
|
||||
if r.Port > 0 {
|
||||
v.Set("port", strconv.Itoa(r.Port))
|
||||
} else {
|
||||
v.Set("port", "")
|
||||
}
|
||||
v.Set("login", r.Login)
|
||||
v.Set("password", r.Password)
|
||||
v.Set("db", r.Database)
|
||||
|
||||
queries := nonEmpty(r.Queries)
|
||||
if r.EncodeBase64 {
|
||||
v.Set("encodeBase64", "1")
|
||||
for _, q := range queries {
|
||||
v.Add("q[]", base64.StdEncoding.EncodeToString([]byte(q)))
|
||||
}
|
||||
} else {
|
||||
for _, q := range queries {
|
||||
v.Add("q[]", q)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r Request) Encode() string {
|
||||
return r.Form().Encode()
|
||||
}
|
||||
|
||||
func nonEmpty(in []string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user