4 Commits

Author SHA1 Message Date
a571832239 Add support for region paths
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-04-01 00:04:27 -04:00
20ae76ff06 Fix distance sorting, add env variable for local IP to test
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-03-31 22:43:18 -04:00
b4ed1fc1a3 Check redirect path and add trailing slash if necessary
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-03-31 22:23:51 -04:00
aa8a187bda Mimic existing mirrors endpoint, add geoip
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/tag Build is passing
2022-03-31 22:04:19 -04:00
6 changed files with 239 additions and 92 deletions

View File

@ -46,6 +46,17 @@ func reloadConfig() {
// Reload server list
reloadServers()
// Create mirror map
mirrors := make(map[string][]*Server)
for _, server := range servers {
mirrors[server.Continent] = append(mirrors[server.Continent], server)
}
mirrors["default"] = append(mirrors["NA"], mirrors["EU"]...)
mirrorMap = mirrors
// Check top choices size
if topChoices > len(servers) {
topChoices = len(servers)
@ -150,36 +161,42 @@ func addServer(server ServerConfig, u *url.URL) *Server {
Path: u.Path,
Latitude: server.Latitude,
Longitude: server.Longitude,
Continent: server.Continent,
Weight: server.Weight,
}
// Defaults to 10 to allow servers to be set lower for lower priority
if s.Weight == 0 {
s.Weight = 1
s.Weight = 10
}
ips, err := net.LookupIP(u.Host)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"server": s.Host,
}).Warning("Could not resolve address")
return nil
}
var city City
err = db.Lookup(ips[0], &city)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"server": s.Host,
"ip": ips[0],
}).Warning("Could not geolocate address")
return nil
}
if s.Continent == "" {
s.Continent = city.Continent.Code
}
if s.Latitude == 0 && s.Longitude == 0 {
ips, err := net.LookupIP(u.Host)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"server": s.Host,
}).Warning("Could not resolve address")
return nil
}
var city City
err = db.Lookup(ips[0], &city)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"server": s.Host,
"ip": ips[0],
}).Warning("Could not geolocate address")
return nil
}
s.Latitude = city.Location.Latitude
s.Longitude = city.Location.Longitude
}

View File

@ -1,5 +1,6 @@
# GeoIP Database Path
geodb: GeoLite2-City.mmdb
dl_map: userdata.csv
# LRU Cache Size (in items)
cacheSize: 1024
@ -13,25 +14,26 @@ cacheSize: 1024
servers:
- server: armbian.12z.eu/apt/
- server: armbian.chi.auroradev.org/apt/
weight: 5
weight: 15
latitude: 41.8879
longitude: -88.1995
- server: armbian.hosthatch.com/apt/
- server: armbian.lv.auroradev.org/apt/
weight: 5
weight: 15
- server: armbian.site-meganet.com/apt/
- server: armbian.systemonachip.net/apt/
- server: armbian.tnahosting.net/apt/
weight: 5
weight: 15
- server: au-mirror.bret.dk/armbian/apt/
- server: es-mirror.bret.dk/armbian/apt/
- server: imola.armbian.com/apt/
- server: mirror.iscas.ac.cn/armbian/
- server: mirror.sjtu.edu.cn/armbian/
- server: mirrors.aliyun.com/armbian/
continent: AS
- server: mirrors.bfsu.edu.cn/armbian/
- server: mirrors.dotsrc.org/armbian-apt/
weight: 5
weight: 15
- server: mirrors.netix.net/armbian/apt/
- server: mirrors.nju.edu.cn/armbian/
- server: mirrors.sustech.edu.cn/armbian/
@ -41,3 +43,5 @@ servers:
- server: sg-mirror.bret.dk/armbian/apt/
- server: stpete-mirror.armbian.com/apt/
- server: xogium.performanceservers.nl/apt/
- server: github.com/armbian/mirror/releases/download/
continent: GITHUB

118
http.go
View File

@ -3,20 +3,40 @@ package main
import (
"encoding/json"
"fmt"
"github.com/jmcvetta/randutil"
"net"
"net/http"
"net/url"
"os"
"path"
"strings"
)
func statusHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func legacyMirrorsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
mirrorOutput := make(map[string][]string)
for region, mirrors := range mirrorMap {
list := make([]string, len(mirrors))
for i, mirror := range mirrors {
list[i] = r.URL.Scheme + "://" + mirror.Host + "/" + strings.TrimLeft(mirror.Path, "/")
}
mirrorOutput[region] = list
}
json.NewEncoder(w).Encode(mirrorOutput)
}
func mirrorsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(servers)
}
@ -30,16 +50,57 @@ func redirectHandler(w http.ResponseWriter, r *http.Request) {
ip := net.ParseIP(ipStr)
// TODO: This is temporary to allow testing on private addresses.
if ip.IsPrivate() {
ip = net.ParseIP("1.1.1.1")
if ip.IsLoopback() || ip.IsPrivate() {
overrideIP := os.Getenv("OVERRIDE_IP")
if overrideIP == "" {
overrideIP = "1.1.1.1"
}
ip = net.ParseIP(overrideIP)
}
server, distance, err := servers.Closest(ip)
var server *Server
var distance float64
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
if strings.HasPrefix(r.URL.Path, "/region") {
parts := strings.Split(r.URL.Path, "/")
// region = parts[2]
if mirrors, ok := mirrorMap[parts[2]]; ok {
choices := make([]randutil.Choice, len(mirrors))
for i, item := range mirrors {
if !item.Available {
continue
}
choices[i] = randutil.Choice{
Weight: item.Weight,
Item: item,
}
}
choice, err := randutil.WeightedChoice(choices)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
server = choice.Item.(*Server)
r.URL.Path = strings.Join(parts[3:], "/")
}
}
if server == nil {
server, distance, err = servers.Closest(ip)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
scheme := r.URL.Scheme
@ -51,12 +112,22 @@ func redirectHandler(w http.ResponseWriter, r *http.Request) {
redirectPath := path.Join(server.Path, r.URL.Path)
if dlMap != nil {
if newPath, exists := dlMap[strings.TrimLeft(r.URL.Path, "/")]; exists {
p := r.URL.Path
if p[0] != '/' {
p = "/" + p
}
if newPath, exists := dlMap[p]; exists {
downloadsMapped.Inc()
redirectPath = path.Join(server.Path, newPath)
}
}
if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(redirectPath, "/") {
redirectPath += "/"
}
u := &url.URL{
Scheme: scheme,
Host: server.Host,
@ -66,13 +137,16 @@ func redirectHandler(w http.ResponseWriter, r *http.Request) {
server.Redirects.Inc()
redirectsServed.Inc()
w.Header().Set("X-Geo-Distance", fmt.Sprintf("%f", distance))
if distance > 0 {
w.Header().Set("X-Geo-Distance", fmt.Sprintf("%f", distance))
}
w.Header().Set("Location", u.String())
w.WriteHeader(http.StatusFound)
}
func reloadHandler(w http.ResponseWriter, r *http.Request) {
reloadMap()
reloadConfig()
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
@ -88,3 +162,25 @@ func dlMapHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(dlMap)
}
func geoIPHandler(w http.ResponseWriter, r *http.Request) {
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ip := net.ParseIP(ipStr)
var city City
err = db.Lookup(ip, &city)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(city)
}

38
main.go
View File

@ -18,8 +18,9 @@ import (
)
var (
db *maxminddb.Reader
servers ServerList
db *maxminddb.Reader
servers ServerList
mirrorMap map[string][]*Server
dlMap map[string]string
@ -38,18 +39,42 @@ var (
topChoices int
)
// City represents a MaxmindDB city
type City struct {
type LocationLookup struct {
Location struct {
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
} `maxminddb:"location"`
}
// City represents a MaxmindDB city
type City struct {
Continent struct {
Code string `maxminddb:"code" json:"code"`
GeoNameID uint `maxminddb:"geoname_id" json:"geoname_id"`
Names map[string]string `maxminddb:"names" json:"names"`
} `maxminddb:"continent" json:"continent"`
Country struct {
GeoNameID uint `maxminddb:"geoname_id" json:"geoname_id"`
IsoCode string `maxminddb:"iso_code" json:"iso_code"`
Names map[string]string `maxminddb:"names" json:"names"`
} `maxminddb:"country" json:"country"`
Location struct {
AccuracyRadius uint16 `maxminddb:"accuracy_radius" json:'accuracy_radius'`
Latitude float64 `maxminddb:"latitude" json:"latitude"`
Longitude float64 `maxminddb:"longitude" json:"longitude"`
} `maxminddb:"location"`
RegisteredCountry struct {
GeoNameID uint `maxminddb:"geoname_id" json:"geoname_id"`
IsoCode string `maxminddb:"iso_code" json:"iso_code"`
Names map[string]string `maxminddb:"names" json:"names"`
} `maxminddb:"registered_country" json:"registered_country"`
}
type ServerConfig struct {
Server string `mapstructure:"server" yaml:"server"`
Latitude float64 `mapstructure:"latitude" yaml:"latitude"`
Longitude float64 `mapstructure:"longitude" yaml:"longitude"`
Continent string `mapstructure:"continent"`
Weight int `mapstructure:"weight" yaml:"weight"`
}
@ -86,10 +111,13 @@ func main() {
r.Use(RealIPMiddleware)
r.Use(logger.Logger("router", log.StandardLogger()))
r.Head("/status", statusHandler)
r.Get("/status", statusHandler)
r.Get("/mirrors", mirrorsHandler)
r.Get("/mirrors", legacyMirrorsHandler)
r.Get("/mirrors.json", mirrorsHandler)
r.Post("/reload", reloadHandler)
r.Get("/dl_map", dlMapHandler)
r.Get("/geoip", geoIPHandler)
r.Get("/metrics", promhttp.Handler().ServeHTTP)
r.NotFound(redirectHandler)

3
map.go
View File

@ -4,7 +4,6 @@ import (
"encoding/csv"
"io"
"os"
"strings"
)
func loadMap(file string) (map[string]string, error) {
@ -33,7 +32,7 @@ func loadMap(file string) (map[string]string, error) {
return nil, err
}
m[strings.TrimLeft(row[0], "/")] = strings.TrimLeft(row[1], "/")
m[row[0]] = row[1]
}
return m, nil

View File

@ -21,13 +21,14 @@ var (
)
type Server struct {
Available bool
Host string
Path string
Latitude float64
Longitude float64
Weight int
Redirects prometheus.Counter
Available bool `json:"available"`
Host string `json:"host"`
Path string `json:"path"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Weight int `json:"weight"`
Continent string `json:"continent"`
Redirects prometheus.Counter `json:"-"`
}
func (server *Server) checkStatus() {
@ -122,55 +123,59 @@ type ComputedDistance struct {
// 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.
// Return values are the closest server, the distance, and if an error occurred.
func (s ServerList) Closest(ip net.IP) (*Server, float64, error) {
i, exists := serverCache.Get(ip.String())
choiceInterface, exists := serverCache.Get(ip.String())
if exists {
return i.(ComputedDistance).Server, i.(ComputedDistance).Distance, nil
}
if !exists {
var city LocationLookup
err := db.Lookup(ip, &city)
var city City
err := db.Lookup(ip, &city)
if err != nil {
return nil, -1, err
}
c := make(DistanceList, len(s))
for i, server := range s {
if !server.Available {
continue
if err != nil {
return nil, -1, err
}
c[i] = ComputedDistance{
Server: server,
Distance: Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude),
c := make(DistanceList, len(s))
for i, server := range s {
if !server.Available {
continue
}
distance := Distance(city.Location.Latitude, city.Location.Longitude, server.Latitude, server.Longitude)
c[i] = ComputedDistance{
Server: server,
Distance: distance,
}
}
// Sort by distance
sort.Slice(c, func(i int, j int) bool {
return c[i].Distance < c[j].Distance
})
choices := make([]randutil.Choice, topChoices)
for i, item := range c[0:topChoices] {
if item.Server == nil {
continue
}
choices[i] = randutil.Choice{
Weight: item.Server.Weight,
Item: item,
}
}
choiceInterface = choices
serverCache.Add(ip.String(), choiceInterface)
}
// 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())
choice, err := randutil.WeightedChoice(choiceInterface.([]randutil.Choice))
if err != nil {
return nil, -1, err
@ -178,8 +183,6 @@ func (s ServerList) Closest(ip net.IP) (*Server, float64, error) {
dist := choice.Item.(ComputedDistance)
serverCache.Add(ip.String(), dist)
return dist.Server, dist.Distance, nil
}