d417efc got this backwards, in both the code comment and the installer summary.
It claimed authlib appends /session/minecraft/hasJoined itself, so the property
should be given the base URL only. Velocity does not work that way, and a real
login says so: pointed at http://127.0.0.1:8081, a Mojang login arrives at nano
as
GET /?username=FLYEMOJ1&serverId=-23ae0b50...
with no path at all. Velocity appends the query string to the property verbatim
and issues the request itself; authlib is not in the loop. nano has no route on
/, so it answers 404 and Velocity kicks the player with authservers_down.
Velocity's own default for the property is the full URL,
https://sessionserver.mojang.com/session/minecraft/hasJoined, which is the same
thing said another way.
With the full endpoint URL the same account logs straight in, so both the
nano.go header and summary_nano now print
-Dmojang.sessionserver=http://127.0.0.1:8081/session/minecraft/hasJoined
and note that the flag belongs between `java` and `-jar`.
Verified against Velocity 3.5.1 + Paper 26.2 on the deploy host: a Mojang login
reaches the backend with its real Mojang UUID unchanged, and a LittleSkin login
under the same username reaches it as UUIDv3(felisAuthNS, "littleskin:"+id) --
two different players on the backend, which is the point.
77 lines
3.4 KiB
Go
77 lines
3.4 KiB
Go
package main
|
|
|
|
// felis nano: the Felis-nano hasJoined multiplexer as a felis subcommand — the lightweight
|
|
// serve path for a third-party server operator who wants multi-Yggdrasil federation without a
|
|
// full Felis control plane (no k3s, no Postgres, no DB). It reads [[auth_source]] from
|
|
// felis.toml, leads with Mojang as the code-owned identity anchor (正版优先), and serves the
|
|
// vanilla sessionserver hasJoined endpoint. Point Velocity at it with
|
|
// -Dmojang.sessionserver=http://127.0.0.1:8081/session/minecraft/hasJoined
|
|
// — Velocity's property takes the FULL endpoint URL, path included (its default is the
|
|
// full https://sessionserver.mojang.com/session/minecraft/hasJoined), and Velocity issues
|
|
// that request itself rather than through authlib. Then it verifies logins against Mojang
|
|
// plus every configured third-party source. Serving a proxy on another host means binding
|
|
// off-loopback with -listen; see the flag below for why that is an explicit opt-in.
|
|
//
|
|
// This is the no-database delivery of the identical brain `felis api` mounts through its
|
|
// route table (internal/api.HasJoinedHandler). `felis setup --nano` / the bootstrap nano
|
|
// choice install this as the runtime service; here it just serves.
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"felis.lolicon.best/internal/api"
|
|
"felis.lolicon.best/internal/config"
|
|
)
|
|
|
|
// nanoStubRepo satisfies api.Repo but implements only the one method handleHasJoined calls.
|
|
// The reclaim username blacklist is a felis-api/DB concern; a nano host has no Postgres, so
|
|
// nothing is barred here. ponytail: a real blacklist would need the very DB nano exists to
|
|
// avoid — YAGNI until a nano host grows a reclaim store.
|
|
type nanoStubRepo struct{ api.Repo }
|
|
|
|
func (nanoStubRepo) IsUsernameBlacklisted(context.Context, string) (bool, error) { return false, nil }
|
|
|
|
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")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
|
|
cfg, err := config.LoadNano(*cfgPath)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "felis nano:", err)
|
|
return 1
|
|
}
|
|
|
|
handler := api.HasJoinedHandler(authSourcesFromConfig(cfg.AuthSources), nanoStubRepo{})
|
|
fmt.Fprintf(stderr, "felis nano: hasJoined multiplexer on %s — Mojang + %d third-party source(s)\n", *listen, len(cfg.AuthSources))
|
|
for i, s := range cfg.AuthSources {
|
|
fmt.Fprintf(stderr, " [%d] %s -> %s\n", i+1, s.Tag, s.URL)
|
|
}
|
|
|
|
// Log each request so a live login attempt is visible while testing against a real
|
|
// Velocity — "is authlib even reaching me?" is the first question during verification.
|
|
logged := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(stderr, "felis nano: %s %s\n", r.Method, r.RequestURI)
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
|
|
srv := newAPIServer(*listen, logged)
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
fmt.Fprintln(stderr, "felis nano:", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|