diff options
author | EuAndreh <eu@euandre.org> | 2024-05-07 11:49:29 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-05-07 12:23:25 -0300 |
commit | f96f51fc600556b002376a71123ba07fbb2a5b68 (patch) | |
tree | c5901b0fc2169d3f8a26d673f8b9415d3afdd8eb | |
parent | Rename from "papo" to "papod" (diff) | |
download | papod-f96f51fc600556b002376a71123ba07fbb2a5b68.tar.gz papod-f96f51fc600556b002376a71123ba07fbb2a5b68.tar.xz |
src/papod.go: Add message parsing code with some tests
-rw-r--r-- | src/papod.go | 167 | ||||
-rw-r--r-- | tests/papod_test.go | 342 |
2 files changed, 496 insertions, 13 deletions
diff --git a/src/papod.go b/src/papod.go index 413b902..38f6d4c 100644 --- a/src/papod.go +++ b/src/papod.go @@ -1,7 +1,8 @@ package papo import ( - // "bufio" + "bufio" + "bytes" "crypto/rand" "database/sql" "encoding/hex" @@ -14,7 +15,9 @@ import ( "math/big" "net" "os" + "regexp" "runtime/debug" + "strings" "sync" "syscall" "time" @@ -247,14 +250,18 @@ func MakeCounter(label string) func(...any) { var EmitActiveConnection = MakeGauge("active-connections") var EmitNicksInChannel = MakeGauge("nicks-in-channel") -var EmitReceivedCommand = MakeCounter("received-command") +var EmitReceivedMessage = MakeCounter("received-message") const pingFrequency = time.Duration(30) * time.Second const pongMaxLatency = time.Duration(5) * time.Second func Fatal(err error) { - slog.Error("fatal-error", "error", err, "stack", string(debug.Stack())) + Error( + "Fatal error", "fatal-error", + "error", err, + "stack", string(debug.Stack()), + ) syscall.Kill(os.Getpid(), syscall.SIGABRT) os.Exit(3) } @@ -274,28 +281,160 @@ type Context struct { tx chan int } +type Connection struct { + conn net.Conn + // id *UUID + id string + isAuthenticated bool +} + +type MessageParams struct { + Middle []string + Trailing string +} + type Message struct { - Prefix string + Prefix string + Command string + Params MessageParams + Raw string +} + +var ( + CmdUser = Message { Command: "USER" } +) + +func SplitOnCRLF(data []byte, _atEOF bool) (int, []byte, error) { + idx := bytes.Index(data, []byte { '\r', '\n' }) + if idx == -1 { + return 0, nil, nil + } + + return idx + 2, data[0:idx], nil +} + +func SplitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) { + advance, token, error := SplitOnCRLF(data, atEOF) + + if len(token) == 0 { + return advance, nil, error + } + + return advance, token, error } -func ReadLoop(ctx *Context, conn net.Conn) { - fmt.Println("ReadLoop") +func SplitSpaces(r rune) bool { + return r == ' ' +} + +func ParseMessageParams(params string) MessageParams { + const sep = " :" + + var middle string + var trailing string + + idx := strings.Index(params, sep) + if idx == -1 { + middle = params + trailing = "" + } else { + middle = params[:idx] + trailing = params[idx + len(sep):] + } + + return MessageParams { + Middle: strings.FieldsFunc(middle, SplitSpaces), + Trailing: trailing, + } } -func WriteLoop(ctx *Context, conn net.Conn) { +var MessageRegex = regexp.MustCompilePOSIX( + // <prefix> <command> <params> + //1 2 3 4 + `^(:([^ ]+) +)?([a-zA-Z]+|[0-9]{3}) *( .*)$`, + // ^^^^ FIXME: test these spaces +) +func ParseMessage(rawMessage string) (Message, error) { + var msg Message + + components := MessageRegex.FindStringSubmatch(rawMessage) + if components == nil { + return msg, nil + } + + msg = Message { + Prefix: components[2], + Command: components[3], + Params: ParseMessageParams(components[4]), + Raw: rawMessage, + } + return msg, nil +} + +func HandleMessage(msg Message) { + fmt.Printf("msg: %#v\n", msg) +} + +func ReplyAnonymous() { +} + +func PersistMessage(msg Message) { +} + +func ActionsFor(msg Message) []int { + return []int { } +} + +func RunAction(action int) { +} + +func ProcessMessage(ctx *Context, connection *Connection, rawMessage string) { + msg, err := ParseMessage(rawMessage) + if err != nil { + return + } + + if msg.Command == CmdUser.Command { + connection.id = msg.Params.Middle[0] + connection.isAuthenticated = true + } + + if !connection.isAuthenticated { + go ReplyAnonymous() + return + } + + for _, action := range ActionsFor(msg) { + RunAction(action) + } +} + +func ReadLoop(ctx *Context, connection *Connection) { + scanner := bufio.NewScanner(connection.conn) + scanner.Split(SplitOnRawMessage) + for scanner.Scan() { + ProcessMessage(ctx, connection, scanner.Text()) + } +} + +func WriteLoop(ctx *Context, connection *Connection) { fmt.Println("WriteLoop") } -func PingLoop(ctx *Context, conn net.Conn) { +func PingLoop(ctx *Context, connection *Connection) { fmt.Println("PingLoop") } func HandleConnection(ctx *Context, conn net.Conn) { EmitActiveConnection.Inc() // FIXME: WaitGroup here? - go ReadLoop(ctx, conn) - go WriteLoop(ctx, conn) - go PingLoop(ctx, conn) + connection := Connection { + conn: conn, + isAuthenticated: false, + } + go ReadLoop(ctx, &connection) + go WriteLoop(ctx, &connection) + go PingLoop(ctx, &connection) } func IRCdLoop(ctx *Context, publicSocketPath string) { @@ -306,8 +445,9 @@ func IRCdLoop(ctx *Context, publicSocketPath string) { for { conn, err := listener.Accept() if err != nil { - slog.Warn( + Warning( "Error accepting a public IRCd connection", + "accept-connection", "err", err, ) // conn.Close() // FIXME: is conn nil? @@ -325,8 +465,9 @@ func CommandListenerLoop(ctx *Context, commandSocketPath string) { for { conn, err := listener.Accept() if err != nil { - slog.Warn( + Warning( "Error accepting a command connection", + "accept-command", "err", err, ) continue diff --git a/tests/papod_test.go b/tests/papod_test.go index efac1bc..9b7cd9f 100644 --- a/tests/papod_test.go +++ b/tests/papod_test.go @@ -1,16 +1,358 @@ package papo_test import ( + "bufio" "bytes" "encoding/json" "fmt" "log/slog" + "reflect" + "strings" "testing" "euandre.org/papo/src" ) +func errorIf(t *testing.T, err error) { + if err != nil { + t.Errorf("Unexpected error: %#v\n", err) + } +} + +func assertEqual(t *testing.T, given any, expected any) { + if !reflect.DeepEqual(given, expected) { + t.Errorf("given != expected") + t.Errorf("given: %#v\n", given) + t.Errorf("expected: %#v\n", expected) + } +} + +func TestSplitOnCRLF(t *testing.T) { + inputs := []string { + "", + "\r\n", + "abc\r\n", + "abc\r\n ", + "abc\r\n \r\n", + " \r\n \r\n", + "aaa\r\nbbb\r\nccc\r\n", + "\r\nsplit \r \n CRLF\r\n\r\n", + } + expected := [][]string { + nil, + { "" }, + { "abc" }, + { "abc" }, + { "abc", " " }, + { " ", " " }, + { "aaa", "bbb", "ccc" }, + { "", "split \r \n CRLF", "" }, + } + given := make([][]string, len(inputs)) + + for i, input := range inputs { + scanner := bufio.NewScanner(strings.NewReader(input)) + scanner.Split(papo.SplitOnCRLF) + for scanner.Scan() { + given[i] = append(given[i], scanner.Text()) + } + + err := scanner.Err() + errorIf(t, err) + } + + assertEqual(t, given, expected) +} + +func TestSplitOnRawMessage(t *testing.T) { + inputs := []string { + "first message\r\nsecond message\r\n", + "message 1\r\n\r\nmessage 2\r\n\r\nignored", + } + expected := [][]string { + { "first message", "second message" }, + { "message 1", "message 2" }, + } + given := make([][]string, len(inputs)) + + for i, input := range inputs { + scanner := bufio.NewScanner(strings.NewReader(input)) + scanner.Split(papo.SplitOnRawMessage) + for scanner.Scan() { + given[i] = append(given[i], scanner.Text()) + } + + err := scanner.Err() + errorIf(t, err) + } + + assertEqual(t, given, expected) +} + +func TestParseMessageParams(t *testing.T) { + inputs := []string { + "", + " ", + " :", + " : ", + ": ", + ": ", + " : ", + " :", + " :", + "a", + "ab", + "a b", + "a b c", + "a b:c", + "a b:c:", + "a b :c", + "a b :c:", + "a b :c ", + "a b : c", + "a b : c ", + "a b : c :", + "a b : c : ", + "a b : c :d", + "a b : c :d ", + "a b : c : d ", + } + expected := []papo.MessageParams { + papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: " ", + }, + papo.MessageParams { + Middle: []string { ":" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { ":" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: " ", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "ab" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a", "b", "c" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a", "b:c" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a", "b:c:" }, + Trailing: "", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c:", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c ", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c ", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c : ", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :d", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :d ", + }, + papo.MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c : d ", + }, + } + given := make([]papo.MessageParams, len(inputs)) + + for i, input := range inputs { + given[i] = papo.ParseMessageParams(input) + } + + assertEqual(t, given, expected) +} + +func TestParseMessage(t *testing.T) { + inputs := []string { + "NICK joebloe ", + "USER joebloe 0.0.0.0 joe :Joe Bloe", + ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", + ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", + ":pre USER joebloe: 0:0:0:0 joe::a: : Joe Bloe ", + ":pre USER :Joe Bloe", + ":pre USER : Joe Bloe", + ":pre USER : Joe Bloe", + ":pre USER : ", + ":pre USER :", + } + expected := []papo.Message { + papo.Message { + Prefix: "", + Command: "NICK", + Params: papo.MessageParams { + Middle: []string { "joebloe" }, + Trailing: "", + }, + Raw: "NICK joebloe ", + }, + papo.Message { + Prefix: "", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: "Joe Bloe", + }, + Raw: "USER joebloe 0.0.0.0 joe :Joe Bloe", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: "Joe Bloe", + }, + Raw: ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: " Joe Bloe ", + }, + Raw: ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { + "joebloe:", "0:0:0:0", "joe::a:", + }, + Trailing: " Joe Bloe ", + }, + Raw: ":pre USER joebloe: 0:0:0:0 joe::a: : Joe Bloe ", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { }, + Trailing: "Joe Bloe", + }, + Raw: ":pre USER :Joe Bloe", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { }, + Trailing: " Joe Bloe", + }, + Raw: ":pre USER : Joe Bloe", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { }, + Trailing: " Joe Bloe", + }, + Raw: ":pre USER : Joe Bloe", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { }, + Trailing: " ", + }, + Raw: ":pre USER : ", + }, + papo.Message { + Prefix: "pre", + Command: "USER", + Params: papo.MessageParams { + Middle: []string { }, + Trailing: "", + }, + Raw: ":pre USER :", + }, + } + given := make([]papo.Message, len(inputs)) + + for i, input := range inputs { + parsed, err := papo.ParseMessage(input) + errorIf(t, err) + given[i] = parsed + } + + assertEqual(t, given, expected) +} + func TestSetLoggerOutput(t *testing.T) { return type entry struct { |