LoadNano filled in [server] listen = "0.0.0.0:8080" when it was unset, and a test pinned that value, but felis nano never reads it: it binds the -listen flag, which the installer sets from FELIS_NANO_LISTEN. An operator moving nano off loopback by writing [server] listen in its config got connection refused from the proxy and no hint that the key did nothing. LoadNano no longer sets the default, and nano prints a line naming the ignored value and the address it actually binds whenever the key is set. It is a warning rather than a load error so a full felis.toml copied onto a nano host keeps starting. The assertion that pinned the unused default is removed along with it. The new test runs cmdNano against a config that sets [server] listen and one that does not, with an unbindable -listen so it returns after loading. The first must warn and the second must not; with the old default restored, the second prints a warning about 0.0.0.0:8080.
111 lines
3.7 KiB
Go
111 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// [server] listen in a nano config reads like the bind address but is not one; nano must
|
|
// say so. The -listen value cannot be bound, so cmdNano returns right after loading.
|
|
func TestNanoWarnsThatServerListenIsIgnored(t *testing.T) {
|
|
cfg := filepath.Join(t.TempDir(), "felis.toml")
|
|
if err := os.WriteFile(cfg, []byte("[server]\nlisten = \"0.0.0.0:9999\"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stderr bytes.Buffer
|
|
if rc := cmdNano([]string{"-config", cfg, "-listen", "127.0.0.1:-1"}, io.Discard, &stderr); rc != 1 {
|
|
t.Fatalf("cmdNano = %d, want 1 from the unbindable -listen", rc)
|
|
}
|
|
if !strings.Contains(stderr.String(), `listen = "0.0.0.0:9999" is ignored`) {
|
|
t.Fatalf("stderr %q should say the configured listen is ignored", stderr.String())
|
|
}
|
|
|
|
// With no [server] table at all there is nothing to warn about.
|
|
if err := os.WriteFile(cfg, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stderr.Reset()
|
|
_ = cmdNano([]string{"-config", cfg, "-listen", "127.0.0.1:-1"}, io.Discard, &stderr)
|
|
if strings.Contains(stderr.String(), "is ignored") {
|
|
t.Fatalf("stderr %q warns about a listen the operator never set", stderr.String())
|
|
}
|
|
}
|
|
|
|
// A stop signal that lands while a login is waiting on an upstream must let that login
|
|
// finish: the request is answered, and serveNano returns only afterwards.
|
|
func TestNanoDrainsInFlightLoginOnShutdown(t *testing.T) {
|
|
entered, release := make(chan struct{}), make(chan struct{})
|
|
srv := newAPIServer("", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(entered)
|
|
<-release
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
done := make(chan int, 1)
|
|
go func() { done <- serveNano(ctx, srv, ln, io.Discard) }()
|
|
|
|
got := make(chan int, 1)
|
|
go func() {
|
|
resp, err := http.Get("http://" + ln.Addr().String() + "/session/minecraft/hasJoined")
|
|
if err != nil {
|
|
got <- -1
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
got <- resp.StatusCode
|
|
}()
|
|
<-entered
|
|
stop()
|
|
select {
|
|
case <-done:
|
|
t.Fatal("serveNano returned while a login was still in flight")
|
|
case <-time.After(200 * time.Millisecond):
|
|
}
|
|
close(release)
|
|
if code := <-got; code != http.StatusNoContent {
|
|
t.Fatalf("in-flight login got %d, want its answer (204)", code)
|
|
}
|
|
if rc := <-done; rc != 0 {
|
|
t.Fatalf("serveNano = %d after a clean drain, want 0", rc)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// any source.
|
|
func TestNanoRequestLogIsQuotedAndCapped(t *testing.T) {
|
|
const rlo = rune(0x202e) // RIGHT-TO-LEFT OVERRIDE
|
|
var log bytes.Buffer
|
|
h := nanoHandler(nil, &log)
|
|
target := "/session/minecraft/hasJoined?username=" + string(rlo) + "evil" + string([]byte{0x9b}) + "31m" + strings.Repeat("a", 4096)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
|
|
|
|
line := log.String()
|
|
if strings.ContainsRune(line, rlo) || !utf8.ValidString(line) {
|
|
t.Fatalf("raw caller bytes reached the log: %q", line)
|
|
}
|
|
if escaped := strings.Trim(strconv.QuoteRune(rlo), "'"); !strings.Contains(line, escaped) {
|
|
t.Fatalf("log line %q should show the override escaped as %s", line, escaped)
|
|
}
|
|
if len(line) > 2*nanoLogURIMax {
|
|
t.Fatalf("log line is %d bytes for a %d-byte URI; want it capped", len(line), len(target))
|
|
}
|
|
}
|