From 8710d61f10315964357e0a1dfa98abbc942bb339 Mon Sep 17 00:00:00 2001 From: Weilu Jia Date: Sun, 22 May 2022 14:47:27 -0700 Subject: [PATCH] Initial rating-update support --- ggst/api_definitions.go | 58 ++++++++++ ggst/parser.go | 72 +++++++++++++ ggst/protocol_decoder.go | 43 ++++++++ go.mod | 3 + go.sum | 12 +++ main.go | 2 + proxy/proxy.go | 6 ++ proxy/rating_update.go | 221 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 417 insertions(+) create mode 100644 ggst/api_definitions.go create mode 100644 ggst/parser.go create mode 100644 ggst/protocol_decoder.go create mode 100644 proxy/rating_update.go diff --git a/ggst/api_definitions.go b/ggst/api_definitions.go new file mode 100644 index 0000000..804b25d --- /dev/null +++ b/ggst/api_definitions.go @@ -0,0 +1,58 @@ +package ggst + +type StatReqHeader struct { + _msgpack struct{} `msgpack:",as_array"` + UserID string // 18 digit User ID + Hash string // 13 character response hash from /api/user/login + Unk3 int // Always 2 + Version string // Some version string. Same as ResponseHeader.Version + Unk4 int // Always 3 +} + +type StatGetReqPayload struct { + _msgpack struct{} `msgpack:",as_array"` + OtherUserID string // 18 digit User ID of other player. Empty string if self. + Type int // Request type. 1: Vs stats (tension use, RC usage, perfects, etc). 2: Battle Record/Battle Chart. 6: Single player stats (eg mission, story). 7: Levels, Floor, Name, etc. 8: Unknown. 9: News. + Unk2 int // Set to -1 normally. Sometimes set to 1 on Type 1 requests. + Page int // Seems to be page number or character ID. -1 if N/A. 0 for first page. Only used by Type 6 and 8. + Unk3 int // Set to -1 normally. Sometimes set to -2 on Type 1 requests. + Unk4 int // -1 +} + +type StatGetRequest struct { + _msgpack struct{} `msgpack:",as_array"` + Header StatReqHeader + Payload StatGetReqPayload +} + +type StatGetRespHeader struct { + _msgpack struct{} `msgpack:",as_array"` + Hash string // Some sort of incrementing 13 char hash + Unk1 int // Unknown, always 0 + Timestamp string // Current time in "YYYY/MM/DD HH:MM:SS" in UTC + Version1 string // Some version string. "0.1.1" in v1.16. "0.0.7" in v1.10. "0.0.6" in v1.07. "0.0.5" in v1.06, was "0.0.4" in v1.05 + Version2 string // Another version string. Always 0.0.2 + Version3 string // Another version string. Always 0.0.2 + Unk2 string // Unknown, empty string + Unk3 string // Unknown, empty string +} + +type Payload struct { + _msgpack struct{} `msgpack:",as_array"` + Unk1 int // Unknown, always 0. + Payload map[string]interface{} +} + +type StatGetRespPayload struct { + _msgpack struct{} `msgpack:",as_array"` + Unk1 int // Unknown, always 0. + JSON RawJSON +} + +type RawJSON map[string]interface{} + +type StatGetResponse struct { + _msgpack struct{} `msgpack:",as_array"` + Header StatGetRespHeader + Payload StatGetRespPayload +} diff --git a/ggst/parser.go b/ggst/parser.go new file mode 100644 index 0000000..52c0363 --- /dev/null +++ b/ggst/parser.go @@ -0,0 +1,72 @@ +package ggst + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/vmihailenco/msgpack/v5" +) + +type Decoder struct { + d *msgpack.Decoder +} + +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{d: msgpack.NewDecoder(r)} +} + +func (dec *Decoder) Decode(v interface{}) error { + return dec.d.Decode(v) +} + +type Encoder struct { + e *msgpack.Encoder +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{e: msgpack.NewEncoder(w)} +} + +func (dec *Encoder) Encode(v interface{}) error { + return dec.e.Encode(v) +} + +func Unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} + +func Marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func UnmarshalStatResp(data []byte) (*StatGetResponse, error) { + parsedResp := &StatGetResponse{} + err := Unmarshal(data, parsedResp) + if err != nil { + return nil, err + } + return parsedResp, err +} + +func (J *RawJSON) DecodeMsgpack(dec *msgpack.Decoder) error { + s, err := dec.DecodeString() + if err != nil { + return err + } + jDecoder := json.NewDecoder(bytes.NewBufferString(s)) + jDecoder.UseNumber() // Things like AccountID are very long numbers + jDecoder.Decode(J) + if err != nil { + return err + } + return nil +} + +func (J *RawJSON) EncodeMsgpack(enc *msgpack.Encoder) error { + b, err := json.Marshal(J) + if err != nil { + return err + } + return enc.EncodeString(string(b)) +} diff --git a/ggst/protocol_decoder.go b/ggst/protocol_decoder.go new file mode 100644 index 0000000..57e6851 --- /dev/null +++ b/ggst/protocol_decoder.go @@ -0,0 +1,43 @@ +package ggst + +import ( + "bytes" + "encoding/hex" + "fmt" + "net/http" + "strings" + + "github.com/vmihailenco/msgpack/v5" +) + +func ParseReq(r *http.Request, v interface{}) error { + req, err := hex.DecodeString(strings.TrimRight(r.FormValue("data"), "\x00")) // Clean up input + if err != nil { + fmt.Println(err) + } + + err = msgpack.Unmarshal(req, &v) + if err != nil { + return err + } + return nil +} + +// BufferedResponseWriter is a wrapper around http.ResponseWriter that buffers the response for later use. +type BufferedResponseWriter struct { + HttpHeader http.Header + StatusCode int + Body bytes.Buffer +} + +func (b *BufferedResponseWriter) Header() http.Header { + return b.HttpHeader +} + +func (b *BufferedResponseWriter) WriteHeader(code int) { + b.StatusCode = code +} + +func (b *BufferedResponseWriter) Write(data []byte) (int, error) { + return b.Body.Write(data) +} diff --git a/go.mod b/go.mod index 0ad0fa6..78f5549 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,14 @@ require ( golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 ) +require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tc-hib/winres v0.1.6 // indirect github.com/urfave/cli/v2 v2.4.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/image v0.0.0-20220321031419-a8550c1d254a // indirect ) diff --git a/go.sum b/go.sum index 99b98bc..426d528 100644 --- a/go.sum +++ b/go.sum @@ -5,17 +5,23 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tc-hib/go-winres v0.2.4 h1:UcRtbsvJ1R1uls6oId20vYG3I1vDttScsU7A3186Ays= github.com/tc-hib/go-winres v0.2.4/go.mod h1:lTPf0MW3eu6rmvMyLrPXSy6xsSz4t5dRxB7dc5YFP6k= github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8= @@ -23,6 +29,10 @@ github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8na github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg= @@ -37,3 +47,5 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index abafe5a..58d3ba1 100755 --- a/main.go +++ b/main.go @@ -294,6 +294,7 @@ func main() { var unsafeCacheFollow = flag.Bool("unsafe-cache-follow", false, "UNSAFE: Cache first get_follow and get_block calls and return cached version on subsequent calls.") var ungaBunga = flag.Bool("unga-bunga", UngaBungaMode != "", "UNSAFE: Enable all unsafe speedups for maximum speed. Please read https://github.com/optix2000/totsugeki/blob/master/UNSAFE_SPEEDUPS.md") var iKnowWhatImDoing = flag.Bool("i-know-what-im-doing", false, "UNSAFE: Suppress any UNSAFE warnings. I hope you know what you're doing...") + var ratingUpdate = flag.Bool("rating-update", false, "Display ratings from ratingupdate.info instead of character levels.") var ver = flag.Bool("version", false, "Print the version number and exit.") flag.Parse() @@ -404,6 +405,7 @@ func main() { PredictReplay: *unsafePredictReplay, CacheEnv: *unsafeCacheEnv, CacheFollow: *unsafeCacheFollow, + RatingUpdate: *ratingUpdate, }) fmt.Println("Started Proxy Server on port 21611.") diff --git a/proxy/proxy.go b/proxy/proxy.go index 4b0823f..cffafd5 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -34,6 +34,7 @@ type StriveAPIProxyOptions struct { PredictReplay bool CacheEnv bool CacheFollow bool + RatingUpdate bool } func (s *StriveAPIProxy) proxyRequest(r *http.Request) (*http.Response, error) { @@ -210,6 +211,11 @@ func CreateStriveProxy(listen string, GGStriveAPIURL string, PatchedAPIURL strin r.Use(middleware.Logger) r.Use(proxy.CacheInvalidationHandler) + if options.RatingUpdate { + ru := NewRatingUpdate() + r.Use(ru.RatingUpdateHandler) + } + if options.AsyncStatsSet { statsSet = proxy.HandleStatsSet proxy.statsQueue = proxy.startStatsSender() diff --git a/proxy/rating_update.go b/proxy/rating_update.go new file mode 100644 index 0000000..e8891e5 --- /dev/null +++ b/proxy/rating_update.go @@ -0,0 +1,221 @@ +package proxy + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/optix2000/totsugeki/ggst" + "github.com/vmihailenco/msgpack/v5" +) + +const ratingUpdateURL = "http://ratingupdate.info/api/player_rating/%s" + +// var characters = map[string]string{ +// // "SOL": "SO", +// // "KYK": "KY", +// // "MAY": "MA", +// // "AXL": "AX", +// // "CHP": "CH", +// // "POT": "PO", +// // "FAU": "FA", +// "MLL": "MI", +// "ZAT": "ZT", +// // "RAM": "RA", +// // "LEO": "LE", +// // "NAG": "NA", +// // "GIO": "GI", +// // "ANJ": "AN", +// // "INO": "IN", +// "GLD": "GO", +// "JKO": "JC", +// } + +var characters = map[string]int{ + "SOL": 0, + "KYK": 1, + "MAY": 2, + "AXL": 3, + "CHP": 4, + "POT": 5, + "FAU": 6, + "MLL": 7, + "ZAT": 8, + "RAM": 9, + "LEO": 10, + "NAG": 11, + "GIO": 12, + "ANJ": 13, + "INO": 14, + "GLD": 15, + "JKO": 16, + "COS": 17, + "BKN": 18, + "TST": 19, +} + +type RatingUpdate struct { + client http.Client +} + +func (ru *RatingUpdate) RatingUpdateHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/statistics/get" { + ru.InjectRating(next, w, r) + } else { + next.ServeHTTP(w, r) + } + }) +} + +func NewRatingUpdate() *RatingUpdate { + return &RatingUpdate{ + client: http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 1, + MaxConnsPerHost: 2, + IdleConnTimeout: 30 * time.Second, + }, + }, + } +} + +func (ru *RatingUpdate) InjectRating(next http.Handler, w http.ResponseWriter, r *http.Request) { + reqBody := &bytes.Buffer{} + r.Body = io.NopCloser(io.TeeReader(r.Body, reqBody)) // Tee body so it can be read multiple times + + parsedReq := &ggst.StatGetRequest{} + err := ggst.ParseReq(r, parsedReq) + r.Body = io.NopCloser(reqBody) // Reset request body for next handler + if err != nil || + parsedReq.Payload.Type != 7 || // We only care about injecting ratings into character levels + parsedReq.Payload.OtherUserID == "" { // Abort if we are fetching our own rating. Injecting our own rating will break our R-Code. + if err != nil { + fmt.Println(err) + } + next.ServeHTTP(w, r) + return + } + + userID, err := strconv.ParseUint(parsedReq.Payload.OtherUserID, 10, 64) + if err != nil { + fmt.Println(err) + next.ServeHTTP(w, r) + } + + wg := sync.WaitGroup{} + var ratings Ratings + wg.Add(1) + go func() { + ratings, err = ru.FetchRatings(uint64(userID)) + wg.Done() + }() + + ww := &ggst.BufferedResponseWriter{ + HttpHeader: http.Header{}, + StatusCode: 0, + Body: bytes.Buffer{}, + } + + next.ServeHTTP(ww, r) + + if err != nil { + fmt.Println(err) + w.Write(ww.Body.Bytes()) + return + } + + for k, v := range ww.Header() { // Copy headers across + if k == "Content-Length" { + continue + } + w.Header()[k] = v + } + + parsedResp, err := ggst.UnmarshalStatResp(ww.Body.Bytes()) + if err != nil { + fmt.Println(err) + w.Write(ww.Body.Bytes()) + return + } + + wg.Wait() // Wait for fetchRatings to finish + for k := range parsedResp.Payload.JSON { + if strings.HasSuffix(k, "Lv") { + + if err != nil { + fmt.Println(err) + continue + } + idx := convertCharacter(k[0:3]) + if idx == -1 { + fmt.Println("Unknown character:", k[0:3]) + continue + } + if len(ratings) <= idx { + fmt.Println("No rating for:", k[0:3]) + continue + } + rating := ratings[idx] + parsedResp.Payload.JSON[k] = int(math.Round(rating.Value)) + } + } + + out, err := msgpack.Marshal(parsedResp) + if err != nil { + fmt.Println(err) + } + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out))) + w.Write(out) +} + +type Ratings []Rating + +type Rating struct { + Value float64 + Deviation float64 +} + +func (ru *RatingUpdate) FetchRatings(userID uint64) (Ratings, error) { + url := fmt.Sprintf(ratingUpdateURL, convertUser(userID)) + resp, err := ru.client.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode > 299 { + return nil, fmt.Errorf("HTTP error: %s. URL: %s", resp.Status, url) + } + d := json.NewDecoder(resp.Body) + + ratings := &Ratings{} + err = d.Decode(ratings) + if err != nil { + return nil, err + } + + return (*ratings), nil +} + +func convertCharacter(character string) int { + if val, ok := characters[character]; ok { + return val + } else { + return -1 + } +} + +func convertUser(userID uint64) string { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, userID) + return strings.ToUpper(hex.EncodeToString(b)) +}