From 7ae48d42c46966342e2eadf918f855e378bfd790 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 21 Aug 2019 10:15:45 +0200 Subject: [PATCH] little bit better, runs now --- .gitignore | 1 + Makefile | 16 ++++++++-- main.go | 44 +++++++++++++++++++------ sircd.yaml | 3 ++ sircd/client.go | 21 ++++++------ sircd/irc.go | 83 ++++++++++++++++++++++++++++++++++++++++++------ sircd/message.go | 2 ++ sircd/server.go | 45 ++++++++++++++++---------- sircd/session.go | 66 +++++++++++++++++++++++++++++++------- 9 files changed, 224 insertions(+), 57 deletions(-) create mode 100644 .gitignore create mode 100644 sircd.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c98d72 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/ircd diff --git a/Makefile b/Makefile index 0d683d0..b2d9530 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,19 @@ +VERSION := $(shell git rev-parse --short HEAD) +BUILDTIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') + +GOLDFLAGS += -X main.Version=$(VERSION) +GOLDFLAGS += -X main.Buildtime=$(BUILDTIME) +GOFLAGS = -ldflags "$(GOLDFLAGS)" + default: run -run: *.go */*.go - go run main.go +run: build + ./ircd + +build: ./ircd + +./ircd: *.go */*.go + go build -o $@ $(GOFLAGS) . fmt: go fmt *.go diff --git a/main.go b/main.go index 8f07efe..0109875 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,44 @@ package main +import "fmt" import "github.com/sirupsen/logrus" import "github.com/sneak/sircd/sircd" -import "time" +import "github.com/spf13/viper" + +var Version string +var Buildtime string func main() { - var log = logrus.New() + + // set up logging + log := logrus.New() log.SetLevel(logrus.DebugLevel) + log.SetReportCaller(true) log.Println("sircd starting up") - s := sircd.NewSircd() - s.SetServerName("irc.example.com") - s.SetLogger(log) - go s.Start() - for s.Running { - time.Sleep(1 * time.Second) - } + + c := viper.New() + // default config variables + c.SetDefault("myhostname", "irc.example.com") + c.SetDefault("network", "ExampleNet") + c.SetDefault("admin", "webmaster@example.com") + c.SetDefault("version", Version) + c.SetDefault("buildtime", Buildtime) + + // read config file + c.SetConfigName("sircd") // name of config file (without extension) + c.AddConfigPath("/etc/sircd/") // path to look for the config file in + c.AddConfigPath("$HOME/.config/sircd") // call multiple times to add many search paths + c.AddConfigPath(".") // optionally look for config in the working directory + err := c.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + + // instantiate server + s := sircd.New(c) + // give it our logger + s.SetLogger(log) + + // run it + s.Start() } diff --git a/sircd.yaml b/sircd.yaml new file mode 100644 index 0000000..463ec90 --- /dev/null +++ b/sircd.yaml @@ -0,0 +1,3 @@ +myhostname: irc.example.com +network: ExampleNet +admin: webmaster@example.com diff --git a/sircd/client.go b/sircd/client.go index ce1a883..61edf13 100644 --- a/sircd/client.go +++ b/sircd/client.go @@ -2,14 +2,14 @@ package sircd import ( "bytes" + "fmt" "github.com/google/uuid" "github.com/sirupsen/logrus" "net" "strings" - "fmt" ) -func newIrcClient(conn net.Conn, log *logrus.Logger, mc chan *ircMessage, s *sircd) *ircClient { +func newIrcClient(conn net.Conn, log *logrus.Logger, mc chan *ircMessage, s *ircd) *ircClient { c := new(ircClient) c.Id = uuid.New().String() c.conn = conn @@ -23,7 +23,7 @@ func newIrcClient(conn net.Conn, log *logrus.Logger, mc chan *ircMessage, s *sir } type ircClient struct { - //FIXME add a mutex and protect during writes + //FIXME add a mutex and protect during writes Id string conn net.Conn log *logrus.Logger @@ -31,7 +31,7 @@ type ircClient struct { inputBytes *bytes.Buffer outputBytes *bytes.Buffer mc chan *ircMessage - server *sircd + server *ircd } func (c *ircClient) ServerName() string { @@ -54,8 +54,8 @@ func (c *ircClient) CleanupAndClose() { } func (c *ircClient) AppendStringToOutputBuffer(input string) { - // woo boilerplate because no polymorphism - c.AppendBytesToOutputBuffer([]byte(input)) + // woo boilerplate because no polymorphism + c.AppendBytesToOutputBuffer([]byte(input)) } func (c *ircClient) AppendBytesToOutputBuffer(input []byte) { @@ -67,10 +67,10 @@ func (c *ircClient) FlushOutputBuffer() bool { if err != nil { c.log.Debugf("failed to write completely to client<%s>, marking dead", c.Id) c.Kill() - return false + return false } else { c.log.Debugf("wrote %d bytes to client<%s>", numBytes, c.Id) - return true + return true } } @@ -129,6 +129,8 @@ func (c *ircClient) AppendInputBuffer(input []byte) { } func (c *ircClient) ParseInputBuffer() { + //FIXME update a timer here on the client when a line is parsed + //successfully and time them out after a while c.log.Debugf("my input buffer is %d bytes", c.inputBytes.Len()) c.log.Debugf("my input buffer is: '%s'", c.inputBytes.String()) line, err := c.inputBytes.ReadString(byte('\n')) @@ -136,7 +138,8 @@ func (c *ircClient) ParseInputBuffer() { c.mc <- c.ParseSingleInput(line) c.ParseInputBuffer() } else { - c.log.Debugf("error parsing input buffer: ", err.Error()) + // error parsing input buffer, probably don't have a full line yet + return } c.log.Debugf("my input buffer is %d bytes", c.inputBytes.Len()) c.log.Debugf("my input buffer is: '%s'", c.inputBytes.String()) diff --git a/sircd/irc.go b/sircd/irc.go index 5d5fdf4..1554ed3 100644 --- a/sircd/irc.go +++ b/sircd/irc.go @@ -1,6 +1,8 @@ package sircd -func (s *sircd) processIRCMessage(m *ircMessage) { +func (s *ircd) processIRCMessage(m *ircMessage) { + // FIXME put all of these in a map of string->function + // and dispatch them that way instead switch m.command { case "CAP": s.processCAPCommand(m) @@ -11,25 +13,88 @@ func (s *sircd) processIRCMessage(m *ircMessage) { default: s.processUnknownCommand(m) } - s.log.Infof(m.String()) - s.log.Infof("%+v", m) + s.log.Infof("client<%s> sent %+v", m.from.Id, m) } -func (s *sircd) processUnknownCommand(m *ircMessage) { +func (s *ircd) sayHello(c *ircClient) { + //001 welcome + //002 host/version decl + //003, 004, 005, 251, 252, 253, 254, 255, 256, 266 + //375 (begin motd) + //372 motd (repeating) + //376 end motd + // change user mode (from server) :sneak!sneak@butt9q8.a.b.IP MODE sneak :+x + /* +:irc.butt.es 001 sneak :Welcome to the Buttes IRC Network sneak!sneak@1.2.3.4 +:irc.butt.es 002 sneak :Your host is irc.butt.es, running version InspIRCd-3 +:irc.butt.es 003 sneak :This server was created 09:33:24 Aug 05 2019 +:irc.butt.es 004 sneak irc.butt.es InspIRCd-3 BHIRSWcghiorswxz ACFHIMNOPQRSTXYZbcefijklmnoprstvwz :FHIXYZbefjklovw +:irc.butt.es 005 sneak ACCEPT=30 AWAYLEN=200 CALLERID=g CASEMAPPING=ascii CHANLIMIT=#:20 CHANMODES=IXYZbew,k,FHfjl,ACMNOPQRSTcimnprstz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e EXTBAN=,ACGNOQRSTUacjmprz :are supported by this server +:irc.butt.es 005 sneak HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=IXbew:512 MAXTARGETS=20 MODES=20 NAMESX NETWORK=Buttes NICKLEN=32 OPERLOG OVERRIDE :are supported by this server +:irc.butt.es 005 sneak PREFIX=(ov)@+ REMOVE SAFELIST SECURELIST SILENCE=32 STATUSMSG=@+ TOPICLEN=500 USERIP USERLEN=11 VBANLIST WHOX :are supported by this server +:irc.butt.es 251 sneak :There are 34 users and 33 invisible on 2 servers +:irc.butt.es 252 sneak 2 :operator(s) online +:irc.butt.es 253 sneak 1 :unknown connections +:irc.butt.es 254 sneak 33 :channels formed +:irc.butt.es 255 sneak :I have 60 clients and 1 servers +:irc.butt.es 265 sneak :Current local users: 60 Max: 65 +:irc.butt.es 266 sneak :Current global users: 67 Max: 72 +:irc.butt.es 375 sneak :irc.butt.es message of the day +:irc.butt.es 372 sneak :- ____ _ _ _____ _____ _____ ____ +:irc.butt.es 372 sneak :- | __ )| | | |_ _|_ _| ____/ ___| +:irc.butt.es 372 sneak :- | _ \| | | | | | | | | _| \___ \ + +:irc.butt.es 376 sneak :End of message of the day. +:irc.butt.es 396 sneak butt9q8.a.b.IP :is now your displayed host +:sneak!sneak@butt9q8.a.b.IP MODE sneak :+x +*/ +} + +func (s *ircd) processUnknownCommand(m *ircMessage) { m.from.RespUnknownCommand(m.command) + if m.from.session.CheckState("init") { + // if they are sending weird commands before NICK/USER, just drop + // them + // FIXME send an error + m.from.Kill() + } } -func (s *sircd) processNICKCommand(m *ircMessage) { +func (s *ircd) processNICKCommand(m *ircMessage) { //FIXME check if nick is in use //FIXME check if nick is valid - m.from.session.SetNick(m.params[0]) + s.log.Infof("%+v", m) + s.log.Infof("%+v", m.params) + s.log.Infof("%+v", m.from) + s.log.Infof("%+v", m.from.session) + if len(m.params) == 1 { + m.from.session.SetNick(m.params[0]) + s.log.Infof("conn<%s> NICK %s", m.from.Id, m.params[0]) + } } -func (s *sircd) processUSERCommand(m *ircMessage) { - m.from.session.SetUserInfo(m.params[0]) +func (s *ircd) processUSERCommand(m *ircMessage) { + // FIXME fail/disconnect if nick not yet set + // FIXME fail if run a second time + // FIXME tbh the client connection should be an FSM that limits usage of + // certain valid commands per-state + //params: + if !m.from.session.CheckState("init") { + // can only do this when going init->normal + // FIXME send an error + m.from.Kill() + } + + if m.from.session.SetUserInfo(m.params) { //this changes the session state + s.log.Infof("%+v", m.from.session) + s.sayHello(m.from) + } else { + // FIXME send an error + m.from.Kill() // bye felicia + } } -func (s *sircd) processCAPCommand(m *ircMessage) { +func (s *ircd) processCAPCommand(m *ircMessage) { s.log.Debugln("ignoring CAP command, unsupported") // pass } diff --git a/sircd/message.go b/sircd/message.go index d768f20..fcd8f94 100644 --- a/sircd/message.go +++ b/sircd/message.go @@ -56,7 +56,9 @@ func parseIrcLine(line string) (*ircMessage, error) { } func NewIrcMessageFromString(line string, from *ircClient) *ircMessage { + msg, err := parseIrcLine(line) + msg.from = from if err != nil { panic("wat") diff --git a/sircd/server.go b/sircd/server.go index 582a85b..f0d359a 100644 --- a/sircd/server.go +++ b/sircd/server.go @@ -1,11 +1,10 @@ package sircd -import ( - "github.com/sirupsen/logrus" - "net" -) +import "github.com/sirupsen/logrus" +import "github.com/spf13/viper" +import "net" -type sircd struct { +type ircd struct { Running bool serverName string log *logrus.Logger @@ -17,6 +16,7 @@ type sircd struct { newClients chan *ircClient deadClients chan *ircClient messageQueue chan *ircMessage + c *viper.Viper } const ( @@ -25,23 +25,25 @@ const ( CONN_TYPE = "tcp" ) -func NewSircd() *sircd { - s := new(sircd) +func New(config *viper.Viper) *ircd { + s := new(ircd) s.Running = true s.ircClients = make(map[*ircClient]bool) + s.c = config + s.serverName = s.c.GetString("myhostname") return s } -func (s *sircd) SetLogger(logger *logrus.Logger) { +func (s *ircd) SetLogger(logger *logrus.Logger) { s.log = logger } -func (s *sircd) SetServerName(name string) { +func (s *ircd) SetServerName(name string) { s.serverName = name } -func (s *sircd) Start() { - +func (s *ircd) Start() { + s.log.Infof("sircd version=%s buildtime=%s starting.", s.c.GetString("version"), s.c.GetString("buildtime")) s.newClients = make(chan *ircClient, 128) s.deadClients = make(chan *ircClient, 128) s.messageQueue = make(chan *ircMessage, 128) @@ -51,7 +53,7 @@ func (s *sircd) Start() { if err != nil { s.log.Fatalln("Error listening:", err.Error()) } - s.log.Println("Listening for irc on " + CONN_HOST + ":" + CONN_PORT) + s.log.Println("Listening for irc proto on " + CONN_HOST + ":" + CONN_PORT) go func() { conn, err := s.ircListener.Accept() @@ -61,10 +63,22 @@ func (s *sircd) Start() { s.newClients <- newIrcClient(conn, s.log, s.messageQueue, s) }() - go s.mainloop() + // FIXME have this do 'go s.handlemessages()' and process an input channel + // of messages from all clients in its own goroutine, and then call + // directly into the main loop here and only do client i/o in this + // goroutine + go s.processMessages() + s.mainLoop() } -func (s *sircd) mainloop() { +func (s *ircd) processMessages() { + for { + message := <-s.messageQueue + s.processIRCMessage(message) + } +} + +func (s *ircd) mainLoop() { for { select { case client := <-s.newClients: @@ -76,6 +90,7 @@ func (s *sircd) mainloop() { nbyte, err := client.conn.Read(buf) if err != nil { //this will surface it in the deadClients channel + //reader below client.Kill() break } else { @@ -89,8 +104,6 @@ func (s *sircd) mainloop() { case deadClient := <-s.deadClients: delete(s.ircClients, deadClient) deadClient.CleanupAndClose() - case message := <-s.messageQueue: - s.processIRCMessage(message) } } } diff --git a/sircd/session.go b/sircd/session.go index ffe1fcd..ea7ac4b 100644 --- a/sircd/session.go +++ b/sircd/session.go @@ -1,25 +1,67 @@ package sircd -type ircNick string -type ircRealName string -type hostname string +import log "github.com/sirupsen/logrus" type ircUserSession struct { - //FIXME add a mutex and protect during writes - nick ircNick - realname ircRealName - host hostname + //FIXME add a mutex and protect during writes + nick string + user string + specifiedHostname string + realName string + remoteHost string + state string + // FIXME make the connection check its state for invalidity periodically + // and .Kill() it if the irc protocol session wants the client + // connection gone + invalid bool } func NewIrcUserSession() *ircUserSession { // FIXME get conn.RemoteAddr passed in and stringify it here and put it - // in the session + // in the session s := new(ircUserSession) - s.nick = '*' //default for s2c messages pre-NICK + s.nick = "*" //default for s2c messages pre-NICK + s.state = "init" + s.invalid = false return s } -func (s *ircUserSession) SetNick(input string) { - // FIXME check for valid nick-ness - s.nick = input +func (s *ircUserSession) CheckState(wantedState string) bool { + if s.state == wantedState { + return true + } else { + return false + } +} + +func (s *ircUserSession) SetState(newState string) bool { + log.Infof("state changing: %s->%s", s.state, newState) //FIXME remove this + s.state = newState + return true +} + +func (s *ircUserSession) SetNick(input string) bool { + // FIXME check for valid nick-ness + s.nick = input + return true +} + +func (s *ircUserSession) SetUserInfo(params []string) bool { + if !s.CheckState("init") { + // can only do this when going init->normal + return false + } + if s.nick == "*" { + // must set nick first + return false + } + if len(params) >= 4 { + s.user = params[0] + s.specifiedHostname = params[1] + s.realName = params[3] + s.SetState("normal") + return true + } else { + return false + } }