From a89cbe5a93aede3703cd5981ea71827b55db0866 Mon Sep 17 00:00:00 2001 From: Keuin Date: Mon, 12 Feb 2024 18:16:16 +0800 Subject: initial version --- yggdrasil/muxserver.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ yggdrasil/realserver.go | 88 +++++++++++++++++++++++++++++++++++++++++++++ yggdrasil/server.go | 26 ++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 yggdrasil/muxserver.go create mode 100644 yggdrasil/realserver.go create mode 100644 yggdrasil/server.go (limited to 'yggdrasil') diff --git a/yggdrasil/muxserver.go b/yggdrasil/muxserver.go new file mode 100644 index 0000000..7db4021 --- /dev/null +++ b/yggdrasil/muxserver.go @@ -0,0 +1,94 @@ +package yggdrasil + +import ( + "fmt" + "github.com/avast/retry-go" + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "github.com/samber/mo" + "github.com/sourcegraph/conc" + "strings" + "time" +) + +const ( + retryInterval = 100 * time.Millisecond + maxRetryTimes = 3 +) + +type muxServer struct { + subServers []Server +} + +func (m muxServer) Name() string { + return "muxServer[" + strings.Join(lo.Map(m.subServers, func(item Server, _ int) string { + return item.Name() + }), ", ") + "]" +} + +func (m muxServer) HasJoined(username string, serverID string) (*HasJoinedResponse, error) { + var wg conc.WaitGroup + type Ret = *HasJoinedResponse + results := make(chan mo.Result[Ret], len(m.subServers)) + for _, s := range m.subServers { + s := s + wg.Go(func() { + var resp *HasJoinedResponse + err := retry.Do( + func() error { + var err error + resp, err = s.HasJoined(username, serverID) + return err + }, + retry.Delay(retryInterval), + retry.Attempts(maxRetryTimes), + ) + if err != nil { + results <- mo.Err[Ret](fmt.Errorf("call hasJoined on server `%v` failed: %w", + s.Name(), err)) + } else { + results <- mo.Ok[Ret](resp) + } + }) + } + + chPanic := make(chan error, 1) + go func() { + // wait for all async tasks to finish in another async goroutine + // to allow the main request return as early as possible + r := wg.WaitAndRecover() + close(results) + if r != nil { + chPanic <- r.AsError() + } + close(chPanic) + }() + + var last *Ret + for r := range results { + r, err := r.Get() + if err != nil { + log.Error().Err(err).Msg("hasJoined failed") + continue + } + last = &r + // return the first positive result + if r.HasJoined() { + return r, nil + } + } + if last == nil { + // no data generated, all async tasks panicked + err := <-chPanic + log.Error().Err(err).Msg("all hasJoined async query panicked") + return nil, err + } else { + return *last, nil + } +} + +func NewMuxServer(servers ...Server) Server { + return muxServer{ + subServers: servers, + } +} diff --git a/yggdrasil/realserver.go b/yggdrasil/realserver.go new file mode 100644 index 0000000..86f4887 --- /dev/null +++ b/yggdrasil/realserver.go @@ -0,0 +1,88 @@ +package yggdrasil + +import ( + "encoding/json" + "fmt" + "github.com/imroc/req" + "github.com/rs/zerolog/log" + "net/url" + "time" +) + +type realServer struct { + req *req.Req + apiPrefix string + name string +} + +func (r realServer) Name() string { + return r.name +} + +func (r realServer) HasJoined(username string, serverID string) (*HasJoinedResponse, error) { + u, err := url.Parse(r.apiPrefix + "/session/minecraft/hasJoined") + if err != nil { + return nil, fmt.Errorf("url parse: %w", err) + } + q := u.Query() + q.Set("username", username) + q.Set("serverId", serverID) + u.RawQuery = q.Encode() + url2 := u.String() + log.Debug().Str("url", url2).Msg("hasJoined request") + resp, err := r.req.Get(url2) + if err != nil { + return nil, fmt.Errorf("http request: %w", err) + } + respBytes := resp.Bytes() + var resp2 HasJoinedResponse + // decode JSON only if HTTP status code is OK + if resp.Response().StatusCode == 200 { + err := json.Unmarshal(respBytes, &resp2) + if err != nil { + log.Error(). + Str("body", string(respBytes)). + Err(err). + Msg("unmarshal response body JSON failed") + } + } + resp2.StatusCode = resp.Response().StatusCode + resp2.RawBody = respBytes + resp2.ServerName = r.name + log.Debug(). + Int("statusCode", resp2.StatusCode). + Str("rawBody", string(respBytes)). + Msg("hasJoined response") + return &resp2, nil +} + +func NewServer(apiPrefix string, opt ...NewServerOptions) (Server, error) { + name := "" + r := req.New() + if len(opt) > 0 { + if p := opt[0].Proxy; p != "" { + err := r.SetProxyUrl(p) + if err != nil { + return nil, fmt.Errorf("set proxy url: %w", err) + } + } + if t := opt[0].Timeout; t > 0 { + r.SetTimeout(t) + } + if n := opt[0].Name; n != "" { + name = n + } + } + return realServer{ + req: r, + apiPrefix: apiPrefix, + name: name, + }, nil +} + +type NewServerOptions struct { + Name string + Proxy string + // Timeout is HTTP API request timeout + Timeout time.Duration +} diff --git a/yggdrasil/server.go b/yggdrasil/server.go new file mode 100644 index 0000000..818f3ed --- /dev/null +++ b/yggdrasil/server.go @@ -0,0 +1,26 @@ +package yggdrasil + +type Server interface { + // HasJoined returns nil if and only if err != nil + HasJoined(username string, serverID string) (*HasJoinedResponse, error) + // Name returns a human-readable, unique name of this server + Name() string +} + +type HasJoinedResponse struct { + StatusCode int `json:"-"` + RawBody []byte `json:"-"` + ServerName string `json:"-"` + + ID string `json:"id"` + Name string `json:"name"` + Properties []struct { + Name string `json:"name"` + Value string `json:"value"` + Signature string `json:"signature,omitempty"` + } `json:"properties"` +} + +func (r HasJoinedResponse) HasJoined() bool { + return r.StatusCode == 200 && r.ID != "" && r.Name != "" +} -- cgit v1.2.3