2022-01-10 04:47:40 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-01-10 05:11:15 +00:00
|
|
|
"encoding/json"
|
2022-01-10 04:53:23 +00:00
|
|
|
"fmt"
|
2022-04-01 04:04:27 +00:00
|
|
|
"github.com/jmcvetta/randutil"
|
2022-04-02 18:35:46 +00:00
|
|
|
"github.com/spf13/viper"
|
2022-01-10 04:53:23 +00:00
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2022-04-01 02:43:18 +00:00
|
|
|
"os"
|
2022-01-10 04:53:23 +00:00
|
|
|
"path"
|
2022-04-01 02:23:51 +00:00
|
|
|
"strings"
|
2022-01-10 04:47:40 +00:00
|
|
|
)
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// statusHandler is a simple handler that will always return 200 OK with a body of "OK"
|
2022-01-10 05:13:42 +00:00
|
|
|
func statusHandler(w http.ResponseWriter, r *http.Request) {
|
2022-01-10 04:53:23 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2022-08-14 07:42:49 +00:00
|
|
|
|
|
|
|
if r.Method != http.MethodHead {
|
|
|
|
w.Write([]byte("OK"))
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// redirectHandler is the default "not found" handler which handles redirects
|
|
|
|
// if the environment variable OVERRIDE_IP is set, it will use that ip address
|
|
|
|
// this is useful for local testing when you're on the local network
|
2022-01-10 05:13:42 +00:00
|
|
|
func redirectHandler(w http.ResponseWriter, r *http.Request) {
|
2022-01-10 04:53:23 +00:00
|
|
|
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
ip := net.ParseIP(ipStr)
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-04-01 02:43:18 +00:00
|
|
|
if ip.IsLoopback() || ip.IsPrivate() {
|
|
|
|
overrideIP := os.Getenv("OVERRIDE_IP")
|
|
|
|
|
|
|
|
if overrideIP == "" {
|
|
|
|
overrideIP = "1.1.1.1"
|
|
|
|
}
|
|
|
|
|
|
|
|
ip = net.ParseIP(overrideIP)
|
2022-01-10 04:53:23 +00:00
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-04-01 04:04:27 +00:00
|
|
|
var server *Server
|
|
|
|
var distance float64
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// If the path has a prefix of region/NA, it will use specific regions instead
|
|
|
|
// of the default geographical distance
|
2022-04-01 04:04:27 +00:00
|
|
|
if strings.HasPrefix(r.URL.Path, "/region") {
|
|
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
|
|
|
|
|
|
// region = parts[2]
|
2022-04-02 08:47:14 +00:00
|
|
|
if mirrors, ok := regionMap[parts[2]]; ok {
|
2022-04-01 04:04:27 +00:00
|
|
|
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:], "/")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// If none of the above exceptions are matched, we use the geographical distance based on IP
|
2022-04-01 04:04:27 +00:00
|
|
|
if server == nil {
|
|
|
|
server, distance, err = servers.Closest(ip)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2022-01-10 04:53:23 +00:00
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// If we don't have a scheme, we'll use https by default
|
2022-03-31 01:01:25 +00:00
|
|
|
scheme := r.URL.Scheme
|
|
|
|
|
|
|
|
if scheme == "" {
|
|
|
|
scheme = "https"
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// redirectPath is a combination of server path (which can be something like /armbian)
|
|
|
|
// and the URL path.
|
|
|
|
// Example: /armbian + /some/path = /armbian/some/path
|
2022-01-10 05:11:15 +00:00
|
|
|
redirectPath := path.Join(server.Path, r.URL.Path)
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// If we have a dlMap, we map the url to a final path instead
|
2022-01-10 05:11:15 +00:00
|
|
|
if dlMap != nil {
|
2022-04-02 05:21:42 +00:00
|
|
|
if newPath, exists := dlMap[strings.TrimLeft(r.URL.Path, "/")]; exists {
|
2022-01-11 06:05:20 +00:00
|
|
|
downloadsMapped.Inc()
|
2022-01-10 05:11:15 +00:00
|
|
|
redirectPath = path.Join(server.Path, newPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-01 02:23:51 +00:00
|
|
|
if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(redirectPath, "/") {
|
|
|
|
redirectPath += "/"
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// We need to build the final url now
|
2022-01-10 04:53:23 +00:00
|
|
|
u := &url.URL{
|
2022-03-31 01:01:25 +00:00
|
|
|
Scheme: scheme,
|
2022-01-10 04:53:23 +00:00
|
|
|
Host: server.Host,
|
2022-01-10 05:11:15 +00:00
|
|
|
Path: redirectPath,
|
2022-01-10 04:53:23 +00:00
|
|
|
}
|
2022-01-10 04:47:40 +00:00
|
|
|
|
2022-01-11 06:05:20 +00:00
|
|
|
server.Redirects.Inc()
|
|
|
|
redirectsServed.Inc()
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// If we used geographical distance, we add an X-Geo-Distance header for debug.
|
2022-04-01 04:04:27 +00:00
|
|
|
if distance > 0 {
|
|
|
|
w.Header().Set("X-Geo-Distance", fmt.Sprintf("%f", distance))
|
|
|
|
}
|
|
|
|
|
2022-01-10 04:53:23 +00:00
|
|
|
w.Header().Set("Location", u.String())
|
|
|
|
w.WriteHeader(http.StatusFound)
|
|
|
|
}
|
2022-01-10 05:13:42 +00:00
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
// reloadHandler is an http handler which lets us reload the server configuration
|
|
|
|
// It is only enabled when the reloadToken is set in the configuration
|
2022-01-10 05:13:42 +00:00
|
|
|
func reloadHandler(w http.ResponseWriter, r *http.Request) {
|
2022-08-14 07:42:49 +00:00
|
|
|
expectedToken := viper.GetString("reloadToken")
|
|
|
|
|
|
|
|
if expectedToken == "" {
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-02 18:35:46 +00:00
|
|
|
token := r.Header.Get("Authorization")
|
|
|
|
|
|
|
|
if token == "" || !strings.HasPrefix(token, "Bearer") || !strings.Contains(token, " ") {
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
token = token[strings.Index(token, " ")+1:]
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
if token != expectedToken {
|
2022-04-02 18:35:46 +00:00
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-14 07:42:49 +00:00
|
|
|
if err := reloadConfig(); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
w.Write([]byte(err.Error()))
|
|
|
|
return
|
|
|
|
}
|
2022-01-10 05:13:42 +00:00
|
|
|
|
2022-03-31 01:37:47 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
w.Write([]byte("OK"))
|
2022-01-10 05:13:42 +00:00
|
|
|
}
|
2022-01-10 05:15:07 +00:00
|
|
|
|
|
|
|
func dlMapHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if dlMap == nil {
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
json.NewEncoder(w).Encode(dlMap)
|
|
|
|
}
|
2022-04-01 02:04:19 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|