You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

authscript.go 2.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // Copyright (c) 2020 Shivaram Lingamneni
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "bufio"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "os/exec"
  10. "syscall"
  11. "time"
  12. )
  13. // JSON-serializable input and output types for the script
  14. type AuthScriptInput struct {
  15. AccountName string `json:"accountName,omitempty"`
  16. Passphrase string `json:"passphrase,omitempty"`
  17. Certfp string `json:"certfp,omitempty"`
  18. IP string `json:"ip,omitempty"`
  19. }
  20. type AuthScriptOutput struct {
  21. AccountName string `json:"accountName"`
  22. Success bool `json:"success"`
  23. Error string `json:"error"`
  24. }
  25. // internal tupling of output and error for passing over a channel
  26. type authScriptResponse struct {
  27. output AuthScriptOutput
  28. err error
  29. }
  30. func CheckAuthScript(config AuthScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) {
  31. inputBytes, err := json.Marshal(input)
  32. if err != nil {
  33. return
  34. }
  35. cmd := exec.Command(config.Command, config.Args...)
  36. stdin, err := cmd.StdinPipe()
  37. if err != nil {
  38. return
  39. }
  40. stdout, err := cmd.StdoutPipe()
  41. if err != nil {
  42. return
  43. }
  44. channel := make(chan authScriptResponse, 1)
  45. err = cmd.Start()
  46. if err != nil {
  47. return
  48. }
  49. stdin.Write(inputBytes)
  50. stdin.Write([]byte{'\n'})
  51. // lots of potential race conditions here. we want to ensure that Wait()
  52. // will be called, and will return, on the other goroutine, no matter
  53. // where it is blocked. If it's blocked on ReadBytes(), we will kill it
  54. // (first with SIGTERM, then with SIGKILL) and ReadBytes will return
  55. // with EOF. If it's blocked on Wait(), then one of the kill signals
  56. // will succeed and unblock it.
  57. go processAuthScriptOutput(cmd, stdout, channel)
  58. outputTimer := time.NewTimer(config.Timeout)
  59. select {
  60. case response := <-channel:
  61. return response.output, response.err
  62. case <-outputTimer.C:
  63. }
  64. err = errTimedOut
  65. cmd.Process.Signal(syscall.SIGTERM)
  66. termTimer := time.NewTimer(config.Timeout)
  67. select {
  68. case <-channel:
  69. return
  70. case <-termTimer.C:
  71. }
  72. cmd.Process.Kill()
  73. return
  74. }
  75. func processAuthScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan authScriptResponse) {
  76. var response authScriptResponse
  77. var out AuthScriptOutput
  78. reader := bufio.NewReader(stdout)
  79. outBytes, err := reader.ReadBytes('\n')
  80. if err == nil {
  81. err = json.Unmarshal(outBytes, &out)
  82. if err == nil {
  83. response.output = out
  84. if out.Error != "" {
  85. err = fmt.Errorf("Authentication process reported error: %s", out.Error)
  86. }
  87. }
  88. }
  89. response.err = err
  90. // always call Wait() to ensure resource cleanup
  91. err = cmd.Wait()
  92. if err != nil {
  93. response.err = err
  94. }
  95. channel <- response
  96. }