package papod import ( "bufio" "bytes" "database/sql" "errors" "flag" "fmt" "log/slog" "net" "os" "regexp" "runtime/debug" "strings" "sync" "time" g "euandre.org/gobang/src" _ "github.com/mattn/go-sqlite3" ) /* Global variables */ var ( Hostname string Version string Colour string ) var EmitActiveConnection = g.MakeGauge("active-connections") var EmitNicksInChannel = g.MakeGauge("nicks-in-channel") var EmitReceivedMessage = g.MakeCounter("received-message") const pingFrequency = time.Duration(30) * time.Second const pongMaxLatency = time.Duration(5) * time.Second 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) g.FatalIf(err) g.Info("IRCd started", "component-up", "component", "ircd") for { conn, err := listener.Accept() if err != nil { g.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) g.FatalIf(err) g.Info("command listener started", "component-up", "component", "command-listener") for { conn, err := listener.Accept() if err != nil { g.Warning( "Error accepting a command connection", "accept-command", "err", err, ) continue } defer conn.Close() // TODO: handle commands } } func TransactorLoop(ctx *Context) { g.Info("transactor started", "component-up", "component", "transactor") EmitActiveConnection.Inc() for tx := range ctx.tx { fmt.Println(tx) } } func SetHostname() { var err error Hostname, err = os.Hostname() g.FatalIf(err) } func SetEnvironmentVariables() { Version = os.Getenv("PAPOD_VERSION") if Version == "" { Version = "PAPOD-VERSION-UNKNOWN" } Colour = os.Getenv("PAPOD_COLOUR") if Colour == "" { Colour = "PAPOD-COLOUR-UNKNOWN" } } func InitDB(databasePath string) *sql.DB { DB, err := sql.Open("sqlite3", databasePath) g.FatalIf(err) return DB } func Init() { g.Init() SetHostname() SetEnvironmentVariables() } func Start(ctx *Context, publicSocketPath string, commandSocketPath string) { buildInfo, ok := debug.ReadBuildInfo() if !ok { g.Fatal(errors.New("error on debug.ReadBuildInfo()")) } g.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", "papod.db", "The path to the database file", ) publicSocketPath = flag.String( "s", "papod.public.socket", "The path to the socket that handles the public traffic", ) commandSocketPath = flag.String( "S", "papod.command.socket", "The path to the private IPC commands socket", ) ) func Main() { Init() flag.Parse() ctx := BuildContext(*databasePath) Start(ctx, *publicSocketPath, *commandSocketPath) }