Unverified Commit 59ec23d4 authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

test(nano): cover the nano delivery path and its loopback default

felis nano serves the same hasJoined handler as felis api, but behind
nanoStubRepo, which implements only the bar-list lookup and embeds a nil
Repo for everything else. Only the full-api path was tested, against a
complete fake store, so a second store call added to handleHasJoined
would pass CI and panic on every nano login. The loopback default of
-listen, the one thing keeping nano from being an open auth relay, was
not pinned either.

The default moves into a nanoDefaultListen constant, and two tests
cover the path. One serves a login through api.HasJoinedHandler with
nanoStubRepo and a fake identity source and expects the profile back.
The other requires the default to parse as a loopback IP. Taking the
bar-list method off the stub makes the first panic on the nil Repo;
defaulting to 0.0.0.0:8081 or :8081 fails the second.
parent e0ad78af
Loading
Loading
Loading
Loading
+7 −5
Changes for cmd/felis/nano.go: 7 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -48,6 +48,12 @@ type nanoStubRepo struct{ api.Repo }

func (nanoStubRepo) IsUsernameBlacklisted(context.Context, string) (bool, error) { return false, nil }

// nanoDefaultListen is loopback because hasJoined carries no auth token (Velocity speaks the
// vanilla sessionserver protocol), so a public bind is an open auth relay: anyone can point
// their proxy at it and spend this host's egress IP on Mojang. A same-host Velocity reaches
// 127.0.0.1; serving an off-host proxy is an explicit -listen opt-in.
const nanoDefaultListen = "127.0.0.1:8081"

// nanoLogURIMax is room for a real hasJoined query (a 16-character name, a 41-character
// serverId, an address) several times over.
const nanoLogURIMax = 256
@@ -56,11 +62,7 @@ func cmdNano(args []string, stdout, stderr io.Writer) int {
	fs := flag.NewFlagSet("nano", flag.ContinueOnError)
	fs.SetOutput(stderr)
	cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml (reads [[auth_source]])")
	// Loopback default: hasJoined carries no auth token (authlib speaks the vanilla
	// sessionserver protocol), so a public bind is an open auth relay — anyone can point
	// their proxy at it and spend this host's egress IP on Mojang. A same-host Velocity
	// reaches 127.0.0.1; serving an off-host proxy is an explicit -listen opt-in.
	listen := fs.String("listen", "127.0.0.1:8081", "listen address for the hasJoined endpoint")
	listen := fs.String("listen", nanoDefaultListen, "listen address for the hasJoined endpoint")
	if err := fs.Parse(args); err != nil {
		return 2
	}
+29 −0
Changes for cmd/felis/nano_test.go: 29 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@ import (
	"testing"
	"time"
	"unicode/utf8"

	"felis.lolicon.best/internal/api"
)

// [server] listen in a nano config reads like the bind address but is not one; nano must
@@ -85,6 +87,33 @@ func TestNanoDrainsInFlightLoginOnShutdown(t *testing.T) {
	}
}

// The nano delivery path: the shared handler behind nano's stub store must admit a login its
// source validated. nanoStubRepo implements only the bar-list lookup, so a new store call in
// handleHasJoined would reach its nil embedded Repo and panic here, while the full-api tests,
// which use a complete fake store, stay green.
func TestNanoAdmitsAValidatedLogin(t *testing.T) {
	const id = "069a79f444e94726a5befca90e38aaf5"
	ygg := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = io.WriteString(w, `{"id":"`+id+`","name":"Notch"}`)
	}))
	defer ygg.Close()
	h := api.HasJoinedHandler([]api.AuthSource{{Tag: "mojang", URL: ygg.URL, Identity: true}}, nanoStubRepo{})
	w := httptest.NewRecorder()
	h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/session/minecraft/hasJoined?username=Notch&serverId=abc", nil))
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), id) {
		t.Fatalf("code = %d body = %q, want the validated profile", w.Code, w.Body.String())
	}
}

// An unauthenticated relay on a public address spends this host's Mojang rate limit for
// anyone who finds it, so the default bind has to stay loopback.
func TestNanoListensOnLoopbackByDefault(t *testing.T) {
	host, _, err := net.SplitHostPort(nanoDefaultListen)
	if ip := net.ParseIP(host); err != nil || ip == nil || !ip.IsLoopback() {
		t.Fatalf("default -listen %q is not a loopback address", nanoDefaultListen)
	}
}

// The request log prints text the caller chose. A bidi override must not reorder the line,
// an invalid byte must not make journald store the entry as a blob, and a huge query must
// not become a huge log line. serverId is left out so the handler answers without asking