Initial testing and improvements of code #1
|
@ -0,0 +1,127 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrHttpsRedirect = errors.New("unexpected forced https redirect")
|
||||||
|
ErrCertExpired = errors.New("certificate is expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkHttp checks a URL for validity, and checks redirects
|
||||||
|
func checkHttp(server *Server, logFields log.Fields) (bool, error) {
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: server.Host,
|
||||||
|
Path: server.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "ArmbianRouter/1.0 (Go "+runtime.Version()+")")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := checkClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logFields["responseCode"] = res.StatusCode
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound || res.StatusCode == http.StatusNotFound {
|
||||||
|
if res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound {
|
||||||
|
location := res.Header.Get("Location")
|
||||||
|
|
||||||
|
logFields["url"] = location
|
||||||
|
|
||||||
|
// Check that we don't redirect to https from a http url
|
||||||
|
if u.Scheme == "http" {
|
||||||
|
res, err := checkRedirect(location)
|
||||||
|
|
||||||
|
if !res || err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logFields["cause"] = fmt.Sprintf("Unexpected http status %d", res.StatusCode)
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRedirect parses a location header response and checks the scheme
|
||||||
|
func checkRedirect(locationHeader string) (bool, error) {
|
||||||
|
newUrl, err := url.Parse(locationHeader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if newUrl.Scheme == "https" {
|
||||||
|
return false, ErrHttpsRedirect
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTLS checks tls certificates from a host, ensures they're valid, and not expired.
|
||||||
|
func checkTLS(server *Server, logFields log.Fields) (bool, error) {
|
||||||
|
conn, err := tls.Dial("tcp", server.Host+":443", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
err = conn.VerifyHostname(server.Host)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
state := conn.ConnectionState()
|
||||||
|
|
||||||
|
opts := x509.VerifyOptions{
|
||||||
|
CurrentTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cert := range state.PeerCertificates {
|
||||||
|
if _, err := cert.Verify(opts); err != nil {
|
||||||
|
logFields["peerCert"] = cert.Subject.String()
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chain := range state.VerifiedChains {
|
||||||
|
for _, cert := range chain {
|
||||||
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
||||||
|
logFields["cert"] = cert.Subject.String()
|
||||||
|
return false, ErrCertExpired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -39,6 +39,9 @@ func reloadConfig() {
|
||||||
serverCache.Resize(viper.GetInt("cacheSize"))
|
serverCache.Resize(viper.GetInt("cacheSize"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purge the cache to ensure we don't have any invalid servers in it
|
||||||
|
serverCache.Purge()
|
||||||
|
|
||||||
// Set top choice count
|
// Set top choice count
|
||||||
topChoices = viper.GetInt("topChoices")
|
topChoices = viper.GetInt("topChoices")
|
||||||
|
|
||||||
|
|
112
servers.go
112
servers.go
|
@ -7,10 +7,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -18,6 +15,14 @@ import (
|
||||||
var (
|
var (
|
||||||
checkClient = &http.Client{
|
checkClient = &http.Client{
|
||||||
Timeout: 20 * time.Second,
|
Timeout: 20 * time.Second,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
checks = []serverCheck{
|
||||||
|
checkHttp,
|
||||||
|
checkTLS,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,87 +38,45 @@ type Server struct {
|
||||||
LastChange time.Time `json:"lastChange"`
|
LastChange time.Time `json:"lastChange"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serverCheck func(server *Server, logFields log.Fields) (bool, error)
|
||||||
|
|
||||||
|
// checkStatus runs all status checks against a server
|
||||||
func (server *Server) checkStatus() {
|
func (server *Server) checkStatus() {
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://"+server.Host+"/"+strings.TrimLeft(server.Path, "/"), nil)
|
logFields := log.Fields{
|
||||||
|
"host": server.Host,
|
||||||
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)
|
var res bool
|
||||||
|
var err error
|
||||||
|
|
||||||
if err != nil {
|
for _, check := range checks {
|
||||||
|
res, err = check(server, logFields)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logFields["error"] = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res {
|
||||||
if server.Available {
|
if server.Available {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(logFields).Info("Server went offline")
|
||||||
"server": server.Host,
|
|
||||||
"error": err,
|
|
||||||
}).Info("Server went offline")
|
|
||||||
|
|
||||||
server.Available = false
|
server.Available = false
|
||||||
server.LastChange = time.Now()
|
server.LastChange = time.Now()
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(logFields).Debug("Server is still offline")
|
||||||
"server": server.Host,
|
|
||||||
"error": err,
|
|
||||||
}).Debug("Server is still offline")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
} else {
|
||||||
|
|
||||||
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 res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound {
|
|
||||||
location := res.Header.Get("Location")
|
|
||||||
|
|
||||||
responseFields["url"] = location
|
|
||||||
|
|
||||||
log.WithFields(responseFields).Debug("Server responded with redirect")
|
|
||||||
|
|
||||||
newUrl, err := url.Parse(location)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if server.Available {
|
|
||||||
log.WithFields(responseFields).Warning("Server returned invalid url")
|
|
||||||
server.Available = false
|
|
||||||
server.LastChange = time.Now()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if newUrl.Scheme == "https" {
|
|
||||||
if server.Available {
|
|
||||||
responseFields["url"] = location
|
|
||||||
log.WithFields(responseFields).Warning("Server returned https url for http request")
|
|
||||||
server.Available = false
|
|
||||||
server.LastChange = time.Now()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !server.Available {
|
if !server.Available {
|
||||||
server.Available = true
|
server.Available = true
|
||||||
server.LastChange = time.Now()
|
server.LastChange = time.Now()
|
||||||
log.WithFields(responseFields).Info("Server is online")
|
log.WithFields(logFields).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
|
|
||||||
server.LastChange = time.Now()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +186,13 @@ func (s ServerList) Closest(ip net.IP) (*Server, float64, error) {
|
||||||
|
|
||||||
dist := choice.Item.(ComputedDistance)
|
dist := choice.Item.(ComputedDistance)
|
||||||
|
|
||||||
|
if !dist.Server.Available {
|
||||||
|
// Choose a new server and refresh cache
|
||||||
|
serverCache.Remove(ip.String())
|
||||||
|
|
||||||
|
return s.Closest(ip)
|
||||||
|
}
|
||||||
|
|
||||||
return dist.Server, dist.Distance, nil
|
return dist.Server, dist.Distance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +203,7 @@ func hsin(theta float64) float64 {
|
||||||
|
|
||||||
// Distance function returns the distance (in meters) between two points of
|
// Distance function returns the distance (in meters) between two points of
|
||||||
// a given longitude and latitude relatively accurately (using a spherical
|
// a given longitude and latitude relatively accurately (using a spherical
|
||||||
// approximation of the Earth) through the Haversin Distance Formula for
|
// approximation of the Earth) through the Haversine Distance Formula for
|
||||||
// great arc distance on a sphere with accuracy for small distances
|
// great arc distance on a sphere with accuracy for small distances
|
||||||
//
|
//
|
||||||
// point coordinates are supplied in degrees and converted into rad. in the func
|
// point coordinates are supplied in degrees and converted into rad. in the func
|
||||||
|
|
Loading…
Reference in New Issue