diff --git a/chat/client.go b/chat/client.go index 94d4442..0e854d0 100644 --- a/chat/client.go +++ b/chat/client.go @@ -6,8 +6,8 @@ import ( ) const ( - pingPeriod = time.Millisecond * 5000 - writeDeadlinePeriod = time.Second * 2 + pingPeriod = time.Millisecond * 60000 + writeDeadlinePeriod = time.Second * 60 ) // Client represents client(browser) connected via websocket diff --git a/client_to_server_handler.go b/client_to_server_handler.go new file mode 100644 index 0000000..fd7d381 --- /dev/null +++ b/client_to_server_handler.go @@ -0,0 +1,37 @@ +package main + +import ( + "io" + "lottip/chat" + "lottip/protocol" +) + +type ClientToServerHandler struct { + connInfo *ConnectionInfo + queryChan chan chat.Cmd + connStateChan chan chat.ConnState + server io.Writer +} + +// CLIENT to SERVER +func (pp *ClientToServerHandler) Write(buffer []byte) (n int, err error) { + extractPacketsFromBuffer(pp.connInfo, pp.connInfo.clientPacketFragment, buffer, func(packet []byte) { + // Switch based on the state + fsm := pp.connInfo.fsm + + if ok, _ := fsm.IsInState(StateIdle); ok { + pp.connStateChan <- chat.ConnState{pp.connInfo.ConnId, protocol.ConnStateFinished} + } else if ok, _ := fsm.IsInState(StateAuthRequested); ok { + fsm.Fire(MsgLogin, packet) + } else { + if len(packet) < 4 { + LogInvalid(pp.connInfo, "?Request?", packet) + } else { + // We are processing queries + fsm.Fire(GetPacketType(packet), packet) + } + } + }) + + return len(buffer), nil +} diff --git a/docker/.gitattributes b/docker/.gitattributes new file mode 100644 index 0000000..6c860db --- /dev/null +++ b/docker/.gitattributes @@ -0,0 +1,2 @@ +Dockerfile text eol=LF +*.sh text eol=LF \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 48f4a83..bbe54d2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,17 @@ FROM frolvlad/alpine-go - RUN apk update && apk upgrade && \ apk add --no-cache bash git -RUN go get github.com/orderbynull/lottip && \ - go get github.com/mjibson/esc && \ - go install github.com/mjibson/esc && \ - cd /root/go/src/github.com/orderbynull/lottip && \ - /root/go/bin/esc -o fs.go -prefix web -include=".*\.css|.*\.js|.*\.html|.*\.png" web && \ - go build +RUN git clone http://github.com/rkoshy/lottip +WORKDIR lottip +RUN go get github.com/mjibson/esc +RUN go install github.com/mjibson/esc +RUN /root/go/bin/esc -o fs.go -prefix web -include=".*\.css|.*\.js|.*\.html|.*\.png" web +RUN go get +RUN go install +WORKDIR / +RUN rm -rf /root/go/bin/esc && rm -rf lottip ADD run.sh /run.sh RUN chmod +x /run.sh @@ -18,6 +20,3 @@ EXPOSE 4041 EXPOSE 9999 CMD /run.sh - - - diff --git a/docker/run.sh b/docker/run.sh index 693da62..fdf8a6e 100644 --- a/docker/run.sh +++ b/docker/run.sh @@ -14,11 +14,30 @@ LOTTIP_GUI="${LOTTIP_GUI:-0.0.0.0:9999}" # MySQL DSN (credentials) LOTTIP_DSN="${LOTTIP_DSN:-root:root@/}" +# Log directory +LOTTIP_LOG_DIRECTORY="${LOTTIP_LOG_DIRECTORY:-./logs}" -# Run lottip +# Log filename +LOTTIP_LOGFILE="${LOTTIP_LOGFILE:-logfile.log}" + +# Query Log filename +LOTTIP_QUERY_LOGFILE="${LOTTIP_QUERY_LOGFILE:-queries.log}" -/root/go/src/github.com/orderbynull/lottip/lottip \ +# Enable +if [ "${LOTTIP_CONSOLE_LOGGING}" == "true" ]; +then + LOTTIP_CONSOLE_LOGGING="--enable-console-logging" +else + LOTTIP_CONSOLE_LOGGING="" +fi + +# Run lottip +/root/go/bin/lottip \ --proxy "$LOTTIP_PROXY" \ --mysql "$LOTTIP_MYSQL" \ - --gui "$LOTTIP_GUI" \ - --mysql-dsn "$LOTTIP_DSN" + --gui-addr "$LOTTIP_GUI" \ + --mysql-dsn "$LOTTIP_DSN" \ + "$LOTTIP_CONSOLE_LOGGING" \ + --query-log-file "$LOTTIP_QUERY_LOGFILE" \ + --log-file "$LOTTIP_LOGFILE" \ + --log-directory "$LOTTIP_LOG_DIRECTORY" \ No newline at end of file diff --git a/fs.go b/fs.go index e94e9b9..60ec5d4 100644 --- a/fs.go +++ b/fs.go @@ -6,6 +6,8 @@ import ( "bytes" "compress/gzip" "encoding/base64" + "fmt" + "io" "io/ioutil" "net/http" "os" @@ -100,7 +102,24 @@ func (f *_escFile) Close() error { } func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { - return nil, nil + if !f.isDir { + return nil, fmt.Errorf(" escFile.Readdir: '%s' is not directory", f.name) + } + + fis, ok := _escDirs[f.local] + if !ok { + return nil, fmt.Errorf(" escFile.Readdir: '%s' is directory, but we have no info about content of this dir, local=%s", f.name, f.local) + } + limit := count + if count <= 0 || limit > len(fis) { + limit = len(fis) + } + + if len(fis) == 0 && count > 0 { + return nil, io.EOF + } + + return fis[0:limit], nil } func (f *_escFile) Stat() (os.FileInfo, error) { @@ -191,9 +210,10 @@ func FSMustString(useLocal bool, name string) string { var _escData = map[string]*_escFile{ "/css/bootstrap-theme.min.css": { + name: "bootstrap-theme.min.css", local: "web/css/bootstrap-theme.min.css", size: 23409, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/+xcX4+bOhZ/v5+CVVV1pkoIISF/GHW02u7VqlK7D7tdaaXVPhhsMugmgMCZSTWa735l GzIGbIPtRNOHFo06sc/5+cA5/mH7nMzs419+cz46f8tzXOESFM7jwl24a+fmAeMinM12CEdNnxvnh1si @@ -246,9 +266,10 @@ iwOjoHFbAAA= }, "/css/bootstrap.min.css": { + name: "bootstrap.min.css", local: "web/css/bootstrap.min.css", size: 121200, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/+z9ba/jOLIniL+fT6HOQqEyKy2nJD8d26jz7/vvGcxt4PZ9sdMLDFCdu6Al2lalLKkl +TyUx/PZF+KTyGBQkn1OVfdi+yZulw/5YzAYDDLIEBn88uMf/ov3o/f/L4qmbipSek+z6Wy68j4em6bc @@ -580,32 +601,60 @@ z//jP37aFUVTNxUpp6c0n8Z1PT2R0vvxy/8TAAD//1h8f0Zw2QEA `, }, + "/css/prism.css": { + name: "prism.css", + local: "web/css/prism.css", + size: 2447, + modtime: 1655701668, + compressed: ` +H4sIAAAAAAAC/5xV32/bRgx+lv4KosGaxLBkp/nRRUaGdunDVrTAgHR7GfZA6Sjr6tNRuzvFcYr878Wd +f0iKmyDoiwHS5MfvE8njZAR/GWnrjzdwkr45Tadx5Vxjs8mk8e6vNi24ngheasUo0srV6sBVVJO9CgGv +Fep5i3OyV/Z/9bpR7Vxqe6XZ1KjkPSXLSjqyDRYEo0k8GY1iGEFITb9aEFRiqxwESCjZwEe8xZvCyMaN +4frmBlAL+OPL508+7Xe0JIA1CMxzRQ6OPNlsMtnYnuuxD3yHravYwCdC+IcMt7EvHhcs6N9CobWjq1db +4smr/8ZxY374B3yLo4IVmwxyhcViFkc5Fou54VaLDDRrmsWRozuX2AoFLzOYwklzB0H1LI5K1i4psZZq +lcE1a8sK7Rg+s8aCx3D4XgtU5G0+HMPh33mrXbs1a9YcvtwWyMp7yuCE6m1RVHKuM1BUulkchaJJyMig +MT5tyUYEj9Rzz9d3ZevODeHisXNpsOn5lNSUVCTnlcvgJD2fxXGU1HyfOMw3dM5mcZTwI8fAiqNkSflC +uqRaNRVpu/tyAWrfafd8j+yH+KmOZVnAtKSocJL1GJ5q7V7kk+Oxj/lU5B6oH6DBeGz09KfoID8VZ2Xx +vKoXCXqRlpfJ+GkF72oSEv2Ga7den6d2Lnpm6X5Y8sHjT0ZwzYIgV1wsrF/rZ2AaFCJM/npnajRzqTNI +z6mG6SyO+JZMqXwJbB0H/plmd9QYOobf4Kfei8GHKc/LN+V0tiH+p/b7FGA98ZeUGqhI1zJyNoJMYlDI +1maQngbvYPu3C/wQx6njBWn/NNak3XhrN4YVz3em4MKtGtrZhUCHvefPKnQ0N7jqYzatLlyL2zHZhB5c +Xl72ozTWmwPwLY7Yv0VulUH6doBkuCHjVrv6DjtuObMi1Dtbt3VOpqPK2jrsSbOrOmfVSSNFjsSA4fS8 +X3097NxBonMm8bw7TGek7igVFXbReSuVkx0/qS2ZRxUvLqf9il4t9iuSdrInvzWe/24OCmvhMRHrVoqG +3oFEvKBTnMXRZARfKmmhm0sIMbBEC1I70oIE5Ct/g2FzN7kE53PCWU79rA7GurIKj6ZjmP4yhpOp/03P +j/vy0JlW0fBz3qJqO9eCVv7e9BlP32Ifo/TDFd6y7Tf32xG60s/68OHs/cWv/URDc7rrmlE3bAbjcYtG +Yq4GKDRsz35SziqQDXd4uTmI3unTtlkOlSx2UaFBGay9ffR1r0P51lhfvyLVhIjvAQAA//+RcBgJjwkA +AA== +`, + }, + "/css/style.css": { + name: "style.css", local: "web/css/style.css", - size: 2605, - modtime: 1508271191, + size: 2602, + modtime: 1655701668, compressed: ` -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 `, }, "/images/favicon.png": { + name: "favicon.png", local: "web/images/favicon.png", size: 1156, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/wCEBHv7iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c 6QAABD5JREFUWAm9V11oVFcQ/nbdJpYmaVYMW9JYdqO4aMQ+uIa29i8QrCQiNRqqhPoSQcyLoghB0ZDI @@ -631,86 +680,91 @@ eIjnYvqd06ub8oymFAdfcM/rae8fI0VDB1TFcSmd6UQrHZDMGmTT6nkjY7oxSWytdGAdQ/47+XGdLIM1 }, "/index.html": { + name: "index.html", local: "web/index.html", - size: 7050, - modtime: 1508308025, + size: 7232, + modtime: 1655701668, compressed: ` -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 -8euNa0Tx/wAAAP//m9xQloobAAA= +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 +UfwWAAD//8ZWTmRAHAAA `, }, "/js/app.js": { + name: "app.js", local: "web/js/app.js", - size: 8139, - modtime: 1507905576, + size: 8257, + modtime: 1655701668, compressed: ` -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 `, }, "/js/bootstrap.min.js": { + name: "bootstrap.min.js", local: "web/js/bootstrap.min.js", size: 37045, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/8y9a3fbuNUo/L2/gsLMwwAjiJaTXp5SYbQyyfR9c047M2cm065zXLUFSfDi0KSGpO2k lvrbz8KVAAlKdnpZ54stkgC4sbHvewO8+GrxC+8r7+um6bu+JXvv7kXwIviNB4u+34cXFzntY/UsSJob @@ -880,9 +934,10 @@ pH4cDg9HLOWlvPU1xxI/5FSW1gu8jRqgUbf3zd7s0zd785G285gqs8IQm/8bAAD//y+jxWq1kAAA }, "/js/clipboard.min.js": { + name: "clipboard.min.js", local: "web/js/clipboard.min.js", size: 4298, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/5RXS4/juBG+969g66CQaK3ivlrhGoPeWWCBTTLYmQAJDCPgo/RI06SGou12bP33gNTT bm0jezIt1otffVUs4vyghauMxnkiyCU6aAl5pUFGj5S6cw0mR3sjDwo23U8Kb7WxrqECk3U0aEd0lO70 @@ -916,9 +971,10 @@ vZ97W9ImDwuPjI9wEIkcbu3FcIubcEnGNwJzspZDLMOQvhDL9KZoSfbwvwAAAP//LDCmqMoQAAA= }, "/js/jquery.min.js": { + name: "jquery.min.js", local: "web/js/jquery.min.js", size: 97163, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/9z9+5PbNrIojv9+/4oR14cBLEgjOcneE8oYlmM7m2Tz2jh7sjkUvQWSIEWJIjUUNY8M uX/7t9AASJCi7Ow991t161Muj0jiDTQa3Y1+XD+fXG3/duLl49Xdcr58Mf/sqr5CIdYfvypOecSqtMiv @@ -1490,9 +1546,10 @@ jKDI8z9pZprYN724RUAqBAH+zsRdbq9DgtpgwpIeJpCdiYamWevj7eYvOidr9PWrpYcU+HmzLuoq2/Mx }, "/js/lodash.min.js": { + name: "lodash.min.js", local: "web/js/lodash.min.js", size: 72772, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/+y9+3fTOrMA+nv/iiaHz1siSpu0PJ2qvkApsKGUzXODky/LTeRGkMpBkfugzv3b79LI suXEKbC/c84v96yu1UiyNHqNRqPRzGj79u2Nzdub/8+Uj5iYMx1+lYyj+WRzCj9bo+RsO/+4mW1+EGMm @@ -1903,10 +1960,121 @@ nCT/DQAA//9VhbzqRBwBAA== `, }, + "/js/prism.js": { + name: "prism.js", + local: "web/js/prism.js", + size: 12851, + modtime: 1655701668, + compressed: ` +H4sIAAAAAAAC/6R7a3fiOLbo9/4VwdPXZRWKofrcddcsKBXjgJJ4yti0bZJKA11HAYW4Y2zaFpXKRNzf +ftaWbB5JavqsNR+CZT32e2/tLSut9yejIilX/4xOPti//Jfd/uleiHXZabXW0P1Hac/zVWuRP2Zpzhb2 +vVilfxP3fMVLoiaYKcuWG7bkJSn/TM11ulkmWUmyvFixNPkXP328TwQv12zOT963fvrGipOvJU/viLHJ +FvwuyfjCaBDxtOb53cljki3yx55+dN6acZ0XD7y4SPNblkbzfM1NE6CdJFkpWDZ/c0oPZnSet1gxSu42 +2VwkeWZt0DOQMyet6S1wYfU6wAjqnVqT6ePprImmt60EZ6SNh+R5xbINSzsbW0Exzaph6368SEp2m3KN +fMjLki35JcsWKS9er/k3k/FGJGnnmWfzfME7Na0n3MrQc8HFpshOsiNuexl/PLm2MhskhLmV2fM8EzwT +CGc2SxNWoo5TFOzJTkr1tDLUy+wVW1scdTK74OuUzbnVMltLbJhste4aaN/7UfWm4qhzumm3WRtGTgy0 +xYB5R6rFd4QGt3/wubDXRS5ymGOLPBJFki3tOUtTiyO7TJM5t/6OTz+gLc5v/3AXb8Hh9tevyULKCp42 +ilGRr3khniyODRg28PM3lm54p9nMtgjrNVs8T/PsQI6FxbHQemc465aPiZjfW4IIKZ+3eGiD9JUkLY7Q +85yV3NBYjU5yZ2WkmqFItTjCYpLNUEUmtLt3eWEB9OQkyU4Yed6qKYRhjrh9z8rgMdtRniDTtNgkmZHC +4pNkhgVC3QoY6yrkSmNGp9b8m/h78NOxGJnMdsjsu7ygbH5v7cWJM/TMYFjLYIswQ90Fv2ObVNQI+HaL +l1x4lU8fKQM463LTbMxtwUthcXuesrL02YqjLuKE22tW8EzQlK94JmpGeO9wpr1iIPA5knKCjSzPuDFD +kw8zW+Re/siLPiu5hTp6ZIvnmwJARvMiWYs9Neg5ubMOwgOpw8Min28Aea2SbJOmXZh7BMhIslcz63f7 +aGZXFE/P4r7IH0/AzWhR5MV2rljg2ooyYrWYOJn8bk2LaTZ7P7Us+z3q2M2O3Zyin1uJzb/zucXtUrD5 +A7Ct+AWiMg2hIDvcS15Lrzx7itkSJGYZpSYa7WxLgG0VKLmziomY2WUxJySrGYGu7QH72y1OSmcukm+H +6sQZLrRKFUBiZPmp0cy6vFs5B6mU5iWlAGKZCissyUorQxWuRvvFiNiNfOi+MoiKqEaj2G7xbtfoPPPv +gmeLzrGhasFU1q582Brau0UTPnshjQwB3ySbiFlteMUWJ1nJC3HG7/LigPkCA/t1FCDa+w+go0kxwwl5 +3u5QpMqZkWL2hQ+nSBljSghH9fRcU5S9nJuDvyeTHOjMZ6ibvQYmpZVM0hlhk3SGtgCsJGJS7JiCNknw +AbX24Dw6lA0+FmRGCClNkzdIYZqWuE9AeCQB90+2eHAeHe0xuMACM/TMCIOA2FWR7CjoHMskA5lkb8mk +0DE+wynOJukMCylTpODlRHWU5DDa5qhbB9oGIaWUbJJYOZr1qgh41CmlVbVIo425leMCp5gh1HndDy4A +Q9vtFlfJCaQC98nyPk2W98JJ0xemN7QPB8fZghdW7aFgOeh4tZrwyrO0Vz2DDG7Z/KFT4MpJeNHhuOQp +n4u86LyDXX6iHO09MWodnhozfPJW7wlMxyevFr1eUE9+t+0O7fs8fyjtYpNZxq3yhdMdAyxNDSwQFjav +Ag/RycJ+01Z7tM3W6/TJEvaODfvPDS+eoooTJ00tYdd8IYTfwsrS9LRGsycB8O+siuGEtLuM7OmZJM3m +rIsOtFLFE4vhRpsQkmFh13I+1E017QeqqYzvYLOD7ZSRwygjZt2DrYscbmN1KjTHxlFiVDarrKhpnOwV +1xSVJ73cIxPTNNYFNwghiZ3lC66AH+2FEDIOiEj+AyK0/6XkuZJth+/icEfgZcFWK1Z0GFapJ7cF/y76 +OpfcdndRIoedL91rgy/6+YIT/qbGdQA2cIpwWmvUTrKMF5fx0COvwBwDYXeCF4eGkr6wq3m+WqdccDVS +mGYVdHao0Da5s96iq2RZIp5O5/d8/qAWN1IbuK430R9j+ZYnC+stVKj7A1zH9Kd2JWcVOaEk0FWAtsuS +qGxe9VhD+y5Jeabyq9LOs5UuFMhhUpZb3F4wwdAWl/Y6L0VVTVj/jALfLlW2ndw9Wc87Rac7A9d61ozj +ZLXii4QJ3k/zknca7S1CW56W/CS3DnzPqqbv2MB7eAh1dwuUc+kiplqCDn3zh/FSWd7OFLO9fRbb7lu6 +qWQs8geeJf/iVSxTr7DB1P0qcC04Fjvpv2VpR1CuD6R3zE8NHzDteN/ievWbmQzUWTqROky7dB4HSQuB +9KW74GBnJ3ryVqcnYBBJzbtrMczse84WmCP8eufGCU738FUeIsDQxFupiIAk5LnOMfJZt479daFYol7Z +mZT7omZD2t3Nx9JOebYU991mc6OTH9NM7TnblJyQvGlgo7mpHKmrC+xyspnhJZnbSVYmC47vSKMxt9M8 +f7jl90m2wPeqY1lwvnjCCzLXhSuI614VHGsmBC8ye6mqek30N7LvrwtLC1UFRmuSrMrN0+z9zy00ac+6 +u6kk5Ev6fW3t15b5pphz/K1pLA20rVld76FLOccrwuyMfxf4iSTdVYOQwhYsSU2zAdw/fSKpXXA2v0fd +pyZZ2aoSrcSEV2Sl1mqyH+phZQzVnE9Z1ajlltxZDevhqNBHev0t/k4+KMko2TesW/Iva42fcIbvEEK3 +BWcPSuyP5NZOsgX/jp261bydtGc1WSPypBQ7eklwd/SRPHZRTTZ+PSG5s0anL9l8IiNcdR3TDSlDkm34 +zowisupGBzK0Rh8dKQ3tb/tiLtLAUDcikRbg92YTqImOqfl+eoofSFYdJjzhEcIVv6fkScewA0G18YMS +1I6qY1lRAjLCAXmo4LXxI8Le7vWxSWtVYZ88NR9qKlLT9D9VVmCaVtUivt50/yAre13wb93ANK0/iGsV ++A8cIPzUJMEO3p+q97te0ddbgZXjZe8gkFG8RB2KF5iqDWdVgeoj7JkmtFfYQ/jDx+/aWr5CDropeafy +S6yo6vjbbh0zNF34CX9FODXNr5ruNzipRiCT3m63FscMZ3UwaiN8uCvpwngywwXhaoJSn9J/t2gQwpXi +uyiz15vy3ioqReOCFHpmXb9uLdjZVKDuPLM0hdSdLX5ULOqAztK0W0CRAz9Qb2NoaEyQvBeb7K/WT7gq +0NU2XylnF7MxI+2uIMWEqbxUAMwtjkE/neuDPOlab21QZ0LVpcocwrFqVyd0kLrCqwp2pNAvGh9pS0tI +CXmd7tjuAP9rD5jbKSuFC4ZLdKyFul0dOBTKPJhpCtNkkw9VoE8ItGuTZVVUIAlmkzYUne1ZZeYJqs8Q +2B5zYmkonFRHbaq4AvPRLVCdam1x9noKPxjvcqXnWgBgIrVswDLq/loWexLc44Qh0xGK1diKihqNSuxy +Bj2PMCyUtasTMg282cQHHP75+mRkh6LdZR8L0xR7+2XNJhJEaJO1KhwCaRTZDsUpYZCHVoe/ZHiYWuyS +uROwxUIfbL0MhLu0VO0Zx3s0R7UsDGN35PbG6R96Fk2icWwRrpOLZ3V0y/XZcWWVnczitYXiAmHBlh2j +XLPMwKr04GVnYqh4ZGC9coaZEEVyuxFcFdcHSRuGmqfazE3zBe0J6r0sNsFLq1qT2RU6nKDO7kX7cfKy +wnws2NrArK5wDKN7lAQxe08hSpvEODGaefMdMd41rcOxST7TTrcrpgx1/v3nJhddAzXfGe8qIRsfjSaz +BVs2350o0gDWnso/8iSzVAn2znjXTJvGJ5heSbVpfGzVq41PxhY3NvbL88iNzRYL+o1nwktKwTNemKY1 +/HdfDqR8vcYyqqLBeCM+qxJhzYqS10UEhkR1Vx6A7ausmZHMPq4PupujYuOwQBCHh1OTYoYLhDCDQmcO +Ky20xY0PoL6uDiX1Gd/hsauFugcn9uh5WH1lkfL4cMZSBR5XkqlrJcLtsphjddju1Iq1DODvVEMxkFqg +X0ijjRBu1K+1M+2OZAvOFk+RYIJ3jTRnC+2YBGJzkgleMHWyqrpMk5smtxf8jhe9HYDXKhkEw6qs9nK2 +4AsDF6ijv3nZBf9zw0vhZMmKAfvnBVvx3r8btPaLSy7iZMXzjbAK/OH/7UL4cGupb26o+9YntVW+2KTc +NPXT5t/XeSFK07SOO4iKXgi/BUKn5aZp6UYV6PSC7k/6m9f+wLL8M4VKb6WOIJ6rJLvTsn6Xk9+n0xmy +ep1pa/p+Mi2n0ex9b/p+2pJWr3N6KqetaUv+DdnvUQvvqwcoVvE3ViTgGZ3JHuI/rInx7r81wKkGJ61e +Y/oBaUTN6YcW1kWHgtH6x2T6aP88a7ZmWMfgl+T9Q9FnyHevYP6iYcrpL9Nf0PvpLweAX5Jam3anNb21 +eh3n6kL2g7Efy3M3jGJ5HoRDJ5aeE8XS6zsRlR715dD5IofuQA5dXw6DgfSDaxkGY38go/FQjmEesnpk +Wr6fWqiV4Af+9JgXixpHP3YDXzqDgXTOYxpKx7sIQje+HErH86TjqT7f8W5+o9Lxb6QzGnk30omkE/Wl +M44vg9D9zdFAxnHw1fX7IR1SP5ZnTv/zeCTPBmfyjF64vjyj4Wfq0RvocC9cmOL6Tngjz9xYnnnBmTwL +Ak/9UMeXZyF1PsuzMLiOqDyLQ0rl2dj7LM9uZB9o6ztR3xnQQU8qWfQvHdeH31CzRUMZ0Rj1ZP+S9j9b +vc4ocH317gUw3RtHMQ3pQPYDx6NRn8p+4HlOrJ7joR/1ZD8YKk7g6cZWrxPTAVLdo7Ga5/u0D8N+5Eax +nulHcei4uhk7rh/BMufMo2qhH7v+WK28omEs+yFVCMMgimR/HIbUBzRfB9D7NXaH1W8UO8OR/DqOaAhg +xmEUhLJ/0/eoHDixY/U6Z05Eox7qwTsFlO6QqrcbOTjr9+WAOp4X9AHugMJr3x06Hjw9J4S+c2fsxfB0 +fRrKAfWcGzqAJ1VLPHfoxjSMenJA/Rs5oFFf/YTuGQzHNBy6vhvFbl8O3Aj4hWffCQfw/Aw/sev3410j +DK5VO3TPxjFgCuQgGKt1YTCSg/FweAO/I6vXOXeV+AbjkecqFqgXAZPuOepJ6its1Fd6HUjqw9+F60Pf +eChpGHpXHjyCMJI06jsjsBn6pU9HsaRfaN/qdcYxIKBf3CiO4AEDIw8Min6JqT+gA3lO4/6lPHepN4gk +UAQ/3rnTj4OwdlD3C0z0Agec1fOCa9e/ALe1ep0T6vQvT8LgGvWgp0/hl7oXvjwPKY3pl/jAUM7DYCjP +x54nz8e+9tALGgxpHN5YvQ7YKVW9qCcvvODM8eRFEAfyInT8WF6EwXgkLx1/4NFQXjrRpbx0roCSy8Ab +eEH/s7wMxqF0B9SP3RggfnX9SNlj4KGedM+le+EHIZXucBSEsXT9Af0iXV9x7fpgIK7vB4Mz6frBGMbV +arB614/phRoHawHvUK0rx4NGIF3/KvgM41Hg6agBZgU6/Wfg+vIzvYl68rPredJz/Iuxc0GreEedK4h3 +59C8op5UBik916d+oB6R/o1D4NQLnIEEe/ekYtgL/AvwEggyIGskvSAYyaET9y+tXke59ZAO3PGwngXM +6JlDGl5QCLEDj0Lv0PXB+4fBQP245y6NdONGDgM/vpTDsRe7Vq9zQJAKPnIUeDcXgY+kr3h3PGiMQ3hC +4JI+/RJLP5B+4O/jkz/2PPdc+uMhDd2+DM7Pe/AT0TjqycCXwYj6Vq8DgSAKxmBZv45peCNV4ISAF4xi +d+j+RlUjgLmO593AQDigoQzGYHo0lJWbBVc0lCMnjF3H00+lpxEN+xDiRu5VEMuR5/jHTMlRSPt0oJgN +ad+NXN03gvgyCumVHIXuEML9KFTLQvfK9egFjeQoDPpKC+MQCBiNzzy3L0djkPuvY7f/WYaOGykPliF1 +BlEPHp4MaT/wz92LcUhlSM9pSP0+jWRIPQo7Qkh9ZwiPEXWAxcq5QjrynL7qV+EE6Azpr2NXQYncC1+B +juJAd8Sh249lSONxCLKLpOtfKDBgyTJ0Ly5jGQaeB/sd7LwxhJ4wuFaeCrv3xdgd9ANPRrBK7WPh2KMy +cq7owbYU9S/p0JER8DSQEQUXlxENXceDUPdbRX1Eo0grsd4OIgrM1S+XIO7oMriW0eU4HgTXvozc4Qjw +DR3PA8lHvjOKLoNYRsGQyihQUop+9WQUOyHA0gxGsROriB6p5hgeoTuiAxndRDEdKvxShayoVz1HINiY +qqgdhKDsOqTpHcKBUF8Fu8j9TQ1cUl/CdgVdsM9Bn+vfvHbEOBj1ZBw6Sgs6cVFbXhy6FxBz4nDsq+0h +juivY8eT8c0ISBv7Z5AP0YEc+3orj1Vbb3bQAhMY++6vYyrHvgoYY1/b+dgHg4BJo3pnBVp6chxBbBpH +6i+U4wgM/8rxxoDxSiUhVXajPBt+dFJyA9KVVy69lteOG58Hobx2Qt/1LyJ5DbK4vqQhldeXEGyv3Rji +0wnY13gkXYj410H4WV6H7iE1N9QJ9aW02zxPOatTyDgcU3nueBFVYaS6t7ZZ3fICZrS/T6YLdno3a05v +5fR2umhC9mpPF+9RT07PoNVUS/I1L5jIi05rctp8P22R//P7/59J0+zJqZzKnmyQnvxo9TrkU09+lJ9Q +T36afCKzntRJpj+QZzS+ptSXA/dKur50PfczlS4E7c9U+kEslW9f0C8jGaq+CHQWnaj2l6Dibg0Z8obp +JHnSnUxnFvpvbM9a2+5PjYNbQPpIq7oWxsoyWWZSHp/tvbgl8uqugoBikNd3R3bXr/jBSR5Xpyf3SWlX +t6VKkljPW8zR/uCorG9I6dK6jQVpd8VHvv9UIhC35/es6OcL7ghLIEKMqTAO+9pAS9Yk/7Unoz6lyrZ8 +f0pCnksuBhUtR3e0XlJ59A4E493FzM6/k9OrpRmqj0nF/mzk1Jo+otbyxY2T3ZGbyMfrdf0Ze4u6xg63 +0VDlsnHABnQVpglKME11TaWYgWZI1dafW+EFc6xUtStv+Ran/E7ERbJ6+/bgjuDfp2WzhQ0DbXGRLO// +N0umZfPnaolgt2WcR2s252XnbZ5JW2ZS/l98CEC0ljjjjyf6zKvZzND+dAhtsboaW8Z5zG7/91Crj1jG +ybPRzJrG1sDG0kAYDAp446v8G48LlqRQw/4Vg+97P7eWq0osaqmbJSJhqZdk/JzzxV9JFaJJ0Ztmclqg +1jGcxfENjProqb4H2Pp98vs0mmbTYvYeqtcItZarnfVnYA+7D1Y9K7PLvBDWm0LaHbbWX9K2CB8ufi27 +340mTMDGcgWiM1CHow7f4uQl0UdY9ly/QfnbegalNI2fTZCL+jwHcn2p64zoyyy9v7c7St1/b+9vuBFu +l+s0EZYxzQyEC9LuFh/FPraoywQNq7TEpJihjwS8dXcRVd0fq9a3rGnZRK0lwjlpV2fayR4O0/pJSWkl +EzZD3eyjlTdJqu+tsRkB/E1o4ZykaKtvo1VsGjuHFDXjmYG2W/yfHDrxtw+cqvvUlj5aqu512X4dX653 +d87VRzRuPRvaIE9F5RRGp9HGdafWuO6CSHIqimRVzYAwsXvfIn2HvP5StFj84EbJa4v/S0q7yZ1lNbhd +ciGSbFlK2figPjXUHRNjf5X+tA6lao80Zsg0NQJ1llrf97R4fTsF/3gtbrQRmM9urmnumtVtJT9fcCkb +XN/jeK5N8vWkroIDs9R3p/pqk/jB1aa9kcK2Mr9P0gVAKXFCDAPn8MNI4wNOSbubfiz2dprW1xaKSTrr +lmRPTI+RRrtj/E3w7wJQlzvUpmmxXt6seq7U56Lk6BXD1gYG0QdKrBLh09NUnzDveFVEFrwOMrXYa71+ +5nw9ZMXDZl3/b0HS5K8vPjXz7hu9JLN3O6Q1x3vNI6yFeiDzw7tZ6gt3NQPQQaOZ12sOgequQ8jbo9V/ +MRehrYW6P/1PAAAA//9Y78ahMzIAAA== +`, + }, + "/js/vue.min.js": { + name: "vue.min.js", local: "web/js/vue.min.js", size: 78479, - modtime: 1507905576, + modtime: 1655701668, compressed: ` H4sIAAAAAAAC/7y9a3PbuLYo+P38CovlYYAtWJHT+9w7TQatSpx0J915deKkH2ptFU1BNmIZUIOQHzF1 f/vUWgBIkFKy96mZmi8SiReBBWBhvfHwH4P/OvjHwaeNGH2uDq4fjb4bjSGBlPTg0fj4n0ePxsf/++D5 @@ -2391,22 +2559,78 @@ aCSrmoLxkaWR2HzdQ0eaxhIcvcc3PC0z/jO/zUXBVcIVNKM6CYf43hF0lFT48cqoOBxEA191WsIROTdl }, "/": { + name: "/", + local: `web`, + isDir: true, + }, + + "/.idea": { + name: ".idea", + local: `web/.idea`, + isDir: true, + }, + + "/.idea/copyright": { + name: "copyright", + local: `web/.idea/copyright`, + isDir: true, + }, + + "/.idea/inspectionProfiles": { + name: "inspectionProfiles", + local: `web/.idea/inspectionProfiles`, isDir: true, - local: "web", }, "/css": { + name: "css", + local: `web/css`, isDir: true, - local: "web/css", }, "/images": { + name: "images", + local: `web/images`, isDir: true, - local: "web/images", }, "/js": { + name: "js", + local: `web/js`, isDir: true, - local: "web/js", + }, +} + +var _escDirs = map[string][]os.FileInfo{ + + "web": { + _escData["/index.html"], + }, + + "web/.idea": {}, + + "web/.idea/copyright": {}, + + "web/.idea/inspectionProfiles": {}, + + "web/css": { + _escData["/css/bootstrap-theme.min.css"], + _escData["/css/bootstrap.min.css"], + _escData["/css/prism.css"], + _escData["/css/style.css"], + }, + + "web/images": { + _escData["/images/favicon.png"], + }, + + "web/js": { + _escData["/js/app.js"], + _escData["/js/bootstrap.min.js"], + _escData["/js/clipboard.min.js"], + _escData["/js/jquery.min.js"], + _escData["/js/lodash.min.js"], + _escData["/js/prism.js"], + _escData["/js/vue.min.js"], }, } diff --git a/fsm.go b/fsm.go new file mode 100644 index 0000000..7b28ffb --- /dev/null +++ b/fsm.go @@ -0,0 +1,207 @@ +package main + +import ( + "context" + "fmt" + "github.com/qmuntal/stateless" + "lottip/chat" + "lottip/protocol" + "net" + "reflect" + "time" +) + +type MySQLProtocolFSM struct { + *stateless.StateMachine + connectionInfo *ConnectionInfo + clientConn net.Conn + serverConn net.Conn +} + +const ( + StateIdle = "Idle" + StateAuthRequested = "AuthRequested" + StateAuthSent = "AuthSent" + StateAuthorized = "stateAuthorized" + StateUnauthorized = "stateUnauthorized" +) + +const ( + PacketReceived = "PacketReceived" + MsgServerHello = "ServerHello" + MsgLogin = "Login" + MsgOK = "OK" + MsgERROR = "ERROR" + MsgServerToClient = "MsgServerToClient" +) + +func CreateStateMachine(ci *ConnectionInfo, clientConn net.Conn, serverConn net.Conn, cmdChan chan chat.Cmd, resultChan chan chat.CmdResult, stateChan chan chat.ConnState) *MySQLProtocolFSM { + fsm := stateless.NewStateMachine(StateIdle) + + fsm.SetTriggerParameters(PacketReceived, reflect.TypeOf([]byte{})) + fsm.OnUnhandledTrigger(func(ctx context.Context, state stateless.State, trigger stateless.Trigger, unmetGuards []string) error { + LogOther(ci, "fsm - Unhandled event", "%d in state %s", trigger, state) + return nil + }) + + fsm.Configure(StateIdle). + Permit(MsgServerHello, StateAuthRequested, func(ctx context.Context, args ...interface{}) bool { + // Server's initial response asking for loging info -- passthrough after changing compression flag + packet := args[0].([]byte) + index := 4 + if packet[index] == 0x0A { + // Valid protocol, skip server version + for ; packet[index] != 0 && index < len(packet); index++ { + } + index++ + // Skip thread-id + index += 4 + // Skip salt + for ; packet[index] != 0 && index < len(packet); index++ { + } + index++ + + packet[index] = packet[index] & 0xDF + LogResponse(ci, packet, "Handshake", "Server Handshake/Challenge response (forcing uncompressed protocol)") + + //LogOther(ci, "fsm - Writing to client", "% x", packet) + clientConn.Write(packet) + return true + } else { + LogOther(ci, "INVALID PROTOCOL", "%x is not a supported protocol version", packet[4]) + + clientConn.Close() + serverConn.Close() + return false + } + }) + + fsm.Configure(StateAuthRequested). + Permit(MsgLogin, StateAuthSent, func(ctx context.Context, args ...interface{}) bool { + // Client's Auth Info + packet := args[0].([]byte) + + start := 3 + 1 + 2 + 2 + 4 + 1 + for ; packet[start] == 0; start++ { + } + stop := start + 1 + for ; packet[stop] != 0; stop++ { + } + ci.User = string(packet[start:stop]) + LogRequest(ci, packet, "Login", "Authorizing as user: '%s' (forcing uncompressed protocol)", ci.User) + + // Disable compression + packet[4] = packet[4] & 0xDF + + //LogOther(ci, "fsm - Writing to server", "% x", packet) + serverConn.Write(packet) + return true + }) + + fsm.Configure(StateAuthSent). + Permit(MsgOK, StateAuthorized, func(ctx context.Context, args ...interface{}) bool { + packet := args[0].([]byte) + LogResponse(ci, packet, "Authorized", "Client auth successful") + //LogOther(ci, "fsm - Writing to client", "% x", packet) + clientConn.Write(packet) + return true + }). + Permit(MsgERROR, StateUnauthorized, func(ctx context.Context, args ...interface{}) bool { + // Server's initial response asking for loging info -- passthrough after changing compression flag + packet := args[0].([]byte) + LogResponse(ci, packet, "Unauthorized", "Client auth failed") + //LogOther(ci, "fsm - Writing to client", "% x", packet) + clientConn.Write(packet) + return true + }) + + fsm.Configure(StateAuthorized). + InternalTransition(protocol.ComChangeUser, func(ctx context.Context, args ...interface{}) error { + // Do not allow this! + packet := args[0].([]byte) + start := 3 + 1 + 2 + 2 + 4 + 1 + for ; packet[start] == 0; start++ { + } + stop := start + 1 + for ; packet[stop] != 0; stop++ { + } + LogRequest(ci, packet, "ChangeUser", "Will reject request to change user to %s", string(packet[start:stop])) + return nil + }). + InternalTransition(protocol.ComPing, func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + LogRequest(ci, packet, "Ping") + serverConn.Write(packet) + return nil + }). + InternalTransition(protocol.ComCreateDB, logAndSendQueryToServer(ci, serverConn, "CreateDB", cmdChan)). + InternalTransition(protocol.ComDropDB, logAndSendQueryToServer(ci, serverConn, "DropDB", cmdChan)). + InternalTransition(protocol.ComShutdown, logAndDrop(ci, "Shutdown", cmdChan)). + InternalTransition(protocol.ComProcessKill, logAndSendQueryToServer(ci, serverConn, "ProcessKill", cmdChan)). + InternalTransition(protocol.ComQuery, logAndSendQueryToServer(ci, serverConn, "Query", cmdChan)). + InternalTransition(protocol.ComStmtPrepare, logAndSendQueryToServer(ci, serverConn, "StmtPrepare", cmdChan)). + InternalTransition(protocol.ComStmtExecute, logAndSendQueryToServer(ci, serverConn, "StmtExecute", cmdChan)). + InternalTransition(protocol.ComStmtClose, logAndSendQueryToServer(ci, serverConn, "StmtClose", cmdChan)). + InternalTransition(protocol.ComStmtSendLongData, logAndSendQueryToServer(ci, serverConn, "StmtSendLongData", cmdChan)). + InternalTransition(protocol.ComStmtReset, logAndSendQueryToServer(ci, serverConn, "StmtReset", cmdChan)). + InternalTransition(protocol.ComQuit, logAndSendQueryToServer(ci, serverConn, "Quit", cmdChan)). + InternalTransition(protocol.ComBinlogDump, logAndDrop(ci, "BinlogDump", cmdChan)). + InternalTransition(protocol.ComBinlogDump, logAndDrop(ci, "BinlogDump", cmdChan)). + InternalTransition(protocol.ComTableDump, logAndDrop(ci, "TableDump", cmdChan)). + InternalTransition(protocol.ComConnectOut, logAndDrop(ci, "ConnectOut", cmdChan)). + InternalTransition(MsgOK, func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + LogResponse(ci, packet, "OK") + duration := fmt.Sprintf("%.3f", time.Since(*ci.timer).Seconds()) + resultChan <- chat.CmdResult{ci.ConnId, ci.QueryId, protocol.ResponseOk, "", duration} + clientConn.Write(packet) + return nil + }). + InternalTransition(MsgERROR, func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + LogResponse(ci, packet, "ERROR") + duration := fmt.Sprintf("%.3f", time.Since(*ci.timer).Seconds()) + resultChan <- chat.CmdResult{ci.ConnId, ci.QueryId, protocol.ResponseErr, "", duration} + clientConn.Write(packet) + return nil + }). + InternalTransition(MsgServerToClient, func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + LogResponsePacket(ci, packet) + clientConn.Write(packet) + return nil + }) + + return &MySQLProtocolFSM{fsm, ci, clientConn, serverConn} +} + +func logAndSendQueryToServer(ci *ConnectionInfo, serverConn net.Conn, command string, cmdChan chan chat.Cmd) func(ctx context.Context, args ...interface{}) error { + return func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + query := string(packet[5:]) + LogRequest(ci, packet, command, query) + + ci.QueryId++ + *ci.timer = time.Now() + + cmdChan <- chat.Cmd{ci.ConnId, ci.QueryId, "", query, nil, false} + //LogOther(ci, "fsm - Writing to server", "% x", packet) + serverConn.Write(packet) + return nil + } +} + +func logAndDrop(ci *ConnectionInfo, command string, cmdChan chan chat.Cmd) func(ctx context.Context, args ...interface{}) error { + return func(ctx context.Context, args ...interface{}) error { + packet := args[0].([]byte) + query := string(packet[5:]) + LogRequest(ci, packet, "BLOCKED:"+command, query) + cmdChan <- chat.Cmd{ci.ConnId, ci.QueryId, "", query, nil, false} + LogOther(ci, "fsm - Dropping packet", "% x", packet) + return nil + } +} + +func (fsm *MySQLProtocolFSM) ProcessPacket(packet []byte) { + fsm.Fire(MsgERROR) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6c50c11 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module lottip + +go 1.18 + +require ( + github.com/go-sql-driver/mysql v1.6.0 + github.com/gorilla/websocket v1.5.0 + github.com/olekukonko/tablewriter v0.0.5 +) + +require ( + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect +) + +require ( + github.com/kjk/dailyrotate v0.0.0-20220410204435-837f1ef47fc4 + github.com/pubnative/mysqlproto-go v0.0.0-20210816144457-71d8293daef4 + github.com/qmuntal/stateless v1.6.0 + github.com/rs/zerolog v1.27.0 + github.com/stretchr/testify v1.7.1 // indirect +) diff --git a/http.go b/http.go index 1cd794c..13ee964 100644 --- a/http.go +++ b/http.go @@ -2,13 +2,13 @@ package main import ( "fmt" - "log" + "github.com/rs/zerolog/log" + "lottip/chat" "net/http" "encoding/json" "github.com/gorilla/websocket" "github.com/olekukonko/tablewriter" - "github.com/orderbynull/lottip/chat" ) const ( @@ -23,7 +23,7 @@ func runHttpServer(hub *chat.Hub) { conn, err := upgr.Upgrade(w, r, nil) if err != nil { - log.Println(err) + log.Error().Err(err).Msg("Could not upgrade connection from " + r.RemoteAddr + " to websocket") return } @@ -46,7 +46,7 @@ func runHttpServer(hub *chat.Hub) { http.HandleFunc("/execute", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { - log.Println(err) + log.Error().Err(err).Msg("Could not parse query execution request from " + r.RemoteAddr) return } @@ -80,5 +80,8 @@ func runHttpServer(hub *chat.Hub) { http.Handle(webRoute, http.FileServer(FS(*useLocalUI))) - log.Fatal(http.ListenAndServe(*guiAddr, nil)) + err := http.ListenAndServe(*guiAddr, nil) + if err != nil { + log.Fatal().Err(err).Msg("Could not start HTTP server at " + *guiAddr) + } } diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..d502ada --- /dev/null +++ b/logger.go @@ -0,0 +1,88 @@ +package main + +import ( + "reflect" +) + +const hex = "01234567890ABCDEF" + +func LogInvalid(info *ConnectionInfo, entryType string, packet []byte) { + event := queryLogger.Error().Str("client", info.ClientAddress).Int("clientPort", info.ClientPort). + Str("server", info.ServerAddress).Int("serverPort", info.ServerPort). + Str("user", info.User).Str("type", entryType) + + if info.QueryId > 0 { + event.Int("queryId", info.QueryId) + } + + event.Int("packetType", int(GetPacketType(packet))) + + event.Bytes("packet", packet).Send() +} +func LogRequest(info *ConnectionInfo, packet []byte, entryType string, args ...interface{}) { + if *logRequests || *logAll { + sender := "client" + doLogging(&sender, info, entryType, args) + } + LogRequestPacket(info, packet) +} + +func LogResponse(info *ConnectionInfo, packet []byte, entryType string, args ...interface{}) { + if *logResponses || *logAll { + sender := "server" + doLogging(&sender, info, entryType, args) + } + LogResponsePacket(info, packet) +} + +func LogResponsePacket(info *ConnectionInfo, packet []byte) { + if *logPackets { + sender := "server" + args := make([]interface{}, 2) + args[0] = "% x" + args[1] = packet + doLogging(&sender, info, "Response Packet", args) + } +} + +func LogRequestPacket(info *ConnectionInfo, packet []byte) { + if *logPackets { + sender := "client" + args := make([]interface{}, 2) + args[0] = "% x" + args[1] = packet + doLogging(&sender, info, "Response Packet", args) + } +} + +func LogOther(info *ConnectionInfo, entryType string, args ...interface{}) { + if *logAll { + doLogging(nil, info, entryType, args) + } +} + +func doLogging(sender *string, info *ConnectionInfo, entryType string, args []interface{}) { + event := queryLogger.Info().Str("client", info.ClientAddress).Int("clientPort", info.ClientPort). + Str("server", info.ServerAddress).Int("serverPort", info.ServerPort). + Str("user", info.User).Str("type", entryType) + + if sender != nil { + event.Str("sender", *sender) + } + + if info.QueryId > 0 { + event.Int("queryId", info.QueryId) + } + + if len(args) > 0 { + if len(args) == 1 { + if reflect.TypeOf(args[0]).Kind() == reflect.String { + event.Msg(args[0].(string)) + } + } else { + event.Msgf(args[0].(string), args[1:]...) + } + } else { + event.Send() + } +} diff --git a/main.go b/main.go index 8509b1d..947a836 100644 --- a/main.go +++ b/main.go @@ -3,29 +3,112 @@ package main import ( "flag" "fmt" + "github.com/kjk/dailyrotate" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "io" + "lottip/chat" + "os" + "strings" "time" - - "github.com/orderbynull/lottip/chat" ) var ( - proxyAddr = flag.String("proxy", "127.0.0.1:4041", "Proxy :") - mysqlAddr = flag.String("mysql", "127.0.0.1:3306", "MySQL :") - guiAddr = flag.String("gui", "127.0.0.1:9999", "Web UI :") - useLocalUI = flag.Bool("use-local", false, "Use local UI instead of embed") - mysqlDsn = flag.String("mysql-dsn", "", "MySQL DSN for query execution capabilities") + proxyAddr = flag.String("proxy", "127.0.0.1:4041", "Proxy :") + logRequests = flag.Bool("log-requests", false, "Enable logging of requests") + logResponses = flag.Bool("log-responses", false, "Enable logging of responses") + logPackets = flag.Bool("log-packets", false, "Enable logging of packets") + logAll = flag.Bool("log-all", true, "Enable logging of requests, responses, and other events") + logToConsole = flag.Bool("enable-console-logging", false, "Enable logging to console") + logToFile = flag.Bool("enable-file-logging", true, "Enable logging to console") + logDirectory = flag.String("log-directory", "./logs", "Set the query log directory") + queryLogFile = flag.String("query-log-file", "queries.log", "Set the query log file name") + logFile = flag.String("log-file", "logfile.log", "Set the query log file name") + mysqlAddr = flag.String("mysql", "127.0.0.1:3306", "MySQL :") + guiAddr = flag.String("gui-addr", "127.0.0.1:9999", "Web UI :") + guiEnabled = flag.Bool("gui-enabled", false, "Enable the web-gui server") + useLocalUI = flag.Bool("use-local", false, "Use local UI instead of embed") + mysqlDsn = flag.String("mysql-dsn", "", "MySQL DSN for query execution capabilities") + + queryLogger zerolog.Logger + logFileStartTime time.Time ) func appReadyInfo(appReadyChan chan bool) { <-appReadyChan time.Sleep(1 * time.Second) - fmt.Printf("Forwarding queries from `%s` to `%s` \n", *proxyAddr, *mysqlAddr) - fmt.Printf("Web gui available at `http://%s` \n", *guiAddr) + log.Info().Msgf("Forwarding queries from `%s` to `%s`", *proxyAddr, *mysqlAddr) + log.Info().Msgf("Web gui available at `http://%s`", *guiAddr) +} + +func newRollingFile(directory string, filename string) io.Writer { + if err := os.MkdirAll(directory, 0744); err != nil { + log.Error().Err(err).Str("path", directory).Msg("can't create log directory") + return nil + } + + logFileWriter, err := dailyrotate.NewFileWithPathGenerator(func(time time.Time) string { + logFileStartTime = time.Local() + return directory + "/" + filename + }, func(filename string, didRotate bool) { + if didRotate { + // Then rename the file + timeFormatString := ".2006-01-02" + rolledName := directory + "/" + filename + logFileStartTime.Format(timeFormatString) + os.Rename(filename, rolledName) + } + }) + + if err != nil { + log.Error().Err(err).Str("file", directory+"/"+filename).Msg("Can't create log file") + return nil + } + + return logFileWriter } func main() { flag.Parse() + if *logAll || *logRequests || *logResponses || *logPackets { + var queryWriters []io.Writer + if *logToConsole { + output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02 15:04:05.000"} + output.FormatLevel = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("%-6s", i)) + } + queryWriters = append(queryWriters, output) + } + + if *logToFile { + queryWriters = append(queryWriters, newRollingFile(*logDirectory, *queryLogFile)) + } + + queryLogMultiWriter := io.MultiWriter(queryWriters...) + + queryLogger = zerolog.New(queryLogMultiWriter).With().Timestamp().Logger() + } + + zerolog.TimeFieldFormat = "2006-01-02 15:04:05.000" + zerolog.TimestampFieldName = "logTimestamp" + + var writers []io.Writer + if *logToConsole { + output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02 15:04:05.000"} + output.FormatLevel = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("%-6s", i)) + } + writers = append(writers, output) + } + + if *logToFile { + writers = append(writers, newRollingFile(*logDirectory, *logFile)) + } + + logMultiWriter := io.MultiWriter(writers...) + + log.Logger = zerolog.New(logMultiWriter).With().Timestamp().Logger() + cmdChan := make(chan chat.Cmd) cmdResultChan := make(chan chat.CmdResult) connStateChan := make(chan chat.ConnState) @@ -34,7 +117,9 @@ func main() { hub := chat.NewHub(cmdChan, cmdResultChan, connStateChan) go hub.Run() - go runHttpServer(hub) + if guiEnabled != nil && *guiEnabled { + go runHttpServer(hub) + } go appReadyInfo(appReadyChan) p := MySQLProxyServer{cmdChan, cmdResultChan, connStateChan, appReadyChan, *mysqlAddr, *proxyAddr} diff --git a/packetizer.go b/packetizer.go new file mode 100644 index 0000000..7639743 --- /dev/null +++ b/packetizer.go @@ -0,0 +1,60 @@ +package main + +func extractPacketsFromBuffer(ci *ConnectionInfo, packetFragment *[]byte, buffer []byte, processPacket func(packet []byte)) { + //LogOther(ci, "extractPacketsFromBuffer() Entry", "% x", buffer) + + var packet []byte + if len(*packetFragment) > 0 { + packet = append(*packetFragment, buffer...) + *packetFragment = []byte{} + } else { + packet = buffer + } + + //LogOther(ci, "extractPacketsFromBuffer() Combined", "% x", packet) + + offset := uint32(0) + bufferLen := uint32(len(packet)) + for { + if bufferLen == offset { + // Nothing else + break + } else if offset < bufferLen && bufferLen-offset >= 4 { + packetSize := uint32(packet[offset+0]) | uint32(packet[offset+1])<<8 | uint32(packet[offset+2])<<16 + if bufferLen >= offset+packetSize+4 { + temp := make([]byte, 3+1+packetSize) + copy(temp, packet[offset:offset+3+1+packetSize]) + + // Now process the packet based on the current state of the connection + //seqNum := int(temp[3]) + + //LogOther(ci, "extractPacketsFromBuffer() Packet", "% x", temp) + processPacket(temp) + + //fsm.Fire(protocol.PacketReceived, temp) + //if temp[4] == 0 { + // okPacket() + //} else if temp[4] == mysqlproto.ERR_PACKET { + // errCode := int32(temp[5]) | int32(temp[6])<<8 + // errorPacket(errCode, string(temp[7:])) + //} else if temp[4] == mysqlproto.EOF_PACKET { + // eofPacket(seqNum) + //} else { + // // Data packets + // dataPacket(temp) + //} + offset += packetSize + 4 + continue + } + } + + if bufferLen-offset > 0 { + *packetFragment = make([]byte, bufferLen-offset) + copy(*packetFragment, packet[offset:bufferLen]) + } else { + // We are done + *packetFragment = []byte{} + } + break + } +} diff --git a/protocol/const.go b/protocol/const.go index e090f2c..0ab3e58 100644 --- a/protocol/const.go +++ b/protocol/const.go @@ -29,28 +29,28 @@ const ( comInitDB ComQuery ComFieldList - comCreateDB - comDropDB + ComCreateDB + ComDropDB comRefresh - comShutdown + ComShutdown comStatistics comProcessInfo comConnect - comProcessKill + ComProcessKill comDebug - comPing + ComPing comTime comDelayedInsert - comChangeUser - comBinlogDump - comTableDump - comConnectOut - comRegisterSlave + ComChangeUser + ComBinlogDump + ComTableDump + ComConnectOut + ComRegisterSlave ComStmtPrepare ComStmtExecute - comStmtSendLongData + ComStmtSendLongData ComStmtClose - comStmtReset + ComStmtReset comSetOption comStmtFetch ) diff --git a/protocol/decoder.go b/protocol/decoder.go deleted file mode 100644 index ccf7a7b..0000000 --- a/protocol/decoder.go +++ /dev/null @@ -1,608 +0,0 @@ -package protocol - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "math" - "strconv" -) - -var errInvalidPacketLength = errors.New("protocol: Invalid packet length") -var errInvalidPacketType = errors.New("protocol: Invalid packet type") -var errFieldTypeNotImplementedYet = errors.New("protocol: Required field type not implemented yet") - -func GetPacketType(packet []byte) byte { - return packet[4] -} - -type ErrResponse struct { - Message string -} - -// DecodeOkResponse decodes ERR_Packet from server. -// Part of basic packet structure shown below. -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> PacketType (0xFF) -// if clientCapabilities & clientProtocol41 -// { -// string<1> SqlStateMarker (#) -// string<5> SqlState -// } -// string ErrorMessage -func DecodeErrResponse(packet []byte) (string, error) { - if err := checkPacketLength(8, packet); err != nil { - return "", err - } - - return string(packet[7:]), nil -} - -// OkResponse represents packet sent from the server to the client to signal successful completion of a command -// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html -type OkResponse struct { - PacketType byte - AffectedRows uint64 - LastInsertID uint64 -} - -// DecodeOkResponse decodes OK_Packet from server. -// Part of basic packet structure shown below. -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> PacketType (0x00 or 0xFE) -// int AffectedRows -// int LastInsertID -// ... more ... -func DecodeOkResponse(packet []byte) (*OkResponse, error) { - - // Min packet length = header(4 bytes) + PacketType(1 byte) - if err := checkPacketLength(5, packet); err != nil { - return nil, err - } - - r := bytes.NewReader(packet) - - // Skip packet header - if err := SkipPacketHeader(r); err != nil { - return nil, err - } - - // Skip packet type - if _, err := r.Seek(1, io.SeekCurrent); err != nil { - return nil, err - } - - affectedRows, _ := ReadLenEncodedInteger(r) - lastInsertID, _ := ReadLenEncodedInteger(r) - - return &OkResponse{packet[4], affectedRows, lastInsertID}, nil -} - -// HandshakeV10 represents sever's initial handshake packet -// See https://mariadb.com/kb/en/mariadb/1-connecting-connecting/#initial-handshake-packet -type HandshakeV10 struct { - ProtocolVersion byte - ServerVersion string - ConnectionID uint32 - ServerCapabilities uint32 - AuthPlugin string -} - -// DecodeHandshakeV10 decodes initial handshake request from server. -// Basic packet structure shown below. -// See http://imysql.com/mysql-internal-manual/connection-phase-packets.html#packet-Protocol::HandshakeV10 -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> ProtocolVersion -// string ServerVersion -// int<4> ConnectionID -// string<8> AuthPluginDataPart1 (authentication seed) -// string<1> Reserved (always 0x00) -// int<2> ServerCapabilities (1st part) -// int<1> ServerDefaultCollation -// int<2> StatusFlags -// int<2> ServerCapabilities (2nd part) -// if capabilities & clientPluginAuth -// { -// int<1> AuthPluginDataLength -// } -// else -// { -// int<1> 0x00 -// } -// string<10> Reserved (all 0x00) -// if capabilities & clientSecureConnection -// { -// string[$len] AuthPluginDataPart2 ($len=MAX(13, AuthPluginDataLength - 8)) -// } -// if capabilities & clientPluginAuth -// { -// string[NUL] AuthPluginName -// } -func DecodeHandshakeV10(packet []byte) (*HandshakeV10, error) { - // TODO: Add length check - - r := bytes.NewReader(packet) - - // Skip packet header - if err := SkipPacketHeader(r); err != nil { - return nil, err - } - - // Read ProtocolVersion - protoVersion, _ := r.ReadByte() - - // Read ServerVersion - serverVersion := ReadNullTerminatedString(r) - - // Read ConnectionID - connectionIDBuf := make([]byte, 4) - if _, err := r.Read(connectionIDBuf); err != nil { - return nil, err - } - connectionID := binary.LittleEndian.Uint32(connectionIDBuf) - - // Skip AuthPluginData and filler (always 0x00) - if _, err := r.Seek(9, io.SeekCurrent); err != nil { - return nil, err - } - - // Read ServerCapabilities - serverCapabilitiesLowerBuf := make([]byte, 2) - if _, err := r.Read(serverCapabilitiesLowerBuf); err != nil { - return nil, err - } - - // Skip ServerDefaultCollation and StatusFlags - if _, err := r.Seek(3, io.SeekCurrent); err != nil { - return nil, err - } - - // Read ExServerCapabilities - serverCapabilitiesHigherBuf := make([]byte, 2) - if _, err := r.Read(serverCapabilitiesHigherBuf); err != nil { - return nil, err - } - - // Compose ServerCapabilities from 2 bufs - var serverCapabilitiesBuf []byte - serverCapabilitiesBuf = append(serverCapabilitiesBuf, serverCapabilitiesLowerBuf...) - serverCapabilitiesBuf = append(serverCapabilitiesBuf, serverCapabilitiesHigherBuf...) - serverCapabilities := binary.LittleEndian.Uint32(serverCapabilitiesBuf) - - var authPluginDataLength byte - if serverCapabilities&clientPluginAuth != 0 { - var err error - authPluginDataLength, err = r.ReadByte() - if err != nil { - return nil, err - } - } - - // Skip reserved (all 0x00) - if _, err := r.Seek(10, io.SeekCurrent); err != nil { - return nil, err - } - - if serverCapabilities&clientSecureConnection != 0 { - skip := int64(math.Max(13, float64(authPluginDataLength)-8)) - // Skip reserved (all 0x00) - if _, err := r.Seek(skip, io.SeekCurrent); err != nil { - return nil, err - } - } - - var authPlugin string - if serverCapabilities&clientPluginAuth != 0 { - authPlugin = ReadNullTerminatedString(r) - } - - return &HandshakeV10{ - ProtocolVersion: protoVersion, - ServerVersion: serverVersion, - ConnectionID: connectionID, - ServerCapabilities: serverCapabilities, - AuthPlugin: authPlugin, - }, nil -} - -// HandshakeResponse41 represents handshake response packet sent by 4.1+ clients supporting clientProtocol41 capability, -// if the server announced it in its initial handshake packet. -// See http://imysql.com/mysql-internal-manual/connection-phase-packets.html#packet-Protocol::HandshakeResponse41 -type HandshakeResponse41 struct { - ClientCapabilities uint32 - ClientCharset byte -} - -// DecodeHandshakeResponse41 decodes handshake response packet send by client. -// TODO: Add packet struct comment -// TODO: Add packet length check -func DecodeHandshakeResponse41(packet []byte) (*HandshakeResponse41, error) { - r := bytes.NewReader(packet) - - // Skip packet header - if err := SkipPacketHeader(r); err != nil { - return nil, err - } - - // Read CapabilityFlags - clientCapabilitiesBuf := make([]byte, 4) - if _, err := r.Read(clientCapabilitiesBuf); err != nil { - return nil, err - } - clientCapabilities := binary.LittleEndian.Uint32(clientCapabilitiesBuf) - - // Skip MaxPacketSize - if _, err := r.Seek(4, io.SeekCurrent); err != nil { - return nil, err - } - - // Read Charset - charset, err := r.ReadByte() - if err != nil { - return nil, err - } - - return &HandshakeResponse41{clientCapabilities, charset}, nil -} - -// QueryRequest represents COM_QUERY or COM_STMT_PREPARE command sent by client to server. -type QueryRequest struct { - Query string // SQL query value -} - -// DecodeQueryRequest decodes COM_QUERY and COM_STMT_PREPARE requests from client. -// Basic packet structure shown below. -// See https://mariadb.com/kb/en/mariadb/com_query/ and https://mariadb.com/kb/en/mariadb/com_stmt_prepare/ -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> Command COM_QUERY (0x03) or COM_STMT_PREPARE (0x16) -// string SQLStatement -func DecodeQueryRequest(packet []byte) (*QueryRequest, error) { - - // Min packet length = header(4 bytes) + command(1 byte) + SQLStatement(at least 1 byte) - if len(packet) < 6 { - return nil, errInvalidPacketLength - } - - // Fifth byte is command - if packet[4] != ComQuery && packet[4] != ComStmtPrepare { - return nil, errInvalidPacketType - } - - return &QueryRequest{ReadEOFLengthString(packet[5:])}, nil -} - -// ComStmtPrepareOkResponse represents COM_STMT_PREPARE_OK response structure. -type ComStmtPrepareOkResponse struct { - StatementID uint32 // ID of prepared statement - ParametersNum uint16 // Num of prepared parameters -} - -// DecodeComStmtPrepareOkResponse decodes COM_STMT_PREPARE_OK response from MySQL server. -// Basic packet structure shown below. -// See https://mariadb.com/kb/en/mariadb/com_stmt_prepare/#COM_STMT_PREPARE_OK -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> Command COM_STMT_PREPARE_OK (0x00) -// int<4> StatementID -// int<2> NumberOfColumns -// int<2> NumberOfParameters -// string<1> -// int<2> NumberOfWarnings -func DecodeComStmtPrepareOkResponse(packet []byte) (*ComStmtPrepareOkResponse, error) { - - // Min packet length = header(4 bytes) + command(1 byte) + statementID(4 bytes) - // + number of columns (2 bytes) + number of parameters (2 bytes) - // + (1 byte) + number of warnings (2 bytes) - if len(packet) < 16 { - return nil, errInvalidPacketLength - } - - // Fifth byte is command - if packet[4] != responsePrepareOk { - return nil, errInvalidPacketType - } - - statementID := binary.LittleEndian.Uint32(packet[5:9]) - parametersNum := binary.LittleEndian.Uint16(packet[11:13]) - - return &ComStmtPrepareOkResponse{StatementID: statementID, ParametersNum: parametersNum}, nil -} - -// ComStmtExecuteRequest represents COM_STMT_EXECUTE request structure. -type ComStmtExecuteRequest struct { - StatementID uint32 // ID of prepared statement - PreparedParameters []PreparedParameter // Slice of prepared parameters -} - -// PreparedParameter structure represents single prepared parameter structure for COM_STMT_EXECUTE request. -type PreparedParameter struct { - FieldType byte // Type of prepared parameter. See https://mariadb.com/kb/en/mariadb/resultset/#field-types - Flag byte // Unused - Value string // String value of any prepared parameter passed with COM_STMT_EXECUTE request -} - -// DecodeComStmtExecuteRequest decodes COM_STMT_EXECUTE packet sent by MySQL client. -// Basic packet structure shown below. -// See https://mariadb.com/kb/en/mariadb/com_stmt_execute/ -// -// int<3> PacketLength -// int<1> PacketNumber -// int<1> COM_STMT_EXECUTE (0x17) -// int<4> StatementID -// int<1> Flags -// int<4> IterationCount = 1 -// if (ParamCount > 0) -// { -// byte<(ParamCount + 7) / 8> NullBitmap -// byte<1>: SendTypeToServer = 0 or 1 -// if (SendTypeToServer) -// { -// Foreach parameter -// { -// byte<1>: FieldType -// byte<1>: ParameterFlag -// } -// } -// Foreach parameter -// { -// byte BinaryParameterValue -// } -// } -func DecodeComStmtExecuteRequest(packet []byte, paramsCount uint16) (*ComStmtExecuteRequest, error) { - - // Min packet length = header(4 bytes) + command(1 byte) + statementID(4 bytes) - // + flags(1 byte) + iteration count(4 bytes) - if err := checkPacketLength(14, packet); err != nil { - return nil, err - } - - // Fifth byte is command - if packet[4] != ComStmtExecute { - return nil, errInvalidPacketType - } - - r := bytes.NewReader(packet) - - // Skip packet header - if err := SkipPacketHeader(r); err != nil { - return nil, err - } - - // Skip to statementID position - if _, err := r.Seek(1, io.SeekCurrent); err != nil { - return nil, err - } - - // Read StatementID - statementIDBuf := make([]byte, 4) - if _, err := r.Read(statementIDBuf); err != nil { - return nil, err - } - statementID := binary.LittleEndian.Uint32(statementIDBuf) - - // Skip to NullBitmap position - if _, err := r.Seek(5, io.SeekCurrent); err != nil { - return nil, err - } - - // Make buffer for n=paramsCount prepared parameters - parameters := make([]PreparedParameter, paramsCount) - - if paramsCount > 0 { - nullBitmapLength := int64((paramsCount + 7) / 8) - - // Skip to SendTypeToServer position - if _, err := r.Seek(nullBitmapLength, io.SeekCurrent); err != nil { - return nil, err - } - - // Read SendTypeToServer - sendTypeToServer, err := r.ReadByte() - if err != nil { - return nil, err - } - - if sendTypeToServer == 1 { - for index := range parameters { - - // Read parameter FieldType and ParameterFlag - parameterMeta := make([]byte, 2) - if _, err := r.Read(parameterMeta); err != nil { - return nil, err - } - - parameters[index].FieldType = parameterMeta[0] - parameters[index].Flag = parameterMeta[1] - } - } - - var fieldDecoderError error - var fieldValue string - - for index, parameter := range parameters { - switch parameter.FieldType { - - // MYSQL_TYPE_VAR_STRING (length encoded string) - case fieldTypeString: - fieldValue, fieldDecoderError = DecodeFieldTypeString(r) - - // MYSQL_TYPE_LONGLONG - case fieldTypeLongLong: - fieldValue, fieldDecoderError = DecodeFieldTypeLongLong(r) - - // MYSQL_TYPE_DOUBLE - case fieldTypeDouble: - fieldValue, fieldDecoderError = DecodeFieldTypeDouble(r) - - // Field with missing decoder - default: - return nil, errFieldTypeNotImplementedYet - } - - // Return with first decoding error - if fieldDecoderError != nil { - return nil, fieldDecoderError - } - - parameters[index].Value = fieldValue - fieldValue = "" - } - } - - return &ComStmtExecuteRequest{StatementID: statementID, PreparedParameters: parameters}, nil -} - -// DecodeFieldTypeString decodes MYSQL_TYPE_VAR_STRING field (length-encoded string) -// See https://mariadb.com/kb/en/mariadb/resultset/#field-types -func DecodeFieldTypeString(r *bytes.Reader) (string, error) { - str, _, err := ReadLenEncodedString(r) - - // io.EOF is ok since reader may be empty already because of empty prepared parameter value - if err != nil && err != io.EOF { - return "", err - } - - return str, nil -} - -// DecodeFieldTypeLongLong decodes MYSQL_TYPE_LONGLONG field -// See https://mariadb.com/kb/en/mariadb/resultset/#field-types -func DecodeFieldTypeLongLong(r *bytes.Reader) (string, error) { - var bigIntValue int64 - - if err := binary.Read(r, binary.LittleEndian, &bigIntValue); err != nil { - return "", nil - } - - return strconv.FormatInt(bigIntValue, 10), nil -} - -// DecodeFieldTypeDouble decodes MYSQL_TYPE_DOUBLE field -// See https://mariadb.com/kb/en/mariadb/resultset/#field-types -func DecodeFieldTypeDouble(r *bytes.Reader) (string, error) { - // Read 8 bytes required for float64 - doubleLengthBuf := make([]byte, 8) - if _, err := r.Read(doubleLengthBuf); err != nil { - return "", err - } - - doubleBits := binary.LittleEndian.Uint64(doubleLengthBuf) - doubleValue := math.Float64frombits(doubleBits) - - return strconv.FormatFloat(doubleValue, 'f', doubleDecodePrecision, 64), nil -} - -// ReadLenEncodedInteger returns parsed length-encoded integer and it's offset. -// See https://mariadb.com/kb/en/mariadb/protocol-data-types/#length-encoded-integers -func ReadLenEncodedInteger(r *bytes.Reader) (value uint64, offset uint64) { - firstLenEncIntByte, err := r.ReadByte() - if err != nil { - return 0, 0 - } - - switch firstLenEncIntByte { - case 0xfb: - value = 0 - offset = 1 - - case 0xfc: - data := make([]byte, 2) - _, err = r.Read(data) - if err != nil { - return 0, 0 - } - value = uint64(data[0]) | uint64(data[1])<<8 - offset = 3 - - case 0xfd: - data := make([]byte, 3) - _, err = r.Read(data) - if err != nil { - return 0, 0 - } - value = uint64(data[0]) | uint64(data[1])<<8 | uint64(data[2])<<16 - offset = 4 - - case 0xfe: - data := make([]byte, 8) - _, err = r.Read(data) - if err != nil { - return 0, 0 - } - value = uint64(data[0]) | uint64(data[1])<<8 | uint64(data[2])<<16 | - uint64(data[3])<<24 | uint64(data[4])<<32 | uint64(data[5])<<40 | - uint64(data[6])<<48 | uint64(data[7])<<56 - offset = 9 - - default: - value = uint64(firstLenEncIntByte) - offset = 1 - } - - return value, offset -} - -// ReadLenEncodedString returns parsed length-encoded string and it's length. -// Length-encoded strings are prefixed by a length-encoded integer which describes -// the length of the string, followed by the string value. -// See https://mariadb.com/kb/en/mariadb/protocol-data-types/#length-encoded-strings -func ReadLenEncodedString(r *bytes.Reader) (string, uint64, error) { - strLen, _ := ReadLenEncodedInteger(r) - - strBuf := make([]byte, strLen) - if _, err := r.Read(strBuf); err != nil { - return "", 0, err - } - - return string(strBuf), strLen, nil -} - -// ReadEOFLengthString returns parsed EOF-length string. -// EOF-length strings are those strings whose length will be calculated by the packet remaining length. -// See https://mariadb.com/kb/en/mariadb/protocol-data-types/#end-of-file-length-strings -func ReadEOFLengthString(data []byte) string { - return string(data) -} - -// ReadNullTerminatedString reads bytes from reader until 0x00 byte -// See https://mariadb.com/kb/en/mariadb/protocol-data-types/#null-terminated-strings -func ReadNullTerminatedString(r *bytes.Reader) string { - var str []byte - for { - //TODO: Check for error - b, _ := r.ReadByte() - if b == 0x00 { - return string(str) - } else { - str = append(str, b) - } - } -} - -// SkipPacketHeader rewinds reader to packet payload -func SkipPacketHeader(r *bytes.Reader) error { - if _, err := r.Seek(4, io.SeekStart); err != nil { - return err - } - - return nil -} - -// checkPacketLength checks if packet length meets expected value -func checkPacketLength(expected int, packet []byte) error { - if len(packet) < expected { - return errInvalidPacketLength - } - - return nil -} diff --git a/protocol/decoder_test.go b/protocol/decoder_test.go deleted file mode 100644 index ed33adc..0000000 --- a/protocol/decoder_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package protocol - -import ( - "bytes" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestDecodeOkResponse(t *testing.T) { - - type DecodeOkResponseAssert struct { - Packet []byte - HasError bool - Error error - OkResponse - } - - testData := []*DecodeOkResponseAssert{ - { - []byte{ - 0x30, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x22, 0x00, 0x00, 0x00, 0x28, 0x52, 0x6f, 0x77, 0x73, - 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x3a, 0x20, 0x31, 0x20, 0x20, 0x43, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x64, 0x3a, 0x20, 0x31, 0x20, 0x20, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, - 0x73, 0x3a, 0x20, 0x30, - }, - false, - nil, - OkResponse{0x00, uint64(1), uint64(0)}, - }, - { - []byte{0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}, - false, - nil, - OkResponse{0x00, uint64(0), uint64(0)}, - }, - { - []byte{0x07, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00, 0x00}, - false, - nil, - OkResponse{0x00, uint64(1), uint64(2)}, - }, - } - - for _, asserted := range testData { - decoded, err := DecodeOkResponse(asserted.Packet) - - assert.Nil(t, err) - - if err == nil { - assert.Equal(t, asserted.OkResponse.PacketType, decoded.PacketType) - assert.Equal(t, asserted.OkResponse.AffectedRows, decoded.AffectedRows) - assert.Equal(t, asserted.OkResponse.LastInsertID, decoded.LastInsertID) - } - } -} - -func TestDecodeHandshakeV10(t *testing.T) { - - type DecodeHandshakeV10Assert struct { - Packet []byte - HasError bool - Error error - ProtocolVersion byte - ServerVersion string - ConnectionID uint32 - AuthPlugin string - ServerCapabilities map[uint32]bool - } - - testData := []*DecodeHandshakeV10Assert{ - { - []byte{ - 0x4a, 0x00, 0x00, 0x00, 0x0a, 0x35, 0x2e, 0x35, 0x2e, 0x35, 0x36, 0x00, 0x5e, 0x06, 0x00, 0x00, - 0x48, 0x6a, 0x5b, 0x6a, 0x24, 0x71, 0x30, 0x3a, 0x00, 0xff, 0xf7, 0x08, 0x02, 0x00, 0x0f, 0x80, - 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0x43, 0x40, 0x56, 0x6e, - 0x4b, 0x68, 0x4a, 0x79, 0x46, 0x30, 0x5a, 0x00, 0x6d, 0x79, 0x73, 0x71, 0x6c, 0x5f, 0x6e, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x00, - }, - false, - nil, - byte(10), - "5.5.56", - uint32(1630), - "mysql_native_password", - map[uint32]bool{ - clientLongPassword: true, clientFoundRows: true, clientLongFlag: true, - clientConnectWithDB: true, clientNoSchema: true, clientCompress: true, clientODBC: true, - clientLocalFiles: true, clientIgnoreSpace: true, clientProtocol41: true, clientInteractive: true, - clientSSL: false, clientIgnoreSIGPIPE: true, clientTransactions: true, clientMultiStatements: true, - clientMultiResults: true, clientPSMultiResults: true, clientPluginAuth: true, clientConnectAttrs: false, - clientPluginAuthLenEncClientData: false, clientCanHandleExpiredPasswords: false, - clientSessionTrack: false, clientDeprecateEOF: false, - }, - }, - { - []byte{ - 0x4a, 0x00, 0x00, 0x00, 0x0a, 0x35, 0x2e, 0x37, 0x2e, 0x31, 0x38, 0x00, 0x0f, 0x00, 0x00, 0x00, - 0x15, 0x12, 0x4b, 0x1f, 0x70, 0x2b, 0x33, 0x55, 0x00, 0xff, 0xff, 0x08, 0x02, 0x00, 0xff, 0xc1, - 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x30, 0x0d, 0x0a, 0x28, - 0x06, 0x4a, 0x12, 0x5e, 0x45, 0x18, 0x05, 0x00, 0x6d, 0x79, 0x73, 0x71, 0x6c, 0x5f, 0x6e, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x00, - }, - false, - nil, - byte(10), - "5.7.18", - uint32(15), - "mysql_native_password", - map[uint32]bool{ - clientLongPassword: true, clientFoundRows: true, clientLongFlag: true, - clientConnectWithDB: true, clientNoSchema: true, clientCompress: true, clientODBC: true, - clientLocalFiles: true, clientIgnoreSpace: true, clientProtocol41: true, clientInteractive: true, - clientSSL: true, clientIgnoreSIGPIPE: true, clientTransactions: true, clientMultiStatements: true, - clientMultiResults: true, clientPSMultiResults: true, clientPluginAuth: true, clientConnectAttrs: true, - clientPluginAuthLenEncClientData: true, clientCanHandleExpiredPasswords: true, - clientSessionTrack: true, clientDeprecateEOF: true, - }, - }, - } - - for _, asserted := range testData { - decoded, err := DecodeHandshakeV10(asserted.Packet) - - if err != nil { - assert.Equal(t, asserted.Error, err) - } else { - assert.Equal(t, asserted.ProtocolVersion, decoded.ProtocolVersion) - assert.Equal(t, asserted.ServerVersion, decoded.ServerVersion) - assert.Equal(t, asserted.ConnectionID, decoded.ConnectionID) - assert.Equal(t, asserted.AuthPlugin, decoded.AuthPlugin) - - for flag, isSet := range asserted.ServerCapabilities { - if isSet { - assert.True(t, decoded.ServerCapabilities&flag > 0) - if decoded.ServerCapabilities&flag == 0 { - println(flag) - } - } else { - assert.True(t, decoded.ServerCapabilities&flag == 0) - } - } - } - } -} - -func TestDecodeComStmtExecuteRequest(t *testing.T) { - - type DecodeComStmtExecuteRequestAssert struct { - Id int - Packet []byte - HasError bool - Error error - StatementID uint32 - PreparedParameters []string - } - - testData := []*DecodeComStmtExecuteRequestAssert{ - { - 1, - // Incorrect packet type - []byte{ - 0x43, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, - 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0x13, 0x31, 0x2e, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, - 0x39, 0x31, 0x30, 0x31, 0x31, 0x31, 0x45, 0x2b, 0x32, 0x31, 0x06, 0x58, 0x59, 0x5a, 0x5a, 0x5a, - 0x5a, 0x15, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4b, 0x4c, 0x4d, 0x4f, 0x4e, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x59, 0x57, - }, - true, - errInvalidPacketType, - 0, - nil, - }, - { - 2, - // Incorrect packet length - []byte{0x43, 0x00, 0x00, 0x00, 0x17}, - true, - errInvalidPacketLength, - 0, - nil, - }, - { - 3, - // Correct packet with string params - []byte{ - 0x43, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, - 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0x13, 0x31, 0x2e, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, - 0x39, 0x31, 0x30, 0x31, 0x31, 0x31, 0x45, 0x2b, 0x32, 0x31, 0x06, 0x58, 0x59, 0x5a, 0x5a, 0x5a, - 0x5a, 0x15, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4b, 0x4c, 0x4d, 0x4f, 0x4e, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x59, 0x57, - }, - false, - nil, - uint32(1), - []string{"1.2345678910111E+21", "XYZZZZ", "ABCDEFGHIKLMONPQRSTYW"}, - }, - { - 4, - // Correct packet with string params and 0-length last param - []byte{ - 0x6a, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x01, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, - 0x00, 0xfd, 0x00, 0x01, 0x30, 0x01, 0x30, 0x01, 0x30, 0x1a, 0x64, 0x68, 0x63, 0x35, 0x74, 0x62, - 0x6a, 0x32, 0x34, 0x31, 0x72, 0x61, 0x64, 0x64, 0x6d, 0x74, 0x6c, 0x76, 0x65, 0x32, 0x36, 0x72, - 0x76, 0x6b, 0x62, 0x76, 0x13, 0x32, 0x30, 0x31, 0x37, 0x2d, 0x30, 0x36, 0x2d, 0x32, 0x35, 0x20, - 0x31, 0x38, 0x3a, 0x31, 0x39, 0x3a, 0x32, 0x30, 0x0a, 0x32, 0x30, 0x31, 0x37, 0x2d, 0x30, 0x36, - 0x2d, 0x32, 0x35, 0x04, 0x61, 0x75, 0x74, 0x68, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, - }, - false, - nil, - uint32(2), - []string{"0", "0", "0", "dhc5tbj241raddmtlve26rvkbv", "2017-06-25 18:19:20", "2017-06-25", "auth", "login", ""}, - }, - { - 5, - // Correct packet with string params and 0-length last param - []byte{ - 0x6d, 0x00, 0x00, 0x00, 0x17, 0x71, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x01, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, 0x00, 0xfd, - 0x00, 0xfd, 0x00, 0x01, 0x31, 0x01, 0x31, 0x01, 0x30, 0x1a, 0x64, 0x68, 0x63, 0x35, 0x74, 0x62, - 0x6a, 0x32, 0x34, 0x31, 0x72, 0x61, 0x64, 0x64, 0x6d, 0x74, 0x6c, 0x76, 0x65, 0x32, 0x36, 0x72, - 0x76, 0x6b, 0x62, 0x76, 0x13, 0x32, 0x30, 0x31, 0x37, 0x2d, 0x30, 0x36, 0x2d, 0x33, 0x30, 0x20, - 0x30, 0x39, 0x3a, 0x35, 0x36, 0x3a, 0x31, 0x38, 0x0a, 0x32, 0x30, 0x31, 0x37, 0x2d, 0x30, 0x36, - 0x2d, 0x33, 0x30, 0x07, 0x77, 0x69, 0x64, 0x67, 0x65, 0x74, 0x73, 0x05, 0x69, 0x6e, 0x64, 0x65, - 0x78, - }, - false, - nil, - uint32(113), - []string{"1", "1", "0", "dhc5tbj241raddmtlve26rvkbv", "2017-06-30 09:56:18", "2017-06-30", "widgets", "index", ""}, - }, - { - 6, - // Correct packet with longlong params - []byte{ - 0x34, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x00, 0x08, 0x00, 0x05, 0x00, 0x05, 0x00, 0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc7, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xdc, 0x5e, 0x40, - 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xdc, 0x5e, 0xc0, - }, - false, - nil, - uint32(1), - []string{"12345", "-12345", "123.450000", "-123.450000"}, - }, - } - - for _, asserted := range testData { - decoded, err := DecodeComStmtExecuteRequest(asserted.Packet, uint16(len(asserted.PreparedParameters))) - - actualHasError := err != nil - if asserted.HasError != actualHasError { - t.Errorf("ID = %d expected(%t) and actual(%t) errors mismatch", asserted.Id, asserted.HasError, actualHasError) - if actualHasError { - t.Errorf("Actual error: %s", err.Error()) - } - } - - if err != nil { - assert.Equal(t, asserted.Error, err) - } else { - assert.Equal(t, asserted.StatementID, decoded.StatementID) - - for index, parameter := range decoded.PreparedParameters { - assert.Equal(t, asserted.PreparedParameters[index], parameter.Value) - } - } - } -} - -func TestDecodeQueryRequest(t *testing.T) { - - type DecodeQueryRequestAssert struct { - Packet []byte - Query string - HasError bool - Error error - } - - testData := []*DecodeQueryRequestAssert{ - { - // Incorrect packet length - []byte{0x00, 0x00, 0x00, 0x00, 0x00}, - "", - true, - errInvalidPacketLength, - }, - { - // Correct COM_QUERY packet - []byte{ - 0x4a, 0x00, 0x00, 0x00, 0x03, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x20, 0x64, 0x61, 0x74, 0x61, - 0x62, 0x61, 0x73, 0x65, 0x28, 0x29, 0x2c, 0x20, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x28, 0x29, - 0x2c, 0x20, 0x6c, 0x65, 0x66, 0x74, 0x28, 0x75, 0x73, 0x65, 0x72, 0x28, 0x29, 0x2c, 0x69, 0x6e, - 0x73, 0x74, 0x72, 0x28, 0x63, 0x6f, 0x6e, 0x63, 0x61, 0x74, 0x28, 0x75, 0x73, 0x65, 0x72, 0x28, - 0x29, 0x2c, 0x27, 0x40, 0x27, 0x29, 0x2c, 0x27, 0x40, 0x27, 0x29, 0x2d, 0x31, 0x29, - }, - "select database(), schema(), left(user(),instr(concat(user(),'@'),'@')-1)", - false, - nil, - }, - { - // Incorrect packet type - []byte{0x4a, 0x00, 0x00, 0x00, 0x05, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x20, 0x64, 0x61, 0x74}, - "", - true, - errInvalidPacketType, - }, - } - - for _, asserted := range testData { - decoded, err := DecodeQueryRequest(asserted.Packet) - - if err != nil { - assert.Equal(t, asserted.Error, err) - } else { - assert.Equal(t, asserted.Query, decoded.Query) - } - } -} - -func TestDecodeComStmtPrepareOkResponse(t *testing.T) { - - type DecodeComStmtPrepareOkResponseAssert struct { - Packet []byte - HasError bool - Error error - StatementID uint32 - ParametersNum uint16 - } - - testData := []*DecodeComStmtPrepareOkResponseAssert{ - { - // Incorrect packet length - []byte{0x0c, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04}, - true, - errInvalidPacketLength, - 0, - 0, - }, - { - // Correct packet with StatementID = 1 and ParametersNum = 4 - []byte{ - 0x0c, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, - }, - false, - nil, - uint32(1), - uint16(4), - }, - } - - for _, asserted := range testData { - decoded, err := DecodeComStmtPrepareOkResponse(asserted.Packet) - - if err != nil { - assert.Equal(t, asserted.Error, err) - } else { - assert.Equal(t, asserted.StatementID, decoded.StatementID) - assert.Equal(t, asserted.ParametersNum, decoded.ParametersNum) - } - } -} - -//func TestReadLenEncodedString(t *testing.T) { -// expected := "ABCDEFGHIKLMONPQRSTYW" -// packet := []byte{ -// 0x15, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4b, 0x4c, 0x4d, 0x4f, 0x4e, 0x50, -// 0x51, 0x52, 0x53, 0x54, 0x59, 0x57, -// } -// -// decoded, _ := ReadLenEncodedString(packet) -// -// assert.Equal(t, expected, decoded) -//} - -func TestReadEOFLengthString(t *testing.T) { - expected := "SET sql_mode='STRICT_TRANS_TABLES'" - encoded := []byte{ - 0x53, 0x45, 0x54, 0x20, 0x73, 0x71, 0x6c, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x27, 0x53, 0x54, 0x52, - 0x49, 0x43, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x5f, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x53, 0x27, - } - - decoded := ReadEOFLengthString(encoded) - - assert.Equal(t, expected, decoded) -} - -func TestReadNullTerminatedString(t *testing.T) { - x := bytes.NewReader([]byte{0x35, 0x2e, 0x37, 0x2e, 0x31, 0x38, 0x00}) - assert.Equal(t, "5.7.18", ReadNullTerminatedString(x)) -} diff --git a/protocol/packet.go b/protocol/packet.go deleted file mode 100644 index 50273e5..0000000 --- a/protocol/packet.go +++ /dev/null @@ -1,215 +0,0 @@ -package protocol - -import ( - "bytes" - "encoding/binary" - "io" - "net" -) - -//... -type ConnSettings struct { - ClientCapabilities uint32 - ServerCapabilities uint32 - SelectedDb string -} - -//... -func (h *ConnSettings) DeprecateEOFSet() bool { - return ((clientDeprecateEOF & h.ServerCapabilities) != 0) && - ((clientDeprecateEOF & h.ClientCapabilities) != 0) -} - -// ProcessHandshake handles handshake between server and client. -// Returns server and client handshake responses -func ProcessHandshake(client net.Conn, mysql net.Conn) (*HandshakeV10, *HandshakeResponse41, error) { - - // Read server handshake - packet, err := ProxyPacket(mysql, client) - if err != nil { - println(err.Error()) - return nil, nil, err - } - - serverHandshake, err := DecodeHandshakeV10(packet) - if err != nil { - println(err.Error()) - return nil, nil, err - } - - // Read client handshake response - packet, err = ProxyPacket(client, mysql) - if err != nil { - println(err.Error()) - return nil, nil, err - } - - clientHandshake, err := DecodeHandshakeResponse41(packet) - if err != nil { - println(err.Error()) - return nil, nil, err - } - - // Read server OK response - if _, err = ProxyPacket(mysql, client); err != nil { - println(err.Error()) - return nil, nil, err - } - - return serverHandshake, clientHandshake, nil -} - -// ReadPrepareResponse reads response from MySQL server for COM_STMT_PREPARE -// query issued by client. -// ... -func ReadPrepareResponse(conn net.Conn) ([]byte, byte, error) { - pkt, err := ReadPacket(conn) - if err != nil { - return nil, 0, err - } - - switch pkt[4] { - case responsePrepareOk: - numParams := binary.LittleEndian.Uint16(pkt[9:11]) - numColumns := binary.LittleEndian.Uint16(pkt[11:13]) - packetsExpected := 0 - - if numParams > 0 { - packetsExpected += int(numParams) + 1 - } - - if numColumns > 0 { - packetsExpected += int(numColumns) + 1 - } - - var data []byte - var eofCnt int - - data = append(data, pkt...) - - for i := 1; i <= packetsExpected; i++ { - eofCnt++ - pkt, err = ReadPacket(conn) - if err != nil { - return nil, 0, err - } - - data = append(data, pkt...) - } - - return data, ResponseOk, nil - - case ResponseErr: - return pkt, ResponseErr, nil - } - - return nil, 0, nil -} - -func ReadErrMessage(errPacket []byte) string { - return string(errPacket[13:]) -} - -func ReadShowFieldsResponse(conn net.Conn) ([]byte, byte, error) { - return ReadResponse(conn, true) -} - -// ReadResponse ... -func ReadResponse(conn net.Conn, deprecateEof bool) ([]byte, byte, error) { - pkt, err := ReadPacket(conn) - if err != nil { - return nil, 0, err - } - - switch pkt[4] { - case ResponseOk: - return pkt, ResponseOk, nil - - case ResponseErr: - return pkt, ResponseErr, nil - - case responseLocalinfile: - } - - var data []byte - - data = append(data, pkt...) - - if !deprecateEof { - pktReader := bytes.NewReader(pkt[4:]) - columns, _ := ReadLenEncodedInteger(pktReader) - - toRead := int(columns) + 1 - - for i := 0; i < toRead; i++ { - pkt, err := ReadPacket(conn) - if err != nil { - return nil, 0, err - } - - data = append(data, pkt...) - } - } - - for { - pkt, err := ReadPacket(conn) - if err != nil { - return nil, 0, err - } - - data = append(data, pkt...) - - if pkt[4] == responseEof { - break - } - } - - return data, responseResultset, nil -} - -// ReadPacket ... -func ReadPacket(conn net.Conn) ([]byte, error) { - - // Read packet header - header := []byte{0, 0, 0, 0} - if _, err := io.ReadFull(conn, header); err != nil { - return nil, err - } - - // Calculate packet body length - bodyLen := int(uint32(header[0]) | uint32(header[1])<<8 | uint32(header[2])<<16) - - // Read packet body - body := make([]byte, bodyLen) - n, err := io.ReadFull(conn, body) - if err != nil { - return nil, err - } - - return append(header, body[0:n]...), nil -} - -// WritePacket ... -func WritePacket(pkt []byte, conn net.Conn) (int, error) { - n, err := conn.Write(pkt) - if err != nil { - return 0, err - } - - return n, nil -} - -// ProxyPacket ... -func ProxyPacket(src, dst net.Conn) ([]byte, error) { - pkt, err := ReadPacket(src) - if err != nil { - return nil, err - } - - _, err = WritePacket(pkt, dst) - if err != nil { - return nil, err - } - - return pkt, nil -} diff --git a/proxy.go b/proxy.go index 1b7d3b9..518d55c 100644 --- a/proxy.go +++ b/proxy.go @@ -2,59 +2,56 @@ package main import ( "fmt" - "github.com/orderbynull/lottip/chat" - "github.com/orderbynull/lottip/protocol" + "github.com/rs/zerolog/log" "io" - "log" + "lottip/chat" + "lottip/protocol" "net" + "strings" "time" ) -type RequestPacketParser struct { - connId string - queryId *int - queryChan chan chat.Cmd - connStateChan chan chat.ConnState - timer *time.Time -} - -func (pp *RequestPacketParser) Write(p []byte) (n int, err error) { - *pp.queryId++ - *pp.timer = time.Now() - - switch protocol.GetPacketType(p) { - case protocol.ComStmtPrepare: - case protocol.ComQuery: - decoded, err := protocol.DecodeQueryRequest(p) - if err == nil { - pp.queryChan <- chat.Cmd{pp.connId, *pp.queryId, "", decoded.Query, nil, false} - } - case protocol.ComQuit: - pp.connStateChan <- chat.ConnState{pp.connId, protocol.ConnStateFinished} - } - - return len(p), nil +type ServerHandshakeV10 struct { + ProtocolVersion byte + ServerVersion string + ConnectionID uint32 + AuthPluginData []byte + CapabilityFlags uint32 + CharacterSet byte + StatusFlags uint16 + AuthPluginName string } -type ResponsePacketParser struct { - connId string - queryId *int - queryResultChan chan chat.CmdResult - timer *time.Time +type ConnectionInfo struct { + ConnId string + User string + ClientAddress string + ClientPort int + ServerAddress string + ServerPort int + QueryId int + timer *time.Time + clientPacketFragment *[]byte + serverPacketFragment *[]byte + fsm *MySQLProtocolFSM + serverHandshake ServerHandshakeV10 } -func (pp *ResponsePacketParser) Write(p []byte) (n int, err error) { - duration := fmt.Sprintf("%.3f", time.Since(*pp.timer).Seconds()) - - switch protocol.GetPacketType(p) { - case protocol.ResponseErr: - decoded, _ := protocol.DecodeErrResponse(p) - pp.queryResultChan <- chat.CmdResult{pp.connId, *pp.queryId, protocol.ResponseErr, decoded, duration} - default: - pp.queryResultChan <- chat.CmdResult{pp.connId, *pp.queryId, protocol.ResponseOk, "", duration} - } - - return len(p), nil +func createConnectionInfo(id string, client string, clientPort int, server string, serverPort int, clientConn net.Conn, serverConn net.Conn, cmdChan chan chat.Cmd, resultChan chan chat.CmdResult, stateChan chan chat.ConnState) ConnectionInfo { + timer := time.Now() + ci := ConnectionInfo{} + ci.ConnId = id + ci.ClientAddress = client + ci.ClientPort = clientPort + ci.ServerAddress = server + ci.ServerPort = serverPort + ci.timer = &timer + ci.clientPacketFragment = &[]byte{} + ci.serverPacketFragment = &[]byte{} + + ci.fsm = CreateStateMachine(&ci, clientConn, serverConn, cmdChan, resultChan, stateChan) + + return ci } // MySQLProxyServer implements server for capturing and forwarding MySQL traffic. @@ -67,12 +64,53 @@ type MySQLProxyServer struct { proxyHost string } +// handleConnection ... +func (p *MySQLProxyServer) handleConnection(client net.Conn) { + defer client.Close() + + // New connection to MySQL is made per each incoming TCP request to MySQLProxyServer server. + if !strings.Contains(p.mysqlHost, ":") { + p.mysqlHost += ":3306" + } + server, err := net.Dial("tcp", p.mysqlHost) + if err != nil { + log.Fatal().Err(err).Msg("Could not connect to MySQL at " + p.mysqlHost) + return + } + defer server.Close() + + connId := fmt.Sprintf("%s => %s", client.RemoteAddr().String(), server.RemoteAddr().String()) + + defer func() { p.connStateChan <- chat.ConnState{connId, protocol.ConnStateFinished} }() + + clientAddress := client.RemoteAddr().String() + clientPort := -1 + if addr, ok := client.RemoteAddr().(*net.TCPAddr); ok { + clientAddress = addr.IP.String() + clientPort = addr.Port + } + serverAddress := server.RemoteAddr().String() + serverPort := -1 + if addr, ok := server.RemoteAddr().(*net.TCPAddr); ok { + serverAddress = addr.IP.String() + serverPort = addr.Port + } + + connInfo := createConnectionInfo(connId, clientAddress, clientPort, serverAddress, serverPort, client, server, p.cmdChan, p.cmdResultChan, p.connStateChan) + + // Copy bytes from client to server and requestParser + go io.Copy(io.Writer(&ClientToServerHandler{&connInfo, p.cmdChan, p.connStateChan, server}), client) + + // Copy bytes from server to client and responseParser + io.Copy(io.Writer(&ServerToClientHandler{&connInfo, p.cmdResultChan, client}), server) +} + // run starts accepting TCP connection and forwarding it to MySQL server. // Each incoming TCP connection is handled in own goroutine. func (p *MySQLProxyServer) run() { listener, err := net.Listen("tcp", p.proxyHost) if err != nil { - log.Fatal(err.Error()) + log.Fatal().Err(err).Msg("Could not listen on TCP at " + p.proxyHost) } defer listener.Close() @@ -84,35 +122,45 @@ func (p *MySQLProxyServer) run() { for { client, err := listener.Accept() if err != nil { - log.Print(err.Error()) + log.Fatal().Err(err).Msg("Could not accept connection") } go p.handleConnection(client) } } -// handleConnection ... -func (p *MySQLProxyServer) handleConnection(client net.Conn) { - defer client.Close() +// ReadLenEncode used to read variable length. +func readLenEncodedInt(packet []byte, offset uint32) (value uint64, newOffset uint32) { + var u8 uint8 + u8 = packet[offset] + + switch u8 { + case 0xfb: + // nil value + // we set the length to maxuint64. + value = ^uint64(0) + return value, offset + 1 + + case 0xfc: + value = uint64(packet[offset+1]) | uint64(packet[offset+2])<<8 + return value, offset + 3 + + case 0xfd: + value = uint64(packet[offset+1]) | uint64(packet[offset+2])<<8 | uint64(packet[offset+3])<<16 + return value, offset + 4 + + case 0xfe: + value = uint64(packet[offset]) | uint64(packet[offset+1])<<8 | + uint64(packet[offset+2])<<16 | uint64(packet[offset+3])<<24 | + uint64(packet[offset+4])<<32 | uint64(packet[offset+5])<<40 | + uint64(packet[offset+6])<<48 | uint64(packet[offset+7])<<56 + return value, offset + 8 - // New connection to MySQL is made per each incoming TCP request to MySQLProxyServer server. - server, err := net.Dial("tcp", p.mysqlHost) - if err != nil { - log.Print(err.Error()) - return + default: + return uint64(u8), offset + 1 } - defer server.Close() - - connId := fmt.Sprintf("%s => %s", client.RemoteAddr().String(), server.RemoteAddr().String()) - - defer func() { p.connStateChan <- chat.ConnState{connId, protocol.ConnStateFinished} }() - - var queryId int - var timer time.Time - - // Copy bytes from client to server and requestParser - go io.Copy(io.MultiWriter(server, &RequestPacketParser{connId, &queryId, p.cmdChan, p.connStateChan, &timer}), client) +} - // Copy bytes from server to client and responseParser - io.Copy(io.MultiWriter(client, &ResponsePacketParser{connId, &queryId, p.cmdResultChan, &timer}), server) +func GetPacketType(p []byte) byte { + return p[4] } diff --git a/server_to_client_handler.go b/server_to_client_handler.go new file mode 100644 index 0000000..89ac2b6 --- /dev/null +++ b/server_to_client_handler.go @@ -0,0 +1,193 @@ +package main + +import ( + "github.com/pubnative/mysqlproto-go" + "io" + "lottip/chat" + "math" +) + +type ServerToClientHandler struct { + connInfo *ConnectionInfo + queryResultChan chan chat.CmdResult + client io.Writer +} + +// From SERVER => Client +func (pp *ServerToClientHandler) Write(buffer []byte) (n int, err error) { + //duration := fmt.Sprintf("%.3f", time.Since(*pp.connInfo.timer).Seconds()) + + extractPacketsFromBuffer(pp.connInfo, pp.connInfo.serverPacketFragment, buffer, func(packet []byte) { + fsm := pp.connInfo.fsm + if ok, _ := fsm.IsInState(StateIdle); ok { + // Server's initial response asking for login info + //pp.parseServerHandshakeV10(packet) + //logging.LogResponse(pp.connInfo, "Connect", "Client connecting - Server Handshake/Challenge response (forcing uncompressed protocol)") + fsm.Fire(MsgServerHello, packet) + } else if ok, _ := fsm.IsInState(StateAuthSent); ok { + // This must be the OK packet + _, err := mysqlproto.ParseOKPacket(packet[4:], pp.connInfo.serverHandshake.CapabilityFlags) + if err != nil { + _, err := mysqlproto.ParseERRPacket(packet[4:], pp.connInfo.serverHandshake.CapabilityFlags) + if err != nil { + LogInvalid(pp.connInfo, "ERROR: Response is not OK or ERROR packet", packet) + } else { + fsm.Fire(MsgERROR, packet) + } + } else { + fsm.Fire(MsgOK, packet) + } + } else { + _, err := mysqlproto.ParseOKPacket(packet[4:], pp.connInfo.serverHandshake.CapabilityFlags) + if err != nil { + _, err := mysqlproto.ParseERRPacket(packet[4:], pp.connInfo.serverHandshake.CapabilityFlags) + if err != nil { + fsm.Fire(MsgServerToClient, packet) + } else { + fsm.Fire(MsgERROR, packet) + } + } else { + fsm.Fire(MsgOK, packet) + } + } + }) + //// Switch based on the state + //case ConnStateInit: + //// Server's initial response asking for loging info -- passthrough + //buffer[25] = buffer[25] & 0xDF + //logEntry(pp.connInfo, "Connect", "Client connecting - Server Handshake/Challenge response (forcing uncompressed protocol)") + //pp.connInfo.connectionState = ConnStateAuthInfoRequested + // + //case ConnStateAuthInfoSent: + //pp.extractPacketsFromBuffer(buffer, func () { + // pp.queryResultChan <- chat.CmdResult{pp.connInfo.connId, pp.connInfo.queryId, protocol.ResponseOk, "", duration} + // logResponse(pp.connInfo, "Login:OK") + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (errCode int32, errMessage string) { + // pp.queryResultChan <- chat.CmdResult{pp.connInfo.connId, pp.connInfo.queryId, protocol.ResponseErr, fmt.Sprintf("Code: %d => %s", errCode, errMessage), duration} + // logResponse(pp.connInfo, "Login:ERROR", "Code: %d => %s", errCode, errMessage) + // pp.connInfo.connectionState = ConnStateUnauthorized + //}, func (seqNumber int) { + // logResponse(pp.connInfo, "Login:EOF", "Packet %d", seqNumber) + // pp.connInfo.connectionState = ConnStateUnauthorized + //}, func (packet []byte) { + //}) + // + //case ConnStateAuthorized: + //pp.extractPacketsFromBuffer(buffer, func () { + // logResponse(pp.connInfo, "OK") + //}, func (errCode int32, errMessage string) { + // logResponse(pp.connInfo, "ERROR", "Code: %d => %s", errCode, errMessage) + //}, func (seqNumber int) { + // logResponse(pp.connInfo, "EOF", "Packet %d", seqNumber) + //}, func (packet []byte) { + //}) + // + //case ConnStateQueryActive: + //pp.queryResultChan <- chat.CmdResult{pp.connInfo.connId, pp.connInfo.queryId, protocol.ResponseOk, "", duration} + //pp.extractPacketsFromBuffer(buffer, func () { + // logResponse(pp.connInfo, "Query:OK") + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (errCode int32, errMessage string) { + // logResponse(pp.connInfo, "Query:ERR", "Code: %d => %s", errCode, errMessage) + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (seqNumber int) { + // logResponse(pp.connInfo, "Query:EOF", "Packet %d", seqNumber) + // pp.connInfo.connectionState = ConnStateQueryRows + //}, func (packet []byte) { + // columns, _ := readLenEncodedInt(packet, 4) + // fmt.Println("Expecting", columns, "column definitions") + // pp.connInfo.columnCount = int(columns) + // pp.connInfo.connectionState = ConnStateQueryColumnDefs + //}) + // + //case ConnStateQueryColumnDefs: + //pp.extractPacketsFromBuffer(buffer, func () { + // logResponse(pp.connInfo, "Query:OK", "Received this in the unexpectedly in the middle of column-defs") + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (errCode int32, errMessage string) { + // logResponse(pp.connInfo, "Query:ERR", "Code: %d => %s", errCode, errMessage) + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (seqNumber int) { + // logResponse(pp.connInfo, "Query:EOF", "Packet %d", seqNumber) + // pp.connInfo.connectionState = ConnStateQueryRows + //}, func (packet []byte) { + // pp.connInfo.columnCount = pp.connInfo.columnCount - 1 + // fmt.Println("Received a column def packet -", pp.connInfo.columnCount, "packets to go") + //}) + // + //case ConnStateQueryRows: + //pp.queryResultChan <- chat.CmdResult{pp.connInfo.connId, pp.connInfo.queryId, protocol.ResponseOk, "", duration} + //pp.extractPacketsFromBuffer(buffer, func () { + // logResponse(pp.connInfo, "Query:OK") + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (errCode int32, errMessage string) { + // logResponse(pp.connInfo, "Query:ERR", "Code: %d => %s", errCode, errMessage) + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (seqNumber int) { + // logResponse(pp.connInfo, "Query:EOF", "Packet %d", seqNumber) + // pp.connInfo.connectionState = ConnStateAuthorized + //}, func (packet []byte) { + //}) + //} + + //return pp.client.Write(buffer) + return len(buffer), nil +} + +func (pp *ServerToClientHandler) parseServerHandshakeV10(packet []byte) { + _, end := getInt24(packet, 0) + // sequenceNumber := packet[end] + end++ + pp.connInfo.serverHandshake.ProtocolVersion = packet[end] + end++ + start := end + for ; packet[end] != 0; end++ { + } + pp.connInfo.serverHandshake.ServerVersion = string(packet[start:end]) + end++ + pp.connInfo.serverHandshake.ConnectionID, end = getInt32(packet, end) + // Skip the plugin data + pp.connInfo.serverHandshake.AuthPluginData = packet[end : end+7] + end += 8 + // Skip filler + end++ + pp.connInfo.serverHandshake.CapabilityFlags = uint32(packet[end]) | uint32(packet[end+1])<<8 + end += 2 + if end < len(packet) { + pp.connInfo.serverHandshake.CharacterSet = packet[end] + end++ + pp.connInfo.serverHandshake.StatusFlags = uint16(packet[end]) | uint16(packet[end+1])<<8 + end += 2 + pp.connInfo.serverHandshake.CapabilityFlags = pp.connInfo.serverHandshake.CapabilityFlags | (uint32(packet[end])|uint32(packet[end+1])<<8)<<16 + end += 2 + pluginDataLength := 0 + if pp.connInfo.serverHandshake.CapabilityFlags&mysqlproto.CLIENT_PLUGIN_AUTH == mysqlproto.CLIENT_PLUGIN_AUTH { + pluginDataLength = int(packet[end]) + } + end++ + // Skip reserved block + end += 10 + if pp.connInfo.serverHandshake.CapabilityFlags&mysqlproto.CLIENT_SECURE_CONNECTION == mysqlproto.CLIENT_SECURE_CONNECTION { + pluginDataLength := int(math.Max(13, float64(pluginDataLength)-8)) + pp.connInfo.serverHandshake.AuthPluginData = append(pp.connInfo.serverHandshake.AuthPluginData, packet[end:end+pluginDataLength]...) + end += pluginDataLength + if pp.connInfo.serverHandshake.CapabilityFlags&mysqlproto.CLIENT_PLUGIN_AUTH == mysqlproto.CLIENT_PLUGIN_AUTH { + start = end + for ; packet[end] != 0; end++ { + } + pp.connInfo.serverHandshake.AuthPluginName = string(packet[start:end]) + } + } + } +} + +func getInt32(packet []byte, offset int) (uint32, int) { + v := uint32(packet[offset]) << uint32(packet[offset+1]) << 8 << uint32(packet[offset+2]) << 16 << uint32(packet[offset+3]) << 24 + return v, offset + 4 +} + +func getInt24(packet []byte, offset int) (uint32, int) { + v := uint32(packet[offset]) << uint32(packet[offset+1]) << 8 << uint32(packet[offset+2]) << 16 + return v, offset + 3 +} diff --git a/web/css/prism.css b/web/css/prism.css new file mode 100644 index 0000000..b21fa42 --- /dev/null +++ b/web/css/prism.css @@ -0,0 +1,143 @@ +/* PrismJS 1.23.0 +https://prismjs.com/download.html#themes=prism&languages=sql&plugins=normalize-whitespace */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.token.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + /* This background color was intended by the author of this theme. */ + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/web/css/style.css b/web/css/style.css index 6e746c6..e5015c5 100755 --- a/web/css/style.css +++ b/web/css/style.css @@ -125,7 +125,7 @@ cursor: pointer; } #bootstrap-override table tr td.expanded { - white-space: normal; + white-space: pre; } /*Modal window styles*/ #bootstrap-override #results #modal-preloader { diff --git a/web/index.html b/web/index.html index 5e79e28..71778b6 100755 --- a/web/index.html +++ b/web/index.html @@ -2,13 +2,14 @@ -Lottip +Lottip - MS ed. + @@ -96,7 +97,7 @@ - {{query.query}} +
Params: {{param}}
@@ -117,6 +118,7 @@ + \ No newline at end of file diff --git a/web/js/app.js b/web/js/app.js index f2b69db..670e6c8 100755 --- a/web/js/app.js +++ b/web/js/app.js @@ -187,6 +187,8 @@ new Vue({ // Fired when received Cmd data from websocket cmdReceived: function (connId, cmdId, database, query, parameters, executable) { + var nw = Prism.plugins.NormalizeWhitespace; + if (!(connId in this.connections)) { Vue.set(this.connections, connId, {}); } @@ -195,6 +197,7 @@ new Vue({ connId: connId, cmdId: cmdId, database: database, + queryFormatted: Prism.highlight(nw.normalize(query), Prism.languages.sql, 'sql'), query: query, parameters: parameters, expanded: true, diff --git a/web/js/prism.js b/web/js/prism.js new file mode 100644 index 0000000..2711a08 --- /dev/null +++ b/web/js/prism.js @@ -0,0 +1,5 @@ +/* PrismJS 1.23.0 +https://prismjs.com/download.html#themes=prism&languages=sql&plugins=normalize-whitespace */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);y+=m.value.length,m=m.next){var k=m.value;if(r.length>n.length)return;if(!(k instanceof W)){var b,x=1;if(h){if(!(b=z(p,y,n,f)))break;var w=b.index,A=b.index+b[0].length,P=y;for(P+=m.value.length;P<=w;)m=m.next,P+=m.value.length;if(P-=m.value.length,y=P,m.value instanceof W)continue;for(var S=m;S!==r.tail&&(Pl.reach&&(l.reach=N);var j=m.prev;O&&(j=I(r,j,O),y+=O.length),q(r,j,x);var C=new W(o,g?M.tokenize(E,g):E,d,E);if(m=I(r,j,C),L&&I(r,m,L),1l.reach&&(l.reach=_.reach)}}}}}}(e,a,n,a.head,0),function(e){var n=[],r=e.head.next;for(;r!==e.tail;)n.push(r.value),r=r.next;return n}(a)},hooks:{all:{},add:function(e,n){var r=M.hooks.all;r[e]=r[e]||[],r[e].push(n)},run:function(e,n){var r=M.hooks.all[e];if(r&&r.length)for(var t,a=0;t=r[a++];)t(n)}},Token:W};function W(e,n,r,t){this.type=e,this.content=n,this.alias=r,this.length=0|(t||"").length}function z(e,n,r,t){e.lastIndex=n;var a=e.exec(r);if(a&&t&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function i(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function I(e,n,r){var t=n.next,a={value:r,prev:n,next:t};return n.next=a,t.prev=a,e.length++,a}function q(e,n,r){for(var t=n.next,a=0;a"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,t=n.code,a=n.immediateClose;u.postMessage(M.highlight(t,M.languages[r],r)),a&&u.close()},!1)),M;var e=M.util.currentScript();function r(){M.manual||M.highlightAll()}if(e&&(M.filename=e.src,e.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var t=document.readyState;"loading"===t||"interactive"===t&&e&&e.defer?document.addEventListener("DOMContentLoaded",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:_INSERT|COL)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:S|ING)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:TRUE|FALSE|NULL)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|IN|ILIKE|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/}; +!function(){var i=Object.assign||function(e,n){for(var t in n)n.hasOwnProperty(t)&&(e[t]=n[t]);return e};function e(e){this.defaults=i({},e)}function s(e){for(var n=0,t=0;t