Changeset 95 in code for trunk/upstream.go


Ignore:
Timestamp:
Mar 13, 2020, 2:12:44 PM (5 years ago)
Author:
contact
Message:

Add support for SASL authentication

We now store SASL credentials in the database and automatically populate
them on NickServ REGISTER/IDENTIFY.

References: https://todo.sr.ht/~emersion/jounce/10

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/upstream.go

    r93 r95  
    33import (
    44        "crypto/tls"
     5        "encoding/base64"
    56        "fmt"
    67        "io"
     
    1011        "time"
    1112
     13        "github.com/emersion/go-sasl"
    1214        "gopkg.in/irc.v3"
    1315)
     
    4951        history    map[string]uint64
    5052        caps       map[string]string
     53
     54        saslClient  sasl.Client
     55        saslStarted bool
    5156}
    5257
     
    170175                uc.logger.Print(msg)
    171176        case "CAP":
    172                 if len(msg.Params) < 2 {
    173                         return newNeedMoreParamsError(msg.Command)
    174                 }
    175                 caps := strings.Fields(msg.Params[len(msg.Params)-1])
    176                 more := msg.Params[len(msg.Params)-2] == "*"
    177 
    178                 for _, s := range caps {
    179                         kv := strings.SplitN(s, "=", 2)
    180                         k := strings.ToLower(kv[0])
    181                         var v string
    182                         if len(kv) >= 2 {
    183                                 v = kv[1]
    184                         }
    185                         uc.caps[k] = v
    186                 }
    187 
    188                 if !more {
     177                var subCmd string
     178                if err := parseMessageParams(msg, nil, &subCmd); err != nil {
     179                        return err
     180                }
     181                subCmd = strings.ToUpper(subCmd)
     182                subParams := msg.Params[2:]
     183                switch subCmd {
     184                case "LS":
     185                        if len(subParams) < 1 {
     186                                return newNeedMoreParamsError(msg.Command)
     187                        }
     188                        caps := strings.Fields(subParams[len(subParams)-1])
     189                        more := len(subParams) >= 2 && msg.Params[len(subParams)-2] == "*"
     190
     191                        for _, s := range caps {
     192                                kv := strings.SplitN(s, "=", 2)
     193                                k := strings.ToLower(kv[0])
     194                                var v string
     195                                if len(kv) == 2 {
     196                                        v = kv[1]
     197                                }
     198                                uc.caps[k] = v
     199                        }
     200
     201                        if more {
     202                                break // wait to receive all capabilities
     203                        }
     204
     205                        if uc.requestSASL() {
     206                                uc.SendMessage(&irc.Message{
     207                                        Command: "CAP",
     208                                        Params:  []string{"REQ", "sasl"},
     209                                })
     210                                break // we'll send CAP END after authentication is completed
     211                        }
     212
    189213                        uc.SendMessage(&irc.Message{
    190214                                Command: "CAP",
    191215                                Params:  []string{"END"},
    192216                        })
    193                 }
     217                case "ACK", "NAK":
     218                        if len(subParams) < 1 {
     219                                return newNeedMoreParamsError(msg.Command)
     220                        }
     221                        caps := strings.Fields(subParams[0])
     222
     223                        for _, name := range caps {
     224                                if err := uc.handleCapAck(strings.ToLower(name), subCmd == "ACK"); err != nil {
     225                                        return err
     226                                }
     227                        }
     228
     229                        if uc.saslClient == nil {
     230                                uc.SendMessage(&irc.Message{
     231                                        Command: "CAP",
     232                                        Params:  []string{"END"},
     233                                })
     234                        }
     235                default:
     236                        uc.logger.Printf("unhandled message: %v", msg)
     237                }
     238        case "AUTHENTICATE":
     239                if uc.saslClient == nil {
     240                        return fmt.Errorf("received unexpected AUTHENTICATE message")
     241                }
     242
     243                // TODO: if a challenge is 400 bytes long, buffer it
     244                var challengeStr string
     245                if err := parseMessageParams(msg, &challengeStr); err != nil {
     246                        uc.SendMessage(&irc.Message{
     247                                Command: "AUTHENTICATE",
     248                                Params:  []string{"*"},
     249                        })
     250                        return err
     251                }
     252
     253                var challenge []byte
     254                if challengeStr != "+" {
     255                        var err error
     256                        challenge, err = base64.StdEncoding.DecodeString(challengeStr)
     257                        if err != nil {
     258                                uc.SendMessage(&irc.Message{
     259                                        Command: "AUTHENTICATE",
     260                                        Params:  []string{"*"},
     261                                })
     262                                return err
     263                        }
     264                }
     265
     266                var resp []byte
     267                var err error
     268                if !uc.saslStarted {
     269                        _, resp, err = uc.saslClient.Start()
     270                        uc.saslStarted = true
     271                } else {
     272                        resp, err = uc.saslClient.Next(challenge)
     273                }
     274                if err != nil {
     275                        uc.SendMessage(&irc.Message{
     276                                Command: "AUTHENTICATE",
     277                                Params:  []string{"*"},
     278                        })
     279                        return err
     280                }
     281
     282                // TODO: send response in multiple chunks if >= 400 bytes
     283                var respStr = "+"
     284                if resp != nil {
     285                        respStr = base64.StdEncoding.EncodeToString(resp)
     286                }
     287
     288                uc.SendMessage(&irc.Message{
     289                        Command: "AUTHENTICATE",
     290                        Params:  []string{respStr},
     291                })
     292        case rpl_loggedin:
     293                var account string
     294                if err := parseMessageParams(msg, nil, nil, &account); err != nil {
     295                        return err
     296                }
     297                uc.logger.Printf("logged in with account %q", account)
     298        case rpl_loggedout:
     299                uc.logger.Printf("logged out")
     300        case err_nicklocked, rpl_saslsuccess, err_saslfail, err_sasltoolong, err_saslaborted:
     301                var info string
     302                if err := parseMessageParams(msg, nil, &info); err != nil {
     303                        return err
     304                }
     305                switch msg.Command {
     306                case err_nicklocked:
     307                        uc.logger.Printf("invalid nick used with SASL authentication: %v", info)
     308                case err_saslfail:
     309                        uc.logger.Printf("SASL authentication failed: %v", info)
     310                case err_sasltoolong:
     311                        uc.logger.Printf("SASL message too long: %v", info)
     312                }
     313
     314                uc.saslClient = nil
     315                uc.saslStarted = false
     316
     317                uc.SendMessage(&irc.Message{
     318                        Command: "CAP",
     319                        Params:  []string{"END"},
     320                })
    194321        case irc.RPL_WELCOME:
    195322                uc.registered = true
     
    440567                // Ignore
    441568        default:
    442                 uc.logger.Printf("unhandled upstream message: %v", msg)
     569                uc.logger.Printf("unhandled message: %v", msg)
    443570        }
    444571        return nil
     
    478605}
    479606
     607func (uc *upstreamConn) requestSASL() bool {
     608        if uc.network.SASL.Mechanism == "" {
     609                return false
     610        }
     611
     612        v, ok := uc.caps["sasl"]
     613        if !ok {
     614                return false
     615        }
     616        if v != "" {
     617                mechanisms := strings.Split(v, ",")
     618                found := false
     619                for _, mech := range mechanisms {
     620                        if strings.EqualFold(mech, uc.network.SASL.Mechanism) {
     621                                found = true
     622                                break
     623                        }
     624                }
     625                if !found {
     626                        return false
     627                }
     628        }
     629
     630        return true
     631}
     632
     633func (uc *upstreamConn) handleCapAck(name string, ok bool) error {
     634        auth := &uc.network.SASL
     635        switch name {
     636        case "sasl":
     637                if !ok {
     638                        uc.logger.Printf("server refused to acknowledge the SASL capability")
     639                        return nil
     640                }
     641
     642                switch auth.Mechanism {
     643                case "PLAIN":
     644                        uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
     645                        uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
     646                default:
     647                        return fmt.Errorf("unsupported SASL mechanism %q", name)
     648                }
     649
     650                uc.SendMessage(&irc.Message{
     651                        Command: "AUTHENTICATE",
     652                        Params:  []string{auth.Mechanism},
     653                })
     654        }
     655        return nil
     656}
     657
    480658func (uc *upstreamConn) readMessages() error {
    481659        for {
Note: See TracChangeset for help on using the changeset viewer.