mirror of
https://github.com/ergochat/ergo.git
synced 2026-04-26 18:48:22 +02:00
enhanced auth integrations (#2383)
* Harvest cookies from the initial websocket handshake to pass to an auth-script (#2185) * Allow running auth-script and ip-check-script over unix domain socket (#2280)
This commit is contained in:
parent
cfee1917f3
commit
de005ee69f
@ -318,6 +318,8 @@ server:
|
||||
# constant list of args to pass to the command; the actual query
|
||||
# and result are transmitted over stdin/stdout:
|
||||
args: []
|
||||
# alternatively, pass the input to a persistent process over unix domain socket:
|
||||
#socket: "/tmp/ergo_ip_check_sidecar"
|
||||
# timeout for process execution, after which we send a SIGTERM:
|
||||
timeout: 9s
|
||||
# how long after the SIGTERM before we follow up with a SIGKILL:
|
||||
@ -617,6 +619,8 @@ accounts:
|
||||
# constant list of args to pass to the command; the actual authentication
|
||||
# data is transmitted over stdin/stdout:
|
||||
args: []
|
||||
# alternatively, pass the input to a persistent process over unix domain socket:
|
||||
#socket: "/tmp/ergo_auth_sidecar"
|
||||
# should we automatically create users if the plugin returns success?
|
||||
autocreate: true
|
||||
# timeout for process execution, after which we send a SIGTERM:
|
||||
|
||||
@ -2030,15 +2030,20 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||
if certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
func (am *AccountManager) AuthenticateByCertificateOrCookies(client *Client, certfp string, peerCerts []*x509.Certificate, cookies []RequestCookie, authzid string) (err error) {
|
||||
|
||||
clientAccount, err := am.checkCertAuth(client.IP(), certfp, peerCerts, authzid)
|
||||
clientAccount, err := am.checkCertOrCookieAuth(client.IP(), certfp, peerCerts, cookies, authzid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if authzid != "" {
|
||||
if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != clientAccount.NameCasefolded {
|
||||
err = errAuthzidAuthcidMismatch
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if client.registered {
|
||||
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
||||
err = errNickAccountMismatch
|
||||
@ -2049,7 +2054,7 @@ func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp strin
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, authzid string) (clientAccount ClientAccount, err error) {
|
||||
func (am *AccountManager) checkCertOrCookieAuth(ip net.IP, certfp string, peerCerts []*x509.Certificate, cookies []RequestCookie, authzid string) (clientAccount ClientAccount, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
@ -2062,6 +2067,11 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
||||
}
|
||||
}()
|
||||
|
||||
if certfp == "" && len(cookies) == 0 {
|
||||
err = errAccountInvalidCredentials
|
||||
return
|
||||
}
|
||||
|
||||
config := am.server.Config()
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
@ -2071,7 +2081,7 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
||||
}
|
||||
output, err = CheckAuthScript(
|
||||
am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
||||
AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts},
|
||||
AuthScriptInput{Certfp: certfp, IP: ipString, peerCerts: peerCerts, Cookies: cookies},
|
||||
)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
@ -2081,6 +2091,11 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
||||
}
|
||||
}
|
||||
|
||||
if certfp == "" {
|
||||
err = errAccountInvalidCredentials
|
||||
return
|
||||
}
|
||||
|
||||
var account string
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
@ -2096,13 +2111,6 @@ func (am *AccountManager) checkCertAuth(ip net.IP, certfp string, peerCerts []*x
|
||||
return
|
||||
}
|
||||
|
||||
if authzid != "" {
|
||||
if cfAuthzid, cErr := CasefoldName(authzid); cErr != nil || cfAuthzid != account {
|
||||
err = errAuthzidAuthcidMismatch
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
clientAccount, err = am.LoadAccount(account)
|
||||
return
|
||||
|
||||
@ -169,7 +169,7 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if request.AccountName != "" && request.Passphrase != "" {
|
||||
account, err = a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
|
||||
} else if request.Certfp != "" {
|
||||
account, err = a.server.accounts.checkCertAuth(nil, request.Certfp, nil, "")
|
||||
account, err = a.server.accounts.checkCertOrCookieAuth(nil, request.Certfp, nil, nil, "")
|
||||
} else {
|
||||
err = errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
@ -23,6 +23,14 @@ type AuthScriptInput struct {
|
||||
peerCerts []*x509.Certificate
|
||||
IP string `json:"ip,omitempty"`
|
||||
OAuthBearer *oauth2.OAuthBearerOptions `json:"oauth2,omitempty"`
|
||||
Cookies []RequestCookie `json:"cookies,omitempty"`
|
||||
}
|
||||
|
||||
// RequestCookie represents a cookie sent by the client with the original HTTP
|
||||
// websocket upgrade request.
|
||||
type RequestCookie struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type AuthScriptOutput struct {
|
||||
@ -49,7 +57,8 @@ func CheckAuthScript(sem utils.Semaphore, config ScriptConfig, input AuthScriptI
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout)
|
||||
inputBytes = append(inputBytes, '\n')
|
||||
outBytes, err := RunScript(config.Command, config.Socket, config.Args, inputBytes, config.Timeout, config.KillTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -96,7 +105,8 @@ func CheckIPBan(sem utils.Semaphore, config IPCheckScriptConfig, addr net.IP) (o
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
outBytes, err := RunScript(config.Command, config.Args, inputBytes, config.Timeout, config.KillTimeout)
|
||||
inputBytes = append(inputBytes, '\n')
|
||||
outBytes, err := RunScript(config.Command, config.Socket, config.Args, inputBytes, config.Timeout, config.KillTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -180,6 +180,7 @@ type Session struct {
|
||||
socket *Socket
|
||||
realIP net.IP
|
||||
proxiedIP net.IP
|
||||
cookies []RequestCookie
|
||||
rawHostname string
|
||||
hostnameFinalized bool
|
||||
isTor bool
|
||||
@ -325,7 +326,7 @@ type ClientDetails struct {
|
||||
}
|
||||
|
||||
// RunClient sets up a new client and runs its goroutine.
|
||||
func (server *Server) RunClient(conn IRCConn) {
|
||||
func (server *Server) RunClient(conn IRCConn, cookies []RequestCookie) {
|
||||
config := server.Config()
|
||||
wConn := conn.UnderlyingConn()
|
||||
var isBanned, requireSASL bool
|
||||
@ -399,6 +400,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||
isTor: wConn.Tor,
|
||||
hideSTS: wConn.Tor || wConn.HideSTS,
|
||||
connID: connID,
|
||||
cookies: cookies,
|
||||
}
|
||||
session.sasl.Initialize()
|
||||
client.sessions = []*Session{session}
|
||||
|
||||
@ -348,6 +348,7 @@ type AccountConfig struct {
|
||||
|
||||
type ScriptConfig struct {
|
||||
Enabled bool
|
||||
Socket string
|
||||
Command string
|
||||
Args []string
|
||||
Timeout time.Duration
|
||||
@ -1593,6 +1594,19 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
return nil, fmt.Errorf("oauth2 is enabled with auth-script, but no auth-script is enabled")
|
||||
}
|
||||
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
config.Accounts.AuthScript.Socket = strings.TrimPrefix(config.Accounts.AuthScript.Socket, "unix:")
|
||||
if config.Accounts.AuthScript.Command != "" && config.Accounts.AuthScript.Socket != "" {
|
||||
return nil, errors.New("cannot define both command and socket for auth-script")
|
||||
}
|
||||
}
|
||||
if config.Server.IPCheckScript.Enabled {
|
||||
config.Server.IPCheckScript.Socket = strings.TrimPrefix(config.Server.IPCheckScript.Socket, "unix:")
|
||||
if config.Server.IPCheckScript.Command != "" && config.Server.IPCheckScript.Socket != "" {
|
||||
return nil, errors.New("cannot define both command and socket for ip-check-script")
|
||||
}
|
||||
}
|
||||
|
||||
if !config.Accounts.Registration.Enabled {
|
||||
config.Server.supportedCaps.Disable(caps.AccountRegistration)
|
||||
} else {
|
||||
|
||||
@ -360,11 +360,6 @@ func authErrorToMessage(server *Server, err error) (msg string) {
|
||||
func authExternalHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
||||
defer session.sasl.Clear()
|
||||
|
||||
if rb.session.certfp == "" {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
|
||||
return false
|
||||
}
|
||||
|
||||
// EXTERNAL doesn't carry an authentication ID (this is determined from the
|
||||
// certificate), but does carry an optional authorization ID.
|
||||
authzid := string(value)
|
||||
@ -376,9 +371,13 @@ func authExternalHandler(server *Server, client *Client, session *Session, value
|
||||
authzid, deviceID = authzid[:strudelIndex], authzid[strudelIndex+1:]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, authzid)
|
||||
if rb.session.certfp != "" || len(rb.session.cookies) != 0 {
|
||||
err = server.accounts.AuthenticateByCertificateOrCookies(client, rb.session.certfp, rb.session.peerCerts, rb.session.cookies, authzid)
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed; no external credentials found (certificate or cookies)"))
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sendAuthErrorResponse(client, rb, err)
|
||||
return false
|
||||
|
||||
@ -101,7 +101,7 @@ func (nl *NetListener) serve() {
|
||||
if ok {
|
||||
if wConn.ProxyError == nil {
|
||||
confirmProxyData(wConn, "", "", "", nl.server.Config())
|
||||
go nl.server.RunClient(NewIRCStreamConn(wConn))
|
||||
go nl.server.RunClient(NewIRCStreamConn(wConn), nil)
|
||||
} else {
|
||||
nl.server.logger.Error("internal", "PROXY protocol error", nl.addr, wConn.ProxyError.Error())
|
||||
conn.Close()
|
||||
@ -158,6 +158,7 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) {
|
||||
remoteAddr := r.RemoteAddr
|
||||
xff := r.Header.Get("X-Forwarded-For")
|
||||
xfp := r.Header.Get("X-Forwarded-Proto")
|
||||
cookies := extractCookies(r)
|
||||
|
||||
wsUpgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
@ -203,7 +204,7 @@ func (wl *WSListener) handle(w http.ResponseWriter, r *http.Request) {
|
||||
// avoid a DoS attack from buffering excessively large messages:
|
||||
conn.SetReadLimit(int64(maxReadQBytes()))
|
||||
|
||||
go wl.server.RunClient(NewIRCWSConn(conn))
|
||||
go wl.server.RunClient(NewIRCWSConn(conn), cookies)
|
||||
}
|
||||
|
||||
// validate conn.ProxiedIP and conn.Secure against config, HTTP headers, etc.
|
||||
@ -233,3 +234,23 @@ func confirmProxyData(conn *utils.WrappedConn, remoteAddr, xForwardedFor, xForwa
|
||||
xForwardedProto == "https"
|
||||
}
|
||||
}
|
||||
|
||||
func extractCookies(r *http.Request) (result []RequestCookie) {
|
||||
headers := r.Header["Cookie"]
|
||||
if len(headers) != 0 {
|
||||
result = make([]RequestCookie, 0, len(headers))
|
||||
for _, header := range headers {
|
||||
// ParseCookie only returns Name, Value, and Quoted
|
||||
// (unlike ParseSetCookie which returns, e.g. Path and Expires as well)
|
||||
if cookies, err := http.ParseCookie(header); err == nil {
|
||||
for _, cookie := range cookies {
|
||||
result = append(result, RequestCookie{
|
||||
Name: cookie.Name,
|
||||
Value: cookie.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -860,7 +860,7 @@ func nsIdentifyHandler(service *ircService, server *Server, client *Client, comm
|
||||
|
||||
// try certfp
|
||||
if !loginSuccessful && rb.session.certfp != "" {
|
||||
err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, "")
|
||||
err = server.accounts.AuthenticateByCertificateOrCookies(client, rb.session.certfp, rb.session.peerCerts, nil, "")
|
||||
loginSuccessful = (err == nil)
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,12 @@ package irc
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/irc-go/ircreader"
|
||||
)
|
||||
|
||||
// general-purpose scripting API for oragono "plugins"
|
||||
@ -21,7 +24,10 @@ type scriptResponse struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func RunScript(command string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) {
|
||||
func RunScript(command, socket string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) {
|
||||
if socket != "" {
|
||||
return RunScriptOverSocket(socket, input, timeout)
|
||||
}
|
||||
cmd := exec.Command(command, args...)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
@ -38,7 +44,6 @@ func RunScript(command string, args []string, input []byte, timeout, killTimeout
|
||||
return
|
||||
}
|
||||
stdin.Write(input)
|
||||
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
|
||||
@ -81,3 +86,20 @@ func processScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan scriptRes
|
||||
|
||||
channel <- response
|
||||
}
|
||||
|
||||
func RunScriptOverSocket(socket string, input []byte, timeout time.Duration) (output []byte, err error) {
|
||||
sock, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer sock.Close()
|
||||
sock.SetDeadline(time.Now().Add(timeout))
|
||||
_, err = sock.Write(input)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var reader ircreader.Reader
|
||||
reader.Initialize(sock, 1024, 1024*1024)
|
||||
output, err = reader.ReadLine()
|
||||
return
|
||||
}
|
||||
|
||||
@ -419,6 +419,8 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
session.cookies = nil // auth is done, allow GC'ing these later
|
||||
|
||||
if session.client != c {
|
||||
// reattached, bail out.
|
||||
// we'll play the reg burst later, on the new goroutine associated with
|
||||
|
||||
@ -188,7 +188,7 @@ func parseProxyLineV2(line []byte) (ip net.IP, err error) {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// / WrappedConn is a net.Conn with some additional data stapled to it;
|
||||
// WrappedConn is a net.Conn with some additional data stapled to it;
|
||||
// the proxied IP, if one was read via the PROXY protocol, and the listener
|
||||
// configuration.
|
||||
type WrappedConn struct {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user