summaryrefslogtreecommitdiff
path: root/yggdrasil
diff options
context:
space:
mode:
authorKeuin <[email protected]>2024-02-12 18:16:16 +0800
committerKeuin <[email protected]>2024-02-12 18:18:45 +0800
commita89cbe5a93aede3703cd5981ea71827b55db0866 (patch)
tree165089cb6c94e00c5f00d57da1a83e84c46722cb /yggdrasil
initial version
Diffstat (limited to 'yggdrasil')
-rw-r--r--yggdrasil/muxserver.go94
-rw-r--r--yggdrasil/realserver.go88
-rw-r--r--yggdrasil/server.go26
3 files changed, 208 insertions, 0 deletions
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 := "<unnamed server>"
+ 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 != ""
+}