fix(panel): 服务器地址改为一键复制并带上非默认游戏端口,玩家列表去掉无意义的连接地址列,管理员列改称内部地址
This commit is contained in:
20 files changed
+379
-59
No files matched your search
@@ -108,6 +108,10 @@ type VelocityConfig struct {
|
||||
// LobbyImage is the container image for the always-on "lobby" hub (Paper plus
|
||||
// the felis-paper /menu plugin). Same skip-when-empty contract as LoginImage.
|
||||
LobbyImage string `toml:"lobby_image"`
|
||||
// GamePort is the public TCP port the proxy accepts players on (bootstrap's
|
||||
// FELIS_GAME_PORT). The panel adds it to the server addresses players copy
|
||||
// when it is not Minecraft's default; 0 means that default, 25565.
|
||||
GamePort int `toml:"game_port"`
|
||||
}
|
||||
|
||||
// AuthConfig is the [auth] table: the two privileged faces and the access-JWT
|
||||
@@ -459,6 +463,9 @@ func (c *Config) Validate() error {
|
||||
default:
|
||||
return fmt.Errorf("config: [registry] build_user_namespaces %q must be auto, on or off", c.Registry.BuildUserNamespaces)
|
||||
}
|
||||
if p := c.Velocity.GamePort; p < 0 || p > 65535 {
|
||||
return fmt.Errorf("config: [velocity] game_port %d must be 1-65535 (0 keeps 25565)", p)
|
||||
}
|
||||
if n := c.Registry.MaxConcurrentBuilds; n < 0 || n > 6 {
|
||||
return fmt.Errorf("config: [registry] max_concurrent_builds %d must be 1-6 (0 keeps 2)", n)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,25 @@ build_user_namespaces = "yes"
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadGamePort(t *testing.T) {
|
||||
base := `
|
||||
[server]
|
||||
root_domain = "mc.example.net"
|
||||
[database]
|
||||
url = "postgres://felis@db/felis"
|
||||
[velocity]
|
||||
`
|
||||
cfg, err := config.Load(writeTOML(t, base+"game_port = 25570\n"))
|
||||
if err != nil || cfg.Velocity.GamePort != 25570 {
|
||||
t.Fatalf("game_port 25570: cfg=%+v err=%v", cfg, err)
|
||||
}
|
||||
for _, bad := range []string{"-1", "65536"} {
|
||||
if _, err := config.Load(writeTOML(t, base+"game_port = "+bad+"\n")); err == nil || !strings.Contains(err.Error(), "game_port") {
|
||||
t.Errorf("game_port %s: err = %v, want it rejected", bad, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesDefaults(t *testing.T) {
|
||||
cfg, err := config.Load(writeTOML(t, `
|
||||
[server]
|
||||
|
||||
+26
-7
@@ -20,11 +20,13 @@ import (
|
||||
var static embed.FS
|
||||
|
||||
type runtimeConfig struct {
|
||||
APIBase string `json:"apiBase"`
|
||||
RootDomain string `json:"rootDomain"`
|
||||
PanelHostname string `json:"panelHostname,omitempty"`
|
||||
AdminHostname string `json:"adminHostname,omitempty"`
|
||||
Build buildInfo `json:"build"`
|
||||
APIBase string `json:"apiBase"`
|
||||
RootDomain string `json:"rootDomain"`
|
||||
PanelHostname string `json:"panelHostname,omitempty"`
|
||||
AdminHostname string `json:"adminHostname,omitempty"`
|
||||
// GamePort is the public Minecraft port, absent when it is the default 25565.
|
||||
GamePort int `json:"gamePort,omitempty"`
|
||||
Build buildInfo `json:"build"`
|
||||
}
|
||||
|
||||
// buildInfo is the resolved build stamp the panel renders in its version badge.
|
||||
@@ -107,8 +109,10 @@ func parseBuildVersion(raw string) buildInfo {
|
||||
// configured console.<root_domain> and op.console.<root_domain> hostnames (either
|
||||
// may be empty when that face is not deployed); they let the SPA detect which home
|
||||
// it is being served from by comparing location.host, so one bundle can render the
|
||||
// right surface (player console vs SysAdmin console) without a rebuild.
|
||||
func Handler(api http.Handler, rootDomain, panelHost, adminHost, version string) http.Handler {
|
||||
// right surface (player console vs SysAdmin console) without a rebuild. gamePort
|
||||
// is the public Minecraft port ([velocity] game_port), which the SPA appends to
|
||||
// the server addresses players copy; 0 or 25565 leaves them bare.
|
||||
func Handler(api http.Handler, rootDomain, panelHost, adminHost string, gamePort int, version string) http.Handler {
|
||||
files, err := fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -118,6 +122,7 @@ func Handler(api http.Handler, rootDomain, panelHost, adminHost, version string)
|
||||
rootDomain: rootDomain,
|
||||
panelHostname: panelHost,
|
||||
adminHostname: adminHost,
|
||||
gamePort: publicGamePort(gamePort),
|
||||
build: parseBuildVersion(version),
|
||||
files: files,
|
||||
fileServer: http.FileServer(http.FS(files)),
|
||||
@@ -125,11 +130,24 @@ func Handler(api http.Handler, rootDomain, panelHost, adminHost, version string)
|
||||
}
|
||||
}
|
||||
|
||||
// defaultGamePort is the port a Minecraft client dials when the address has none.
|
||||
const defaultGamePort = 25565
|
||||
|
||||
// publicGamePort is the port the SPA should print, or 0 when a bare hostname
|
||||
// already reaches the proxy.
|
||||
func publicGamePort(p int) int {
|
||||
if p == defaultGamePort {
|
||||
return 0
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
api http.Handler
|
||||
rootDomain string
|
||||
panelHostname string
|
||||
adminHostname string
|
||||
gamePort int
|
||||
build buildInfo
|
||||
files fs.FS
|
||||
fileServer http.Handler
|
||||
@@ -208,6 +226,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
RootDomain: h.rootDomain,
|
||||
PanelHostname: h.panelHostname,
|
||||
AdminHostname: h.adminHostname,
|
||||
GamePort: h.gamePort,
|
||||
Build: h.build,
|
||||
})
|
||||
case h.hasStaticFile(r.URL.Path):
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestHandlerServesPanelAndConfig(t *testing.T) {
|
||||
}
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
})
|
||||
h := Handler(api, "example.test", "console.example.test", "op.console.example.test", "v1.2.3")
|
||||
h := Handler(api, "example.test", "console.example.test", "op.console.example.test", 0, "v1.2.3")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
@@ -65,6 +65,27 @@ func TestHandlerServesPanelAndConfig(t *testing.T) {
|
||||
// carry. The "+g<sha>" form is what deploy/bootstrap.sh's dev channel links in, and
|
||||
// it is the one that regressed: before it was parsed, a dev build fell through to the
|
||||
// default case and the badge rendered the whole stamp as the release with no commit.
|
||||
// The SPA appends the public game port to the addresses players copy, so
|
||||
// /config.json carries it, except when a bare hostname already reaches the proxy.
|
||||
func TestHandlerPublishesNonDefaultGamePort(t *testing.T) {
|
||||
for _, tc := range []struct{ in, want int }{{0, 0}, {25565, 0}, {25570, 25570}} {
|
||||
h := Handler(http.NotFoundHandler(), "example.test", "", "", tc.in, "v1.2.3")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/config.json", nil))
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil {
|
||||
t.Fatalf("decode config: %v", err)
|
||||
}
|
||||
got, present := cfg["gamePort"]
|
||||
switch {
|
||||
case tc.want == 0 && present:
|
||||
t.Errorf("game port %d: gamePort = %v, want absent", tc.in, got)
|
||||
case tc.want != 0 && got != float64(tc.want):
|
||||
t.Errorf("game port %d: gamePort = %v, want %d", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildVersionSplitsBothStampForms(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
raw string
|
||||
@@ -94,7 +115,7 @@ func TestParseBuildVersionSplitsBothStampForms(t *testing.T) {
|
||||
// its hash (and nothing else inline), cannot be framed, and cache by name:
|
||||
// hashed assets forever, the page itself never without revalidation.
|
||||
func TestHandlerSetsPageSecurityAndCacheHeaders(t *testing.T) {
|
||||
h := Handler(http.NotFoundHandler(), "example.test", "", "", "v1.2.3")
|
||||
h := Handler(http.NotFoundHandler(), "example.test", "", "", 0, "v1.2.3")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
// Through the tunnel: TLS to the origin as well, the edge's scheme in XFP.
|
||||
|
||||
@@ -40,7 +40,7 @@ func newPanelHandler(t *testing.T) http.Handler {
|
||||
api := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
})
|
||||
return Handler(api, "example.test", "", "", "")
|
||||
return Handler(api, "example.test", "", "", 0, "")
|
||||
}
|
||||
|
||||
func TestGuardServesInterstitialForWeChatNavigation(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user