mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00

- Fixed passive fallback logic - Added active fallback setting notify from uptime monitor
246 lines
6.2 KiB
Go
246 lines
6.2 KiB
Go
package uptime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/publicsuffix"
|
|
"imuslab.com/zoraxy/mod/info/logger"
|
|
"imuslab.com/zoraxy/mod/utils"
|
|
)
|
|
|
|
// Create a new uptime monitor
|
|
func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
|
//Create new monitor object
|
|
thisMonitor := Monitor{
|
|
Config: config,
|
|
OnlineStatusLog: map[string][]*Record{},
|
|
}
|
|
|
|
if config.Logger == nil {
|
|
//Use default fmt to log if logger is not provided
|
|
config.Logger, _ = logger.NewFmtLogger()
|
|
}
|
|
|
|
if config.OnlineStateNotify == nil {
|
|
//Use default notify function if not provided
|
|
config.OnlineStateNotify = defaultNotify
|
|
}
|
|
|
|
//Start the endpoint listener
|
|
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
|
|
done := make(chan bool)
|
|
|
|
//Start the uptime check once first before entering loop
|
|
thisMonitor.ExecuteUptimeCheck()
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case t := <-ticker.C:
|
|
thisMonitor.Config.Logger.PrintAndLog(logModuleName, "Uptime updated - "+strconv.Itoa(int(t.Unix())), nil)
|
|
thisMonitor.ExecuteUptimeCheck()
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &thisMonitor, nil
|
|
}
|
|
|
|
func (m *Monitor) ExecuteUptimeCheck() {
|
|
for _, target := range m.Config.Targets {
|
|
//For each target to check online, do the following
|
|
var thisRecord Record
|
|
if target.Protocol == "http" || target.Protocol == "https" {
|
|
online, laterncy, statusCode := m.getWebsiteStatusWithLatency(target.URL)
|
|
thisRecord = Record{
|
|
Timestamp: time.Now().Unix(),
|
|
ID: target.ID,
|
|
Name: target.Name,
|
|
URL: target.URL,
|
|
Protocol: target.Protocol,
|
|
Online: online,
|
|
StatusCode: statusCode,
|
|
Latency: laterncy,
|
|
}
|
|
|
|
} else {
|
|
m.Config.Logger.PrintAndLog(logModuleName, "Unknown protocol: "+target.Protocol, errors.New("unsupported protocol"))
|
|
continue
|
|
}
|
|
|
|
thisRecords, ok := m.OnlineStatusLog[target.ID]
|
|
if !ok {
|
|
//First record. Create the array
|
|
m.OnlineStatusLog[target.ID] = []*Record{&thisRecord}
|
|
} else {
|
|
//Append to the previous record
|
|
thisRecords = append(thisRecords, &thisRecord)
|
|
|
|
//Check if the record is longer than the logged record. If yes, clear out the old records
|
|
if len(thisRecords) > m.Config.MaxRecordsStore {
|
|
thisRecords = thisRecords[1:]
|
|
}
|
|
|
|
m.OnlineStatusLog[target.ID] = thisRecords
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Monitor) AddTargetToMonitor(target *Target) {
|
|
// Add target to Config
|
|
m.Config.Targets = append(m.Config.Targets, target)
|
|
|
|
// Add target to OnlineStatusLog
|
|
m.OnlineStatusLog[target.ID] = []*Record{}
|
|
}
|
|
|
|
func (m *Monitor) RemoveTargetFromMonitor(targetId string) {
|
|
// Remove target from Config
|
|
for i, target := range m.Config.Targets {
|
|
if target.ID == targetId {
|
|
m.Config.Targets = append(m.Config.Targets[:i], m.Config.Targets[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove target from OnlineStatusLog
|
|
delete(m.OnlineStatusLog, targetId)
|
|
}
|
|
|
|
// Scan the config target. If a target exists in m.OnlineStatusLog no longer
|
|
// exists in m.Monitor.Config.Targets, it remove it from the log as well.
|
|
func (m *Monitor) CleanRecords() {
|
|
// Create a set of IDs for all targets in the config
|
|
targetIDs := make(map[string]bool)
|
|
for _, target := range m.Config.Targets {
|
|
targetIDs[target.ID] = true
|
|
}
|
|
|
|
// Iterate over all log entries and remove any that have a target ID that
|
|
// is not in the set of current target IDs
|
|
newStatusLog := m.OnlineStatusLog
|
|
for id, _ := range m.OnlineStatusLog {
|
|
_, idExistsInTargets := targetIDs[id]
|
|
if !idExistsInTargets {
|
|
delete(newStatusLog, id)
|
|
}
|
|
}
|
|
|
|
m.OnlineStatusLog = newStatusLog
|
|
}
|
|
|
|
/*
|
|
Web Interface Handler
|
|
*/
|
|
|
|
func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) {
|
|
id, _ := utils.GetPara(r, "id")
|
|
if id == "" {
|
|
js, _ := json.Marshal(m.OnlineStatusLog)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(js)
|
|
} else {
|
|
//Check if that id exists
|
|
log, ok := m.OnlineStatusLog[id]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
js, _ := json.MarshalIndent(log, "", " ")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(js)
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
Utilities
|
|
*/
|
|
|
|
// Get website stauts with latency given URL, return is conn succ and its latency and status code
|
|
func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
|
start := time.Now().UnixNano() / int64(time.Millisecond)
|
|
statusCode, err := getWebsiteStatus(url)
|
|
end := time.Now().UnixNano() / int64(time.Millisecond)
|
|
if err != nil {
|
|
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
|
|
m.Config.OnlineStateNotify(url, false)
|
|
return false, 0, 0
|
|
} else {
|
|
diff := end - start
|
|
succ := false
|
|
if statusCode >= 200 && statusCode < 300 {
|
|
//OK
|
|
succ = true
|
|
} else if statusCode >= 300 && statusCode < 400 {
|
|
//Redirection code
|
|
succ = true
|
|
} else {
|
|
succ = false
|
|
}
|
|
m.Config.OnlineStateNotify(url, true)
|
|
return succ, diff, statusCode
|
|
}
|
|
|
|
}
|
|
|
|
func getWebsiteStatus(url string) (int, error) {
|
|
// Create a one-time use cookie jar to store cookies
|
|
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
client := http.Client{
|
|
Jar: jar,
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.Header = http.Header{
|
|
"User-Agent": {"zoraxy-uptime/1.1"},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
//resp, err := client.Get(url)
|
|
if err != nil {
|
|
//Try replace the http with https and vise versa
|
|
rewriteURL := ""
|
|
if strings.Contains(url, "https://") {
|
|
rewriteURL = strings.ReplaceAll(url, "https://", "http://")
|
|
} else if strings.Contains(url, "http://") {
|
|
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
|
}
|
|
|
|
req, _ := http.NewRequest("GET", rewriteURL, nil)
|
|
req.Header = http.Header{
|
|
"User-Agent": {"zoraxy-uptime/1.1"},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
|
|
//Invalid downstream reverse proxy settings, but it is online
|
|
//return SSL handshake failed
|
|
return 525, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
status_code := resp.StatusCode
|
|
return status_code, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
status_code := resp.StatusCode
|
|
return status_code, nil
|
|
}
|