package papo import ( "bufio" "bytes" "crypto/rand" "database/sql" "encoding/hex" "errors" "flag" "fmt" "io" // "log" "log/slog" "math/big" "net" "os" "regexp" "runtime/debug" "strings" "sync" "syscall" "time" _ "github.com/mattn/go-sqlite3" ) /* Global variables */ var ( Hostname string Version string Colour string ) // FIXME: finish rewriting // // lastV7time is the last time we returned stored as: // // 52 bits of time in milliseconds since epoch // 12 bits of (fractional nanoseconds) >> 8 var lastV7Time int64 var timeMu sync.Mutex // getV7Time returns the time in milliseconds and nanoseconds / 256. // The returned (milli << (12 + seq)) is guaranteed to be greater than // (milli << (12 + seq)) returned by any previous call to getV7Time. // `seq` Sequence number is between 0 and 3906 (nanoPerMilli >> 8) func getV7Time(nano int64) (int64, int64) { const nanoPerMilli = 1000000 milli := nano / nanoPerMilli seq := (nano - (milli * nanoPerMilli)) >> 8 now := milli << (12 + seq) timeMu.Lock() defer timeMu.Unlock() if now <= lastV7Time { now = lastV7Time + 1 milli = now >> 12 seq = now & 0xfff } lastV7Time = now return milli, seq } const lengthUUID = 16 type UUID struct { bytes [lengthUUID]byte } func NewUUID() UUID { var buf [lengthUUID]byte _, err := io.ReadFull(rand.Reader, buf[7:]) FatalIf(err) buf[6] = (buf[6] & 0x0f) | 0x40 // Version 4 buf[8] = (buf[8] & 0x3f) | 0x80 // Variant is 10 t, s := getV7Time(time.Now().UnixNano()) buf[0] = byte(t >> 40) buf[1] = byte(t >> 32) buf[2] = byte(t >> 24) buf[3] = byte(t >> 16) buf[4] = byte(t >> 8) buf[5] = byte(t >> 0) buf[6] = 0x70 | (0x0f & byte(s >> 8)) buf[7] = byte(s) return UUID { bytes: buf } } func UUIDToString(uuid UUID) string { const dashCount = 4 const encodedLength = (lengthUUID * 2) + dashCount dst := [encodedLength]byte { 0, 0, 0, 0, 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, '-', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } hex.Encode(dst[ 0:8], uuid.bytes[0:4]) hex.Encode(dst[ 9:13], uuid.bytes[4:6]) hex.Encode(dst[14:18], uuid.bytes[6:8]) hex.Encode(dst[19:23], uuid.bytes[8:10]) hex.Encode(dst[24:36], uuid.bytes[10:]) return string(dst[:]) } func Debug(message string, type_ string, args ...any) { slog.Debug( message, append( []any { "id", UUIDToString(NewUUID()), "kind", "log", "type", type_, }, args..., )..., ) } func Info(message string, type_ string, args ...any) { slog.Info( message, append( []any { "id", UUIDToString(NewUUID()), "kind", "log", "type", type_, }, args..., )..., ) } func Warning(message string, type_ string, args ...any) { slog.Warn( message, append( []any { "id", UUIDToString(NewUUID()), "kind", "log", "type", type_, }, args..., )..., ) } func Error(message string, type_ string, args ...any) { slog.Error( message, append( []any { "id", UUIDToString(NewUUID()), "kind", "log", "type", type_, }, args..., )..., ) } func Metric(type_ string, label string, args ...any) { slog.Info( "_", append( []any { "id", UUIDToString(NewUUID()), "kind", "metric", "type", type_, "label", label, }, args..., )..., ) } type Gauge struct { Inc func(...any) Dec func(...any) } var zero = big.NewInt(0) var one = big.NewInt(1) func MakeGauge(label string, staticArgs ...any) Gauge { count := big.NewInt(0) emitGauge := func(dynamicArgs ...any) { if count.Cmp(zero) == -1 { Error( "Gauge went negative", "process-metric", append( []any { "value", count }, append( staticArgs, dynamicArgs..., )..., )..., ) return // avoid wrong metrics being emitted } Metric( "gauge", label, // TODO: we'll have slices.Concat on Go 1.22 append( []any { "value", count }, append( staticArgs, dynamicArgs..., )..., )..., ) } return Gauge { Inc: func(dynamicArgs ...any) { count.Add(count, one) emitGauge(dynamicArgs...) }, Dec: func(dynamicArgs ...any) { count.Sub(count, one) emitGauge(dynamicArgs...) }, } } func MakeCounter(label string) func(...any) { return func(args ...any) { Metric( "counter", label, append([]any { "value", 1 }, args...)..., ) } } var EmitActiveConnection = MakeGauge("active-connections") var EmitNicksInChannel = MakeGauge("nicks-in-channel") var EmitReceivedMessage = MakeCounter("received-message") const pingFrequency = time.Duration(30) * time.Second const pongMaxLatency = time.Duration(5) * time.Second func Fatal(err error) { Error( "Fatal error", "fatal-error", "error", err, "stack", string(debug.Stack()), ) syscall.Kill(os.Getpid(), syscall.SIGABRT) os.Exit(3) } func FatalIf(err error) { if err != nil { Fatal(err) } } type Channel struct { } type Context struct { dbConn *sql.DB 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 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 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, } } var MessageRegex = regexp.MustCompilePOSIX( // //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, connection *Connection) { fmt.Println("PingLoop") } func HandleConnection(ctx *Context, conn net.Conn) { EmitActiveConnection.Inc() // FIXME: WaitGroup here? connection := Connection { conn: conn, isAuthenticated: false, } go ReadLoop(ctx, &connection) go WriteLoop(ctx, &connection) go PingLoop(ctx, &connection) } func IRCdLoop(ctx *Context, publicSocketPath string) { listener, err := net.Listen("unix", publicSocketPath) FatalIf(err) Info("IRCd started", "component-up", "component", "ircd") for { conn, err := listener.Accept() if err != nil { Warning( "Error accepting a public IRCd connection", "accept-connection", "err", err, ) // conn.Close() // FIXME: is conn nil? continue } go HandleConnection(ctx, conn) // FIXME: where does it get closed } } func CommandListenerLoop(ctx *Context, commandSocketPath string) { listener, err := net.Listen("unix", commandSocketPath) FatalIf(err) Info("command listener started", "component-up", "component", "command-listener") for { conn, err := listener.Accept() if err != nil { Warning( "Error accepting a command connection", "accept-command", "err", err, ) continue } defer conn.Close() // TODO: handle commands } } func TransactorLoop(ctx *Context) { Info("transactor started", "component-up", "component", "transactor") EmitActiveConnection.Inc() for tx := range ctx.tx { fmt.Println(tx) } } func SetLoggerOutput(w io.Writer) { slog.SetDefault(slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions { AddSource: true, })).With( slog.Group( "info", "pid", os.Getpid(), "ppid", os.Getppid(), "puuid", UUIDToString(NewUUID()), ), )) } func SetTraceback() { if os.Getenv("GOTRACEBACK") == "" { debug.SetTraceback("crash") } } func SetHostname() { var err error Hostname, err = os.Hostname() FatalIf(err) } func SetEnvironmentVariables() { Version = os.Getenv("PAPO_VERSION") if Version == "" { Version = "PAPO-VERSION-UNKNOWN" } Colour = os.Getenv("PAPO_COLOUR") if Colour == "" { Colour = "PAPO-COLOUR-UNKNOWN" } } func InitDB(databasePath string) *sql.DB { DB, err := sql.Open("sqlite3", databasePath) FatalIf(err) return DB } func init() { SetLoggerOutput(os.Stdout) SetTraceback() SetHostname() SetEnvironmentVariables() } func Start(ctx *Context, publicSocketPath string, commandSocketPath string) { buildInfo, ok := debug.ReadBuildInfo() if !ok { Fatal(errors.New("error on debug.ReadBuildInfo()")) } Info("-", "lifecycle-event", "event", "starting-server", slog.Group( "go", "version", buildInfo.GoVersion, "settings", buildInfo.Settings, "deps", buildInfo.Deps, ), ) var wg sync.WaitGroup bgRun := func(f func()) { wg.Add(1) go func() { f() wg.Done() }() } bgRun(func() { IRCdLoop(ctx, publicSocketPath) }) bgRun(func() { CommandListenerLoop(ctx, commandSocketPath) }) bgRun(func() { TransactorLoop(ctx) }) wg.Wait() } func BuildContext(databasePath string) *Context { dbConn := InitDB(databasePath) tx := make(chan int, 100) return &Context { dbConn, tx, } } var ( databasePath = flag.String( "f", "papo.db", "The path to the database file", ) publicSocketPath = flag.String( "s", "papo.public.socket", "The path to the socket that handles the public traffic", ) commandSocketPath = flag.String( "S", "papo.command.socket", "The path to the private IPC commands socket", ) ) func Main() { flag.Parse() ctx := BuildContext(*databasePath) Start(ctx, *publicSocketPath, *commandSocketPath) }