1
0
Fork 0
forked from External/ergo

import changes

This commit is contained in:
Failure 2024-07-24 17:10:22 -07:00
parent 30f47a9b22
commit ca62b268b0
16 changed files with 272 additions and 34 deletions

View file

@ -7,7 +7,7 @@ package caps
const (
// number of recognized capabilities:
numCapabs = 34
numCapabs = 35
// length of the uint32 array that represents the bitset:
bitsetLen = 2
)
@ -148,6 +148,8 @@ const (
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
// https://wiki.znc.in/Query_buffers
ZNCSelfMessage Capability = iota
ExtendedNames Capability = iota
)
// `capabilityNames[capab]` is the string name of the capability `capab`
@ -187,5 +189,6 @@ var (
"userhost-in-names",
"znc.in/playback",
"znc.in/self-message",
"cef/extended-names",
}
)

124
irc/cef.go Normal file
View file

@ -0,0 +1,124 @@
package irc
import (
"bufio"
"fmt"
"github.com/ergochat/ergo/irc/caps"
"io"
"net"
"os"
"strings"
)
const PORT = "127.0.0.1:22843"
type CefConnection struct {
connections map[string]net.Conn
server *Server
}
func (instance *CefConnection) CEFMessage(action string, extra ...string) {
if instance == 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", PORT)
if err != nil {
fmt.Println("Unable to open CefConnection listener:", err)
os.Exit(1)
}
fmt.Printf("CefConnection listener on %s active\n", PORT)
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)
continue
}
instance.connections[conn.RemoteAddr().String()] = conn
// pass an accepted connection to a handler goroutine
go handleConnection(conn, instance)
}
}
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
}
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
default:
println("Unknown cef message: ", line[0])
}
}
}

View file

@ -477,6 +477,12 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
if respectAuditorium && memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
continue
}
if rb.session.capabilities.Has(caps.ExtendedNames) {
away, _ := target.Away()
if away {
nick = nick + "*"
}
}
tl.AddParts(memberData.modes.Prefixes(isMultiPrefix), nick)
}
}
@ -795,6 +801,8 @@ 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())
givenMode := func() (givenMode modes.Mode) {
channel.joinPartMutex.Lock()
@ -991,6 +999,7 @@ func (channel *Channel) playJoinForSession(session *Session) {
channel.Names(client, sessionRb)
}
sessionRb.Send(false)
client.server.cefManager.CEFMessage("POLL", channel.NameCasefolded())
}
// Part parts the given client from this channel, with the given message.
@ -1450,6 +1459,7 @@ func (channel *Channel) Quit(client *Client) {
client.server.channels.Cleanup(channel)
}
client.removeChannel(channel)
client.server.cefManager.KickBroadcast(channel.name, client.Username())
}
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
@ -1513,7 +1523,7 @@ func (channel *Channel) Purge(source string) {
now := time.Now().UTC()
for _, member := range members {
tnick := member.Nick()
msgid := utils.GenerateSecretToken()
msgid := utils.GenerateMessageIdStr()
for _, session := range member.Sessions() {
session.sendFromClientInternal(false, now, msgid, source, "*", false, nil, "KICK", chname, tnick, member.t("This channel has been purged by the server administrators and cannot be used"))
}

View file

@ -753,6 +753,7 @@ func csListHandler(service *ircService, server *Server, client *Client, command
}
func csInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if len(params) == 0 {
// #765
listRegisteredChannels(service, client.Account(), rb)
@ -765,37 +766,41 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
return
}
// purge status
if client.HasRoleCapabs("chanreg") {
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
if err == nil {
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
service.Notice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
if purgeRecord.Reason != "" {
service.Notice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
}
}
} else {
if server.channels.IsPurged(chname) {
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
}
}
var chinfo RegisteredChannel
channel := server.channels.Get(params[0])
if channel != nil {
chinfo = channel.exportSummary()
}
tags := map[string]string{
"target": chinfo.Name,
}
// purge status
if client.HasRoleCapabs("chanreg") {
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
if err == nil {
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname), tags)
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper), tags)
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)), tags)
if purgeRecord.Reason != "" {
service.TaggedNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason), tags)
}
}
} else {
if server.channels.IsPurged(chname) {
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname), tags)
}
}
// channel exists but is unregistered, or doesn't exist:
if chinfo.Founder == "" {
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname), tags)
return
}
service.Notice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
service.Notice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
service.TaggedNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name), tags)
service.TaggedNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder), tags)
service.TaggedNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)), tags)
}
func displayChannelSetting(service *ircService, settingName string, settings ChannelSettings, client *Client, rb *ResponseBuffer) {

View file

@ -1436,6 +1436,10 @@ func composeMultilineBatch(batchID, fromNickMask, fromAccount string, isBot bool
for _, msg := range message.Split {
message := ircmsg.MakeMessage(nil, fromNickMask, command, target, msg.Message)
message.SetTag("batch", batchID)
for k, v := range msg.Tags {
message.SetTag(k, v)
}
if msg.Concat {
message.SetTag(caps.MultilineConcatTag, "")
}

View file

@ -8,7 +8,7 @@ package irc
const (
// maxLastArgLength is used to simply cap off the final argument when creating general messages where we need to select a limit.
// for instance, in MONITOR lists, RPL_ISUPPORT lists, etc.
maxLastArgLength = 400
maxLastArgLength = 1024
// maxTargets is the maximum number of targets for PRIVMSG and NOTICE.
maxTargets = 4
)

View file

@ -410,7 +410,11 @@ func (client *Client) SetMode(mode modes.Mode, on bool) bool {
func (client *Client) SetRealname(realname string) {
client.stateMutex.Lock()
// TODO: make this configurable
client.realname = realname
if len(realname) > 64 {
client.realname = client.realname[:64]
}
alwaysOn := client.registered && client.alwaysOn
client.stateMutex.Unlock()
if alwaysOn {

View file

@ -1128,6 +1128,12 @@ func extjwtHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
}
claims["channel"] = channel.Name()
var channelModeStrings []string
for _, mode := range channel.flags.AllModes() {
channelModeStrings = append(channelModeStrings, mode.String())
}
claims["chanModes"] = channelModeStrings
claims["joined"] = 0
claims["cmodes"] = []string{}
if present, joinTimeSecs, cModes := channel.ClientStatus(client); present {
@ -2234,7 +2240,7 @@ func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.Message, ba
if !isConcat && len(rb.session.batch.message.Split) != 0 {
rb.session.batch.lenBytes++ // bill for the newline
}
rb.session.batch.message.Append(msg.Params[1], isConcat)
rb.session.batch.message.Append(msg.Params[1], isConcat, msg.ClientOnlyTags())
rb.session.batch.lenBytes += len(msg.Params[1])
config := server.Config()
if config.Limits.Multiline.MaxBytes < rb.session.batch.lenBytes {
@ -2317,6 +2323,41 @@ func messageHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
return false
}
// Not really sure how to do this in Go
var endChars = map[int32]bool{
' ': true,
'@': true,
':': true,
'!': true,
'?': true,
}
func detectMentions(message string) (mentions []string) {
buf := ""
mentions = []string{}
working := false
for _, char := range message {
if char == '@' {
working = true
continue
}
if !working {
continue
}
if _, stop := endChars[char]; stop {
working = false
mentions = append(mentions, buf)
buf = ""
} else {
buf += string(char)
}
}
if len(buf) != 0 {
mentions = append(mentions, buf)
}
return
}
func dispatchMessageToTarget(client *Client, tags map[string]string, histType history.ItemType, command, target string, message utils.SplitMessage, rb *ResponseBuffer) {
server := client.server
@ -2333,7 +2374,15 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
}
return
}
// This likely isn't that great for performance. Should figure out some way to deal with this at some point
mentions := detectMentions(message.Message)
channel.SendSplitMessage(command, lowestPrefix, tags, client, message, rb)
for _, mention := range mentions {
user := client.server.clients.Get(mention)
if user != nil {
client.server.cefManager.CEFMessage("MENTION", user.nickCasefolded, channel.Name(), message.Msgid)
}
}
} else if target[0] == '$' && len(target) > 2 && client.Oper().HasRoleCapab("massmessage") {
details := client.Details()
matcher, err := utils.CompileGlob(target[2:], false)
@ -2456,7 +2505,9 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
Message: message,
Tags: tags,
}
client.addHistoryItem(user, item, &details, &tDetails, config)
client.server.cefManager.CEFMessage("MENTION", user.nickCasefolded, client.nick, message.Msgid)
}
}
@ -2745,7 +2796,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
var members []*Client // members of a channel, or both parties of a PM
var canDelete CanDelete
msgid := utils.GenerateSecretToken()
msgid := utils.GenerateMessageIdStr()
time := time.Now().UTC().Round(0)
details := client.Details()
isBot := client.HasMode(modes.Bot)

View file

@ -49,7 +49,7 @@ func (t *JwtServiceConfig) Enabled() bool {
func (t *JwtServiceConfig) Sign(claims MapClaims) (result string, err error) {
claims["exp"] = time.Now().Unix() + int64(t.Expiration/time.Second)
claims["now"] = time.Now().Unix()
if t.rsaPrivateKey != nil {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
return token.SignedString(t.rsaPrivateKey)

View file

@ -4,7 +4,6 @@ import (
"encoding/json"
"github.com/ergochat/ergo/irc/history"
"github.com/ergochat/ergo/irc/utils"
)
// 123 / '{' is the magic number that means JSON;
@ -18,6 +17,7 @@ func unmarshalItem(data []byte, result *history.Item) (err error) {
return json.Unmarshal(data, result)
}
// TODO: probably should convert the internal mysql column to uint
func decodeMsgid(msgid string) ([]byte, error) {
return utils.B32Encoder.DecodeString(msgid)
return []byte(msgid), nil
}

View file

@ -142,7 +142,14 @@ func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAcc
if i == 0 {
msgid = message.Msgid
}
rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, tags, command, target, messagePair.Message)
mergedTags := make(map[string]string)
for k, v := range tags {
mergedTags[k] = v
}
for k, v := range messagePair.Tags {
mergedTags[k] = v
}
rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, isBot, mergedTags, command, target, messagePair.Message)
}
}
}

View file

@ -96,6 +96,9 @@ type Server struct {
semaphores ServerSemaphores
flock flock.Flocker
defcon atomic.Uint32
// CEF
cefManager *CefConnection
}
// NewServer returns a new Oragono server.
@ -163,6 +166,8 @@ func (server *Server) Shutdown() {
func (server *Server) Run() {
defer server.Shutdown()
server.cefManager = cefConnection(server)
for {
select {
case <-server.exitSignals:

View file

@ -107,6 +107,10 @@ func (service *ircService) Notice(rb *ResponseBuffer, text string) {
rb.Add(nil, service.prefix, "NOTICE", rb.target.Nick(), text)
}
func (service *ircService) TaggedNotice(rb *ResponseBuffer, text string, tags map[string]string) {
rb.Add(tags, service.prefix, "NOTICE", rb.target.Nick(), text)
}
// all service commands at the protocol level, by uppercase command name
// e.g., NICKSERV, NS
var ergoServicesByCommandAlias map[string]*ircService

View file

@ -14,6 +14,7 @@ import (
"encoding/hex"
"errors"
"net"
"strconv"
"strings"
"time"
)
@ -31,8 +32,26 @@ var (
const (
SecretTokenLength = 26
MachineId = 1 // Since there's no scaling Ergo, id is fixed at 1. Other things can have 2-127
)
var inc uint64 = 0
// slingamn, if you ever see this, i'm sorry - I just didn't want to attach what i think is redundant data to every
// message.
func GenerateMessageId() uint64 {
inc++
var ts = time.Now().Unix() & 0xffffffffffff
var flake = uint64(ts << 16)
flake |= MachineId << 10
flake |= inc % 0x3ff
return flake
}
func GenerateMessageIdStr() string {
return strconv.FormatUint(GenerateMessageId(), 10)
}
// generate a secret token that cannot be brute-forced via online attacks
func GenerateSecretToken() string {
// 128 bits of entropy are enough to resist any online attack:

View file

@ -16,6 +16,7 @@ func IsRestrictedCTCPMessage(message string) bool {
type MessagePair struct {
Message string
Tags map[string]string
Concat bool // should be relayed with the multiline-concat tag
}
@ -37,19 +38,20 @@ type SplitMessage struct {
func MakeMessage(original string) (result SplitMessage) {
result.Message = original
result.Msgid = GenerateSecretToken()
result.Msgid = GenerateMessageIdStr()
result.SetTime()
return
}
func (sm *SplitMessage) Append(message string, concat bool) {
func (sm *SplitMessage) Append(message string, concat bool, tags map[string]string) {
if sm.Msgid == "" {
sm.Msgid = GenerateSecretToken()
sm.Msgid = GenerateMessageIdStr()
}
sm.Split = append(sm.Split, MessagePair{
Message: message,
Concat: concat,
Tags: tags,
})
}