uptime.go 6.6 KB

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