diff --git a/irc.go b/irc.go index 4e13886..10c0391 100644 --- a/irc.go +++ b/irc.go @@ -3,9 +3,9 @@ // license that can be found in the LICENSE file. /* -This package provides an event based IRC client library. It allows to -register callbacks for the events you need to handle. Its features -include handling standard CTCP, reconnecting on errors and detecting +This package provides an event based IRC client library. It allows to +register callbacks for the events you need to handle. Its features +include handling standard CTCP, reconnecting on errors and detecting stones servers. Details of the IRC protocol can be found in the following RFCs: https://tools.ietf.org/html/rfc1459 @@ -26,6 +26,7 @@ import ( "log" "net" "os" + "strconv" "strings" "time" ) @@ -34,7 +35,6 @@ const ( VERSION = "go-ircevent v2.1" ) - // Read data from a connection. To be used as a goroutine. func (irc *Connection) readLoop() { br := bufio.NewReaderSize(irc.socket, 512) @@ -101,7 +101,6 @@ func (irc *Connection) readLoop() { irc.readerExit <- true } - // Loop to write to a connection. To be used as a goroutine. func (irc *Connection) writeLoop() { for { @@ -139,7 +138,6 @@ func (irc *Connection) writeLoop() { irc.writerExit <- true } - // Pings the server if we have not received any messages for 5 minutes // to keep the connection alive. To be used as a goroutine. func (irc *Connection) pingLoop() { @@ -169,7 +167,6 @@ func (irc *Connection) pingLoop() { } } - // Main loop to control the connection. func (irc *Connection) Loop() { for !irc.stopped { @@ -190,7 +187,6 @@ func (irc *Connection) Loop() { } } - // Quit the current connection and disconnect from the server // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6 func (irc *Connection) Quit() { @@ -199,60 +195,51 @@ func (irc *Connection) Quit() { irc.Disconnect() } - // Use the connection to join a given channel. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1 func (irc *Connection) Join(channel string) { irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel) } - // Leave a given channel. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2 func (irc *Connection) Part(channel string) { irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel) } - // Send a notification to a nickname. This is similar to Privmsg but must not receive replies. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 func (irc *Connection) Notice(target, message string) { irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message) } - // Send a formated notification to a nickname. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 func (irc *Connection) Noticef(target, format string, a ...interface{}) { irc.Notice(target, fmt.Sprintf(format, a...)) } - // Send (private) message to a target (channel or nickname). // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1 func (irc *Connection) Privmsg(target, message string) { irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message) } - // Send formated string to specified target (channel or nickname). func (irc *Connection) Privmsgf(target, format string, a ...interface{}) { irc.Privmsg(target, fmt.Sprintf(format, a...)) } - // Send raw string. func (irc *Connection) SendRaw(message string) { irc.pwrite <- message + "\r\n" } - // Send raw formated string. func (irc *Connection) SendRawf(format string, a ...interface{}) { irc.SendRaw(fmt.Sprintf(format, a...)) } - // Set (new) nickname. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2 func (irc *Connection) Nick(n string) { @@ -260,27 +247,23 @@ func (irc *Connection) Nick(n string) { irc.SendRawf("NICK %s", n) } - // Determine nick currently used with the connection. func (irc *Connection) GetNick() string { return irc.nickcurrent } - // Query information about a particular nickname. // RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2 func (irc *Connection) Whois(nick string) { irc.SendRawf("WHOIS %s", nick) } - // Query information about a given nickname in the server. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1 func (irc *Connection) Who(nick string) { irc.SendRawf("WHO %s", nick) } - // Set different modes for a target (channel or nickname). // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3 func (irc *Connection) Mode(target string, modestring ...string) { @@ -292,8 +275,7 @@ func (irc *Connection) Mode(target string, modestring ...string) { irc.SendRawf("MODE %s", target) } - -// A disconnect sends all buffered messages (if possible), +// A disconnect sends all buffered messages (if possible), // stops all goroutines and then closes the socket. func (irc *Connection) Disconnect() { irc.endping <- true @@ -314,13 +296,11 @@ func (irc *Connection) Disconnect() { irc.Error <- errors.New("Disconnect Called") } - // Reconnect to a server using the current connection. func (irc *Connection) Reconnect() error { return irc.Connect(irc.server) } - // Connect to a given server using the current connection configuration. // This function also takes care of identification if a password is provided. // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1 @@ -328,7 +308,38 @@ func (irc *Connection) Connect(server string) error { irc.server = server irc.stopped = false - var err error + // make sure everything is ready for connection + if len(irc.server) == 0 { + return errors.New("empty 'server'") + } + if strings.Count(irc.server, ":") != 1 { + return errors.New("wrong number of ':' in address") + } + if strings.Index(irc.server, ":") == 0 { + return errors.New("hostname is missing") + } + if strings.Index(irc.server, ":") == len(irc.server)-1 { + return errors.New("port missing") + } + // check for valid range + ports := strings.Split(irc.server, ":")[1] + port, err := strconv.Atoi(ports) + if err != nil { + return errors.New("extracting port failed") + } + if !((port >= 0) && (port <= 65535)) { + return errors.New("port number outside valid range") + } + if irc.Log == nil { + return errors.New("'Log' points to nil") + } + if len(irc.nick) == 0 { + return errors.New("empty 'user'") + } + if len(irc.user) == 0 { + return errors.New("empty 'user'") + } + if irc.UseTLS { if irc.netsock, err = net.DialTimeout("tcp", irc.server, irc.Timeout); err == nil { irc.socket = tls.Client(irc.netsock, irc.TLSConfig) @@ -357,10 +368,18 @@ func (irc *Connection) Connect(server string) error { return nil } - // Create a connection with the (publicly visible) nickname and username. -// The nickname is later used to address the user. +// The nickname is later used to address the user. Returns nil if nick +// or user are empty. func IRC(nick, user string) *Connection { + // catch invalid values + if len(nick) == 0 { + return nil + } + if len(user) == 0 { + return nil + } + irc := &Connection{ nick: nick, user: user, diff --git a/irc_test.go b/irc_test.go index 02fe5b4..36c4374 100644 --- a/irc_test.go +++ b/irc_test.go @@ -12,6 +12,7 @@ func TestConnection(t *testing.T) { irccon.Debug = true err := irccon.Connect("irc.freenode.net:6667") if err != nil { + t.Log(err.Error()) t.Fatal("Can't connect to freenode.") } irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) @@ -36,6 +37,7 @@ func TestConnectionSSL(t *testing.T) { irccon.UseTLS = true err := irccon.Connect("irc.freenode.net:7000") if err != nil { + t.Log(err.Error()) t.Fatal("Can't connect to freenode.") } irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) @@ -49,6 +51,83 @@ func TestConnectionSSL(t *testing.T) { irccon.Loop() } +func TestConnectionEmtpyServer(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect("") + if err == nil { + t.Fatal("emtpy server string not detected") + } +} + +func TestConnectionDoubleColon(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect("::") + if err == nil { + t.Fatal("wrong number of ':' not detected") + } +} + +func TestConnectionMissingHost(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect(":6667") + if err == nil { + t.Fatal("missing host not detected") + } +} + +func TestConnectionMissingPort(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect("chat.freenode.net:") + if err == nil { + t.Fatal("missing port not detected") + } +} + +func TestConnectionNegativePort(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect("chat.freenode.net:-1") + if err == nil { + t.Fatal("negative port number not detected") + } +} + +func TestConnectionTooLargePort(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + err := irccon.Connect("chat.freenode.net:65536") + if err == nil { + t.Fatal("too large port number not detected") + } +} + +func TestConnectionMissingLog(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + irccon.Log = nil + err := irccon.Connect("chat.freenode.net:6667") + if err == nil { + t.Fatal("missing 'Log' not detected") + } +} + +func TestConnectionEmptyUser(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + // user may be changed after creation + irccon.user = "" + err := irccon.Connect("chat.freenode.net:6667") + if err == nil { + t.Fatal("empty 'user' not detected") + } +} + +func TestConnectionEmptyNick(t *testing.T) { + irccon := IRC("go-eventirc", "go-eventirc") + // nick may be changed after creation + irccon.nick = "" + err := irccon.Connect("chat.freenode.net:6667") + if err == nil { + t.Fatal("empty 'nick' not detected") + } +} + func TestRemoveCallback(t *testing.T) { irccon := IRC("go-eventirc", "go-eventirc") irccon.VerboseCallbackHandler = true @@ -127,3 +206,19 @@ func TestClearCallback(t *testing.T) { t.Error("Callbacks not cleared") } } + +func TestIRCemptyNick(t *testing.T) { + irccon := IRC("", "go-eventirc") + irccon = nil + if irccon != nil { + t.Error("empty nick didn't result in error") + t.Fail() + } +} + +func TestIRCemptyUser(t *testing.T) { + irccon := IRC("go-eventirc", "") + if irccon != nil { + t.Error("empty user didn't result in error") + } +}