diff --git a/cap.go b/cap.go index d478a91..575ceb8 100644 --- a/cap.go +++ b/cap.go @@ -161,7 +161,7 @@ func handleCAP(c *Client, e Event) { c.state.mu.Unlock() if wantsSASL { - c.write(&Event{Command: AUTHENTICATE, Params: []string{"PLAIN"}}) + c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) // Don't "CAP END", since we want to authenticate. return } @@ -172,13 +172,46 @@ func handleCAP(c *Client, e Event) { } } -// SASLAuth contains the user and password needed for PLAIN SASL authentication. -type SASLAuth struct { +// SASLMethod is an representation of what a SASL mechanism should support. +// See SASLExternal and SASLPlain for implementations of this. +type SASLMethod interface { + // Method returns the uppercase version of the SASL mechanism name. + Method() string + // Encode returns the response that the SASL mechanism wants to use, + // chunked out as necessary. if this returns nil, an "AUTHENTICATE +" will + // be used to respond (essentially telling the server that it should handle + // the rest.) + Encode(chunkSize int) (chunks []string) +} + +// SASLExternal implements the "EXTERNAL" SASL type. +type SASLExternal struct { +} + +// Method identifies what type of SASL this implements. +func (sasl *SASLExternal) Method() string { + return "EXTERNAL" +} + +// Encode is not directly used by SASLExternal -- it currently only returns +// nil. +func (sasl *SASLExternal) Encode(chunkSize int) (chunks []string) { + return nil +} + +// SASLPlain contains the user and password needed for PLAIN SASL authentication. +type SASLPlain struct { User string // User is the username for SASL. Pass string // Pass is the password for SASL. } -func (sasl *SASLAuth) encode(chunkSize int) (chunks []string) { +// Method identifies what type of SASL this implements. +func (sasl *SASLPlain) Method() string { + return "PLAIN" +} + +// Encode encodes the plain user+password into a standardized chunk size. +func (sasl *SASLPlain) Encode(chunkSize int) (chunks []string) { in := []byte(sasl.User) in = append(in, 0x0) @@ -213,13 +246,20 @@ func handleSASL(c *Client, e Event) { if len(e.Params) == 1 && e.Params[0] == "+" { // Assume they want us to handle sending auth. - auth := c.Config.SASL.encode(400) + auth := c.Config.SASL.Encode(400) + + if auth == nil { + // Assume the SASL authentication method doesn't need to encode + // data and pass it to the server. + c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) + return + } // Send in 400 byte chunks. If the last chuck is exactly 400 bytes, // send a "AUTHENTICATE +" 0-byte response to let the server know // that we're done. for i := 0; i < len(auth); i++ { - c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[i]}}) + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[i]}, Sensitive: true}) if i-1 == len(auth) && len(auth[i]) == 400 { c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) @@ -236,7 +276,7 @@ func handleSASLError(c *Client, e Event) { // This is supposed to panic. Per the IRCv3 spec, one must disconnect upon // authentication error. Maybe though, just a QUIT would be better? - panic(fmt.Sprintf("unable to use SASL authentication: %s (%s)", e.Command, e.Trailing)) + panic(fmt.Sprintf("unable to use SASL %s authentication: %s (%s)", c.Config.SASL.Method(), e.Command, e.Trailing)) } // handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is diff --git a/cap_test.go b/cap_test.go index 09b074a..9c51cd1 100644 --- a/cap_test.go +++ b/cap_test.go @@ -12,7 +12,7 @@ func TestCapList(t *testing.T) { Server: "irc.example.com", Nick: "test", User: "user", - SASL: &SASLAuth{User: "test", Pass: "example"}, + SASL: &SASLPlain{User: "test", Pass: "example"}, SupportedCaps: map[string][]string{"example": nil}, }) diff --git a/client.go b/client.go index 2aca0d2..22a009c 100644 --- a/client.go +++ b/client.go @@ -80,9 +80,10 @@ type Config struct { // affect during the dial process. Name string // SASL contains the necessary authentication data to authenticate - // with SASL. At this time, only PLAIN is supported. Capability tracking - // must be enabled for this to work, as this requires IRCv3 CAP handling. - SASL *SASLAuth + // with SASL. See the documentation for SASLMethod for what is currently + // supported. Capability tracking must be enabled for this to work, as + // this requires IRCv3 CAP handling. + SASL SASLMethod // Proxy is a proxy based address, used during the dial process when // connecting to the server. This only has an affect during the dial // process. Currently, x/net/proxy only supports socks5, however you can diff --git a/conn.go b/conn.go index a4a4e1e..8bb5dad 100644 --- a/conn.go +++ b/conn.go @@ -304,7 +304,7 @@ func (c *Client) internalConnect(mock net.Conn) error { // Passwords first. if c.Config.ServerPass != "" { - c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}}) + c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true}) } // Then nickname. @@ -315,7 +315,7 @@ func (c *Client) internalConnect(mock net.Conn) error { c.Config.Name = c.Config.User } - c.write(&Event{Command: USER, Params: []string{c.Config.User, "+iw", "*"}, Trailing: c.Config.Name}) + c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*"}, Trailing: c.Config.Name}) // List the IRCv3 capabilities, specifically with the max protocol we // support. @@ -437,7 +437,9 @@ func (c *Client) sendLoop(errs chan error, done chan struct{}, wg *sync.WaitGrou select { case event := <-c.tx: // Log the event. - if !event.Sensitive { + if event.Sensitive { + c.debug.Printf("> %s ***redacted***", event.Command) + } else { c.debug.Print("> ", StripRaw(event.String())) } if c.Config.Out != nil { diff --git a/example_test.go b/example_test.go index 31f308a..3d0cc01 100644 --- a/example_test.go +++ b/example_test.go @@ -19,7 +19,7 @@ func ExampleNew() { Port: 6667, Nick: "test", User: "user", - SASL: &girc.SASLAuth{User: "user1", Pass: "securepass1"}, + SASL: &girc.SASLPlain{User: "user1", Pass: "securepass1"}, Out: os.Stdout, })