websocket.go 4.5 KB

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