3
0
mirror of https://github.com/ergochat/ergo.git synced 2026-04-27 02:58:04 +02:00
ergo/irc/script.go
Shivaram Lingamneni de005ee69f
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)
2026-04-15 01:48:29 -04:00

106 lines
2.5 KiB
Go

// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"bufio"
"io"
"net"
"os/exec"
"syscall"
"time"
"github.com/ergochat/irc-go/ircreader"
)
// general-purpose scripting API for oragono "plugins"
// invoke a command, send it a single newline-terminated string of bytes (typically JSON)
// get back another newline-terminated string of bytes (or an error)
// internal tupling of output and error for passing over a channel
type scriptResponse struct {
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 {
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return
}
channel := make(chan scriptResponse, 1)
err = cmd.Start()
if err != nil {
return
}
stdin.Write(input)
// 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 processScriptOutput(cmd, stdout, channel)
outputTimer := time.NewTimer(timeout)
select {
case response := <-channel:
return response.output, response.err
case <-outputTimer.C:
}
err = errTimedOut
cmd.Process.Signal(syscall.SIGTERM)
termTimer := time.NewTimer(killTimeout)
select {
case <-channel:
return
case <-termTimer.C:
}
cmd.Process.Kill()
return
}
func processScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan scriptResponse) {
var response scriptResponse
reader := bufio.NewReader(stdout)
response.output, response.err = reader.ReadBytes('\n')
// always call Wait() to ensure resource cleanup
err := cmd.Wait()
if err != nil {
response.err = err
}
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
}