2020-06-04 07:18:24 +02:00
|
|
|
// 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 {
|
2020-06-04 08:03:15 +02:00
|
|
|
AccountName string `json:"accountName,omitempty"`
|
|
|
|
Passphrase string `json:"passphrase,omitempty"`
|
|
|
|
Certfp string `json:"certfp,omitempty"`
|
|
|
|
IP string `json:"ip,omitempty"`
|
2020-06-04 07:18:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type AuthScriptOutput struct {
|
2020-06-04 08:03:15 +02:00
|
|
|
AccountName string `json:"accountName"`
|
|
|
|
Success bool `json:"success"`
|
|
|
|
Error string `json:"error"`
|
2020-06-04 07:18:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|