diff --git a/irc/client.go b/irc/client.go index e3ef9034..9c95c3bf 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1354,3 +1354,18 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) { delete(client.invitedTo, casefoldedChannel) return } + +// Implements auto-oper by certfp (scans for an auto-eligible operator block that matches +// the client's cert, then applies it). +func (client *Client) attemptAutoOper(session *Session) { + if client.certfp == "" || client.HasMode(modes.Operator) { + return + } + for _, oper := range client.server.Config().operators { + if oper.Auto && oper.Pass == nil && utils.CertfpsMatch(oper.Fingerprint, client.certfp) { + rb := NewResponseBuffer(session) + applyOper(client, oper, rb) + rb.Send(true) + } + } +} diff --git a/irc/config.go b/irc/config.go index e00a9bac..3d0fe34d 100644 --- a/irc/config.go +++ b/irc/config.go @@ -212,6 +212,7 @@ type OperConfig struct { WhoisLine string `yaml:"whois-line"` Password string Fingerprint string + Auto bool Modes string } @@ -461,6 +462,7 @@ type Oper struct { Vhost string Pass []byte Fingerprint string + Auto bool Modes []modes.ModeChange } @@ -477,11 +479,18 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error } oper.Name = name - oper.Pass, err = decodeLegacyPasswordHash(opConf.Password) - if err != nil { - return nil, err + if opConf.Password != "" { + oper.Pass, err = decodeLegacyPasswordHash(opConf.Password) + if err != nil { + return nil, err + } } oper.Fingerprint = opConf.Fingerprint + oper.Auto = opConf.Auto + + if oper.Pass == nil && oper.Fingerprint == "" { + return nil, fmt.Errorf("Oper %s has neither a password nor a fingerprint", name) + } oper.Vhost = opConf.Vhost class, exists := oc[opConf.Class] diff --git a/irc/handlers.go b/irc/handlers.go index 62f509a2..2c3347c1 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2177,14 +2177,19 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp return false } - authorized := false + // must have a matching oper block and not fail any enabled checks + // (config validation ensures that there is at least one check) oper := server.GetOperator(msg.Params[0]) + authorized := oper != nil if oper != nil { - if utils.CertfpsMatch(oper.Fingerprint, client.certfp) { - authorized = true - } else if 1 < len(msg.Params) { - password := []byte(msg.Params[1]) - authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil) + if oper.Fingerprint != "" && !utils.CertfpsMatch(oper.Fingerprint, client.certfp) { + authorized = false + } else if oper.Pass != nil { + if len(msg.Params) == 1 { + authorized = false + } else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { + authorized = false + } } } if !authorized { @@ -2193,9 +2198,17 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp return true } - oldNickmask := client.NickMaskString() + applyOper(client, oper, rb) + return false +} + +// applies operator status to a client, who MUST NOT already be an operator +func applyOper(client *Client, oper *Oper, rb *ResponseBuffer) { + details := client.Details() + oldNickmask := details.nickMask client.SetOper(oper) - if client.NickMaskString() != oldNickmask { + newNickmask := client.NickMaskString() + if newNickmask != oldNickmask { client.sendChghost(oldNickmask, oper.Vhost) } @@ -2208,17 +2221,14 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp copy(modeChanges[1:], oper.Modes) applied := ApplyUserModeChanges(client, modeChanges, true) - rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator")) - rb.Add(nil, server.name, "MODE", client.nick, applied.String()) + client.server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) - server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name)) - - // client may now be unthrottled by the fakelag system + rb.Broadcast(nil, client.server.name, RPL_YOUREOPER, details.nick, client.t("You are now an IRC operator")) + rb.Broadcast(nil, client.server.name, "MODE", details.nick, applied.String()) for _, session := range client.Sessions() { + // client may now be unthrottled by the fakelag system session.resetFakelag() } - - return false } // PART {,} [] diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 9df090a0..7fcb1b8f 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -77,6 +77,18 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...)) } +// Broadcast adds a standard new message to our queue, then sends an unlabeled copy +// to all other sessions. +func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) { + // can't reuse the IrcMessage object because of tag pollution :-\ + rb.Add(tags, prefix, command, params...) + for _, session := range rb.session.client.Sessions() { + if session != rb.session { + session.Send(tags, prefix, command, params...) + } + } +} + // AddFromClient adds a new message from a specific client to our queue. func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) diff --git a/irc/server.go b/irc/server.go index 5c56ac4d..bb498139 100644 --- a/irc/server.go +++ b/irc/server.go @@ -440,6 +440,9 @@ func (server *Server) playRegistrationBurst(session *Session) { if modestring != "+" { session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring) } + + c.attemptAutoOper(session) + if server.logger.IsLoggingRawIO() { session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) } diff --git a/oragono.yaml b/oragono.yaml index d5638f41..10640958 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -446,13 +446,19 @@ opers: # modes are the modes to auto-set upon opering-up modes: +is acjknoqtux - # password to login with /OPER command - # generated using "oragono genpasswd" + # operators can be authenticated either by password (with the /OPER command), + # or by certificate fingerprint, or both. if a password hash is set, then a + # password is required to oper up (e.g., /OPER dan mypassword). to generate + # the hash, use `oragono genpasswd`. password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu" - # if you're logged in using the client cert with this SHA-256 fingerprint, - # you'll be able to /OPER without a password - fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + # if a SHA-256 certificate fingerprint is configured here, then it will be + # required to /OPER. if you comment out the password hash above, then you can + # /OPER without a password. + #fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + # if 'auto' is set (and no password hash is set), operator permissions will be + # granted automatically as soon as you connect with the right fingerprint. + #auto: true # logging, takes inspiration from Insp logging: