ircd/irc/authscript.go
Shivaram Lingamneni 75e9476026 review fix
2020-06-04 02:07:39 -04:00

111 lines
2.6 KiB
Go

// 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
}