mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-14 07:59:31 +01:00
commit
8720287f3a
@ -1354,3 +1354,18 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) {
|
|||||||
delete(client.invitedTo, casefoldedChannel)
|
delete(client.invitedTo, casefoldedChannel)
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -206,7 +206,7 @@ func init() {
|
|||||||
},
|
},
|
||||||
"OPER": {
|
"OPER": {
|
||||||
handler: operHandler,
|
handler: operHandler,
|
||||||
minParams: 2,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
"PART": {
|
"PART": {
|
||||||
handler: partHandler,
|
handler: partHandler,
|
||||||
|
@ -211,6 +211,8 @@ type OperConfig struct {
|
|||||||
Vhost string
|
Vhost string
|
||||||
WhoisLine string `yaml:"whois-line"`
|
WhoisLine string `yaml:"whois-line"`
|
||||||
Password string
|
Password string
|
||||||
|
Fingerprint string
|
||||||
|
Auto bool
|
||||||
Modes string
|
Modes string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,6 +461,8 @@ type Oper struct {
|
|||||||
WhoisLine string
|
WhoisLine string
|
||||||
Vhost string
|
Vhost string
|
||||||
Pass []byte
|
Pass []byte
|
||||||
|
Fingerprint string
|
||||||
|
Auto bool
|
||||||
Modes []modes.ModeChange
|
Modes []modes.ModeChange
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,9 +479,17 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
|||||||
}
|
}
|
||||||
oper.Name = name
|
oper.Name = name
|
||||||
|
|
||||||
|
if opConf.Password != "" {
|
||||||
oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
|
oper.Pass, err = decodeLegacyPasswordHash(opConf.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
oper.Vhost = opConf.Vhost
|
||||||
|
@ -2170,28 +2170,50 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPER <name> <password>
|
// OPER <name> [password]
|
||||||
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
if client.HasMode(modes.Operator) {
|
if client.HasMode(modes.Operator) {
|
||||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!"))
|
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
authorized := false
|
// must pass at least one check, and all enabled checks
|
||||||
|
var checkPassed, checkFailed bool
|
||||||
oper := server.GetOperator(msg.Params[0])
|
oper := server.GetOperator(msg.Params[0])
|
||||||
if oper != nil {
|
if oper != nil {
|
||||||
password := []byte(msg.Params[1])
|
if oper.Fingerprint != "" {
|
||||||
authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil)
|
if utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
|
||||||
|
checkPassed = true
|
||||||
|
} else {
|
||||||
|
checkFailed = true
|
||||||
}
|
}
|
||||||
if !authorized {
|
}
|
||||||
|
if !checkFailed && oper.Pass != nil {
|
||||||
|
if len(msg.Params) == 1 || bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil {
|
||||||
|
checkFailed = true
|
||||||
|
} else {
|
||||||
|
checkPassed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkPassed || checkFailed {
|
||||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
|
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
|
||||||
client.Quit(client.t("Password incorrect"), rb.session)
|
client.Quit(client.t("Password incorrect"), rb.session)
|
||||||
return true
|
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)
|
client.SetOper(oper)
|
||||||
if client.NickMaskString() != oldNickmask {
|
newNickmask := client.NickMaskString()
|
||||||
|
if newNickmask != oldNickmask {
|
||||||
client.sendChghost(oldNickmask, oper.Vhost)
|
client.sendChghost(oldNickmask, oper.Vhost)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2204,17 +2226,14 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
copy(modeChanges[1:], oper.Modes)
|
copy(modeChanges[1:], oper.Modes)
|
||||||
applied := ApplyUserModeChanges(client, modeChanges, true)
|
applied := ApplyUserModeChanges(client, modeChanges, true)
|
||||||
|
|
||||||
rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
|
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))
|
||||||
rb.Add(nil, server.name, "MODE", client.nick, applied.String())
|
|
||||||
|
|
||||||
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))
|
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())
|
||||||
// client may now be unthrottled by the fakelag system
|
|
||||||
for _, session := range client.Sessions() {
|
for _, session := range client.Sessions() {
|
||||||
|
// client may now be unthrottled by the fakelag system
|
||||||
session.resetFakelag()
|
session.resetFakelag()
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PART <channel>{,<channel>} [<reason>]
|
// PART <channel>{,<channel>} [<reason>]
|
||||||
@ -2641,7 +2660,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if 0 < len(info.Fingerprint) && client.certfp != info.Fingerprint {
|
if 0 < len(info.Fingerprint) && !utils.CertfpsMatch(info.Fingerprint, client.certfp) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,7 +349,7 @@ The NPC command is used to send an action to the target as the source.
|
|||||||
Requires the roleplay mode (+E) to be set on the target.`,
|
Requires the roleplay mode (+E) to be set on the target.`,
|
||||||
},
|
},
|
||||||
"oper": {
|
"oper": {
|
||||||
text: `OPER <name> <password>
|
text: `OPER <name> [password]
|
||||||
|
|
||||||
If the correct details are given, gives you IRCop privs.`,
|
If the correct details are given, gives you IRCop privs.`,
|
||||||
},
|
},
|
||||||
|
@ -77,6 +77,18 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str
|
|||||||
rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
|
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.
|
// 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) {
|
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...)
|
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
|
||||||
|
@ -440,6 +440,9 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
|||||||
if modestring != "+" {
|
if modestring != "+" {
|
||||||
session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring)
|
session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.attemptAutoOper(session)
|
||||||
|
|
||||||
if server.logger.IsLoggingRawIO() {
|
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."))
|
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."))
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -68,3 +69,15 @@ func GenerateSecretKey() string {
|
|||||||
rand.Read(buf[:])
|
rand.Read(buf[:])
|
||||||
return base64.RawURLEncoding.EncodeToString(buf[:])
|
return base64.RawURLEncoding.EncodeToString(buf[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCertfp(certfp string) string {
|
||||||
|
return strings.ToLower(strings.Replace(certfp, ":", "", -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience to compare certfps as returned by different tools, e.g., openssl vs. oragono
|
||||||
|
func CertfpsMatch(storedCertfp, suppliedCertfp string) bool {
|
||||||
|
if storedCertfp == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return normalizeCertfp(storedCertfp) == normalizeCertfp(suppliedCertfp)
|
||||||
|
}
|
||||||
|
@ -81,3 +81,21 @@ func BenchmarkMungeSecretToken(b *testing.B) {
|
|||||||
t = MungeSecretToken(t)
|
t = MungeSecretToken(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCertfpComparisons(t *testing.T) {
|
||||||
|
opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9"
|
||||||
|
oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9"
|
||||||
|
badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c8"
|
||||||
|
if !CertfpsMatch(opensslFP, oragonoFP) {
|
||||||
|
t.Error("these certs should match")
|
||||||
|
}
|
||||||
|
if !CertfpsMatch(oragonoFP, opensslFP) {
|
||||||
|
t.Error("these certs should match")
|
||||||
|
}
|
||||||
|
if CertfpsMatch("", "") {
|
||||||
|
t.Error("empty stored certfp should not match empty provided certfp")
|
||||||
|
}
|
||||||
|
if CertfpsMatch(opensslFP, badFP) {
|
||||||
|
t.Error("these certs should not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
19
oragono.yaml
19
oragono.yaml
@ -120,8 +120,9 @@ server:
|
|||||||
webirc:
|
webirc:
|
||||||
# one webirc block -- should correspond to one set of gateways
|
# one webirc block -- should correspond to one set of gateways
|
||||||
-
|
-
|
||||||
# tls fingerprint the gateway must connect with to use this webirc block
|
# SHA-256 fingerprint of the TLS certificate the gateway must use to connect
|
||||||
fingerprint: 938dd33f4b76dcaf7ce5eb25c852369cb4b8fb47ba22fc235aa29c6623a5f182
|
# (comment this out to use passwords only)
|
||||||
|
fingerprint: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
||||||
|
|
||||||
# password the gateway uses to connect, made with oragono genpasswd
|
# password the gateway uses to connect, made with oragono genpasswd
|
||||||
password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee"
|
password: "$2a$04$sLEFDpIOyUp55e6gTMKbOeroT6tMXTjPFvA0eGvwvImVR9pkwv7ee"
|
||||||
@ -445,10 +446,20 @@ opers:
|
|||||||
# modes are the modes to auto-set upon opering-up
|
# modes are the modes to auto-set upon opering-up
|
||||||
modes: +is acjknoqtux
|
modes: +is acjknoqtux
|
||||||
|
|
||||||
# password to login with /OPER command
|
# operators can be authenticated either by password (with the /OPER command),
|
||||||
# generated using "oragono genpasswd"
|
# 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"
|
password: "$2a$04$LiytCxaY0lI.guDj2pBN4eLRD5cdM2OLDwqmGAgB6M2OPirbF5Jcu"
|
||||||
|
|
||||||
|
# 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, takes inspiration from Insp
|
||||||
logging:
|
logging:
|
||||||
-
|
-
|
||||||
|
Loading…
Reference in New Issue
Block a user