summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2024-05-07 11:49:29 -0300
committerEuAndreh <eu@euandre.org>2024-05-07 12:23:25 -0300
commitf96f51fc600556b002376a71123ba07fbb2a5b68 (patch)
treec5901b0fc2169d3f8a26d673f8b9415d3afdd8eb
parentRename from "papo" to "papod" (diff)
downloadpapod-f96f51fc600556b002376a71123ba07fbb2a5b68.tar.gz
papod-f96f51fc600556b002376a71123ba07fbb2a5b68.tar.xz
src/papod.go: Add message parsing code with some tests
-rw-r--r--src/papod.go167
-rw-r--r--tests/papod_test.go342
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 {