uptime.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package uptime
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/http"
  6. "net/http/cookiejar"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "golang.org/x/net/publicsuffix"
  11. "imuslab.com/zoraxy/mod/info/logger"
  12. "imuslab.com/zoraxy/mod/utils"
  13. )
  14. // Create a new uptime monitor
  15. func NewUptimeMonitor(config *Config) (*Monitor, error) {
  16. //Create new monitor object
  17. thisMonitor := Monitor{
  18. Config: config,
  19. OnlineStatusLog: map[string][]*Record{},
  20. }
  21. if config.Logger == nil {
  22. //Use default fmt to log if logger is not provided
  23. config.Logger, _ = logger.NewFmtLogger()
  24. }
  25. if config.OnlineStateNotify == nil {
  26. //Use default notify function if not provided
  27. config.OnlineStateNotify = defaultNotify
  28. }
  29. //Start the endpoint listener
  30. ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
  31. done := make(chan bool)
  32. //Start the uptime check once first before entering loop
  33. thisMonitor.ExecuteUptimeCheck()
  34. go func() {
  35. for {
  36. select {
  37. case <-done:
  38. return
  39. case t := <-ticker.C:
  40. thisMonitor.Config.Logger.PrintAndLog(logModuleName, "Uptime updated - "+strconv.Itoa(int(t.Unix())), nil)
  41. thisMonitor.ExecuteUptimeCheck()
  42. }
  43. }
  44. }()
  45. return &thisMonitor, nil
  46. }
  47. func (m *Monitor) ExecuteUptimeCheck() {
  48. for _, target := range m.Config.Targets {
  49. //For each target to check online, do the following
  50. var thisRecord Record
  51. if target.Protocol == "http" || target.Protocol == "https" {
  52. online, laterncy, statusCode := m.getWebsiteStatusWithLatency(target.URL)
  53. thisRecord = Record{
  54. Timestamp: time.Now().Unix(),
  55. ID: target.ID,
  56. Name: target.Name,
  57. URL: target.URL,
  58. Protocol: target.Protocol,
  59. Online: online,
  60. StatusCode: statusCode,
  61. Latency: laterncy,
  62. }
  63. } else {
  64. m.Config.Logger.PrintAndLog(logModuleName, "Unknown protocol: "+target.Protocol, errors.New("unsupported protocol"))
  65. continue
  66. }
  67. thisRecords, ok := m.OnlineStatusLog[target.ID]
  68. if !ok {
  69. //First record. Create the array
  70. m.OnlineStatusLog[target.ID] = []*Record{&thisRecord}
  71. } else {
  72. //Append to the previous record
  73. thisRecords = append(thisRecords, &thisRecord)
  74. //Check if the record is longer than the logged record. If yes, clear out the old records
  75. if len(thisRecords) > m.Config.MaxRecordsStore {
  76. thisRecords = thisRecords[1:]
  77. }
  78. m.OnlineStatusLog[target.ID] = thisRecords
  79. }
  80. }
  81. }
  82. func (m *Monitor) AddTargetToMonitor(target *Target) {
  83. // Add target to Config
  84. m.Config.Targets = append(m.Config.Targets, target)
  85. // Add target to OnlineStatusLog
  86. m.OnlineStatusLog[target.ID] = []*Record{}
  87. }
  88. func (m *Monitor) RemoveTargetFromMonitor(targetId string) {
  89. // Remove target from Config
  90. for i, target := range m.Config.Targets {
  91. if target.ID == targetId {
  92. m.Config.Targets = append(m.Config.Targets[:i], m.Config.Targets[i+1:]...)
  93. break
  94. }
  95. }
  96. // Remove target from OnlineStatusLog
  97. delete(m.OnlineStatusLog, targetId)
  98. }
  99. // Scan the config target. If a target exists in m.OnlineStatusLog no longer
  100. // exists in m.Monitor.Config.Targets, it remove it from the log as well.
  101. func (m *Monitor) CleanRecords() {
  102. // Create a set of IDs for all targets in the config
  103. targetIDs := make(map[string]bool)
  104. for _, target := range m.Config.Targets {
  105. targetIDs[target.ID] = true
  106. }
  107. // Iterate over all log entries and remove any that have a target ID that
  108. // is not in the set of current target IDs
  109. newStatusLog := m.OnlineStatusLog
  110. for id, _ := range m.OnlineStatusLog {
  111. _, idExistsInTargets := targetIDs[id]
  112. if !idExistsInTargets {
  113. delete(newStatusLog, id)
  114. }
  115. }
  116. m.OnlineStatusLog = newStatusLog
  117. }
  118. /*
  119. Web Interface Handler
  120. */
  121. func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) {
  122. id, _ := utils.GetPara(r, "id")
  123. if id == "" {
  124. js, _ := json.Marshal(m.OnlineStatusLog)
  125. w.Header().Set("Content-Type", "application/json")
  126. w.Write(js)
  127. } else {
  128. //Check if that id exists
  129. log, ok := m.OnlineStatusLog[id]
  130. if !ok {
  131. http.NotFound(w, r)
  132. return
  133. }
  134. js, _ := json.MarshalIndent(log, "", " ")
  135. w.Header().Set("Content-Type", "application/json")
  136. w.Write(js)
  137. }
  138. }
  139. /*
  140. Utilities
  141. */
  142. // Get website stauts with latency given URL, return is conn succ and its latency and status code
  143. func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
  144. start := time.Now().UnixNano() / int64(time.Millisecond)
  145. statusCode, err := getWebsiteStatus(url)
  146. end := time.Now().UnixNano() / int64(time.Millisecond)
  147. if err != nil {
  148. m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
  149. m.Config.OnlineStateNotify(url, false)
  150. return false, 0, 0
  151. } else {
  152. diff := end - start
  153. succ := false
  154. if statusCode >= 200 && statusCode < 300 {
  155. //OK
  156. succ = true
  157. } else if statusCode >= 300 && statusCode < 400 {
  158. //Redirection code
  159. succ = true
  160. } else {
  161. succ = false
  162. }
  163. m.Config.OnlineStateNotify(url, true)
  164. return succ, diff, statusCode
  165. }
  166. }
  167. func getWebsiteStatus(url string) (int, error) {
  168. // Create a one-time use cookie jar to store cookies
  169. jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
  170. if err != nil {
  171. return 0, err
  172. }
  173. client := http.Client{
  174. Jar: jar,
  175. Timeout: 5 * time.Second,
  176. }
  177. req, _ := http.NewRequest("GET", url, nil)
  178. req.Header = http.Header{
  179. "User-Agent": {"zoraxy-uptime/1.1"},
  180. }
  181. resp, err := client.Do(req)
  182. //resp, err := client.Get(url)
  183. if err != nil {
  184. //Try replace the http with https and vise versa
  185. rewriteURL := ""
  186. if strings.Contains(url, "https://") {
  187. rewriteURL = strings.ReplaceAll(url, "https://", "http://")
  188. } else if strings.Contains(url, "http://") {
  189. rewriteURL = strings.ReplaceAll(url, "http://", "https://")
  190. }
  191. req, _ := http.NewRequest("GET", rewriteURL, nil)
  192. req.Header = http.Header{
  193. "User-Agent": {"zoraxy-uptime/1.1"},
  194. }
  195. resp, err := client.Do(req)
  196. if err != nil {
  197. if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
  198. //Invalid downstream reverse proxy settings, but it is online
  199. //return SSL handshake failed
  200. return 525, nil
  201. }
  202. return 0, err
  203. }
  204. defer resp.Body.Close()
  205. status_code := resp.StatusCode
  206. return status_code, nil
  207. }
  208. defer resp.Body.Close()
  209. status_code := resp.StatusCode
  210. return status_code, nil
  211. }