client: Handle capabilities, improve handling and testing

This commit is contained in:
Daniel Oaks 2016-02-10 21:37:18 +10:00
parent 6d960c81bc
commit 1cb8e4762b
5 changed files with 195 additions and 26 deletions

79
client/capabilities.go Normal file

@ -0,0 +1,79 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package gircclient
import "strings"
import "sort"
// ClientCapabilities holds the capabilities that can and have been enabled on
// a ServerConnection.
type ClientCapabilities struct {
Avaliable map[string]*string
Enabled map[string]*string
Wanted []string
}
// NewClientCapabilities returns a newly-initialised ClientCapabilities.
func NewClientCapabilities() ClientCapabilities {
var cc ClientCapabilities
cc.Avaliable = make(map[string]*string, 0)
cc.Enabled = make(map[string]*string, 0)
cc.Wanted = make([]string, 0)
return cc
}
// AddWantedCaps adds the given capabilities to our list of capabilities that
// we want from the server.
func (cc *ClientCapabilities) AddWantedCaps(caps ...string) {
for _, name := range caps {
// I'm not sure how fast this is, but speed isn't too much of a concern
// here. Adding 'wanted capabilities' is something that generally only
// happens at startup anyway.
i := sort.Search(len(cc.Wanted), func(i int) bool { return cc.Wanted[i] >= name })
if i >= len(cc.Wanted) || cc.Wanted[i] != name {
cc.Wanted = append(cc.Wanted, name)
sort.Strings(cc.Wanted)
}
}
}
// AddCaps adds capabilities from LS lists to our Avaliable map.
func (cc *ClientCapabilities) AddCaps(tags []string) {
var name string
var value *string
for _, tag := range tags {
if strings.Contains(tag, "=") {
vals := strings.SplitN(tag, "=", 2)
name = vals[0]
value = &vals[1]
} else {
name = tag
value = nil
}
cc.Avaliable[name] = value
}
}
// ToRequestLine returns a line of capabilities to request, to be used in a
// CAP REQ line.
func (cc *ClientCapabilities) ToRequestLine() string {
var caps []string
caps = make([]string, 0)
for _, name := range cc.Wanted {
_, capIsAvailable := cc.Avaliable[name]
_, capIsEnabled := cc.Enabled[name]
if capIsAvailable && !capIsEnabled {
caps = append(caps, name)
}
}
return strings.Join(caps, " ")
}

@ -17,8 +17,9 @@ import (
// ServerConnection is a connection to a single server.
type ServerConnection struct {
Name string
Connected bool
Name string
Connected bool
Registered bool
// internal stuff
connection net.Conn
@ -26,8 +27,8 @@ type ServerConnection struct {
eventsOut eventmgr.EventManager
// data we keep track of
//features ServerFeatures
//caps ClientCapabilities
// Features ServerFeatures
Caps ClientCapabilities
// details users must supply before connection
Nick string
@ -36,6 +37,19 @@ type ServerConnection struct {
InitialRealName string
}
// newServerConnection returns an initialised ServerConnection, for internal
// use.
func newServerConnection() *ServerConnection {
var sc ServerConnection
sc.Caps = NewClientCapabilities()
sc.Caps.AddWantedCaps("account-notify", "away-notify", "extended-join", "multi-prefix", "sasl")
sc.Caps.AddWantedCaps("account-tag", "chghost", "echo-message", "invite-notify", "server-time", "userhost-in-names")
return &sc
}
// Connect connects to the given address.
func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Config) error {
var conn net.Conn
@ -54,9 +68,7 @@ func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Con
sc.connection = conn
sc.Connected = true
sc.Nick = sc.InitialNick
sc.Send(nil, "", "NICK", sc.InitialNick)
sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName)
sc.Send(nil, "", "CAP", "LS", "302")
go sc.receiveLoop()
@ -97,7 +109,8 @@ func (sc *ServerConnection) receiveLoop() {
info["command"] = message.Command
info["params"] = message.Params
sc.dispatchIn(message.Command, info)
// IRC commands are case-insensitive
sc.dispatchIn(strings.ToUpper(message.Command), info)
}
sc.connection.Close()

44
client/handlers.go Normal file

@ -0,0 +1,44 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package gircclient
import (
"strings"
"github.com/DanielOaks/girc-go/eventmgr"
)
// welcomeHandler sets the nick to the first parameter of the 001 message.
// This ensures that when we connect to IRCds that silently truncate the
// nickname, we keep the correct one.
func welcomeHandler(event string, info eventmgr.InfoMap) {
sc := info["server"].(*ServerConnection)
sc.Nick = info["params"].([]string)[0]
}
func capHandler(event string, info eventmgr.InfoMap) {
sc := info["server"].(*ServerConnection)
params := info["params"].([]string)
subcommand := strings.ToUpper(params[1])
if !sc.Registered && (subcommand == "ACK" || subcommand == "NAK") {
sendRegistration(sc)
} else if subcommand == "LS" {
if len(params) > 3 {
sc.Caps.AddCaps(strings.Split(params[3], " "))
} else {
sc.Caps.AddCaps(strings.Split(params[2], " "))
capsToRequest := sc.Caps.ToRequestLine()
if len(capsToRequest) > 0 {
sc.Send(nil, "", "CAP", "REQ", capsToRequest)
}
}
}
}
func sendRegistration(sc *ServerConnection) {
sc.Nick = sc.InitialNick
sc.Send(nil, "", "NICK", sc.InitialNick)
sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName)
}

@ -20,14 +20,6 @@ type Reactor struct {
eventsToRegister []eventRegistration
}
// welcomeHandler sets the nick to the first parameter of the 001 message.
// This ensures that when we connect to IRCds that silently truncate the
// nickname, we keep the correct one.
func welcomeHandler(event string, info eventmgr.InfoMap) {
server := info["server"].(*ServerConnection)
server.Nick = info["params"].([]string)[0]
}
// NewReactor returns a new, empty Reactor.
func NewReactor() Reactor {
var newReactor Reactor
@ -36,6 +28,7 @@ func NewReactor() Reactor {
newReactor.eventsToRegister = make([]eventRegistration, 0)
// add the default handlers
newReactor.RegisterEvent("in", "CAP", capHandler, -10)
newReactor.RegisterEvent("in", "001", welcomeHandler, -10)
return newReactor
@ -43,15 +36,15 @@ func NewReactor() Reactor {
// CreateServer creates a ServerConnection and returns it.
func (r *Reactor) CreateServer(name string) *ServerConnection {
var sc ServerConnection
sc := newServerConnection()
r.ServerConnections[name] = &sc
r.ServerConnections[name] = sc
for _, e := range r.eventsToRegister {
sc.RegisterEvent(e.Direction, e.Name, e.Handler, e.Priority)
}
return &sc
return sc
}
// Shutdown shuts down all ServerConnections.

@ -15,6 +15,8 @@ import (
"runtime"
"testing"
"time"
"github.com/DanielOaks/girc-go/ircmsg"
)
func TestPlainConnection(t *testing.T) {
@ -95,13 +97,56 @@ func TestTLSConnection(t *testing.T) {
testServerConnection(t, reactor, client, listener)
}
func sendMessage(conn net.Conn, tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
ircmsg := ircmsg.MakeMessage(tags, prefix, command, params...)
line, err := ircmsg.Line()
if err != nil {
return
}
fmt.Fprintf(conn, line)
// need to wait for a quick moment here for TLS to do this properly
runtime.Gosched()
waitTime, _ := time.ParseDuration("10ms")
time.Sleep(waitTime)
}
func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnection, listener net.Listener) {
conn, _ := listener.Accept()
reader := bufio.NewReader(conn)
// test each message in sequence
var message string
// CAP
message, _ = reader.ReadString('\n')
if message != "CAP LS 302\r\n" {
t.Error(
"Did not receive CAP LS message, received: [",
message,
"]",
)
return
}
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "*", "multi-prefix userhost-in-names sasl=PLAIN")
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "chghost")
message, _ = reader.ReadString('\n')
if message != "CAP REQ :chghost multi-prefix sasl userhost-in-names\r\n" {
t.Error(
"Did not receive CAP REQ message, received: [",
message,
"]",
)
return
}
// these should be silently ignored
fmt.Fprintf(conn, "\r\n\r\n\r\n")
sendMessage(conn, nil, "example.com", "CAP", "*", "ACK", "chghost multi-prefix userhost-in-names sasl")
// NICK/USER
message, _ = reader.ReadString('\n')
if message != "NICK coolguy\r\n" {
t.Error(
@ -123,12 +168,7 @@ func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnectio
}
// make sure nick changes properly
// need to wait for a quick moment here for TLS to do this properly
fmt.Fprintf(conn, "\r\n\r\n\r\n") // these should be silently ignored
fmt.Fprintf(conn, ":example.com 001 dan :Welcome to the gIRC-Go Test Network!\r\n")
runtime.Gosched()
waitTime, _ := time.ParseDuration("10ms")
time.Sleep(waitTime)
sendMessage(conn, nil, "example.com", "001", "dan", "Welcome to the gIRC-Go Test Network!")
if client.Nick != "dan" {
t.Error(