fix(nano): always relay properties as an array
sessionProfile tagged properties with omitempty, so an upstream answer of "properties": [] (or null, or no key at all) reached Velocity with no properties key. A Yggdrasil root may legitimately answer that way for a player without a skin. Velocity 3.5.1's GameProfile deserializer passes the missing key on as null and ImmutableList.copyOf throws, so that player hangs at login with nothing logged, even though the same answer sent straight to Velocity is accepted. Mojang always sends textures, which is why the premium path and the hardware runs never hit it. Drop omitempty and replace a nil slice with an empty one before the response is written. Removing omitempty alone is not enough: a nil slice marshals as null, which Velocity rejects the same way. The new subtest feeds the relay [], null and a missing key and expects "properties":[] every time. The previous handler fails all three.
This commit is contained in:
2 files changed
+35
-2
No files matched your search
@@ -66,11 +66,14 @@ type AuthSource struct {
|
||||
}
|
||||
|
||||
// sessionProfile is the Mojang hasJoined contract. properties is relayed verbatim
|
||||
// (json.RawMessage) so a source's signed textures survive the multiplexer untouched.
|
||||
// (json.RawMessage) so a source's signed textures survive the multiplexer untouched, and
|
||||
// it is always emitted as an array: Velocity's GameProfile parser throws on a missing or
|
||||
// null properties key, while a Yggdrasil root may legitimately send [] or omit it for a
|
||||
// player with no skin.
|
||||
type sessionProfile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Properties []json.RawMessage `json:"properties,omitempty"`
|
||||
Properties []json.RawMessage `json:"properties"`
|
||||
}
|
||||
|
||||
// HasJoinedHandler returns an http.Handler serving only the Felis-nano hasJoined
|
||||
@@ -151,6 +154,9 @@ func (a *API) handleHasJoined(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Emit the canonical UUID undashed — the 32-hex form authlib's GameProfile expects.
|
||||
prof.ID = hex.EncodeToString(canonical[:])
|
||||
if prof.Properties == nil {
|
||||
prof.Properties = []json.RawMessage{} // a nil slice would marshal as null
|
||||
}
|
||||
writeJSON(w, http.StatusOK, prof)
|
||||
}
|
||||
|
||||
|
||||
@@ -189,6 +189,33 @@ func TestHasJoined(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// A skinless player's profile may come back with properties [], null or absent. The
|
||||
// relay must still send an array: Velocity's GameProfile parser throws on a missing or
|
||||
// null key and the login hangs, where the same answer sent straight to Velocity works.
|
||||
t.Run("properties always emitted as an array", func(t *testing.T) {
|
||||
for _, upstream := range []string{
|
||||
`{"id":"` + notchMojangID + `","name":"Notch","properties":[]}`,
|
||||
`{"id":"` + notchMojangID + `","name":"Notch","properties":null}`,
|
||||
`{"id":"` + notchMojangID + `","name":"Notch"}`,
|
||||
} {
|
||||
src := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(upstream))
|
||||
}))
|
||||
api := newTestAPI(newFakeRepo(), newFakeCluster())
|
||||
api.AuthSources = []AuthSource{{Tag: "mojang", URL: src.URL, Identity: true}}
|
||||
|
||||
w := getHasJoined(api.InternalHandler(), "Notch", "abc")
|
||||
src.Close()
|
||||
var body map[string]json.RawMessage
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || w.Code != http.StatusOK {
|
||||
t.Fatalf("upstream %s: code = %d body = %q", upstream, w.Code, w.Body.String())
|
||||
}
|
||||
if got := string(body["properties"]); got != "[]" {
|
||||
t.Errorf("upstream %s: properties = %q, want []", upstream, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// A root that answers with a redirect is skipped, not followed: following it lets that
|
||||
// root aim this host at arbitrary URLs, including its own hasJoined route, which re-enters
|
||||
// the scan and multiplies the upstream traffic one login causes.
|
||||
|
||||
Reference in new issue
Block a user