websocket.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package nginx_log
  2. import (
  3. "encoding/json"
  4. "io"
  5. "net/http"
  6. "os"
  7. "runtime"
  8. "github.com/0xJacky/Nginx-UI/internal/helper"
  9. "github.com/0xJacky/Nginx-UI/internal/nginx"
  10. "github.com/0xJacky/Nginx-UI/internal/nginx_log"
  11. "github.com/0xJacky/Nginx-UI/internal/nginx_log/utils"
  12. "github.com/gin-gonic/gin"
  13. "github.com/gorilla/websocket"
  14. "github.com/nxadm/tail"
  15. "github.com/pkg/errors"
  16. "github.com/uozi-tech/cosy"
  17. "github.com/uozi-tech/cosy/logger"
  18. )
  19. // getLogPath resolves the log file path based on the provided control parameters
  20. // It checks if the path is under the whitelist directories
  21. func getLogPath(control *controlStruct) (logPath string, err error) {
  22. // If direct log path is provided, use it
  23. if control.Path != "" {
  24. logPath = control.Path
  25. // Check if logPath is under one of the paths in LogDirWhiteList
  26. if !utils.IsValidLogPath(logPath) {
  27. return "", nginx_log.ErrLogPathIsNotUnderTheLogDirWhiteList
  28. }
  29. return
  30. }
  31. // Otherwise, use default log path based on type
  32. switch control.Type {
  33. case "error":
  34. path := nginx.GetErrorLogPath()
  35. if path == "" {
  36. err = nginx_log.ErrErrorLogPathIsEmpty
  37. return
  38. }
  39. logPath = path
  40. case "access":
  41. fallthrough
  42. default:
  43. path := nginx.GetAccessLogPath()
  44. if path == "" {
  45. err = nginx_log.ErrAccessLogPathIsEmpty
  46. return
  47. }
  48. logPath = path
  49. }
  50. // check if logPath is under one of the paths in LogDirWhiteList
  51. if !utils.IsValidLogPath(logPath) {
  52. return "", nginx_log.ErrLogPathIsNotUnderTheLogDirWhiteList
  53. }
  54. return
  55. }
  56. // tailNginxLog tails the specified log file and sends each line to the websocket
  57. func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
  58. defer func() {
  59. if err := recover(); err != nil {
  60. buf := make([]byte, 1024)
  61. runtime.Stack(buf, false)
  62. logger.Errorf("%s\n%s", err, buf)
  63. return
  64. }
  65. }()
  66. control := <-controlChan
  67. for {
  68. logPath, err := getLogPath(&control)
  69. if err != nil {
  70. errChan <- err
  71. return
  72. }
  73. seek := tail.SeekInfo{
  74. Offset: 0,
  75. Whence: io.SeekEnd,
  76. }
  77. stat, err := os.Stat(logPath)
  78. if os.IsNotExist(err) {
  79. errChan <- cosy.WrapErrorWithParams(nginx_log.ErrLogFileNotExists, logPath)
  80. return
  81. }
  82. if !stat.Mode().IsRegular() {
  83. errChan <- cosy.WrapErrorWithParams(nginx_log.ErrLogFileNotRegular, logPath)
  84. return
  85. }
  86. // Create a tail
  87. t, err := tail.TailFile(logPath, tail.Config{Follow: true,
  88. ReOpen: true, Location: &seek})
  89. if err != nil {
  90. errChan <- errors.Wrap(err, "error tailing log")
  91. return
  92. }
  93. for {
  94. var next = false
  95. select {
  96. case line := <-t.Lines:
  97. // Print the text of each received line
  98. if line == nil {
  99. continue
  100. }
  101. err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
  102. if err != nil {
  103. if helper.IsUnexpectedWebsocketError(err) {
  104. errChan <- errors.Wrap(err, "error tailNginxLog write message")
  105. }
  106. return
  107. }
  108. case control = <-controlChan:
  109. next = true
  110. break
  111. }
  112. if next {
  113. break
  114. }
  115. }
  116. }
  117. }
  118. // handleLogControl processes websocket control messages
  119. func handleLogControl(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
  120. defer func() {
  121. if err := recover(); err != nil {
  122. buf := make([]byte, 1024)
  123. runtime.Stack(buf, false)
  124. logger.Errorf("%s\n%s", err, buf)
  125. return
  126. }
  127. }()
  128. for {
  129. msgType, payload, err := ws.ReadMessage()
  130. if err != nil && helper.IsUnexpectedWebsocketError(err) {
  131. errChan <- errors.Wrap(err, "error handleLogControl read message")
  132. return
  133. }
  134. if msgType != websocket.TextMessage {
  135. errChan <- nginx_log.ErrInvalidWebSocketMessageType
  136. return
  137. }
  138. var msg controlStruct
  139. err = json.Unmarshal(payload, &msg)
  140. if err != nil {
  141. errChan <- errors.Wrap(err, "error ReadWsAndWritePty json.Unmarshal")
  142. return
  143. }
  144. controlChan <- msg
  145. }
  146. }
  147. // Log handles websocket connection for real-time log viewing
  148. func Log(c *gin.Context) {
  149. var upGrader = websocket.Upgrader{
  150. CheckOrigin: func(r *http.Request) bool {
  151. return true
  152. },
  153. }
  154. // upgrade http to websocket
  155. ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
  156. if err != nil {
  157. logger.Error(err)
  158. return
  159. }
  160. defer ws.Close()
  161. errChan := make(chan error, 1)
  162. controlChan := make(chan controlStruct, 1)
  163. go tailNginxLog(ws, controlChan, errChan)
  164. go handleLogControl(ws, controlChan, errChan)
  165. if err = <-errChan; err != nil {
  166. logger.Error(err)
  167. _ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
  168. return
  169. }
  170. }