Let the config choose protocol 2 or 3, and fall back for one session only.
The default stays v2. A reflector that refuses that version and names the other gets one session of it, then the station announces the configured protocol again. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -49,7 +49,7 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
challenge := make([]byte, 20)
|
||||
for i := range challenge {
|
||||
challenge[i] = byte(i + 1)
|
||||
@@ -98,6 +98,14 @@ func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []st
|
||||
return loggedIn{st: st, clock: clock, refl: refl, cancel: cancel, id: id, path: path}
|
||||
}
|
||||
|
||||
func expectVersion(t *testing.T, refl *reflector, major, minor uint16) {
|
||||
t.Helper()
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, major, minor) {
|
||||
t.Fatalf("version type %d body %v, want %d.%d", typ, body, major, minor)
|
||||
}
|
||||
}
|
||||
|
||||
func declineTo2(t *testing.T, refl *reflector) {
|
||||
t.Helper()
|
||||
typ, body := refl.readFrame(t)
|
||||
@@ -163,8 +171,8 @@ func TestDropRetriesUntilQuit(t *testing.T) {
|
||||
in.clock.Advance(2 * time.Second)
|
||||
in.refl.accept(t)
|
||||
typ, body := in.refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry protocol type %d body %v, want 3.0", typ, body)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("retry protocol type %d body %v, want 2.0", typ, body)
|
||||
}
|
||||
in.cancel()
|
||||
in.clock.Advance(2 * time.Second)
|
||||
@@ -281,7 +289,7 @@ func TestMissingOpusStopsRetries(t *testing.T) {
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
challenge := make([]byte, 20)
|
||||
refl.writeFrame(t, 10, appendBytes(nil, challenge))
|
||||
refl.readFrame(t)
|
||||
|
||||
+72
-6
@@ -20,6 +20,7 @@ type loaded struct {
|
||||
callsign string
|
||||
authKey string
|
||||
talkGroup int
|
||||
protocolMajor uint16
|
||||
certificateDir string
|
||||
certFatal string
|
||||
enableLogging bool
|
||||
@@ -33,6 +34,7 @@ type fileShape struct {
|
||||
Callsign jsontext.Value `json:"callsign"`
|
||||
AuthKey jsontext.Value `json:"authKey"`
|
||||
TalkGroup jsontext.Value `json:"talkGroup"`
|
||||
Protocol jsontext.Value `json:"protocol"`
|
||||
CertificateDir jsontext.Value `json:"certificateDir"`
|
||||
EnableLogging jsontext.Value `json:"enableLogging"`
|
||||
LogFile jsontext.Value `json:"logFile"`
|
||||
@@ -44,6 +46,7 @@ type templateFile struct {
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
Protocol string `json:"protocol"`
|
||||
CertificateDir string `json:"certificateDir"`
|
||||
EnableLogging bool `json:"enableLogging"`
|
||||
LogFile string `json:"logFile"`
|
||||
@@ -76,13 +79,14 @@ func loadConfig(path string) (configLoad, error) {
|
||||
return parseConfig(path, body, true)
|
||||
}
|
||||
|
||||
// ConfigForm is the eight fields the Config window edits.
|
||||
// ConfigForm is the fields the Config window edits.
|
||||
type ConfigForm struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
Protocol string `json:"protocol"`
|
||||
CertificateDir string `json:"certificateDir"`
|
||||
EnableLogging bool `json:"enableLogging"`
|
||||
LogFile string `json:"logFile"`
|
||||
@@ -101,7 +105,7 @@ func templateForm() (ConfigForm, error) {
|
||||
if err != nil {
|
||||
return ConfigForm{}, err
|
||||
}
|
||||
return ConfigForm{CertificateDir: "./funknetz", LogFile: logFile}, nil
|
||||
return ConfigForm{Protocol: "v2", CertificateDir: "./funknetz", LogFile: logFile}, nil
|
||||
}
|
||||
|
||||
func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
|
||||
@@ -114,7 +118,8 @@ func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
|
||||
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
|
||||
}
|
||||
certBad := present(shape.CertificateDir) && shape.CertificateDir.Kind() != jsontext.KindString
|
||||
if block, notice := blockedLog(shape); block || certBad {
|
||||
protoBad := present(shape.Protocol) && shape.Protocol.Kind() != jsontext.KindString
|
||||
if block, notice := blockedLog(shape); block || certBad || protoBad {
|
||||
cfg, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
return refused(shape, err)
|
||||
@@ -125,7 +130,7 @@ func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
|
||||
}
|
||||
return finish(shape, applyCertificate(cfg, shape, path))
|
||||
}
|
||||
if missingCertificate(shape) || missingLog(shape) {
|
||||
if missingCertificate(shape) || missingLog(shape) || missingProtocol(shape) {
|
||||
if !operationalKindsOK(shape) {
|
||||
_, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
@@ -165,7 +170,7 @@ func refused(shape fileShape, cause error) (configLoad, error) {
|
||||
func formFromShape(shape fileShape) ConfigForm {
|
||||
fallback, err := templateForm()
|
||||
if err != nil {
|
||||
fallback = ConfigForm{CertificateDir: "./funknetz"}
|
||||
fallback = ConfigForm{Protocol: "v2", CertificateDir: "./funknetz"}
|
||||
}
|
||||
return mergeSalvage(shape, fallback)
|
||||
}
|
||||
@@ -192,6 +197,11 @@ func mergeSalvage(shape fileShape, fallback ConfigForm) ConfigForm {
|
||||
fallback.TalkGroup = n
|
||||
}
|
||||
}
|
||||
if s, err := stringField("protocol", shape.Protocol); err == nil && present(shape.Protocol) {
|
||||
if label, ok := protocolLabel(s); ok {
|
||||
fallback.Protocol = label
|
||||
}
|
||||
}
|
||||
if s, err := stringField("certificateDir", shape.CertificateDir); err == nil && present(shape.CertificateDir) {
|
||||
fallback.CertificateDir = s
|
||||
}
|
||||
@@ -225,6 +235,10 @@ func parseOperational(shape fileShape) (loaded, error) {
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
protocolMajor, err := protocolField(shape.Protocol)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
@@ -237,7 +251,45 @@ func parseOperational(shape fileShape) (loaded, error) {
|
||||
if !callsignPattern.MatchString(call) {
|
||||
return loaded{}, fmt.Errorf("callsign is invalid")
|
||||
}
|
||||
return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg}, nil
|
||||
return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg, protocolMajor: protocolMajor}, nil
|
||||
}
|
||||
|
||||
func protocolField(v jsontext.Value) (uint16, error) {
|
||||
if !present(v) {
|
||||
return 2, nil
|
||||
}
|
||||
s, err := stringField("protocol", v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return 2, nil
|
||||
}
|
||||
if _, ok := protocolLabel(s); !ok {
|
||||
return 0, fmt.Errorf("protocol is invalid")
|
||||
}
|
||||
if strings.TrimSpace(s) == "v3" {
|
||||
return 3, nil
|
||||
}
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
func protocolLabel(s string) (string, bool) {
|
||||
switch strings.TrimSpace(s) {
|
||||
case "v2":
|
||||
return "v2", true
|
||||
case "v3":
|
||||
return "v3", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func protocolName(major uint16) string {
|
||||
if major == 3 {
|
||||
return "v3"
|
||||
}
|
||||
return "v2"
|
||||
}
|
||||
|
||||
func stringField(name string, v jsontext.Value) (string, error) {
|
||||
@@ -306,6 +358,14 @@ func missingCertificate(shape fileShape) bool {
|
||||
return !present(shape.CertificateDir)
|
||||
}
|
||||
|
||||
func missingProtocol(shape fileShape) bool {
|
||||
if !present(shape.Protocol) {
|
||||
return true
|
||||
}
|
||||
s, err := stringField("protocol", shape.Protocol)
|
||||
return err == nil && strings.TrimSpace(s) == ""
|
||||
}
|
||||
|
||||
func operationalKindsOK(shape fileShape) bool {
|
||||
if present(shape.Host) && shape.Host.Kind() != jsontext.KindString {
|
||||
return false
|
||||
@@ -322,6 +382,9 @@ func operationalKindsOK(shape fileShape) bool {
|
||||
if present(shape.TalkGroup) && shape.TalkGroup.Kind() != jsontext.KindNumber {
|
||||
return false
|
||||
}
|
||||
if present(shape.Protocol) && shape.Protocol.Kind() != jsontext.KindString {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -348,6 +411,9 @@ func withLogDefaults(shape fileShape) (fileShape, error) {
|
||||
shape.Callsign = rawOr(shape.Callsign, `""`)
|
||||
shape.AuthKey = rawOr(shape.AuthKey, `""`)
|
||||
shape.TalkGroup = rawOr(shape.TalkGroup, "0")
|
||||
if missingProtocol(shape) {
|
||||
shape.Protocol = jsontext.Value(`"v2"`)
|
||||
}
|
||||
shape.CertificateDir = rawOr(shape.CertificateDir, `"./funknetz"`)
|
||||
shape.EnableLogging = rawOr(shape.EnableLogging, "false")
|
||||
if !present(shape.LogFile) {
|
||||
|
||||
+27
-10
@@ -49,6 +49,9 @@ func TestMissingConfigOpensWithoutCreating(t *testing.T) {
|
||||
if form.EnableLogging {
|
||||
t.Fatal("EnableLogging = true, want false")
|
||||
}
|
||||
if form.Protocol != "v2" {
|
||||
t.Fatalf("Protocol = %q, want v2", form.Protocol)
|
||||
}
|
||||
if form.CertificateDir != "./funknetz" {
|
||||
t.Fatalf("CertificateDir = %q, want ./funknetz", form.CertificateDir)
|
||||
}
|
||||
@@ -59,7 +62,7 @@ func TestMissingConfigOpensWithoutCreating(t *testing.T) {
|
||||
|
||||
func TestExistingConfigIsNotRewritten(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"certificateDir\":\"/var/funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"/var/funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||
if err := os.WriteFile(path, original, 0o640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -111,6 +114,8 @@ func TestInvalidConfigNamesTheField(t *testing.T) {
|
||||
{name: "talk group negative", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: -3, want: "talkGroup is invalid"},
|
||||
{name: "talk group type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":"20"}`, want: "talkGroup has the wrong type"},
|
||||
{name: "bad callsign", host: "127.0.0.1", port: 5308, callsign: "HELLO", authKey: "key", talkGroup: 1, want: "callsign is invalid"},
|
||||
{name: "protocol type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":1,"protocol":2}`, want: "protocol has the wrong type"},
|
||||
{name: "protocol value", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":1,"protocol":"v1"}`, want: "protocol is invalid"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -281,6 +286,9 @@ func TestMissingLogKeysAreRewritten(t *testing.T) {
|
||||
if got["logFile"] != filepath.Join(home, "funknetz.log") {
|
||||
t.Fatalf("logFile = %#v", got["logFile"])
|
||||
}
|
||||
if got["protocol"] != "v2" {
|
||||
t.Fatalf("protocol = %#v, want v2", got["protocol"])
|
||||
}
|
||||
if got["certificateDir"] != "./funknetz" {
|
||||
t.Fatalf("certificateDir = %#v, want ./funknetz", got["certificateDir"])
|
||||
}
|
||||
@@ -359,9 +367,9 @@ func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
||||
detail string
|
||||
same bool
|
||||
}{
|
||||
{name: "relative", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":"funknetz.log"}`, detail: "Cannot write funknetz.log", same: true},
|
||||
{name: "empty", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":" "}`, detail: "Cannot write logFile", same: true},
|
||||
{name: "tilde", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":"./funknetz","enableLogging":true,"logFile":"~/funknetz.log"}`, detail: "Cannot write ~/funknetz.log", same: true},
|
||||
{name: "relative", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":"funknetz.log"}`, detail: "Cannot write funknetz.log", same: true},
|
||||
{name: "empty", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":" "}`, detail: "Cannot write logFile", same: true},
|
||||
{name: "tilde", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":"~/funknetz.log"}`, detail: "Cannot write ~/funknetz.log", same: true},
|
||||
{name: "enable type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":"/tmp/funknetz.log"}`, detail: "enableLogging has the wrong type", same: true},
|
||||
{name: "file type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":1}`, detail: "logFile has the wrong type", same: true},
|
||||
{name: "both types", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":1}`, detail: "enableLogging has the wrong type", same: true},
|
||||
@@ -420,7 +428,7 @@ func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
||||
|
||||
func assertKeyOrder(t *testing.T, body []byte) {
|
||||
t.Helper()
|
||||
keys := []string{`"host"`, `"port"`, `"callsign"`, `"authKey"`, `"talkGroup"`, `"certificateDir"`, `"enableLogging"`, `"logFile"`}
|
||||
keys := []string{`"host"`, `"port"`, `"callsign"`, `"authKey"`, `"talkGroup"`, `"protocol"`, `"certificateDir"`, `"enableLogging"`, `"logFile"`}
|
||||
last := -1
|
||||
for _, key := range keys {
|
||||
i := strings.Index(string(body), key)
|
||||
@@ -533,7 +541,7 @@ func TestValidSaveCreatesFileAndConnects(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := fmt.Sprintf("{\"host\":\"127.0.0.1\",\"port\":%d,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":%q}\n", port, logFile)
|
||||
want := fmt.Sprintf("{\"host\":\"127.0.0.1\",\"port\":%d,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":%q}\n", port, logFile)
|
||||
if string(body) != want {
|
||||
t.Fatalf("file = %s, want %s", body, want)
|
||||
}
|
||||
@@ -570,7 +578,7 @@ func TestMissingCertificateDirIsWrittenAfterTalkGroup(t *testing.T) {
|
||||
if err := jsonv2.Unmarshal(body, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["authKey"] != "a b" || got["callsign"] != "DL1ABC" || got["certificateDir"] != "./funknetz" {
|
||||
if got["authKey"] != "a b" || got["callsign"] != "DL1ABC" || got["certificateDir"] != "./funknetz" || got["protocol"] != "v2" {
|
||||
t.Fatalf("fields = %#v", got)
|
||||
}
|
||||
if strings.Contains(string(body), t.TempDir()) {
|
||||
@@ -597,7 +605,7 @@ func TestCertificateDirStopsBeforeASocket(t *testing.T) {
|
||||
}
|
||||
defer ln.Close()
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
body := fmt.Sprintf(`{"host":"127.0.0.1","port":%d,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"certificateDir":%s,"enableLogging":false,"logFile":"/tmp/funknetz.log"}`, port, tt.dir)
|
||||
body := fmt.Sprintf(`{"host":"127.0.0.1","port":%d,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":%s,"enableLogging":false,"logFile":"/tmp/funknetz.log"}`, port, tt.dir)
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -635,13 +643,22 @@ func TestCertificateDirStopsBeforeASocket(t *testing.T) {
|
||||
|
||||
func configJSON(t testing.TB, host string, port int, callsign, authKey string, talkGroup int) string {
|
||||
t.Helper()
|
||||
body, err := jsonv2.Marshal(map[string]any{
|
||||
return configJSONProtocol(t, host, port, callsign, authKey, talkGroup, "")
|
||||
}
|
||||
|
||||
func configJSONProtocol(t testing.TB, host string, port int, callsign, authKey string, talkGroup int, protocol string) string {
|
||||
t.Helper()
|
||||
fields := map[string]any{
|
||||
"host": host,
|
||||
"port": port,
|
||||
"callsign": callsign,
|
||||
"authKey": authKey,
|
||||
"talkGroup": talkGroup,
|
||||
})
|
||||
}
|
||||
if protocol != "" {
|
||||
fields["protocol"] = protocol
|
||||
}
|
||||
body, err := jsonv2.Marshal(fields)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@ func (s *Station) applyLogging(prev, next loaded) {
|
||||
|
||||
func connectionChanged(prev, next loaded) bool {
|
||||
return prev.host != next.host || prev.port != next.port || prev.callsign != next.callsign ||
|
||||
prev.authKey != next.authKey || prev.talkGroup != next.talkGroup || prev.certificateDir != next.certificateDir
|
||||
prev.authKey != next.authKey || prev.talkGroup != next.talkGroup || prev.protocolMajor != next.protocolMajor ||
|
||||
prev.certificateDir != next.certificateDir
|
||||
}
|
||||
|
||||
func readForm(path string) (ConfigForm, string, bool) {
|
||||
@@ -147,7 +148,7 @@ func readForm(path string) (ConfigForm, string, bool) {
|
||||
if err != nil {
|
||||
form, ferr := templateForm()
|
||||
if ferr != nil {
|
||||
form = ConfigForm{CertificateDir: "./funknetz"}
|
||||
form = ConfigForm{Protocol: "v2", CertificateDir: "./funknetz"}
|
||||
}
|
||||
return form, fmt.Errorf("the file could not be read: %w", err).Error(), false
|
||||
}
|
||||
@@ -189,6 +190,7 @@ func validateForm(configPath string, form ConfigForm) (loaded, ConfigForm, error
|
||||
Callsign: cfg.callsign,
|
||||
AuthKey: cfg.authKey,
|
||||
TalkGroup: cfg.talkGroup,
|
||||
Protocol: protocolName(cfg.protocolMajor),
|
||||
CertificateDir: strings.TrimSpace(form.CertificateDir),
|
||||
EnableLogging: form.EnableLogging,
|
||||
LogFile: form.LogFile,
|
||||
@@ -217,6 +219,10 @@ func shapeFromForm(form ConfigForm) (fileShape, error) {
|
||||
if err != nil {
|
||||
return fileShape{}, err
|
||||
}
|
||||
protocol, err := jsonValue(form.Protocol)
|
||||
if err != nil {
|
||||
return fileShape{}, err
|
||||
}
|
||||
dir, err := jsonValue(form.CertificateDir)
|
||||
if err != nil {
|
||||
return fileShape{}, err
|
||||
@@ -235,6 +241,7 @@ func shapeFromForm(form ConfigForm) (fileShape, error) {
|
||||
Callsign: call,
|
||||
AuthKey: key,
|
||||
TalkGroup: tg,
|
||||
Protocol: protocol,
|
||||
CertificateDir: dir,
|
||||
EnableLogging: en,
|
||||
LogFile: logFile,
|
||||
@@ -256,6 +263,7 @@ func writeForm(path string, form ConfigForm) error {
|
||||
Callsign: form.Callsign,
|
||||
AuthKey: form.AuthKey,
|
||||
TalkGroup: form.TalkGroup,
|
||||
Protocol: form.Protocol,
|
||||
CertificateDir: form.CertificateDir,
|
||||
EnableLogging: form.EnableLogging,
|
||||
LogFile: form.LogFile,
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestSaveRewritesAndReconnects(t *testing.T) {
|
||||
assertKeyOrder(t, body)
|
||||
|
||||
in.refl.accept(t)
|
||||
declineTo2(t, in.refl)
|
||||
expectVersion(t, in.refl, 2, 0)
|
||||
challenge := make([]byte, 20)
|
||||
for i := range challenge {
|
||||
challenge[i] = byte(i + 1)
|
||||
@@ -439,6 +439,7 @@ func TestEachConnectionFieldReconnects(t *testing.T) {
|
||||
{name: "host", edit: func(f *station.ConfigForm, _ int) { f.Host = "localhost" }},
|
||||
{name: "callsign", edit: func(f *station.ConfigForm, _ int) { f.Callsign = "DL1ABD" }},
|
||||
{name: "authKey", edit: func(f *station.ConfigForm, _ int) { f.AuthKey = "other-key" }},
|
||||
{name: "protocol", edit: func(f *station.ConfigForm, _ int) { f.Protocol = "v3" }},
|
||||
{name: "certificateDir", edit: func(f *station.ConfigForm, _ int) { f.CertificateDir = "./radios" }},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -488,7 +489,7 @@ func TestEachConnectionFieldReconnects(t *testing.T) {
|
||||
}
|
||||
|
||||
func fullConfig(host string, port int, callsign, authKey string, talkGroup int, logFile string) string {
|
||||
return "{\"host\":\"" + host + "\",\"port\":" + strconv.Itoa(port) + ",\"callsign\":\"" + callsign + "\",\"authKey\":\"" + authKey + "\",\"talkGroup\":" + strconv.Itoa(talkGroup) + ",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":\"" + logFile + "\"}\n"
|
||||
return "{\"host\":\"" + host + "\",\"port\":" + strconv.Itoa(port) + ",\"callsign\":\"" + callsign + "\",\"authKey\":\"" + authKey + "\",\"talkGroup\":" + strconv.Itoa(talkGroup) + ",\"protocol\":\"v2\",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":\"" + logFile + "\"}\n"
|
||||
}
|
||||
|
||||
func connStillOpen(conn net.Conn) error {
|
||||
|
||||
@@ -194,6 +194,23 @@ func parseVersion(b []byte) (uint16, uint16, error) {
|
||||
}
|
||||
|
||||
var atMostVersion = regexp.MustCompile(`^Unsupported protocol version \d+\.\d+\. May be at most (\d+)\.(\d+)\.$`)
|
||||
var atLeastVersion = regexp.MustCompile(`^Unsupported protocol version \d+\.\d+\. Must be at least (\d+)\.(\d+)\.$`)
|
||||
|
||||
// wantsOther reports that the reflector refused the announced major and named the other one.
|
||||
func wantsOther(announced uint16, msg string) bool {
|
||||
if announced >= 3 {
|
||||
return fallback20(msg)
|
||||
}
|
||||
return fallback30(msg)
|
||||
}
|
||||
|
||||
// acceptsOther reports that a downgrade names the protocol this station did not announce.
|
||||
func acceptsOther(announced, gotMajor, gotMinor uint16) bool {
|
||||
if announced >= 3 {
|
||||
return gotMajor == 2
|
||||
}
|
||||
return gotMajor == 3 && gotMinor == 0
|
||||
}
|
||||
|
||||
// fallback20 reports that the reflector's maximum is at least 2.0 and below 3.0.
|
||||
func fallback20(msg string) bool {
|
||||
@@ -209,6 +226,20 @@ func fallback20(msg string) bool {
|
||||
return !versionBelow(major, minor, 2, 0) && versionBelow(major, minor, 3, 0)
|
||||
}
|
||||
|
||||
// fallback30 reports that the reflector's minimum is exactly 3.0.
|
||||
func fallback30(msg string) bool {
|
||||
m := atLeastVersion.FindStringSubmatch(msg)
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
major, err1 := strconv.Atoi(m[1])
|
||||
minor, err2 := strconv.Atoi(m[2])
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return major == 3 && minor == 0
|
||||
}
|
||||
|
||||
func versionBelow(major, minor, maj, min int) bool {
|
||||
if major != maj {
|
||||
return major < maj
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestUDPLimitRunsBeforeAnyDatagram(t *testing.T) {
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
for step := 0; step < 6; step++ {
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(10 * time.Second)
|
||||
@@ -144,8 +144,8 @@ func TestUDPLimitRunsBeforeAnyDatagram(t *testing.T) {
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 3.0", typ, body)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 2.0", typ, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestOversizedFrameBeforeAuthRetries(t *testing.T) {
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
payload := make([]byte, 65)
|
||||
var hdr [4]byte
|
||||
binary.BigEndian.PutUint32(hdr[:], 65)
|
||||
@@ -178,8 +178,8 @@ func TestOversizedFrameBeforeAuthRetries(t *testing.T) {
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 3.0", typ, body)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 2.0", typ, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestApplicationMessageBeforeLoginRetries(t *testing.T) {
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
declineTo2(t, refl)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
refl.writeFrame(t, 100, serverInfo(1, nil, []string{"OPUS"}))
|
||||
got, err := st.Wait(t.Context(), func(s station.State) bool { return s.Detail == "Protocol error" })
|
||||
if err != nil {
|
||||
@@ -211,8 +211,8 @@ func TestApplicationMessageBeforeLoginRetries(t *testing.T) {
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("retry type %d body %v", typ, body)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("retry type %d body %v, want 2.0", typ, body)
|
||||
}
|
||||
if st.State().Detail != "Protocol error" {
|
||||
t.Fatalf("Detail = %q, want the reason kept", st.State().Detail)
|
||||
|
||||
@@ -41,6 +41,7 @@ func configWithDir(t testing.TB, host string, port int, callsign, authKey string
|
||||
"callsign": callsign,
|
||||
"authKey": authKey,
|
||||
"talkGroup": talkGroup,
|
||||
"protocol": "v3",
|
||||
"certificateDir": certDir,
|
||||
"enableLogging": false,
|
||||
"logFile": logPath,
|
||||
@@ -509,7 +510,7 @@ func TestCADigestMismatchIsLoggedAndKept(t *testing.T) {
|
||||
}
|
||||
body, err := jsonv2.Marshal(map[string]any{
|
||||
"host": "127.0.0.1", "port": refl.port, "callsign": "DL1ABC", "authKey": "key",
|
||||
"talkGroup": 20, "certificateDir": certDir, "enableLogging": true, "logFile": logPath,
|
||||
"talkGroup": 20, "protocol": "v3", "certificateDir": certDir, "enableLogging": true, "logFile": logPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestVersionErrorFallsBackWhenTheMaximumIs2(t *testing.T) {
|
||||
} {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -51,7 +51,7 @@ func TestVersionErrorBelow2Stops(t *testing.T) {
|
||||
} {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -93,7 +93,7 @@ func TestDowngradeOtherThan2Stops(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.want, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -150,3 +150,65 @@ func TestDowngradeDoesNotCreateTheCertificateDirectory(t *testing.T) {
|
||||
t.Fatal("created the certificate directory on a 2.0 fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredV2FallsBackTo3ThenReturns(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() {
|
||||
// Run's error is observed through State, which is the station seam.
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
refl.writeFrame(t, 13, appendStr(nil, "Unsupported protocol version 2.0. Must be at least 3.0."))
|
||||
refl.accept(t)
|
||||
expectVersion(t, refl, 3, 0)
|
||||
refl.tcp.Close()
|
||||
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Disconnected" }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock.BlockUntilWaiting(t)
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.accept(t)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
}
|
||||
|
||||
func TestV2DoesNotFallBackAbove3(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go func() {
|
||||
// Run's error is observed through State, which is the station seam.
|
||||
_ = st.Run(ctx)
|
||||
}()
|
||||
refl.accept(t)
|
||||
expectVersion(t, refl, 2, 0)
|
||||
msg := "Unsupported protocol version 2.0. Must be at least 3.1."
|
||||
refl.writeFrame(t, 13, appendStr(nil, msg))
|
||||
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == msg })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Connection != "Disconnected" || got.Protocol != "" {
|
||||
t.Fatalf("State = %+v", got)
|
||||
}
|
||||
clock.Advance(2 * time.Second)
|
||||
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
|
||||
if conn, err := refl.ln.Accept(); err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("retried")
|
||||
}
|
||||
}
|
||||
|
||||
+36
-15
@@ -30,11 +30,15 @@ var (
|
||||
// Run keeps the reflector session until ctx is canceled.
|
||||
func (s *Station) Run(ctx context.Context) error {
|
||||
defer s.Close()
|
||||
major := uint16(3)
|
||||
var major uint16
|
||||
holdMajor := false
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
if !holdMajor {
|
||||
major = s.configuredMajor()
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.fatal != "" || !s.loaded {
|
||||
s.mu.Unlock()
|
||||
@@ -61,6 +65,7 @@ func (s *Station) Run(ctx context.Context) error {
|
||||
stale := s.epoch != epoch
|
||||
s.mu.Unlock()
|
||||
if stale {
|
||||
holdMajor = false
|
||||
if conn != nil {
|
||||
// This dial used the config from before Save.
|
||||
_ = conn.Close()
|
||||
@@ -72,7 +77,6 @@ func (s *Station) Run(ctx context.Context) error {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
major = 3
|
||||
s.mu.Lock()
|
||||
s.noteDownLocked(s.clock.Now(), true)
|
||||
s.mu.Unlock()
|
||||
@@ -82,7 +86,7 @@ func (s *Station) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.session(ctx, conn, major, epoch)
|
||||
err = s.session(ctx, conn, major, epoch, major == s.configuredMajor())
|
||||
conn.Close()
|
||||
fallback := errors.Is(err, errFallback)
|
||||
reconnect := errors.Is(err, errReconnect)
|
||||
@@ -111,9 +115,10 @@ func (s *Station) Run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
if fallback {
|
||||
major = 2
|
||||
major = otherMajor(major)
|
||||
holdMajor = true
|
||||
} else {
|
||||
major = 3
|
||||
holdMajor = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -124,7 +129,7 @@ func (s *Station) Run(ctx context.Context) error {
|
||||
}
|
||||
fatal := s.fatal != ""
|
||||
s.mu.Unlock()
|
||||
major = 3
|
||||
holdMajor = false
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -297,7 +302,23 @@ const (
|
||||
phChallenge
|
||||
)
|
||||
|
||||
func (s *Station) session(ctx context.Context, conn net.Conn, major uint16, epoch uint64) error {
|
||||
func (s *Station) configuredMajor() uint16 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cfg.protocolMajor == 3 {
|
||||
return 3
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func otherMajor(major uint16) uint16 {
|
||||
if major == 2 {
|
||||
return 3
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func (s *Station) session(ctx context.Context, conn net.Conn, major uint16, epoch uint64, allowFallback bool) error {
|
||||
s.mu.Lock()
|
||||
s.conn = conn
|
||||
stale := s.epoch != epoch
|
||||
@@ -428,7 +449,7 @@ func (s *Station) session(ctx context.Context, conn net.Conn, major uint16, epoc
|
||||
if udpRx.Before(ex.limit) {
|
||||
ex.limit = udpRx
|
||||
}
|
||||
err := s.onFrame(ctx, wire, major, tcpf, ex)
|
||||
err := s.onFrame(ctx, wire, major, tcpf, ex, allowFallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -512,14 +533,14 @@ func (s *Station) onClock(now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f frame, ex *exchange) error {
|
||||
func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f frame, ex *exchange, allowFallback bool) error {
|
||||
switch f.typ {
|
||||
case tcpHeartbeat:
|
||||
return nil
|
||||
case tcpError:
|
||||
return s.onReflectorError(f.body, major)
|
||||
return s.onReflectorError(f.body, major, allowFallback)
|
||||
case tcpProtoDown:
|
||||
return s.onDowngrade(f.body, major)
|
||||
return s.onDowngrade(f.body, major, allowFallback)
|
||||
}
|
||||
if !ex.authed && f.typ >= 100 {
|
||||
return s.retry("Protocol error", nil)
|
||||
@@ -530,25 +551,25 @@ func (s *Station) onFrame(ctx context.Context, wire *framed, major uint16, f fra
|
||||
return s.onV3(ctx, wire, f, ex)
|
||||
}
|
||||
|
||||
func (s *Station) onReflectorError(body []byte, major uint16) error {
|
||||
func (s *Station) onReflectorError(body []byte, major uint16, allowFallback bool) error {
|
||||
msg, _, err := parseString(body)
|
||||
if err != nil {
|
||||
s.endFatalCause("could not be read", err, true)
|
||||
return fmt.Errorf("could not be read: %w", err)
|
||||
}
|
||||
if major >= 3 && fallback20(msg) {
|
||||
if allowFallback && wantsOther(major, msg) {
|
||||
return errFallback
|
||||
}
|
||||
s.endFatal(msg, true)
|
||||
return errStopped
|
||||
}
|
||||
|
||||
func (s *Station) onDowngrade(body []byte, major uint16) error {
|
||||
func (s *Station) onDowngrade(body []byte, major uint16, allowFallback bool) error {
|
||||
gotMajor, gotMinor, err := parseVersion(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if major >= 3 && gotMajor == 2 {
|
||||
if allowFallback && acceptsOther(major, gotMajor, gotMinor) {
|
||||
return errFallback
|
||||
}
|
||||
s.endFatal("Unsupported protocol version "+strconvU(gotMajor)+"."+strconvU(gotMinor), true)
|
||||
|
||||
@@ -36,14 +36,8 @@ func TestLoginAnnouncesProtocolAndJoinsTheTalkGroup(t *testing.T) {
|
||||
|
||||
refl.accept(t)
|
||||
typ, body := refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 3, 0) {
|
||||
t.Fatalf("first frame type %d body %v, want protocol 3.0", typ, body)
|
||||
}
|
||||
refl.writeFrame(t, 6, versionWire(2, 0))
|
||||
refl.accept(t)
|
||||
typ, body = refl.readFrame(t)
|
||||
if typ != 5 || !versionIs(body, 2, 0) {
|
||||
t.Fatalf("fallback frame type %d body %v, want protocol 2.0", typ, body)
|
||||
t.Fatalf("first frame type %d body %v, want protocol 2.0", typ, body)
|
||||
}
|
||||
|
||||
challenge := make([]byte, 20)
|
||||
|
||||
Reference in New Issue
Block a user