1
0

uptime.go 6.5 KB

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