Merge branch 'master' into feature/webirc

This commit is contained in:
Qais Patankar 2017-12-24 19:08:02 +00:00 committed by GitHub
commit d5dbd683d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 231 additions and 41 deletions

141
irc.go
View File

@ -74,9 +74,9 @@ func (irc *Connection) readLoop() {
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg)) irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
} }
irc.Lock() irc.lastMessageMutex.Lock()
irc.lastMessage = time.Now() irc.lastMessage = time.Now()
irc.Unlock() irc.lastMessageMutex.Unlock()
event, err := parseToEvent(msg) event, err := parseToEvent(msg)
event.Connection = irc event.Connection = irc
if err == nil { if err == nil {
@ -87,6 +87,17 @@ func (irc *Connection) readLoop() {
} }
} }
// Unescape tag values as defined in the IRCv3.2 message tags spec
// http://ircv3.net/specs/core/message-tags-3.2.html
func unescapeTagValue(value string) string {
value = strings.Replace(value, "\\:", ";", -1)
value = strings.Replace(value, "\\s", " ", -1)
value = strings.Replace(value, "\\\\", "\\", -1)
value = strings.Replace(value, "\\r", "\r", -1)
value = strings.Replace(value, "\\n", "\n", -1)
return value
}
//Parse raw irc messages //Parse raw irc messages
func parseToEvent(msg string) (*Event, error) { func parseToEvent(msg string) (*Event, error) {
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
@ -95,6 +106,26 @@ func parseToEvent(msg string) (*Event, error) {
if len(msg) < 5 { if len(msg) < 5 {
return nil, errors.New("Malformed msg from server") return nil, errors.New("Malformed msg from server")
} }
if msg[0] == '@' {
// IRCv3 Message Tags
if i := strings.Index(msg, " "); i > -1 {
event.Tags = make(map[string]string)
tags := strings.Split(msg[1:i], ";")
for _, data := range tags {
parts := strings.SplitN(data, "=", 2)
if len(parts) == 1 {
event.Tags[parts[0]] = ""
} else {
event.Tags[parts[0]] = unescapeTagValue(parts[1])
}
}
msg = msg[i+1 : len(msg)]
} else {
return nil, errors.New("Malformed msg from server")
}
}
if msg[0] == ':' { if msg[0] == ':' {
if i := strings.Index(msg, " "); i > -1 { if i := strings.Index(msg, " "); i > -1 {
event.Source = msg[1:i] event.Source = msg[1:i]
@ -166,9 +197,11 @@ func (irc *Connection) pingLoop() {
select { select {
case <-ticker.C: case <-ticker.C:
//Ping if we haven't received anything from the server within the keep alive period //Ping if we haven't received anything from the server within the keep alive period
irc.lastMessageMutex.Lock()
if time.Since(irc.lastMessage) >= irc.KeepAlive { if time.Since(irc.lastMessage) >= irc.KeepAlive {
irc.SendRawf("PING %d", time.Now().UnixNano()) irc.SendRawf("PING %d", time.Now().UnixNano())
} }
irc.lastMessageMutex.Unlock()
case <-ticker2.C: case <-ticker2.C:
//Ping at the ping frequency //Ping at the ping frequency
irc.SendRawf("PING %d", time.Now().UnixNano()) irc.SendRawf("PING %d", time.Now().UnixNano())
@ -355,6 +388,20 @@ func (irc *Connection) Connected() bool {
// 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.Lock()
defer irc.Unlock()
if irc.end != nil {
close(irc.end)
}
irc.end = nil
if irc.pwrite != nil {
close(irc.pwrite)
}
irc.Wait()
if irc.socket != nil { if irc.socket != nil {
irc.socket.Close() irc.socket.Close()
} }
@ -435,26 +482,96 @@ func (irc *Connection) Connect(server string) error {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password) irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
} }
resChan := make(chan *SASLResult) err = irc.negotiateCaps()
if err != nil {
return err
}
realname := irc.user
if irc.RealName != "" {
realname = irc.RealName
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, realname)
return nil
}
// Negotiate IRCv3 capabilities
func (irc *Connection) negotiateCaps() error {
saslResChan := make(chan *SASLResult)
if irc.UseSASL {
irc.RequestCaps = append(irc.RequestCaps, "sasl")
irc.setupSASLCallbacks(saslResChan)
}
if len(irc.RequestCaps) == 0 {
return nil
}
cap_chan := make(chan bool, len(irc.RequestCaps))
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) != 3 {
return
}
command := e.Arguments[1]
if command == "LS" {
missing_caps := len(irc.RequestCaps)
for _, cap_name := range strings.Split(e.Arguments[2], " ") {
for _, req_cap := range irc.RequestCaps {
if cap_name == req_cap {
irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name)
missing_caps--
}
}
}
for i := 0; i < missing_caps; i++ {
cap_chan <- true
}
} else if command == "ACK" || command == "NAK" {
for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") {
if cap_name == "" {
continue
}
if command == "ACK" {
irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name)
}
cap_chan <- true
}
}
})
irc.pwrite <- "CAP LS\r\n"
if irc.UseSASL { if irc.UseSASL {
irc.setupSASLCallbacks(resChan)
irc.pwrite <- fmt.Sprintf("CAP LS\r\n")
// request SASL
irc.pwrite <- fmt.Sprintf("CAP REQ :sasl\r\n")
// if sasl request doesn't complete in 15 seconds, close chan and timeout
select { select {
case res := <-resChan: case res := <-saslResChan:
if res.Failed { if res.Failed {
close(resChan) close(saslResChan)
return res.Err return res.Err
} }
case <-time.After(time.Second * 15): case <-time.After(time.Second * 15):
close(resChan) close(saslResChan)
return errors.New("SASL setup timed out. This shouldn't happen.") return errors.New("SASL setup timed out. This shouldn't happen.")
} }
} }
// Wait for all capabilities to be ACKed or NAKed before ending negotiation
for i := 0; i < len(irc.RequestCaps); i++ {
<-cap_chan
}
irc.pwrite <- fmt.Sprintf("CAP END\r\n")
realname := irc.user
if irc.RealName != "" {
realname = irc.RealName
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick) irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user) irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, realname)
return nil return nil
} }

View File

@ -13,13 +13,17 @@ import (
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int { func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
id := 0 id := 0
if _, ok := irc.events[eventcode]; !ok {
irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if !ok {
irc.events[eventcode] = make(map[int]func(*Event)) irc.events[eventcode] = make(map[int]func(*Event))
id = 0 id = 0
} else { } else {
id = len(irc.events[eventcode]) id = len(irc.events[eventcode])
} }
irc.events[eventcode][id] = callback irc.events[eventcode][id] = callback
irc.eventsMutex.Unlock()
return id return id
} }
@ -28,15 +32,20 @@ func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int
func (irc *Connection) RemoveCallback(eventcode string, i int) bool { func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
if ok {
if _, ok := event[i]; ok { if _, ok := event[i]; ok {
delete(irc.events[eventcode], i) delete(irc.events[eventcode], i)
irc.eventsMutex.Unlock()
return true return true
} }
irc.Log.Printf("Event found, but no callback found at id %d\n", i) irc.Log.Printf("Event found, but no callback found at id %d\n", i)
irc.eventsMutex.Unlock()
return false return false
} }
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found") irc.Log.Println("Event not found")
return false return false
} }
@ -46,10 +55,14 @@ func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
func (irc *Connection) ClearCallback(eventcode string) bool { func (irc *Connection) ClearCallback(eventcode string) bool {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if _, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
_, ok := irc.events[eventcode]
if ok {
irc.events[eventcode] = make(map[int]func(*Event)) irc.events[eventcode] = make(map[int]func(*Event))
irc.eventsMutex.Unlock()
return true return true
} }
irc.eventsMutex.Unlock()
irc.Log.Println("Event not found") irc.Log.Println("Event not found")
return false return false
@ -59,7 +72,10 @@ func (irc *Connection) ClearCallback(eventcode string) bool {
func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) { func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) {
eventcode = strings.ToUpper(eventcode) eventcode = strings.ToUpper(eventcode)
if event, ok := irc.events[eventcode]; ok { irc.eventsMutex.Lock()
event, ok := irc.events[eventcode]
irc.eventsMutex.Unlock()
if ok {
if _, ok := event[i]; ok { if _, ok := event[i]; ok {
event[i] = callback event[i] = callback
return return
@ -109,7 +125,10 @@ func (irc *Connection) RunCallbacks(event *Event) {
event.Arguments[len(event.Arguments)-1] = msg event.Arguments[len(event.Arguments)-1] = msg
} }
if callbacks, ok := irc.events[event.Code]; ok { irc.eventsMutex.Lock()
callbacks, ok := irc.events[event.Code]
irc.eventsMutex.Unlock()
if ok {
if irc.VerboseCallbackHandler { if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event) irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
} }
@ -121,12 +140,15 @@ func (irc *Connection) RunCallbacks(event *Event) {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
} }
if callbacks, ok := irc.events["*"]; ok { irc.eventsMutex.Lock()
allcallbacks, ok := irc.events["*"]
irc.eventsMutex.Unlock()
if ok {
if irc.VerboseCallbackHandler { if irc.VerboseCallbackHandler {
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event) irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
} }
for _, callback := range callbacks { for _, callback := range allcallbacks {
callback(event) callback(event)
} }
} }
@ -136,9 +158,6 @@ func (irc *Connection) RunCallbacks(event *Event) {
func (irc *Connection) setupCallbacks() { func (irc *Connection) setupCallbacks() {
irc.events = make(map[string]map[int]func(*Event)) irc.events = make(map[string]map[int]func(*Event))
//Handle error events.
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
//Handle ping events //Handle ping events
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) }) irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })

47
irc_parse_test.go Normal file
View File

@ -0,0 +1,47 @@
package irc
import (
"fmt"
"testing"
)
func checkResult(t *testing.T, event *Event) {
if event.Nick != "nick" {
t.Fatal("Parse failed: nick")
}
if event.User != "~user" {
t.Fatal("Parse failed: user")
}
if event.Code != "PRIVMSG" {
t.Fatal("Parse failed: code")
}
if event.Arguments[0] != "#channel" {
t.Fatal("Parse failed: channel")
}
if event.Arguments[1] != "message text" {
t.Fatal("Parse failed: message")
}
}
func TestParse(t *testing.T) {
event, err := parseToEvent(":nick!~user@host PRIVMSG #channel :message text")
if err != nil {
t.Fatal("Parse PRIVMSG failed")
}
checkResult(t, event)
}
func TestParseTags(t *testing.T) {
event, err := parseToEvent("@tag;+tag2=raw+:=,escaped\\:\\s\\\\ :nick!~user@host PRIVMSG #channel :message text")
if err != nil {
t.Fatal("Parse PRIVMSG with tags failed")
}
checkResult(t, event)
fmt.Printf("%s", event.Tags)
if _, ok := event.Tags["tag"]; !ok {
t.Fatal("Parsing value-less tag failed")
}
if event.Tags["+tag2"] != "raw+:=,escaped; \\" {
t.Fatal("Parsing tag failed")
}
}

View File

@ -43,7 +43,6 @@ func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
result <- &SASLResult{true, errors.New(e.Arguments[1])} result <- &SASLResult{true, errors.New(e.Arguments[1])}
}) })
irc.AddCallback("903", func(e *Event) { irc.AddCallback("903", func(e *Event) {
irc.SendRaw("CAP END")
result <- &SASLResult{false, nil} result <- &SASLResult{false, nil}
}) })
irc.AddCallback("904", func(e *Event) { irc.AddCallback("904", func(e *Event) {

View File

@ -34,7 +34,7 @@ func TestConnectionSASL(t *testing.T) {
err := irccon.Connect(SASLServer) err := irccon.Connect(SASLServer)
if err != nil { if err != nil {
t.Fatal("SASL failed") t.Fatalf("SASL failed: %s", err)
} }
irccon.Loop() irccon.Loop()
} }

View File

@ -15,21 +15,26 @@ import (
type Connection struct { type Connection struct {
sync.Mutex sync.Mutex
sync.WaitGroup sync.WaitGroup
Debug bool Debug bool
Error chan error Error chan error
Password string WebIRC string
UseTLS bool Password string
UseSASL bool UseTLS bool
SASLLogin string UseSASL bool
SASLPassword string RequestCaps []string
SASLMech string AcknowledgedCaps []string
WebIRC string SASLLogin string
TLSConfig *tls.Config SASLPassword string
Version string SASLMech string
Timeout time.Duration TLSConfig *tls.Config
PingFreq time.Duration Version string
KeepAlive time.Duration Timeout time.Duration
Server string PingFreq time.Duration
KeepAlive time.Duration
Server string
RealName string // The real name we want to display.
// If zero-value defaults to the user.
socket net.Conn socket net.Conn
pwrite chan string pwrite chan string
@ -40,9 +45,11 @@ type Connection struct {
user string user string
registered bool registered bool
events map[string]map[int]func(*Event) events map[string]map[int]func(*Event)
eventsMutex sync.Mutex
QuitMessage string QuitMessage string
lastMessage time.Time lastMessage time.Time
lastMessageMutex sync.Mutex
VerboseCallbackHandler bool VerboseCallbackHandler bool
Log *log.Logger Log *log.Logger
@ -60,6 +67,7 @@ type Event struct {
Source string //<host> Source string //<host>
User string //<usr> User string //<usr>
Arguments []string Arguments []string
Tags map[string]string
Connection *Connection Connection *Connection
} }