From 0bbb5d121d5f346ce7e1a335a91e3989e1bf089f Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Wed, 15 Apr 2020 18:14:17 +1000 Subject: [PATCH] Basic EXTJWT support --- conventional.yaml | 7 +++++ default.yaml | 7 +++++ go.mod | 2 +- go.sum | 2 ++ irc/channel.go | 11 ++++++++ irc/commands.go | 4 +++ irc/config.go | 6 ++-- irc/handlers.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++ irc/help.go | 5 ++++ irc/modes/modes.go | 12 ++++++++ 10 files changed, 121 insertions(+), 3 deletions(-) diff --git a/conventional.yaml b/conventional.yaml index 0577fc78..87215dd6 100644 --- a/conventional.yaml +++ b/conventional.yaml @@ -161,6 +161,13 @@ server: # - "192.168.1.1" # - "192.168.10.1/24" + # these services can integrate with the ircd using JSON Web Tokens (https://jwt.io) + # sometimes referred to with 'EXTJWT' + jwt-services: + # # service name -> secret string the service uses to verify our tokens + # call-host: call-hosting-secret-token + # image-host: image-hosting-secret-token + # allow use of the RESUME extension over plaintext connections: # do not enable this unless the ircd is only accessible over internal networks allow-plaintext-resume: false diff --git a/default.yaml b/default.yaml index c5d2aadc..b8fa06ae 100644 --- a/default.yaml +++ b/default.yaml @@ -187,6 +187,13 @@ server: # - "192.168.1.1" # - "192.168.10.1/24" + # these services can integrate with the ircd using JSON Web Tokens (https://jwt.io) + # sometimes referred to with 'EXTJWT' + jwt-services: + # # service name -> secret string the service uses to verify our tokens + # call-host: call-hosting-secret-token + # image-host: image-hosting-secret-token + # allow use of the RESUME extension over plaintext connections: # do not enable this unless the ircd is only accessible over internal networks allow-plaintext-resume: false diff --git a/go.mod b/go.mod index 67a5537a..e32c09c0 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.14 require ( code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/go-ldap/ldap/v3 v3.1.10 github.com/go-sql-driver/mysql v1.5.0 github.com/gorilla/websocket v1.4.2 - github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect github.com/goshuirc/irc-go v0.0.0-20200311142257-57fd157327ac github.com/onsi/ginkgo v1.12.0 // indirect github.com/onsi/gomega v1.9.0 // indirect diff --git a/go.sum b/go.sum index 9358a88c..a7f8428d 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk github.com/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65/go.mod h1:GYIaL2hleNQvfMUBTes1Zd/lDTyI/p2hv3kYB4jssyU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= diff --git a/irc/channel.go b/irc/channel.go index 1692eb40..8693e037 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -539,6 +539,17 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin } } +func (channel *Channel) ClientModeStrings(client *Client) []string { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + modes, present := channel.members[client] + if !present { + return []string{} + } else { + return modes.Strings() + } +} + func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { channel.stateMutex.RLock() founder := channel.registeredFounder diff --git a/irc/commands.go b/irc/commands.go index aff2d57a..01423c65 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -130,6 +130,10 @@ func init() { minParams: 1, oper: true, }, + "EXTJWT": { + handler: extjwtHandler, + minParams: 1, + }, "HELP": { handler: helpHandler, minParams: 0, diff --git a/irc/config.go b/irc/config.go index c9db3f0b..0e4e906e 100644 --- a/irc/config.go +++ b/irc/config.go @@ -502,8 +502,9 @@ type Config struct { MOTDFormatting bool `yaml:"motd-formatting"` ProxyAllowedFrom []string `yaml:"proxy-allowed-from"` proxyAllowedFromNets []net.IPNet - WebIRC []webircConfig `yaml:"webirc"` - MaxSendQString string `yaml:"max-sendq"` + WebIRC []webircConfig `yaml:"webirc"` + JwtServices map[string]string `yaml:"jwt-services"` + MaxSendQString string `yaml:"max-sendq"` MaxSendQBytes int AllowPlaintextResume bool `yaml:"allow-plaintext-resume"` Compatibility struct { @@ -1177,6 +1178,7 @@ func (config *Config) generateISupport() (err error) { isupport.Add("CHANTYPES", chanTypes) isupport.Add("ELIST", "U") isupport.Add("EXCEPTS", "") + isupport.Add("EXTJWT", "1") isupport.Add("INVEX", "") isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen)) isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes))) diff --git a/irc/handlers.go b/irc/handlers.go index f1639b7f..df7b1215 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -20,6 +20,7 @@ import ( "strings" "time" + "github.com/dgrijalva/jwt-go" "github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" @@ -911,6 +912,73 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res return killClient } +// EXTJWT [service_name] +func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + expireInSeconds := int64(30) + + accountName := client.AccountName() + if accountName == "*" { + accountName = "" + } + + claims := jwt.MapClaims{ + "exp": time.Now().Unix() + expireInSeconds, + "iss": server.name, + "sub": client.Nick(), + "account": accountName, + "umodes": []string{}, + } + + if msg.Params[0] != "*" { + channel := server.channels.Get(msg.Params[0]) + if channel == nil { + rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_CHANNEL", client.t("No such channel")) + return false + } + + claims["channel"] = channel.Name() + claims["joined"] = 0 + claims["cmodes"] = []string{} + if channel.hasClient(client) { + claims["joined"] = time.Now().Unix() - 100 //TODO(dan): um we need to store when clients joined for reals + claims["cmodes"] = channel.ClientModeStrings(client) + } + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // we default to a secret of `*`. if you want a real secret setup a service in the config~ + service := "*" + secret := "*" + if 1 < len(msg.Params) { + service = strings.ToLower(msg.Params[1]) + + c := server.Config() + var exists bool + secret, exists = c.Server.JwtServices[service] + if !exists { + rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_SERVICE", client.t("No such service")) + return false + } + } + + tokenString, err := token.SignedString([]byte(secret)) + + if err == nil { + maxTokenLength := 400 + + for maxTokenLength < len(tokenString) { + rb.Add(nil, server.name, "EXTJWT", msg.Params[0], service, "*", tokenString[:maxTokenLength]) + tokenString = tokenString[maxTokenLength:] + } + rb.Add(nil, server.name, "EXTJWT", msg.Params[0], service, tokenString) + } else { + rb.Add(nil, server.name, "FAIL", "EXTJWT", "UNKNOWN_ERROR", client.t("Could not generate EXTJWT token")) + } + + return false +} + // HELP [] func helpHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { argument := strings.ToLower(strings.TrimSpace(strings.Join(msg.Params, " "))) diff --git a/irc/help.go b/irc/help.go index 4dbd2c82..18ebf3e0 100644 --- a/irc/help.go +++ b/irc/help.go @@ -198,6 +198,11 @@ ON specifies that the ban is to be set on that specific server. [reason] and [oper reason], if they exist, are separated by a vertical bar (|). If "DLINE LIST" is sent, the server sends back a list of our current DLINEs.`, + }, + "extjwt": { + text: `EXTJWT [service_name] + +Get a JSON Web Token for target (either * or a channel name).`, }, "help": { text: `HELP diff --git a/irc/modes/modes.go b/irc/modes/modes.go index ae2d9224..89216cf8 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -388,6 +388,18 @@ func (set *ModeSet) String() (result string) { return buf.String() } +// Strings returns the modes in this set. +func (set *ModeSet) Strings() (result []string) { + if set == nil { + return + } + + for _, mode := range set.AllModes() { + result = append(result, mode.String()) + } + return +} + // Prefixes returns a list of prefixes for the given set of channel modes. func (set *ModeSet) Prefixes(isMultiPrefix bool) (prefixes string) { if set == nil {