Merge pull request #27 from tpltnt/master

sanity checks IRC() and Connect()
This commit is contained in:
Thomas Jager 2014-02-16 14:23:08 +01:00
commit accfd72b17
2 changed files with 142 additions and 28 deletions

75
irc.go
View File

@ -3,9 +3,9 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
/* /*
This package provides an event based IRC client library. It allows to This package provides an event based IRC client library. It allows to
register callbacks for the events you need to handle. Its features register callbacks for the events you need to handle. Its features
include handling standard CTCP, reconnecting on errors and detecting include handling standard CTCP, reconnecting on errors and detecting
stones servers. stones servers.
Details of the IRC protocol can be found in the following RFCs: Details of the IRC protocol can be found in the following RFCs:
https://tools.ietf.org/html/rfc1459 https://tools.ietf.org/html/rfc1459
@ -26,6 +26,7 @@ import (
"log" "log"
"net" "net"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -34,7 +35,6 @@ const (
VERSION = "go-ircevent v2.1" VERSION = "go-ircevent v2.1"
) )
// Read data from a connection. To be used as a goroutine. // Read data from a connection. To be used as a goroutine.
func (irc *Connection) readLoop() { func (irc *Connection) readLoop() {
br := bufio.NewReaderSize(irc.socket, 512) br := bufio.NewReaderSize(irc.socket, 512)
@ -101,7 +101,6 @@ func (irc *Connection) readLoop() {
irc.readerExit <- true irc.readerExit <- true
} }
// Loop to write to a connection. To be used as a goroutine. // Loop to write to a connection. To be used as a goroutine.
func (irc *Connection) writeLoop() { func (irc *Connection) writeLoop() {
for { for {
@ -139,7 +138,6 @@ func (irc *Connection) writeLoop() {
irc.writerExit <- true irc.writerExit <- true
} }
// Pings the server if we have not received any messages for 5 minutes // Pings the server if we have not received any messages for 5 minutes
// to keep the connection alive. To be used as a goroutine. // to keep the connection alive. To be used as a goroutine.
func (irc *Connection) pingLoop() { func (irc *Connection) pingLoop() {
@ -169,7 +167,6 @@ func (irc *Connection) pingLoop() {
} }
} }
// Main loop to control the connection. // Main loop to control the connection.
func (irc *Connection) Loop() { func (irc *Connection) Loop() {
for !irc.stopped { for !irc.stopped {
@ -190,7 +187,6 @@ func (irc *Connection) Loop() {
} }
} }
// Quit the current connection and disconnect from the server // Quit the current connection and disconnect from the server
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6
func (irc *Connection) Quit() { func (irc *Connection) Quit() {
@ -199,60 +195,51 @@ func (irc *Connection) Quit() {
irc.Disconnect() irc.Disconnect()
} }
// Use the connection to join a given channel. // Use the connection to join a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1
func (irc *Connection) Join(channel string) { func (irc *Connection) Join(channel string) {
irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel) irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel)
} }
// Leave a given channel. // Leave a given channel.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2
func (irc *Connection) Part(channel string) { func (irc *Connection) Part(channel string) {
irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel) 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. // 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 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Notice(target, message string) { func (irc *Connection) Notice(target, message string) {
irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message) irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message)
} }
// Send a formated notification to a nickname. // Send a formated notification to a nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
func (irc *Connection) Noticef(target, format string, a ...interface{}) { func (irc *Connection) Noticef(target, format string, a ...interface{}) {
irc.Notice(target, fmt.Sprintf(format, a...)) irc.Notice(target, fmt.Sprintf(format, a...))
} }
// Send (private) message to a target (channel or nickname). // Send (private) message to a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1
func (irc *Connection) Privmsg(target, message string) { func (irc *Connection) Privmsg(target, message string) {
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message) irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message)
} }
// Send formated string to specified target (channel or nickname). // Send formated string to specified target (channel or nickname).
func (irc *Connection) Privmsgf(target, format string, a ...interface{}) { func (irc *Connection) Privmsgf(target, format string, a ...interface{}) {
irc.Privmsg(target, fmt.Sprintf(format, a...)) irc.Privmsg(target, fmt.Sprintf(format, a...))
} }
// Send raw string. // Send raw string.
func (irc *Connection) SendRaw(message string) { func (irc *Connection) SendRaw(message string) {
irc.pwrite <- message + "\r\n" irc.pwrite <- message + "\r\n"
} }
// Send raw formated string. // Send raw formated string.
func (irc *Connection) SendRawf(format string, a ...interface{}) { func (irc *Connection) SendRawf(format string, a ...interface{}) {
irc.SendRaw(fmt.Sprintf(format, a...)) irc.SendRaw(fmt.Sprintf(format, a...))
} }
// Set (new) nickname. // Set (new) nickname.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2
func (irc *Connection) Nick(n string) { func (irc *Connection) Nick(n string) {
@ -260,27 +247,23 @@ func (irc *Connection) Nick(n string) {
irc.SendRawf("NICK %s", n) irc.SendRawf("NICK %s", n)
} }
// Determine nick currently used with the connection. // Determine nick currently used with the connection.
func (irc *Connection) GetNick() string { func (irc *Connection) GetNick() string {
return irc.nickcurrent return irc.nickcurrent
} }
// Query information about a particular nickname. // Query information about a particular nickname.
// RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2 // RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2
func (irc *Connection) Whois(nick string) { func (irc *Connection) Whois(nick string) {
irc.SendRawf("WHOIS %s", nick) irc.SendRawf("WHOIS %s", nick)
} }
// Query information about a given nickname in the server. // Query information about a given nickname in the server.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1
func (irc *Connection) Who(nick string) { func (irc *Connection) Who(nick string) {
irc.SendRawf("WHO %s", nick) irc.SendRawf("WHO %s", nick)
} }
// Set different modes for a target (channel or nickname). // Set different modes for a target (channel or nickname).
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3 // RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3
func (irc *Connection) Mode(target string, modestring ...string) { 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) 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. // stops all goroutines and then closes the socket.
func (irc *Connection) Disconnect() { func (irc *Connection) Disconnect() {
irc.endping <- true irc.endping <- true
@ -314,13 +296,11 @@ func (irc *Connection) Disconnect() {
irc.Error <- errors.New("Disconnect Called") irc.Error <- errors.New("Disconnect Called")
} }
// Reconnect to a server using the current connection. // Reconnect to a server using the current connection.
func (irc *Connection) Reconnect() error { func (irc *Connection) Reconnect() error {
return irc.Connect(irc.server) return irc.Connect(irc.server)
} }
// Connect to a given server using the current connection configuration. // Connect to a given server using the current connection configuration.
// This function also takes care of identification if a password is provided. // This function also takes care of identification if a password is provided.
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1 // 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.server = server
irc.stopped = false 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.UseTLS {
if irc.netsock, err = net.DialTimeout("tcp", irc.server, irc.Timeout); err == nil { if irc.netsock, err = net.DialTimeout("tcp", irc.server, irc.Timeout); err == nil {
irc.socket = tls.Client(irc.netsock, irc.TLSConfig) irc.socket = tls.Client(irc.netsock, irc.TLSConfig)
@ -357,10 +368,18 @@ func (irc *Connection) Connect(server string) error {
return nil return nil
} }
// Create a connection with the (publicly visible) nickname and username. // 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 { func IRC(nick, user string) *Connection {
// catch invalid values
if len(nick) == 0 {
return nil
}
if len(user) == 0 {
return nil
}
irc := &Connection{ irc := &Connection{
nick: nick, nick: nick,
user: user, user: user,

View File

@ -12,6 +12,7 @@ func TestConnection(t *testing.T) {
irccon.Debug = true irccon.Debug = true
err := irccon.Connect("irc.freenode.net:6667") err := irccon.Connect("irc.freenode.net:6667")
if err != nil { if err != nil {
t.Log(err.Error())
t.Fatal("Can't connect to freenode.") t.Fatal("Can't connect to freenode.")
} }
irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") })
@ -36,6 +37,7 @@ func TestConnectionSSL(t *testing.T) {
irccon.UseTLS = true irccon.UseTLS = true
err := irccon.Connect("irc.freenode.net:7000") err := irccon.Connect("irc.freenode.net:7000")
if err != nil { if err != nil {
t.Log(err.Error())
t.Fatal("Can't connect to freenode.") t.Fatal("Can't connect to freenode.")
} }
irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") })
@ -49,6 +51,83 @@ func TestConnectionSSL(t *testing.T) {
irccon.Loop() 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) { func TestRemoveCallback(t *testing.T) {
irccon := IRC("go-eventirc", "go-eventirc") irccon := IRC("go-eventirc", "go-eventirc")
irccon.VerboseCallbackHandler = true irccon.VerboseCallbackHandler = true
@ -127,3 +206,19 @@ func TestClearCallback(t *testing.T) {
t.Error("Callbacks not cleared") 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")
}
}