You've already forked speedtest-go
							
							Migrate code from Go branch to new repo
This commit is contained in:
		
							
								
								
									
										167
									
								
								web/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								web/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"math"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/librespeed/speedtest/config"
 | 
			
		||||
	"github.com/librespeed/speedtest/results"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// get server location from ipinfo.io from start to minimize API access
 | 
			
		||||
	serverLat, serverLng = getServerLocation()
 | 
			
		||||
	// for testing
 | 
			
		||||
	// serverLat, serverLng = 22.7702, 112.9578
 | 
			
		||||
	// serverLat, serverLng = 23.018, 113.7487
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getRandomData(length int) []byte {
 | 
			
		||||
	data := make([]byte, length)
 | 
			
		||||
	if _, err := rand.Read(data); err != nil {
 | 
			
		||||
		log.Fatalf("Failed to generate random data: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getIPInfoURL(address string) string {
 | 
			
		||||
	apiKey := config.LoadedConfig().IPInfoAPIKey
 | 
			
		||||
 | 
			
		||||
	ipInfoURL := `https://ipinfo.io/%s/json`
 | 
			
		||||
	if address != "" {
 | 
			
		||||
		ipInfoURL = fmt.Sprintf(ipInfoURL, address)
 | 
			
		||||
	} else {
 | 
			
		||||
		ipInfoURL = "https://ipinfo.io/json"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if apiKey != "" {
 | 
			
		||||
		ipInfoURL += "?token=" + apiKey
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ipInfoURL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getIPInfo(addr string) results.IPInfoResponse {
 | 
			
		||||
	var ret results.IPInfoResponse
 | 
			
		||||
	resp, err := http.DefaultClient.Get(getIPInfoURL(addr))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error getting response from ipinfo.io: %s", err)
 | 
			
		||||
		return ret
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	raw, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error reading response from ipinfo.io: %s", err)
 | 
			
		||||
		return ret
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(raw, &ret); err != nil {
 | 
			
		||||
		log.Errorf("Error parsing response from ipinfo.io: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getServerLocation() (float64, float64) {
 | 
			
		||||
	conf := config.LoadedConfig()
 | 
			
		||||
 | 
			
		||||
	if conf.ServerLat > 0 && conf.ServerLng > 0 {
 | 
			
		||||
		log.Infof("Configured server coordinates: %.6f, %.6f", conf.ServerLat, conf.ServerLng)
 | 
			
		||||
		return conf.ServerLat, conf.ServerLng
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var ret results.IPInfoResponse
 | 
			
		||||
	resp, err := http.DefaultClient.Get(getIPInfoURL(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error getting repsonse from ipinfo.io: %s", err)
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
	raw, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error reading response from ipinfo.io: %s", err)
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(raw, &ret); err != nil {
 | 
			
		||||
		log.Errorf("Error parsing response from ipinfo.io: %s", err)
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var lat, lng float64
 | 
			
		||||
	if ret.Location != "" {
 | 
			
		||||
		lat, lng = parseLocationString(ret.Location)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Infof("Fetched server coordinates: %.6f, %.6f", lat, lng)
 | 
			
		||||
 | 
			
		||||
	return lat, lng
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseLocationString(location string) (float64, float64) {
 | 
			
		||||
	parts := strings.Split(location, ",")
 | 
			
		||||
	if len(parts) != 2 {
 | 
			
		||||
		log.Errorf("Unknown location format: %s", location)
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lat, err := strconv.ParseFloat(parts[0], 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error parsing latitude: %s", parts[0])
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lng, err := strconv.ParseFloat(parts[1], 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error parsing longitude: %s", parts[0])
 | 
			
		||||
		return 0, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return lat, lng
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func calculateDistance(clientLocation string, unit string) string {
 | 
			
		||||
	clientLat, clientLng := parseLocationString(clientLocation)
 | 
			
		||||
 | 
			
		||||
	radlat1 := float64(math.Pi * serverLat / 180)
 | 
			
		||||
	radlat2 := float64(math.Pi * clientLat / 180)
 | 
			
		||||
 | 
			
		||||
	theta := float64(serverLng - clientLng)
 | 
			
		||||
	radtheta := float64(math.Pi * theta / 180)
 | 
			
		||||
 | 
			
		||||
	dist := math.Sin(radlat1)*math.Sin(radlat2) + math.Cos(radlat1)*math.Cos(radlat2)*math.Cos(radtheta)
 | 
			
		||||
 | 
			
		||||
	if dist > 1 {
 | 
			
		||||
		dist = 1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dist = math.Acos(dist)
 | 
			
		||||
	dist = dist * 180 / math.Pi
 | 
			
		||||
	dist = dist * 60 * 1.1515
 | 
			
		||||
 | 
			
		||||
	unitString := " mi"
 | 
			
		||||
	switch unit {
 | 
			
		||||
	case "km":
 | 
			
		||||
		dist = dist * 1.609344
 | 
			
		||||
		unitString = " km"
 | 
			
		||||
	case "NM":
 | 
			
		||||
		dist = dist * 0.8684
 | 
			
		||||
		unitString = " NM"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%d%s", round(dist), unitString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func round(v float64) int {
 | 
			
		||||
	r := int(math.Round(v))
 | 
			
		||||
	return 10 * ((r + 9) / 10)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								web/web.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								web/web.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi"
 | 
			
		||||
	"github.com/go-chi/chi/middleware"
 | 
			
		||||
	"github.com/go-chi/cors"
 | 
			
		||||
	"github.com/go-chi/render"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	"github.com/librespeed/speedtest/config"
 | 
			
		||||
	"github.com/librespeed/speedtest/results"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// chunk size is 1 mib
 | 
			
		||||
	chunkSize = 1048576
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// generate random data for download test on start to minimize runtime overhead
 | 
			
		||||
	randomData = getRandomData(chunkSize)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ListenAndServe(conf *config.Config) error {
 | 
			
		||||
	r := chi.NewMux()
 | 
			
		||||
	r.Use(middleware.RealIP)
 | 
			
		||||
 | 
			
		||||
	cs := cors.New(cors.Options{
 | 
			
		||||
		AllowedOrigins: []string{"*"},
 | 
			
		||||
		AllowedMethods: []string{"GET", "POST", "OPTIONS"},
 | 
			
		||||
		AllowedHeaders: []string{"*"},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	r.Use(cs.Handler)
 | 
			
		||||
	r.Use(middleware.NoCache)
 | 
			
		||||
	r.Use(middleware.Logger)
 | 
			
		||||
 | 
			
		||||
	log.Infof("Starting backend server on %s", net.JoinHostPort(conf.BindAddress, conf.Port))
 | 
			
		||||
	r.Get("/*", pages)
 | 
			
		||||
	r.HandleFunc("/empty", empty)
 | 
			
		||||
	r.Get("/garbage", garbage)
 | 
			
		||||
	r.Get("/getIP", getIP)
 | 
			
		||||
	r.Get("/results", results.DrawPNG)
 | 
			
		||||
	r.Get("/results/", results.DrawPNG)
 | 
			
		||||
	r.Post("/results/telemetry", results.Record)
 | 
			
		||||
	r.HandleFunc("/stats", results.Stats)
 | 
			
		||||
 | 
			
		||||
	// PHP frontend default values compatibility
 | 
			
		||||
	r.HandleFunc("/empty.php", empty)
 | 
			
		||||
	r.Get("/garbage.php", garbage)
 | 
			
		||||
	r.Get("/getIP.php", getIP)
 | 
			
		||||
	r.Post("/results/telemetry.php", results.Record)
 | 
			
		||||
	r.HandleFunc("/stats.php", results.Stats)
 | 
			
		||||
 | 
			
		||||
	return http.ListenAndServe(net.JoinHostPort(conf.BindAddress, conf.Port), r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pages(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.RequestURI == "/" {
 | 
			
		||||
		r.RequestURI = "/index.html"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uri := strings.Split(r.RequestURI, "?")[0]
 | 
			
		||||
	if strings.HasSuffix(uri, ".html") || strings.HasSuffix(uri, ".js") {
 | 
			
		||||
		http.FileServer(http.Dir("assets")).ServeHTTP(w, r)
 | 
			
		||||
	} else {
 | 
			
		||||
		w.WriteHeader(http.StatusForbidden)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func empty(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	io.Copy(ioutil.Discard, r.Body)
 | 
			
		||||
	r.Body.Close()
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Connection", "keep-alive")
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func garbage(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Description", "File Transfer")
 | 
			
		||||
	w.Header().Set("Content-Type", "application/octet-stream")
 | 
			
		||||
	w.Header().Set("Content-Disposition", "attachment; filename=random.dat")
 | 
			
		||||
	w.Header().Set("Content-Transfer-Encoding", "binary")
 | 
			
		||||
 | 
			
		||||
	// chunk size set to 4 by default
 | 
			
		||||
	chunks := 4
 | 
			
		||||
 | 
			
		||||
	ckSize := r.FormValue("ckSize")
 | 
			
		||||
	if ckSize != "" {
 | 
			
		||||
		i, err := strconv.ParseInt(ckSize, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Errorf("Invalid chunk size: %s", ckSize)
 | 
			
		||||
			log.Warn("Will use default value %d", chunks)
 | 
			
		||||
		} else {
 | 
			
		||||
			// limit max chunk size to 1024
 | 
			
		||||
			if i > 1024 {
 | 
			
		||||
				chunks = 1024
 | 
			
		||||
			} else {
 | 
			
		||||
				chunks = int(i)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < chunks; i++ {
 | 
			
		||||
		if _, err := w.Write(randomData); err != nil {
 | 
			
		||||
			log.Errorf("Error writing back to client at chunk number %d: %s", i, err)
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getIP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	var ret results.Result
 | 
			
		||||
 | 
			
		||||
	clientIP := r.RemoteAddr
 | 
			
		||||
	clientIP = strings.ReplaceAll(clientIP, "::ffff:", "")
 | 
			
		||||
 | 
			
		||||
	ip, _, err := net.SplitHostPort(r.RemoteAddr)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		clientIP = ip
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isSpecialIP := true
 | 
			
		||||
	switch {
 | 
			
		||||
	case clientIP == "::1":
 | 
			
		||||
		ret.ProcessedString = clientIP + " - localhost IPv6 access"
 | 
			
		||||
	case strings.HasPrefix(clientIP, "fe80:"):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - link-local IPv6 access"
 | 
			
		||||
	case strings.HasPrefix(clientIP, "127."):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - localhost IPv4 access"
 | 
			
		||||
	case strings.HasPrefix(clientIP, "10."):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - private IPv4 access"
 | 
			
		||||
	case regexp.MustCompile(`^172\.(1[6-9]|2\d|3[01])\.`).MatchString(clientIP):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - private IPv4 access"
 | 
			
		||||
	case strings.HasPrefix(clientIP, "192.168"):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - private IPv4 access"
 | 
			
		||||
	case strings.HasPrefix(clientIP, "169.254"):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - link-local IPv4 access"
 | 
			
		||||
	case regexp.MustCompile(`^100\.([6-9][0-9]|1[0-2][0-7])\.`).MatchString(clientIP):
 | 
			
		||||
		ret.ProcessedString = clientIP + " - CGNAT IPv4 access"
 | 
			
		||||
	default:
 | 
			
		||||
		isSpecialIP = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isSpecialIP {
 | 
			
		||||
		b, _ := json.Marshal(&ret)
 | 
			
		||||
		if _, err := w.Write(b); err != nil {
 | 
			
		||||
			log.Errorf("Error writing to client: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getISPInfo := r.FormValue("isp") == "true"
 | 
			
		||||
	distanceUnit := r.FormValue("distance")
 | 
			
		||||
 | 
			
		||||
	ret.ProcessedString = clientIP
 | 
			
		||||
 | 
			
		||||
	if getISPInfo {
 | 
			
		||||
		ispInfo := getIPInfo(clientIP)
 | 
			
		||||
		ret.RawISPInfo = ispInfo
 | 
			
		||||
 | 
			
		||||
		removeRegexp := regexp.MustCompile(`AS\d+\s`)
 | 
			
		||||
		isp := removeRegexp.ReplaceAllString(ispInfo.Organization, "")
 | 
			
		||||
 | 
			
		||||
		if isp == "" {
 | 
			
		||||
			isp = "Unknown ISP"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ispInfo.Country != "" {
 | 
			
		||||
			isp += ", " + ispInfo.Country
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ispInfo.Location != "" {
 | 
			
		||||
			isp += " (" + calculateDistance(ispInfo.Location, distanceUnit) + ")"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ret.ProcessedString += " - " + isp
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render.JSON(w, r, ret)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user