Basic EXTJWT support

This commit is contained in:
Daniel Oaks 2020-04-15 18:14:17 +10:00 committed by Shivaram Lingamneni
parent 6ff6225c1e
commit 0bbb5d121d
10 changed files with 121 additions and 3 deletions

@ -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

@ -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

2
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

2
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=

@ -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

@ -130,6 +130,10 @@ func init() {
minParams: 1,
oper: true,
},
"EXTJWT": {
handler: extjwtHandler,
minParams: 1,
},
"HELP": {
handler: helpHandler,
minParams: 0,

@ -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)))

@ -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 <target> [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 [<query>]
func helpHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
argument := strings.ToLower(strings.TrimSpace(strings.Join(msg.Params, " ")))

@ -198,6 +198,11 @@ ON <server> 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 <target> [service_name]
Get a JSON Web Token for target (either * or a channel name).`,
},
"help": {
text: `HELP <argument>

@ -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 {