diff --git a/CHANGELOG.md b/CHANGELOG.md index f8941e94..17d97523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ New release of Oragono! * Added draft IRCv3 capability [draft/sts](http://ircv3.net/specs/core/sts-3.3.html). ### Changed -* Logging is now much better and useful. -* Can now specify years, months and days (e.g. `1y12m30d`) with DLINE and KLINE. +* `DLINE` and `KLINE` now let you specify years, months and days (e.g. `1y12m30d`) in durations. +* Logging is now much more useful, displays colours and can log to disk. +* Socket handling has been rewritten, which means we should support more connections more effectively (thanks dp- for the suggestion!). ### Removed diff --git a/irc/client.go b/irc/client.go index 6fb18540..7ae05696 100644 --- a/irc/client.go +++ b/irc/client.go @@ -74,7 +74,8 @@ type Client struct { // NewClient returns a client with all the appropriate info setup. func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { now := time.Now() - socket := NewSocket(conn) + socket := NewSocket(conn, server.MaxSendQBytes) + go socket.RunSocketWriter() client := &Client{ atime: now, authorized: server.password == nil, diff --git a/irc/config.go b/irc/config.go index 395c8d54..ac66cb7f 100644 --- a/irc/config.go +++ b/irc/config.go @@ -14,6 +14,8 @@ import ( "strings" "time" + "code.cloudfoundry.org/bytefmt" + "github.com/DanielOaks/oragono/irc/custime" "github.com/DanielOaks/oragono/irc/logger" "gopkg.in/yaml.v2" @@ -171,6 +173,8 @@ type Config struct { RestAPI RestAPIConfig `yaml:"rest-api"` CheckIdent bool `yaml:"check-ident"` MOTD string + MaxSendQString string `yaml:"max-sendq"` + MaxSendQBytes uint64 ConnectionLimits ConnectionLimitsConfig `yaml:"connection-limits"` ConnectionThrottle ConnectionThrottleConfig `yaml:"connection-throttling"` } @@ -433,5 +437,10 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Logging = newLogConfigs + config.Server.MaxSendQBytes, err = bytefmt.ToBytes(config.Server.MaxSendQString) + if err != nil { + return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error()) + } + return config, nil } diff --git a/irc/logger/logger.go b/irc/logger/logger.go index 0fea2911..c77b2b5f 100644 --- a/irc/logger/logger.go +++ b/irc/logger/logger.go @@ -212,7 +212,7 @@ func (logger *singleLogger) Log(level Level, logType string, messageParts ...str sep := grey(":") fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05Z")), sep, levelDisplay, sep, section(logType), sep) - fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], section(logType)) + fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType) for i, p := range messageParts { fullStringFormatted += p fullStringRaw += p diff --git a/irc/server.go b/irc/server.go index e8cac62c..1ca7b8a6 100644 --- a/irc/server.go +++ b/irc/server.go @@ -104,6 +104,7 @@ type Server struct { listeners map[string]ListenerInterface listenerUpdateMutex sync.Mutex logger *logger.Manager + MaxSendQBytes uint64 monitoring map[string][]Client motdLines []string name string @@ -214,6 +215,7 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (* }, listeners: make(map[string]ListenerInterface), logger: logger, + MaxSendQBytes: config.Server.MaxSendQBytes, monitoring: make(map[string][]Client), name: config.Server.Name, nameCasefolded: casefoldedName, @@ -1423,6 +1425,18 @@ func (server *Server) rehash() error { accountReg := NewAccountRegistration(config.Accounts.Registration) server.accountRegistration = &accountReg + // set new sendqueue size + if config.Server.MaxSendQBytes != server.MaxSendQBytes { + server.MaxSendQBytes = config.Server.MaxSendQBytes + + // update on all clients + server.clients.ByNickMutex.RLock() + for _, sClient := range server.clients.ByNick { + sClient.socket.MaxSendQBytes = config.Server.MaxSendQBytes + } + server.clients.ByNickMutex.RUnlock() + } + // set RPL_ISUPPORT oldISupportList := server.isupport server.setISupport() diff --git a/irc/socket.go b/irc/socket.go index 2764151c..95952c40 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -10,9 +10,11 @@ import ( "crypto/tls" "encoding/hex" "errors" + "fmt" "io" "net" "strings" + "sync" "time" ) @@ -27,23 +29,33 @@ type Socket struct { Closed bool conn net.Conn reader *bufio.Reader + + MaxSendQBytes uint64 + + lineToSendExists chan bool + linesToSend []string + linesToSendMutex sync.Mutex } // NewSocket returns a new Socket. -func NewSocket(conn net.Conn) Socket { +func NewSocket(conn net.Conn, maxSendQBytes uint64) Socket { return Socket{ - conn: conn, - reader: bufio.NewReader(conn), + conn: conn, + reader: bufio.NewReader(conn), + MaxSendQBytes: maxSendQBytes, + lineToSendExists: make(chan bool), } } // Close stops a Socket from being able to send/receive any more data. func (socket *Socket) Close() { - if socket.Closed { - return - } socket.Closed = true - socket.conn.Close() + + // 'send data' to force close loop to happen + socket.linesToSendMutex.Lock() + socket.linesToSend = append(socket.linesToSend, "") + socket.linesToSendMutex.Unlock() + go socket.fillLineToSendExists() } // CertFP returns the fingerprint of the certificate provided by the client. @@ -104,15 +116,80 @@ func (socket *Socket) Write(data string) error { return io.EOF } - // write data - _, err := socket.conn.Write([]byte(data)) - if err != nil { - socket.Close() - return err - } + socket.linesToSendMutex.Lock() + socket.linesToSend = append(socket.linesToSend, data) + socket.linesToSendMutex.Unlock() + go socket.fillLineToSendExists() + return nil } +// fillLineToSendExists only exists because you can't goroutine single statements. +func (socket *Socket) fillLineToSendExists() { + socket.lineToSendExists <- true +} + +// RunSocketWriter starts writing messages to the outgoing socket. +func (socket *Socket) RunSocketWriter() { + var errOut bool + for { + // wait for new lines + select { + case <-socket.lineToSendExists: + socket.linesToSendMutex.Lock() + + // check sendq + var sendQBytes uint64 + for _, line := range socket.linesToSend { + sendQBytes += uint64(len(line)) + if socket.MaxSendQBytes < sendQBytes { + break + } + } + if socket.MaxSendQBytes < sendQBytes { + socket.conn.Write([]byte("\r\nERROR :SendQ Exceeded\r\n")) + fmt.Println("SendQ exceeded, disconnected client") + break + } + + // get data + data := socket.linesToSend[0] + if len(socket.linesToSend) > 1 { + socket.linesToSend = socket.linesToSend[1:] + } else { + socket.linesToSend = []string{} + } + + // write data + if 0 < len(data) { + _, err := socket.conn.Write([]byte(data)) + if err != nil { + errOut = true + fmt.Println(err.Error()) + break + } + } + + // check if we're closed + if socket.Closed { + socket.linesToSendMutex.Unlock() + break + } + + socket.linesToSendMutex.Unlock() + } + if errOut { + // error out, bad stuff happened + break + } + } + //TODO(dan): empty socket.lineToSendExists queue + socket.conn.Close() + if !socket.Closed { + socket.Closed = true + } +} + // WriteLine writes the given line out of Socket. func (socket *Socket) WriteLine(line string) error { return socket.Write(line + "\r\n") diff --git a/oragono.yaml b/oragono.yaml index d9eec794..f176d1e0 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -65,6 +65,10 @@ server: # if you change the motd, you should move it to ircd.motd motd: oragono.motd + # maximum length of clients' sendQ in bytes + # this should be big enough to hold /LIST and HELP replies + max-sendq: 16k + # maximum number of connections per subnet connection-limits: # whether to throttle limits or not