diff --git a/default.yaml b/default.yaml index 6b95891f..39eef02f 100644 --- a/default.yaml +++ b/default.yaml @@ -1067,3 +1067,9 @@ history: # whether to allow customization of the config at runtime using environment variables, # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true + +cef: + imagor: + url: "https://example.com/embed/" + secret: "secretgoeshere" + redis: "redis://user:password@localhost:6379/0?protocol=3" \ No newline at end of file diff --git a/go.mod b/go.mod index 479308ba..b08b4a75 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,12 @@ require ( require ( github.com/cshum/imagor v1.4.13 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/redis/go-redis/v9 v9.6.1 ) require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/tidwall/btree v1.4.2 // indirect github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/grect v0.1.4 // indirect diff --git a/go.sum b/go.sum index f0bbf4b8..ba268555 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,18 @@ code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9 code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cshum/imagor v1.4.13 h1:BFcSpsTUOJj+Wv5SzDeXa8bhsT/Ehw7EcrFD0UTdpmU= github.com/cshum/imagor v1.4.13/go.mod h1:LHxXgks6Y06GzEHitnlO8vcD5gznxIHWPdvGsnlGpMo= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons= @@ -40,6 +48,8 @@ github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= diff --git a/irc/cef.go b/irc/cef.go index f6b46e9c..c7aeac50 100644 --- a/irc/cef.go +++ b/irc/cef.go @@ -1,134 +1,191 @@ package irc import ( - "bufio" + "context" + "crypto/sha256" + "encoding/json" "fmt" + "github.com/cshum/imagor/imagorpath" "github.com/ergochat/ergo/irc/caps" - "io" - "net" - "os" + "github.com/ergochat/irc-go/ircmsg" + "net/http" + "regexp" "strings" + "time" ) -const LISTEN = "0.0.0.0:22843" +var ctx = context.Background() -type CefConnection struct { - connections map[string]net.Conn - server *Server -} - -func (instance *CefConnection) CEFMessage(action string, extra ...string) { - if instance == nil { +func (channel *Channel) RedisBroadcast(message ...string) { + if channel.server.redis == nil { return } - str := fmt.Sprintf("%s %s\n", action, strings.Join(extra, " ")) - for _, conn := range instance.connections { - conn.Write([]byte(str)) - } -} - -func (instance *CefConnection) KickBroadcast(channel string, user string) { - instance.CEFMessage("KICK", channel, user) -} - -func cefConnection(server *Server) *CefConnection { - // create a tcp listener on the given port - listener, err := net.Listen("tcp", LISTEN) + err := channel.server.redis.Publish(ctx, "channel."+channel.NameCasefolded(), strings.Join(message, " ")).Err() if err != nil { - fmt.Println("Unable to open CefConnection listener:", err) - os.Exit(1) + fmt.Printf("Channel broadcast error: %v", err) } - fmt.Printf("CefConnection listener on %s active\n", LISTEN) - instance := CefConnection{connections: make(map[string]net.Conn), server: server} - go cefListener(listener, &instance) - - return &instance } -func cefListener(listener net.Listener, instance *CefConnection) { - // listen for new connections - for { - conn, err := listener.Accept() - if err != nil { - fmt.Println("failed to accept cef connection, err:", err) +func (client *Client) RedisBroadcast(message ...string) { + err := client.server.redis.Publish(ctx, "user."+client.NickCasefolded(), strings.Join(message, " ")).Err() + if err != nil { + fmt.Printf("User broadcast error: %v", err) + } +} + +func (channel *Channel) Broadcast(command string, params ...string) { + for _, member := range channel.Members() { + for _, session := range member.Sessions() { + session.Send(nil, member.server.name, command, params...) + } + } +} + +// ChannelSub Actions and info centering around channels +func (server *Server) ChannelSub() { + pubsub := server.redis.PSubscribe(ctx, "channel.*") + defer pubsub.Close() + + ch := pubsub.Channel() + + for msg := range ch { + server.logger.Info("RedisMessage", msg.Channel, msg.Payload) + line := strings.Split(msg.Payload, " ") + channelName := strings.SplitN(msg.Channel, ".", 2)[1] + channel := server.channels.Get(channelName) + if len(line) == 0 { + println("Empty string dumped into ", msg.Channel, " channel") + } + if channel == nil { + server.logger.Warning("RedisMessage", "Unknown channel") continue } - instance.connections[conn.RemoteAddr().String()] = conn - // pass an accepted connection to a handler goroutine - go handleConnection(conn, instance) + switch line[0] { + case "VOICEPART": + channel.Broadcast("VOICEPART", channelName, line[1]) + case "VOICESTATE": + channel.Broadcast("VOICESTATE", channelName, line[1]) + case "BROADCASTTO": + for _, person := range channel.Members() { + person.Send(nil, server.name, line[1], line[2:]...) + } + } } } -func handleConnection(conn net.Conn, instance *CefConnection) { - defer delete(instance.connections, conn.RemoteAddr().String()) - reader := bufio.NewReader(conn) - println("Connection with CEF service established") - for { - // read client request data - bytes, err := reader.ReadBytes(byte('\n')) - if err != nil { - if err != io.EOF { - fmt.Println("failed to read data, err:", err) - } - return +// UserSub Handles things pertaining to users +func (server *Server) UserSub() { + pubsub := server.redis.PSubscribe(ctx, "user.*") + defer pubsub.Close() + + ch := pubsub.Channel() + + for msg := range ch { + server.logger.Info("RedisMessage", msg.Channel, msg.Payload) + line := strings.Split(msg.Payload, " ") + userName := strings.SplitN(msg.Channel, ".", 2)[1] + user := server.clients.Get(userName) + if len(line) == 0 { + println("Empty string dumped into ", msg.Channel, " channel") } - convertedLine := string(bytes[:len(bytes)-1]) - line := strings.Split(strings.Trim(convertedLine, " \n"), " ") - fmt.Printf("cef: %+q\n", line) + switch line[0] { - case "PART": - if len(line) == 1 || len(line[1]) == 0 { - println("skipping malformed line") - continue - } - channel := instance.server.channels.Get(line[1]) - for _, member := range channel.Members() { - for _, session := range member.Sessions() { - session.Send(nil, member.server.name, "VOICEPART", line[1], line[2]) - } - } - break - case "VOICESTATE": - channel := instance.server.channels.Get(line[1]) - for _, member := range channel.Members() { - for _, session := range member.Sessions() { - session.Send(nil, member.server.name, "VOICESTATE", line[1], line[2], line[3], line[4]) - } - } - break - case "BROADCASTAS": - // TODO: global broadcast - user := instance.server.clients.Get(line[1]) - if user != nil { - // I'm not too sure what the capability bit is, I think it's just ones that match - for friend := range user.Friends(caps.ExtendedNames) { - friend.Send(nil, user.NickMaskString(), line[2], line[3:]...) - } - } - break - case "BROADCASTTO": - channel := instance.server.channels.Get(line[1]) - if channel != nil { - // I'm not too sure what the capability bit is, I think it's just ones that match - for _, person := range channel.Members() { - person.Send(nil, instance.server.name, line[2], line[3:]...) - } - } - break case "FULLYREMOVE": - user := instance.server.clients.Get(line[1]) if user != nil { user.destroy(nil) - err := instance.server.accounts.Unregister(user.Account(), true) + err := server.accounts.Unregister(user.Account(), true) if err != nil { return } } break - default: - println("Unknown cef message: ", line[0]) + case "BROADCASTAS": + if user != nil { + // I'm not too sure what the capability bit is, I think it's just ones that match + for friend := range user.Friends(caps.ExtendedNames) { + friend.Send(nil, user.NickMaskString(), line[1], line[2:]...) + } + } + break + } + } +} + +func startRedis(server *Server) { + go server.ChannelSub() + go server.UserSub() +} + +func (server *Server) GenerateImagorSignaturesFromMessage(message *ircmsg.Message) string { + line, err := message.Line() + if err == nil { + return server.GenerateImagorSignatures(line) + } + return "" +} + +func (server *Server) GetUrlMime(url string) string { + config := server.Config() + // hacky, should fix + if !strings.Contains(url, "?") { + url += "?" + } + params := imagorpath.Params{ + Image: url, + Meta: true, + } + metaPath := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, config.Cef.Imagor.Secret)) + client := http.Client{ + Timeout: 5 * time.Second, + } + resp, err := client.Get(config.Cef.Imagor.Url + metaPath) + if err != nil { + println("Failed on the initial get") + println(err.Error()) + return "" + } + defer resp.Body.Close() + var meta map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&meta) + if err != nil { + println("Failed on the JSON decode") + return "" + } + contentType, valid := meta["format"].(string) + fmt.Printf("%+v\n", meta) + if !valid { + println("No content type") + return "" + } + return contentType +} + +var urlRegex = regexp.MustCompile("https?:\\/\\/[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?") + +// Process a message to add Imagor signatures +func (server *Server) GenerateImagorSignatures(str string) string { + urls := urlRegex.FindAllString(str, -1) + var sigs []string + for _, url := range urls { + params := imagorpath.Params{ + Image: url, + FitIn: true, + Width: 600, + Height: 600, + } + path := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, server.Config().Cef.Imagor.Secret)) + signature := path[:strings.IndexByte(path, '/')] + contentType := server.GetUrlMime(url) + if contentType != "" { + sigs = append(sigs, signature+"|"+strings.ReplaceAll(contentType, "/", "_")) + } else { + sigs = append(sigs, signature) } } + if len(sigs) > 0 { + return strings.Join(sigs, ",") + } + return "" } diff --git a/irc/channel.go b/irc/channel.go index 03e960b2..45deb1a7 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -802,7 +802,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp client.server.logger.Debug("channels", fmt.Sprintf("%s joined channel %s", details.nick, chname)) // I think this is assured to always be a good join point - client.server.cefManager.CEFMessage("POLL", channel.NameCasefolded()) + channel.RedisBroadcast("VOICEPOLL") givenMode := func() (givenMode modes.Mode) { channel.joinPartMutex.Lock() @@ -999,7 +999,7 @@ func (channel *Channel) playJoinForSession(session *Session) { channel.Names(client, sessionRb) } sessionRb.Send(false) - client.server.cefManager.CEFMessage("POLL", channel.NameCasefolded()) + channel.RedisBroadcast("VOICEPOLL") } // Part parts the given client from this channel, with the given message. @@ -1459,7 +1459,7 @@ func (channel *Channel) Quit(client *Client) { client.server.channels.Cleanup(channel) } client.removeChannel(channel) - client.server.cefManager.KickBroadcast(channel.name, client.Nick()) + channel.Broadcast("KICK", client.NickCasefolded()) } func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) { diff --git a/irc/config.go b/irc/config.go index 5216f16e..db07426f 100644 --- a/irc/config.go +++ b/irc/config.go @@ -709,6 +709,14 @@ type Config struct { } Filename string + + Cef struct { + Imagor struct { + Url string + Secret string + } + Redis string + } } // OperClass defines an assembled operator class. diff --git a/irc/dline.go b/irc/dline.go index 848d7b17..0795614f 100644 --- a/irc/dline.go +++ b/irc/dline.go @@ -275,6 +275,6 @@ func (dm *DLineManager) loadFromDatastore() { }) } -func (s *Server) loadDLines() { - s.dlines = NewDLineManager(s) +func (server *Server) loadDLines() { + server.dlines = NewDLineManager(server) } diff --git a/irc/handlers.go b/irc/handlers.go index 60a87abb..5f9066a2 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -572,7 +572,7 @@ func batchHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon rb.Label = batch.responseLabel for _, msg := range batch.message.Split { - signatures := utils.GenerateImagorSignatures(msg.Message) + signatures := server.GenerateImagorSignatures(msg.Message) if len(signatures) > 0 { if msg.Tags == nil { msg.Tags = make(map[string]string) @@ -2328,7 +2328,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp // each target gets distinct msgids splitMsg := utils.MakeMessage(message) - signatures := utils.GenerateImagorSignaturesFromMessage(&msg) + signatures := server.GenerateImagorSignaturesFromMessage(&msg) if len(signatures) > 0 { if clientOnlyTags == nil { clientOnlyTags = make(map[string]string) @@ -2397,7 +2397,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi for _, mention := range mentions { user := client.server.clients.Get(mention) if user != nil { - client.server.cefManager.CEFMessage("MENTION", user.nickCasefolded, channel.Name(), message.Msgid) + user.RedisBroadcast("MENTION", channel.Name(), message.Msgid) } } } else if target[0] == '$' && len(target) > 2 && client.Oper().HasRoleCapab("massmessage") { @@ -2524,7 +2524,8 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi } client.addHistoryItem(user, item, &details, &tDetails, config) - client.server.cefManager.CEFMessage("MENTION", user.nickCasefolded, client.nick, message.Msgid) + user.RedisBroadcast("MENTION", user.NickCasefolded(), message.Msgid) + } } diff --git a/irc/kline.go b/irc/kline.go index bc1e9123..1babb5d9 100644 --- a/irc/kline.go +++ b/irc/kline.go @@ -253,6 +253,6 @@ func (km *KLineManager) loadFromDatastore() { } -func (s *Server) loadKLines() { - s.klines = NewKLineManager(s) +func (server *Server) loadKLines() { + server.klines = NewKLineManager(server) } diff --git a/irc/server.go b/irc/server.go index 4a9fe1ea..98de78b1 100644 --- a/irc/server.go +++ b/irc/server.go @@ -36,6 +36,8 @@ import ( "github.com/ergochat/ergo/irc/mysql" "github.com/ergochat/ergo/irc/sno" "github.com/ergochat/ergo/irc/utils" + + "github.com/redis/go-redis/v9" ) const ( @@ -98,7 +100,7 @@ type Server struct { defcon atomic.Uint32 // CEF - cefManager *CefConnection + redis *redis.Client } // NewServer returns a new Oragono server. @@ -166,7 +168,12 @@ func (server *Server) Shutdown() { func (server *Server) Run() { defer server.Shutdown() - server.cefManager = cefConnection(server) + redisOpts, err := redis.ParseURL(server.Config().Cef.Redis) + if err != nil { + panic(err) + } + server.redis = redis.NewClient(redisOpts) + startRedis(server) for { select { diff --git a/irc/utils/text.go b/irc/utils/text.go index 80845771..341f3ec3 100644 --- a/irc/utils/text.go +++ b/irc/utils/text.go @@ -4,14 +4,6 @@ package utils import ( - "crypto/sha256" - "encoding/json" - "fmt" - "github.com/cshum/imagor/imagorpath" - "github.com/ergochat/irc-go/ircmsg" - "net/http" - "os" - "regexp" "strings" "time" ) @@ -178,78 +170,3 @@ func BuildTokenLines(lineLen int, tokens []string, delim string) []string { } return tl.Lines() } - -var urlRegex = regexp.MustCompile("https?:\\/\\/[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?") - -func GenerateImagorSignaturesFromMessage(message *ircmsg.Message) string { - line, err := message.Line() - if err == nil { - return GenerateImagorSignatures(line) - } - return "" -} - -var secretKey = os.Getenv("IMAGOR_SECRET") -var baseUrl = os.Getenv("IMAGOR_URL") - -func GetUrlMime(url string) string { - // hacky, should fix - if !strings.Contains(url, "?") { - url += "?" - } - params := imagorpath.Params{ - Image: url, - Meta: true, - } - metaPath := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, secretKey)) - client := http.Client{ - Timeout: 5 * time.Second, - } - resp, err := client.Get(baseUrl + metaPath) - if err != nil { - println("Failed on the initial get") - println(err.Error()) - return "" - } - defer resp.Body.Close() - var meta map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&meta) - if err != nil { - println("Failed on the JSON decode") - return "" - } - contentType, valid := meta["format"].(string) - fmt.Printf("%+v\n", meta) - if !valid { - println("No content type") - return "" - } - return contentType -} - -// Process a message to add Imagor signatures -func GenerateImagorSignatures(str string) string { - urls := urlRegex.FindAllString(str, -1) - var sigs []string - for _, url := range urls { - params := imagorpath.Params{ - Image: url, - FitIn: true, - Width: 600, - Height: 600, - } - path := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, secretKey)) - signature := path[:strings.IndexByte(path, '/')] - contentType := GetUrlMime(url) - if contentType != "" { - sigs = append(sigs, signature+"|"+strings.ReplaceAll(contentType, "/", "_")) - } else { - sigs = append(sigs, signature) - } - - } - if len(sigs) > 0 { - return strings.Join(sigs, ",") - } - return "" -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8216e5bc..fb83aecc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,9 +7,15 @@ github.com/GehirnInc/crypt github.com/GehirnInc/crypt/common github.com/GehirnInc/crypt/internal github.com/GehirnInc/crypt/md5_crypt +# github.com/cespare/xxhash/v2 v2.3.0 +## explicit; go 1.11 +github.com/cespare/xxhash/v2 # github.com/cshum/imagor v1.4.13 ## explicit; go 1.21 github.com/cshum/imagor/imagorpath +# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f +## explicit +github.com/dgryski/go-rendezvous # github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 ## explicit github.com/docopt/docopt-go @@ -46,6 +52,16 @@ github.com/okzk/sdnotify ## explicit; go 1.12 # github.com/onsi/gomega v1.9.0 ## explicit +# github.com/redis/go-redis/v9 v9.6.1 +## explicit; go 1.18 +github.com/redis/go-redis/v9 +github.com/redis/go-redis/v9/internal +github.com/redis/go-redis/v9/internal/hashtag +github.com/redis/go-redis/v9/internal/hscan +github.com/redis/go-redis/v9/internal/pool +github.com/redis/go-redis/v9/internal/proto +github.com/redis/go-redis/v9/internal/rand +github.com/redis/go-redis/v9/internal/util # github.com/tidwall/btree v1.4.2 ## explicit; go 1.18 github.com/tidwall/btree