mirror of
https://github.com/jeremyd/ergo.git
synced 2026-06-17 09:59:45 -07:00
+72
-26
@@ -1029,6 +1029,18 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
|
||||
account, err = am.LoadAccount(accountName)
|
||||
if err == errAccountDoesNotExist && autocreate {
|
||||
err = am.SARegister(accountName, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
account, err = am.LoadAccount(accountName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
|
||||
// XXX check this now, so we don't allow a redundant login for an always-on client
|
||||
// even for a brief period. the other potential source of nick-account conflicts
|
||||
@@ -1048,19 +1060,29 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
}
|
||||
}()
|
||||
|
||||
ldapConf := am.server.Config().Accounts.LDAP
|
||||
if ldapConf.Enabled {
|
||||
config := am.server.Config()
|
||||
if config.Accounts.LDAP.Enabled {
|
||||
ldapConf := am.server.Config().Accounts.LDAP
|
||||
err = ldap.CheckLDAPPassphrase(ldapConf, accountName, passphrase, am.server.logger)
|
||||
if err == nil {
|
||||
account, err = am.LoadAccount(accountName)
|
||||
// autocreate if necessary:
|
||||
if err == errAccountDoesNotExist && ldapConf.Autocreate {
|
||||
err = am.SARegister(accountName, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
account, err = am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
account, err = am.loadWithAutocreation(accountName, ldapConf.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
output, err = CheckAuthScript(config.Accounts.AuthScript,
|
||||
AuthScriptInput{AccountName: accountName, Passphrase: passphrase, IP: client.IP().String()})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
return err
|
||||
}
|
||||
if output.Success {
|
||||
if output.AccountName != "" {
|
||||
accountName = output.AccountName
|
||||
}
|
||||
account, err = am.loadWithAutocreation(accountName, config.Accounts.AuthScript.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1361,15 +1383,50 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string)
|
||||
return unmarshalRegisteredChannels(channelStr)
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) error {
|
||||
func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) (err error) {
|
||||
if certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
var clientAccount ClientAccount
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
} else if !clientAccount.Verified {
|
||||
err = errAccountUnverified
|
||||
return
|
||||
}
|
||||
// TODO(#1109) clean this check up?
|
||||
if client.registered {
|
||||
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
||||
err = errNickAccountMismatch
|
||||
return
|
||||
}
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return
|
||||
}()
|
||||
|
||||
config := am.server.Config()
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
output, err = CheckAuthScript(config.Accounts.AuthScript,
|
||||
AuthScriptInput{Certfp: certfp, IP: client.IP().String()})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
return err
|
||||
}
|
||||
if output.Success && output.AccountName != "" {
|
||||
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var account string
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
err := am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
err = am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
account, _ = tx.Get(certFPKey)
|
||||
if account == "" {
|
||||
return errAccountInvalidCredentials
|
||||
@@ -1386,19 +1443,8 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid s
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
clientAccount, err := am.LoadAccount(account)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !clientAccount.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
if client.registered {
|
||||
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
||||
return errNickAccountMismatch
|
||||
}
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return nil
|
||||
clientAccount, err = am.LoadAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSON-serializable input and output types for the script
|
||||
type AuthScriptInput struct {
|
||||
AccountName string `json:"accountName,omitempty"`
|
||||
Passphrase string `json:"passphrase,omitempty"`
|
||||
Certfp string `json:"certfp,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
type AuthScriptOutput struct {
|
||||
AccountName string `json:"accountName"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// internal tupling of output and error for passing over a channel
|
||||
type authScriptResponse struct {
|
||||
output AuthScriptOutput
|
||||
err error
|
||||
}
|
||||
|
||||
func CheckAuthScript(config AuthScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) {
|
||||
inputBytes, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(config.Command, config.Args...)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
channel := make(chan authScriptResponse, 1)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stdin.Write(inputBytes)
|
||||
stdin.Write([]byte{'\n'})
|
||||
|
||||
// lots of potential race conditions here. we want to ensure that Wait()
|
||||
// will be called, and will return, on the other goroutine, no matter
|
||||
// where it is blocked. If it's blocked on ReadBytes(), we will kill it
|
||||
// (first with SIGTERM, then with SIGKILL) and ReadBytes will return
|
||||
// with EOF. If it's blocked on Wait(), then one of the kill signals
|
||||
// will succeed and unblock it.
|
||||
go processAuthScriptOutput(cmd, stdout, channel)
|
||||
outputTimer := time.NewTimer(config.Timeout)
|
||||
select {
|
||||
case response := <-channel:
|
||||
return response.output, response.err
|
||||
case <-outputTimer.C:
|
||||
}
|
||||
|
||||
err = errTimedOut
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
termTimer := time.NewTimer(config.Timeout)
|
||||
select {
|
||||
case <-channel:
|
||||
return
|
||||
case <-termTimer.C:
|
||||
}
|
||||
|
||||
cmd.Process.Kill()
|
||||
return
|
||||
}
|
||||
|
||||
func processAuthScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan authScriptResponse) {
|
||||
var response authScriptResponse
|
||||
var out AuthScriptOutput
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
outBytes, err := reader.ReadBytes('\n')
|
||||
if err == nil {
|
||||
err = json.Unmarshal(outBytes, &out)
|
||||
if err == nil {
|
||||
response.output = out
|
||||
if out.Error != "" {
|
||||
err = fmt.Errorf("Authentication process reported error: %s", out.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
response.err = err
|
||||
|
||||
// always call Wait() to ensure resource cleanup
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
response.err = err
|
||||
}
|
||||
|
||||
channel <- response
|
||||
}
|
||||
@@ -278,6 +278,16 @@ type AccountConfig struct {
|
||||
Multiclient MulticlientConfig
|
||||
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
|
||||
VHosts VHostConfig
|
||||
AuthScript AuthScriptConfig `yaml:"auth-script"`
|
||||
}
|
||||
|
||||
type AuthScriptConfig struct {
|
||||
Enabled bool
|
||||
Command string
|
||||
Args []string
|
||||
Autocreate bool
|
||||
Timeout time.Duration
|
||||
KillTimeout time.Duration `yaml:"kill-timeout"`
|
||||
}
|
||||
|
||||
// AccountRegistrationConfig controls account registration.
|
||||
|
||||
@@ -64,6 +64,7 @@ var (
|
||||
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
)
|
||||
|
||||
// Socket Errors
|
||||
|
||||
Reference in New Issue
Block a user