2022-01-10 04:47:40 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-03-31 04:56:59 +00:00
|
|
|
"github.com/jmcvetta/randutil"
|
2022-01-11 06:05:20 +00:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2022-01-10 04:53:23 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"math"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"runtime"
|
2022-03-31 04:56:59 +00:00
|
|
|
"sort"
|
2022-01-10 04:53:23 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2022-01-10 04:47:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2022-01-10 04:53:23 +00:00
|
|
|
checkClient = &http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Server struct {
|
2022-01-10 04:53:23 +00:00
|
|
|
Available bool
|
|
|
|
Host string
|
|
|
|
Path string
|
|
|
|
Latitude float64
|
|
|
|
Longitude float64
|
2022-03-31 04:56:59 +00:00
|
|
|
Weight int
|
2022-01-11 06:05:20 +00:00
|
|
|
Redirects prometheus.Counter
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
2022-01-11 06:10:36 +00:00
|
|
|
func (server *Server) checkStatus() {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "https://"+server.Host+"/"+strings.TrimLeft(server.Path, "/"), nil)
|
|
|
|
|
|
|
|
req.Header.Set("User-Agent", "ArmbianRouter/1.0 (Go "+runtime.Version()+")")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
// This should never happen.
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"server": server.Host,
|
|
|
|
"error": err,
|
|
|
|
}).Warning("Invalid request! This should not happen, please check config.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := checkClient.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if server.Available {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"server": server.Host,
|
|
|
|
"error": err,
|
|
|
|
}).Info("Server went offline")
|
|
|
|
|
|
|
|
server.Available = false
|
|
|
|
} else {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"server": server.Host,
|
|
|
|
"error": err,
|
|
|
|
}).Debug("Server is still offline")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
responseFields := log.Fields{
|
|
|
|
"server": server.Host,
|
|
|
|
"responseCode": res.StatusCode,
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound || res.StatusCode == http.StatusNotFound {
|
|
|
|
if !server.Available {
|
|
|
|
server.Available = true
|
|
|
|
log.WithFields(responseFields).Info("Server is online")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.WithFields(responseFields).Debug("Server status not known")
|
|
|
|
|
|
|
|
if server.Available {
|
|
|
|
log.WithFields(responseFields).Info("Server went offline")
|
|
|
|
server.Available = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-10 04:47:40 +00:00
|
|
|
type ServerList []*Server
|
|
|
|
|
|
|
|
func (s ServerList) checkLoop() {
|
2022-01-10 04:53:23 +00:00
|
|
|
t := time.NewTicker(60 * time.Second)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
for {
|
2022-01-11 06:05:20 +00:00
|
|
|
<-t.C
|
2022-03-31 04:56:59 +00:00
|
|
|
s.Check()
|
2022-01-10 04:53:23 +00:00
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
// Check will request the index from all servers
|
|
|
|
// If a server does not respond in 10 seconds, it is considered offline.
|
|
|
|
// This will wait until all checks are complete.
|
2022-01-10 04:47:40 +00:00
|
|
|
func (s ServerList) Check() {
|
2022-01-10 04:53:23 +00:00
|
|
|
var wg sync.WaitGroup
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
for _, server := range s {
|
|
|
|
wg.Add(1)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
go func(server *Server) {
|
2022-01-11 06:05:20 +00:00
|
|
|
defer wg.Done()
|
|
|
|
|
2022-01-11 06:10:36 +00:00
|
|
|
server.checkStatus()
|
2022-01-10 04:53:23 +00:00
|
|
|
}(server)
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
wg.Wait()
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 04:56:59 +00:00
|
|
|
// ComputedDistance is a wrapper that contains a Server and Distance.
|
|
|
|
type ComputedDistance struct {
|
|
|
|
Server *Server
|
|
|
|
Distance float64
|
|
|
|
}
|
|
|
|
|
|
|
|
// DistanceList is a list of Computed Distances with an easy "Choices" func
|
|
|
|
type DistanceList []ComputedDistance
|
|
|
|
|
|
|
|
func (d DistanceList) Choices() []randutil.Choice {
|
|
|
|
c := make([]randutil.Choice, len(d))
|
|
|
|
|
|
|
|
for i, item := range d {
|
|
|
|
c[i] = randutil.Choice{
|
|
|
|
Weight: item.Server.Weight,
|
|
|
|
Item: item,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Closest will use GeoIP on the IP provided and find the closest servers.
|
|
|
|
// When we have a list of x servers closest, we can choose a random or weighted one.
|
2022-01-10 04:53:23 +00:00
|
|
|
// Return values are the closest server, the distance, and if an error occurred.
|
2022-01-10 04:47:40 +00:00
|
|
|
func (s ServerList) Closest(ip net.IP) (*Server, float64, error) {
|
2022-03-31 04:56:59 +00:00
|
|
|
i, exists := serverCache.Get(ip.String())
|
|
|
|
|
|
|
|
if exists {
|
|
|
|
return i.(ComputedDistance).Server, i.(ComputedDistance).Distance, nil
|
|
|
|
}
|
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
var city City
|
|
|
|
err := db.Lookup(ip, &city)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, -1, err
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-03-31 04:56:59 +00:00
|
|
|
c := make(DistanceList, len(s))
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-03-31 04:56:59 +00:00
|
|
|
for i, server := range s {
|
2022-01-10 04:53:23 +00:00
|
|
|
if !server.Available {
|
|
|
|
continue
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-03-31 04:56:59 +00:00
|
|
|
c[i] = ComputedDistance{
|
|
|
|
Server: server,
|
|
|
|
Distance: Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude),
|
2022-01-10 04:53:23 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-03-31 04:56:59 +00:00
|
|
|
// Sort by distance
|
|
|
|
sort.Slice(s, func(i int, j int) bool {
|
|
|
|
return c[i].Distance < c[j].Distance
|
|
|
|
})
|
|
|
|
|
|
|
|
choice, err := randutil.WeightedChoice(c[0:topChoices].Choices())
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, -1, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dist := choice.Item.(ComputedDistance)
|
|
|
|
|
|
|
|
serverCache.Add(ip.String(), dist)
|
|
|
|
|
|
|
|
return dist.Server, dist.Distance, nil
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// haversin(θ) function
|
|
|
|
func hsin(theta float64) float64 {
|
2022-01-10 04:53:23 +00:00
|
|
|
return math.Pow(math.Sin(theta/2), 2)
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Distance function returns the distance (in meters) between two points of
|
|
|
|
// a given longitude and latitude relatively accurately (using a spherical
|
|
|
|
// approximation of the Earth) through the Haversin Distance Formula for
|
|
|
|
// great arc distance on a sphere with accuracy for small distances
|
|
|
|
//
|
|
|
|
// point coordinates are supplied in degrees and converted into rad. in the func
|
|
|
|
//
|
|
|
|
// distance returned is METERS!!!!!!
|
|
|
|
// http://en.wikipedia.org/wiki/Haversine_formula
|
|
|
|
func Distance(lat1, lon1, lat2, lon2 float64) float64 {
|
2022-01-10 04:53:23 +00:00
|
|
|
// convert to radians
|
|
|
|
// must cast radius as float to multiply later
|
|
|
|
var la1, lo1, la2, lo2, r float64
|
|
|
|
la1 = lat1 * math.Pi / 180
|
|
|
|
lo1 = lon1 * math.Pi / 180
|
|
|
|
la2 = lat2 * math.Pi / 180
|
|
|
|
lo2 = lon2 * math.Pi / 180
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
r = 6378100 // Earth radius in METERS
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
// calculate
|
|
|
|
h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
return 2 * r * math.Asin(math.Sqrt(h))
|
|
|
|
}
|