updated to new names/urls

This commit is contained in:
2020-02-25 07:51:22 -08:00
parent 65a8d8d082
commit 674215d0cc
9 changed files with 28 additions and 27 deletions

150
irc/client.go Normal file
View File

@@ -0,0 +1,150 @@
package irc
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"net"
"strings"
)
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
c.log = log
c.mc = mc
c.server = s
c.inputBytes = new(bytes.Buffer)
c.outputBytes = new(bytes.Buffer)
c.session = NewIrcUserSession()
return c
}
type ircClient struct {
//FIXME add a mutex and protect during writes
Id string
conn net.Conn
log *logrus.Logger
session *ircUserSession
inputBytes *bytes.Buffer
outputBytes *bytes.Buffer
mc chan *ircMessage
server *ircd
}
func (c *ircClient) ServerName() string {
return c.server.serverName
}
func (c *ircClient) Kill() {
//server will remove it from the server-managed client list
//and will call c.CleanupAndClose() when it pops from this channel
c.server.deadClients <- c
}
func (c *ircClient) CleanupAndClose() {
//client cleanup
//FIXME truncate client buffers and whatnot here
// server object already handles removing it from the list of clients
// for the server, so don't do it here
c.log.Infof("client=%s cleanup", c.Id)
c.conn.Close()
}
func (c *ircClient) AppendStringToOutputBuffer(input string) {
// woo boilerplate because no polymorphism
c.AppendBytesToOutputBuffer([]byte(input))
}
func (c *ircClient) AppendBytesToOutputBuffer(input []byte) {
c.outputBytes.Write(input)
}
func (c *ircClient) FlushOutputBuffer() bool {
numBytes, err := c.outputBytes.WriteTo(c.conn)
if err != nil {
c.log.Debugf("failed to write completely to client<%s>, marking dead", c.Id)
c.Kill()
return false
} else {
c.log.Debugf("wrote %d bytes to client<%s>", numBytes, c.Id)
return true
}
}
func (c *ircClient) RespUnknownCommand(command string) {
//421 == unknown command
c.SendServerMessage(421, []string{command, "Unknown command. Please try your call again later."})
}
func (c *ircClient) SendServerMessage(code uint16, params []string) bool {
//FIXME this is terrible hacky duct tape code, redo it
p := ""
if len(params) > 0 {
// pop off last item for separate processing
lastelem := params[len(params)-1]
params = params[:len(params)-1]
for _, elem := range params {
if strings.ContainsAny(elem, " \t\r\n") {
c.log.Errorf("wtf %s", elem)
return false
}
}
if strings.ContainsAny(lastelem, "\t\r\n") {
c.log.Errorf("wtf %s", lastelem)
return false
}
if strings.ContainsAny(lastelem, " ") {
lastelem = fmt.Sprintf(":%s", lastelem)
}
p = strings.Join(params, " ")
if len(p) > 0 {
p = " " + lastelem
} else {
p = lastelem
}
}
sendstr := fmt.Sprintf(":%s %d %s", c.ServerName(), code, c.session.nick)
if len(p) > 0 {
sendstr = sendstr + " " + p
}
sendstr = sendstr + "\r\n"
c.log.Debugf("sending string '%s' to client<%s>", sendstr, c.Id)
c.AppendStringToOutputBuffer(sendstr)
return c.FlushOutputBuffer()
}
func (c *ircClient) AppendInputBuffer(input []byte) {
// Read the incoming connection into the buffer.
c.log.Printf("conn<%s>: got %d bytes from net", c.Id, len(input))
c.inputBytes.Write(input)
c.log.Debugf("conn<%s> buffer now %d bytes total", c.Id, c.inputBytes.Len())
c.ParseInputBuffer()
}
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'))
if err == nil {
c.mc <- c.ParseSingleInput(line)
c.ParseInputBuffer()
} else {
// 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())
}
func (c *ircClient) ParseSingleInput(line string) *ircMessage {
return NewIrcMessageFromString(line, c)
}

100
irc/irc.go Normal file
View File

@@ -0,0 +1,100 @@
package irc
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)
case "NICK":
s.processNICKCommand(m)
case "USER":
s.processUSERCommand(m)
default:
s.processUnknownCommand(m)
}
s.log.Infof("client<%s> sent %+v", m.from.Id, m)
}
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 *ircd) processNICKCommand(m *ircMessage) {
//FIXME check if nick is in use
//FIXME check if nick is valid
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 *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: <username> <hostname> <servername> <realname>
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 *ircd) processCAPCommand(m *ircMessage) {
s.log.Debugln("ignoring CAP command, unsupported")
// pass
}

71
irc/message.go Normal file
View File

@@ -0,0 +1,71 @@
package irc
import (
"errors"
"fmt"
"regexp"
"strings"
)
type ircMessage struct {
from *ircClient
//received
tags string
source string
raw string
command string
params []string
}
func parseIrcLine(line string) (*ircMessage, error) {
//FIXME do this at compile or start time instead of every message
ircregex, err := regexp.Compile(`^(\@(\S+) )?(?:[:](\S+) )?(\S+)(?: ([^:].+?))?(?: [:](.+))?$`)
if err != nil {
panic("can't happen")
}
line = strings.TrimRight(line, "\r\n")
m := new(ircMessage)
m.raw = line
if ircregex.MatchString(m.raw) == false {
return nil, errors.New("parse error")
}
matches := ircregex.FindAllStringSubmatch(m.raw, -1)
if len(matches) == 0 {
return nil, errors.New("parse error")
}
match := matches[0]
fmt.Printf("%+v\n", match)
fmt.Printf("%+v\n", len(match))
m.tags = match[2]
m.source = match[3]
m.command = strings.ToUpper(match[4])
if len(match[5]) > 0 {
m.params = strings.Fields(match[5])
}
if len(match[6]) > 0 {
m.params = append(m.params, match[6])
}
return m, nil
}
func NewIrcMessageFromString(line string, from *ircClient) *ircMessage {
msg, err := parseIrcLine(line)
msg.from = from
if err != nil {
panic("wat")
}
return msg
}
func (m *ircMessage) String() string {
return fmt.Sprintf("IRCMessage<%s>('%s')", m.command, strings.Join(m.params, ","))
}

208
irc/responses.go Normal file
View File

@@ -0,0 +1,208 @@
package irc
import log "github.com/sirupsen/logrus"
type ircServerResponse struct {
code uint16
symbol string
longtext string
params *[]string
}
func NewIrcServerResponse(code uint16, params *[]string) {
codes := ircResponseCodes()
r := new(ircServerResponse)
r.code = code
r.symbol = (*codes)[code]
r.params = params
}
func ircResponseCodes() *map[uint16]string {
responseCodeList := map[uint16]string{
1: "RPL_WELCOME",
2: "RPL_YOURHOST",
3: "RPL_CREATED",
4: "RPL_MYINFO",
5: "RPL_ISUPPORT",
10: "RPL_BOUNCE",
15: "RPL_MAP",
17: "RPL_MAPEND",
18: "RPL_MAPSTART",
20: "RPL_HELLO",
42: "RPL_YOURID",
43: "RPL_SAVENICK",
200: "RPL_TRACELINK",
201: "RPL_TRACECONNECTING",
202: "RPL_TRACEHANDSHAKE",
203: "RPL_TRACEUNKNOWN",
204: "RPL_TRACEOPERATOR",
205: "RPL_TRACEUSER",
206: "RPL_TRACESERVER",
207: "RPL_TRACESERVICE",
208: "RPL_TRACENEWTYPE",
209: "RPL_TRACECLASS",
211: "RPL_STATSLINKINFO",
212: "RPL_STATSCOMMANDS",
213: "RPL_STATSCLINE",
214: "RPL_STATSNLINE",
215: "RPL_STATSILINE",
216: "RPL_STATSKLINE",
217: "RPL_STATSQLINE",
218: "RPL_STATSYLINE",
219: "RPL_ENDOFSTATS",
221: "RPL_UMODEIS",
231: "RPL_SERVICEINFO",
232: "RPL_ENDOFSERVICES",
233: "RPL_SERVICE",
234: "RPL_SERVLIST",
235: "RPL_SERVLISTEND",
239: "RPL_STATSIAUTH",
240: "RPL_STATSVLINE",
241: "RPL_STATSLLINE",
242: "RPL_STATSUPTIME",
243: "RPL_STATSOLINE",
244: "RPL_STATSHLINE",
245: "RPL_STATSSLINE",
246: "RPL_STATSPING",
247: "RPL_STATSBLINE",
248: "RPL_STATSDEFINE",
249: "RPL_STATSDEBUG",
250: "RPL_STATSDLINE",
251: "RPL_LUSERCLIENT",
252: "RPL_LUSEROP",
253: "RPL_LUSERUNKNOWN",
254: "RPL_LUSERCHANNELS",
255: "RPL_LUSERME",
256: "RPL_ADMINME",
257: "RPL_ADMINLOC1",
258: "RPL_ADMINLOC2",
259: "RPL_ADMINEMAIL",
261: "RPL_TRACELOG",
262: "RPL_TRACEEND",
263: "RPL_TRYAGAIN",
265: "RPL_LOCALUSERS",
266: "RPL_GLOBALUSERS",
300: "RPL_NONE",
301: "RPL_AWAY",
302: "RPL_USERHOST",
303: "RPL_ISON",
304: "RPL_TEXT",
305: "RPL_UNAWAY",
306: "RPL_NOWAWAY",
311: "RPL_WHOISUSER",
312: "RPL_WHOISSERVER",
313: "RPL_WHOISOPERATOR",
314: "RPL_WHOWASUSER",
315: "RPL_ENDOFWHO",
316: "RPL_WHOISCHANOP",
317: "RPL_WHOISIDLE",
318: "RPL_ENDOFWHOIS",
319: "RPL_WHOISCHANNELS",
321: "RPL_LISTSTART",
322: "RPL_LIST",
323: "RPL_LISTEND",
324: "RPL_CHANNELMODEIS",
325: "RPL_UNIQOPIS",
331: "RPL_NOTOPIC",
332: "RPL_TOPIC",
333: "RPL_TOPIC_WHO_TIME",
341: "RPL_INVITING",
342: "RPL_SUMMONING",
344: "RPL_REOPLIST",
345: "RPL_ENDOFREOPLIST",
346: "RPL_INVITELIST",
347: "RPL_ENDOFINVITELIST",
348: "RPL_EXCEPTLIST",
349: "RPL_ENDOFEXCEPTLIST",
351: "RPL_VERSION",
352: "RPL_WHOREPLY",
353: "RPL_NAMREPLY",
361: "RPL_KILLDONE",
362: "RPL_CLOSING",
363: "RPL_CLOSEEND",
364: "RPL_LINKS",
365: "RPL_ENDOFLINKS",
366: "RPL_ENDOFNAMES",
367: "RPL_BANLIST",
368: "RPL_ENDOFBANLIST",
369: "RPL_ENDOFWHOWAS",
371: "RPL_INFO",
372: "RPL_MOTD",
373: "RPL_INFOSTART",
374: "RPL_ENDOFINFO",
375: "RPL_MOTDSTART",
376: "RPL_ENDOFMOTD",
381: "RPL_YOUREOPER",
382: "RPL_REHASHING",
383: "RPL_YOURESERVICE",
384: "RPL_MYPORTIS",
385: "RPL_NOTOPERANYMORE",
391: "RPL_TIME",
392: "RPL_USERSSTART",
393: "RPL_USERS",
394: "RPL_ENDOFUSERS",
395: "RPL_NOUSERS",
401: "ERR_NOSUCHNICK",
402: "ERR_NOSUCHSERVER",
403: "ERR_NOSUCHCHANNEL",
404: "ERR_CANNOTSENDTOCHAN",
405: "ERR_TOOMANYCHANNELS",
406: "ERR_WASNOSUCHNICK",
407: "ERR_TOOMANYTARGETS",
408: "ERR_NOSUCHSERVICE",
409: "ERR_NOORIGIN",
411: "ERR_NORECIPIENT",
412: "ERR_NOTEXTTOSEND",
413: "ERR_NOTOPLEVEL",
414: "ERR_WILDTOPLEVEL",
415: "ERR_BADMASK",
416: "ERR_TOOMANYMATCHES",
421: "ERR_UNKNOWNCOMMAND",
422: "ERR_NOMOTD",
423: "ERR_NOADMININFO",
424: "ERR_FILEERROR",
431: "ERR_NONICKNAMEGIVEN",
432: "ERR_ERRONEOUSNICKNAME",
433: "ERR_NICKNAMEINUSE",
434: "ERR_SERVICENAMEINUSE",
435: "ERR_SERVICECONFUSED",
436: "ERR_NICKCOLLISION",
437: "ERR_UNAVAILRESOURCE",
441: "ERR_USERNOTINCHANNEL",
442: "ERR_NOTONCHANNEL",
443: "ERR_USERONCHANNEL",
444: "ERR_NOLOGIN",
445: "ERR_SUMMONDISABLED",
446: "ERR_USERSDISABLED",
451: "ERR_NOTREGISTERED",
461: "ERR_NEEDMOREPARAMS",
462: "ERR_ALREADYREGISTRED",
463: "ERR_NOPERMFORHOST",
464: "ERR_PASSWDMISMATCH",
465: "ERR_YOUREBANNEDCREEP",
466: "ERR_YOUWILLBEBANNED",
467: "ERR_KEYSET",
471: "ERR_CHANNELISFULL",
472: "ERR_UNKNOWNMODE",
473: "ERR_INVITEONLYCHAN",
474: "ERR_BANNEDFROMCHAN",
475: "ERR_BADCHANNELKEY",
476: "ERR_BADCHANMASK",
477: "ERR_NOCHANMODES",
478: "ERR_BANLISTFULL",
481: "ERR_NOPRIVILEGES",
482: "ERR_CHANOPRIVSNEEDED",
483: "ERR_CANTKILLSERVER",
484: "ERR_RESTRICTED",
485: "ERR_UNIQOPRIVSNEEDED",
491: "ERR_NOOPERHOST",
492: "ERR_NOSERVICEHOST",
499: "ERR_STATSKLINE",
501: "ERR_UMODEUNKNOWNFLAG",
502: "ERR_USERSDONTMATCH",
708: "RPL_ETRACEFULL",
759: "RPL_ETRACEEND",
}
log.Fatalf("%+v", responseCodeList)
return &responseCodeList
}

116
irc/server.go Normal file
View File

@@ -0,0 +1,116 @@
package irc
import "github.com/sirupsen/logrus"
import "github.com/spf13/viper"
import "net"
type ircd struct {
Running bool
serverName string
log *logrus.Logger
netName string
ircPort uint16
httpPort uint16
ircListener net.Listener
ircClients map[*ircClient]bool
newClients chan *ircClient
deadClients chan *ircClient
messageQueue chan *ircMessage
c *viper.Viper
}
// FIXME pull these from the config
const (
CONN_HOST = "localhost"
CONN_PORT = "6667"
CONN_TYPE = "tcp"
)
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 *ircd) SetLogger(logger *logrus.Logger) {
s.log = logger
}
func (s *ircd) SetServerName(name string) {
s.serverName = name
}
func (s *ircd) Start() {
s.log.Infof(
"sircd version %s (%s) built %s by %s starting.",
s.c.GetString("version"),
s.c.GetString("buildarch"),
s.c.GetString("buildtime"),
s.c.GetString("builduser"),
)
s.newClients = make(chan *ircClient, 128)
s.deadClients = make(chan *ircClient, 128)
s.messageQueue = make(chan *ircMessage, 128)
var err error
s.ircListener, err = net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
if err != nil {
s.log.Fatalln("Error listening:", err.Error())
}
s.log.Println("Listening for irc proto on " + CONN_HOST + ":" + CONN_PORT)
go func() {
conn, err := s.ircListener.Accept()
if err != nil {
s.log.Panicf("Error accepting: ", err.Error())
}
s.newClients <- newIrcClient(conn, s.log, s.messageQueue, s)
}()
// 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 *ircd) processMessages() {
for {
message := <-s.messageQueue
s.processIRCMessage(message)
}
}
func (s *ircd) mainLoop() {
for {
select {
case client := <-s.newClients:
s.ircClients[client] = true
s.log.Println("new irc client: %s", client.Id)
go func() {
buf := make([]byte, 1024*1024)
for {
nbyte, err := client.conn.Read(buf)
if err != nil {
//this will surface it in the deadClients channel
//reader below
client.Kill()
break
} else {
fragment := make([]byte, nbyte)
copy(fragment, buf[:nbyte])
// this will populate the message channel
client.AppendInputBuffer(fragment)
}
}
}()
case deadClient := <-s.deadClients:
delete(s.ircClients, deadClient)
deadClient.CleanupAndClose()
}
}
}

67
irc/session.go Normal file
View File

@@ -0,0 +1,67 @@
package irc
import log "github.com/sirupsen/logrus"
type ircUserSession struct {
//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
s := new(ircUserSession)
s.nick = "*" //default for s2c messages pre-NICK
s.state = "init"
s.invalid = false
return s
}
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
}
}