diff --git a/client-golang/signal-cli-api.go b/client-golang/signal-cli-api.go new file mode 100644 index 00000000..bec826f7 --- /dev/null +++ b/client-golang/signal-cli-api.go @@ -0,0 +1,99 @@ +/** +Accept commands on TCP socket and send to signal-cli daemon on UNIX socket. +Immediate responses are returned via the TCP socket, and other incoming +messages are logged to stdout. + +With this program running as a service, we can follow incoming messages with: + journalctl -fu signal-cli-api --no-tail +**/ + +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "time" +) + +func main() { + unixSocketPath := "/tmp/signal-cli/socket" + unixConn, err := net.Dial("unix", unixSocketPath) + if err != nil { + log.Fatal(err) + } + defer unixConn.Close() + + tcpPort := "5780" + tcpListener, err := net.Listen("tcp", ":"+tcpPort) + if err != nil { + log.Fatal(err) + } + defer tcpListener.Close() + + responses := make(chan string) + + // Read messages from the UNIX socket + go func() { + scanner := bufio.NewScanner(unixConn) + for scanner.Scan() { + resp := scanner.Text() + if isCommandResponse(resp) { + responses <- resp + } else { + log.Println(resp) + } + } + if err := scanner.Err(); err != nil { + log.Println(err) + } + }() + + for { + // Wait for TCP connection + tcpConn, err := tcpListener.Accept() + if err != nil { + log.Println(err) + continue + } + + // Read command + cmd, err := bufio.NewReader(tcpConn).ReadString('\n') + if err != nil { + log.Println(err) + tcpConn.Close() + continue + } + + // Write command to UNIX socket + fmt.Fprint(unixConn, cmd) + + // Read responses + select { + case msg := <-responses: + fmt.Fprintln(tcpConn, msg) + case <-time.After(2 * time.Second): + fmt.Fprint(tcpConn, "Timed out") + } + + // Close TCP connection + tcpConn.Close() + } +} + +/** Check if signal-cli json-rpc message is a command response. + Command response & error formats: + {"jsonrpc":"2.0","result":{...},"id":1678829060000} + {"jsonrpc":"2.0","result":[...],"id":1678829060000} + {"jsonrpc":"2.0","error":{...},"id":1678829060000} + + Incoming message format: + {"jsonrpc":"2.0","method":"receive","params":{...}} + + This assumes signal-cli doesn't change jsonrpc format or version. + It would be more robust to parse the JSON, but shouldn't be needed. +**/ +func isCommandResponse(resp string) bool { + return len(resp) >= 26 && resp[:26] != "{\"jsonrpc\":\"2.0\",\"method\":" +}