You've already forked speedtest-go
							
							Migrate code from Go branch to new repo
This commit is contained in:
		
							
								
								
									
										178
									
								
								results/stats.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								results/stats.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
package results
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	"github.com/librespeed/speedtest/config"
 | 
			
		||||
	"github.com/librespeed/speedtest/database"
 | 
			
		||||
	"github.com/librespeed/speedtest/database/schema"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type StatsData struct {
 | 
			
		||||
	NoPassword bool
 | 
			
		||||
	LoggedIn   bool
 | 
			
		||||
	Data       []schema.TelemetryData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Stats(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 | 
			
		||||
	t, err := template.New("template").Parse(htmlTemplate)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Failed to parse template: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf := config.LoadedConfig()
 | 
			
		||||
	var data StatsData
 | 
			
		||||
 | 
			
		||||
	if conf.StatsPassword == "PASSWORD" {
 | 
			
		||||
		data.NoPassword = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !data.NoPassword {
 | 
			
		||||
		op := r.FormValue("op")
 | 
			
		||||
		c, _ := r.Cookie("logged")
 | 
			
		||||
 | 
			
		||||
		if c != nil && c.Value == "true" {
 | 
			
		||||
			if op == "logout" {
 | 
			
		||||
				cookie := &http.Cookie{
 | 
			
		||||
					Name:  "logged",
 | 
			
		||||
					Value: "false",
 | 
			
		||||
				}
 | 
			
		||||
				http.SetCookie(w, cookie)
 | 
			
		||||
				http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
 | 
			
		||||
			} else {
 | 
			
		||||
				data.LoggedIn = true
 | 
			
		||||
 | 
			
		||||
				id := r.FormValue("id")
 | 
			
		||||
				switch id {
 | 
			
		||||
				case "L100":
 | 
			
		||||
					stats, err := database.DB.FetchLast100()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						log.Errorf("Error fetching data from database: %s", err)
 | 
			
		||||
						w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					data.Data = stats
 | 
			
		||||
				case "":
 | 
			
		||||
				default:
 | 
			
		||||
					stat, err := database.DB.FetchByUUID(id)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						log.Errorf("Error fetching data from database: %s", err)
 | 
			
		||||
						w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
					data.Data = append(data.Data, *stat)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if op == "login" {
 | 
			
		||||
				password := r.FormValue("password")
 | 
			
		||||
				if password == conf.StatsPassword {
 | 
			
		||||
					cookie := &http.Cookie{
 | 
			
		||||
						Name:  "logged",
 | 
			
		||||
						Value: "true",
 | 
			
		||||
					}
 | 
			
		||||
					http.SetCookie(w, cookie)
 | 
			
		||||
					http.Redirect(w, r, "/stats", http.StatusTemporaryRedirect)
 | 
			
		||||
				} else {
 | 
			
		||||
					w.WriteHeader(http.StatusForbidden)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := t.Execute(w, data); err != nil {
 | 
			
		||||
		log.Errorf("Error executing template: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const htmlTemplate = `<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
<title>LibreSpeed - Stats</title>
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	html,body{
 | 
			
		||||
		margin:0;
 | 
			
		||||
		padding:0;
 | 
			
		||||
		border:none;
 | 
			
		||||
		width:100%; min-height:100%;
 | 
			
		||||
	}
 | 
			
		||||
	html{
 | 
			
		||||
		background-color: hsl(198,72%,35%);
 | 
			
		||||
		font-family: "Segoe UI","Roboto",sans-serif;
 | 
			
		||||
	}
 | 
			
		||||
	body{
 | 
			
		||||
		background-color:#FFFFFF;
 | 
			
		||||
		box-sizing:border-box;
 | 
			
		||||
		width:100%;
 | 
			
		||||
		max-width:70em;
 | 
			
		||||
		margin:4em auto;
 | 
			
		||||
		box-shadow:0 1em 6em #00000080;
 | 
			
		||||
		padding:1em 1em 4em 1em;
 | 
			
		||||
		border-radius:0.4em;
 | 
			
		||||
	}
 | 
			
		||||
	h1,h2,h3,h4,h5,h6{
 | 
			
		||||
		font-weight:300;
 | 
			
		||||
		margin-bottom: 0.1em;
 | 
			
		||||
	}
 | 
			
		||||
	h1{
 | 
			
		||||
		text-align:center;
 | 
			
		||||
	}
 | 
			
		||||
	table{
 | 
			
		||||
		margin:2em 0;
 | 
			
		||||
		width:100%;
 | 
			
		||||
	}
 | 
			
		||||
	table, tr, th, td {
 | 
			
		||||
		border: 1px solid #AAAAAA;
 | 
			
		||||
	}
 | 
			
		||||
	th {
 | 
			
		||||
		width: 6em;
 | 
			
		||||
	}
 | 
			
		||||
	td {
 | 
			
		||||
		word-break: break-all;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<h1>LibreSpeed - Stats</h1>
 | 
			
		||||
{{ if .NoPassword }}
 | 
			
		||||
		Please set statistics_password in settings.toml to enable access.
 | 
			
		||||
{{ else if .LoggedIn }}
 | 
			
		||||
	<form action="stats" method="GET"><input type="hidden" name="op" value="logout" /><input type="submit" value="Logout" /></form>
 | 
			
		||||
	<form action="stats" method="GET">
 | 
			
		||||
		<h3>Search test results</h6>
 | 
			
		||||
		<input type="hidden" name="op" value="id" />
 | 
			
		||||
		<input type="text" name="id" id="id" placeholder="Test ID" value=""/>
 | 
			
		||||
		<input type="submit" value="Find" />
 | 
			
		||||
		<input type="submit" onclick="document.getElementById('id').value='L100'" value="Show last 100 tests" />
 | 
			
		||||
	</form>
 | 
			
		||||
 | 
			
		||||
	{{ range $i, $v := .Data }}
 | 
			
		||||
	<table>
 | 
			
		||||
		<tr><th>Test ID</th><td>{{ $v.UUID }}</td></tr>
 | 
			
		||||
		<tr><th>Date and time</th><td>{{ $v.Timestamp }}</td></tr>
 | 
			
		||||
		<tr><th>IP and ISP Info</th><td>{{ $v.IPAddress }}<br/>{{ $v.ISPInfo }}</td></tr>
 | 
			
		||||
		<tr><th>User agent and locale</th><td>{{ $v.UserAgent }}<br/>{{ $v.Language }}</td></tr>
 | 
			
		||||
		<tr><th>Download speed</th><td>{{ $v.Download }}</td></tr>
 | 
			
		||||
		<tr><th>Upload speed</th><td>{{ $v.Upload }}</td></tr>
 | 
			
		||||
		<tr><th>Ping</th><td>{{ $v.Ping }}</td></tr>
 | 
			
		||||
		<tr><th>Jitter</th><td>{{ $v.Jitter }}</td></tr>
 | 
			
		||||
		<tr><th>Log</th><td>{{ $v.Log }}</td></tr>
 | 
			
		||||
		<tr><th>Extra info</th><td>{{ $v.Extra }}</td></tr>
 | 
			
		||||
	</table>
 | 
			
		||||
	{{ end }}
 | 
			
		||||
{{ else }}
 | 
			
		||||
	<form action="stats?op=login" method="POST">
 | 
			
		||||
		<h3>Login</h3>
 | 
			
		||||
		<input type="password" name="password" placeholder="Password" value=""/>
 | 
			
		||||
		<input type="submit" value="Login" />
 | 
			
		||||
	</form>
 | 
			
		||||
{{ end }}
 | 
			
		||||
</body>
 | 
			
		||||
</html>`
 | 
			
		||||
							
								
								
									
										340
									
								
								results/telemetry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								results/telemetry.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,340 @@
 | 
			
		||||
package results
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"image"
 | 
			
		||||
	"image/color"
 | 
			
		||||
	"image/draw"
 | 
			
		||||
	"image/png"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/librespeed/speedtest/config"
 | 
			
		||||
	"github.com/librespeed/speedtest/database"
 | 
			
		||||
	"github.com/librespeed/speedtest/database/schema"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/freetype"
 | 
			
		||||
	"github.com/golang/freetype/truetype"
 | 
			
		||||
	"github.com/oklog/ulid/v2"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"golang.org/x/image/font"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	watermark = "LibreSpeed"
 | 
			
		||||
 | 
			
		||||
	labelMS       = " ms"
 | 
			
		||||
	labelMbps     = "Mbit/s"
 | 
			
		||||
	labelPing     = "Ping"
 | 
			
		||||
	labelJitter   = "Jitter"
 | 
			
		||||
	labelDownload = "Download"
 | 
			
		||||
	labelUpload   = "Upload"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ipv4Regex     = regexp.MustCompile(`(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`)
 | 
			
		||||
	ipv6Regex     = regexp.MustCompile(`(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))`)
 | 
			
		||||
	hostnameRegex = regexp.MustCompile(`"hostname":"([^\\\\"]|\\\\")*"`)
 | 
			
		||||
 | 
			
		||||
	fontLight, fontBold                                          *truetype.Font
 | 
			
		||||
	labelFace, valueFace, smallLabelFace, orgFace, watermarkFace font.Face
 | 
			
		||||
 | 
			
		||||
	canvasWidth, canvasHeight = 800, 600
 | 
			
		||||
	dpi                       = 150.0
 | 
			
		||||
	colorLabel                = image.NewUniform(color.RGBA{40, 40, 40, 255})
 | 
			
		||||
	colorDownload             = image.NewUniform(color.RGBA{96, 96, 170, 255})
 | 
			
		||||
	colorUpload               = image.NewUniform(color.RGBA{96, 96, 96, 255})
 | 
			
		||||
	colorPing                 = image.NewUniform(color.RGBA{170, 96, 96, 255})
 | 
			
		||||
	colorJitter               = image.NewUniform(color.RGBA{170, 96, 96, 255})
 | 
			
		||||
	colorMeasure              = image.NewUniform(color.RGBA{40, 40, 40, 255})
 | 
			
		||||
	colorISP                  = image.NewUniform(color.RGBA{40, 40, 40, 255})
 | 
			
		||||
	colorWatermark            = image.NewUniform(color.RGBA{160, 160, 160, 255})
 | 
			
		||||
	colorSeparator            = image.NewUniform(color.RGBA{192, 192, 192, 255})
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Result struct {
 | 
			
		||||
	ProcessedString string         `json:"processedString"`
 | 
			
		||||
	RawISPInfo      IPInfoResponse `json:"rawIspInfo"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type IPInfoResponse struct {
 | 
			
		||||
	IP           string `json:"ip"`
 | 
			
		||||
	Hostname     string `json:"hostname"`
 | 
			
		||||
	City         string `json:"city"`
 | 
			
		||||
	Region       string `json:"region"`
 | 
			
		||||
	Country      string `json:"country"`
 | 
			
		||||
	Location     string `json:"loc"`
 | 
			
		||||
	Organization string `json:"org"`
 | 
			
		||||
	Postal       string `json:"postal"`
 | 
			
		||||
	Timezone     string `json:"timezone"`
 | 
			
		||||
	Readme       string `json:"readme"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// changed to use Noto Sans instead of OpenSans, due to issue:
 | 
			
		||||
	// https://github.com/golang/freetype/issues/8
 | 
			
		||||
	if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Light.ttf"); err != nil {
 | 
			
		||||
		log.Fatalf("Error opening NotoSansDisplay-Light font: %s", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		f, err := freetype.ParseFont(b)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		fontLight = f
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Medium.ttf"); err != nil {
 | 
			
		||||
		log.Fatalf("Error opening NotoSansDisplay-Medium font: %s", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		f, err := freetype.ParseFont(b)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		fontBold = f
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	labelFace = truetype.NewFace(fontBold, &truetype.Options{
 | 
			
		||||
		Size:    26,
 | 
			
		||||
		DPI:     dpi,
 | 
			
		||||
		Hinting: font.HintingFull,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	valueFace = truetype.NewFace(fontLight, &truetype.Options{
 | 
			
		||||
		Size:    36,
 | 
			
		||||
		DPI:     dpi,
 | 
			
		||||
		Hinting: font.HintingFull,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	smallLabelFace = truetype.NewFace(fontBold, &truetype.Options{
 | 
			
		||||
		Size:    20,
 | 
			
		||||
		DPI:     dpi,
 | 
			
		||||
		Hinting: font.HintingFull,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	orgFace = truetype.NewFace(fontBold, &truetype.Options{
 | 
			
		||||
		Size:    16,
 | 
			
		||||
		DPI:     dpi,
 | 
			
		||||
		Hinting: font.HintingFull,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	watermarkFace = truetype.NewFace(fontLight, &truetype.Options{
 | 
			
		||||
		Size:    14,
 | 
			
		||||
		DPI:     dpi,
 | 
			
		||||
		Hinting: font.HintingFull,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Record(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr)
 | 
			
		||||
	userAgent := r.UserAgent()
 | 
			
		||||
	language := r.Header.Get("Accept-Language")
 | 
			
		||||
 | 
			
		||||
	ispInfo := r.FormValue("ispinfo")
 | 
			
		||||
	download := r.FormValue("dl")
 | 
			
		||||
	upload := r.FormValue("ul")
 | 
			
		||||
	ping := r.FormValue("ping")
 | 
			
		||||
	jitter := r.FormValue("jitter")
 | 
			
		||||
	logs := r.FormValue("log")
 | 
			
		||||
	extra := r.FormValue("extra")
 | 
			
		||||
 | 
			
		||||
	if config.LoadedConfig().RedactIP {
 | 
			
		||||
		ipAddr = "0.0.0.0"
 | 
			
		||||
		ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0")
 | 
			
		||||
		ipv4Regex.ReplaceAllString(logs, "0.0.0.0")
 | 
			
		||||
		ipv6Regex.ReplaceAllString(ispInfo, "0.0.0.0")
 | 
			
		||||
		ipv6Regex.ReplaceAllString(logs, "0.0.0.0")
 | 
			
		||||
		hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`)
 | 
			
		||||
		hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var record schema.TelemetryData
 | 
			
		||||
	record.IPAddress = ipAddr
 | 
			
		||||
	if ispInfo == "" {
 | 
			
		||||
		record.ISPInfo = "{}"
 | 
			
		||||
	} else {
 | 
			
		||||
		record.ISPInfo = ispInfo
 | 
			
		||||
	}
 | 
			
		||||
	record.Extra = extra
 | 
			
		||||
	record.UserAgent = userAgent
 | 
			
		||||
	record.Language = language
 | 
			
		||||
	record.Download = download
 | 
			
		||||
	record.Upload = upload
 | 
			
		||||
	record.Ping = ping
 | 
			
		||||
	record.Jitter = jitter
 | 
			
		||||
	record.Log = logs
 | 
			
		||||
 | 
			
		||||
	t := time.Now()
 | 
			
		||||
	entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
 | 
			
		||||
	uuid := ulid.MustNew(ulid.Timestamp(t), entropy)
 | 
			
		||||
	record.UUID = uuid.String()
 | 
			
		||||
 | 
			
		||||
	err := database.DB.Insert(&record)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error inserting into database: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := w.Write([]byte("id " + uuid.String())); err != nil {
 | 
			
		||||
		log.Errorf("Error writing ID to telemetry request: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DrawPNG(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	uuid := r.FormValue("id")
 | 
			
		||||
	record, err := database.DB.FetchByUUID(uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Errorf("Error querying database: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var result Result
 | 
			
		||||
	if err := json.Unmarshal([]byte(record.ISPInfo), &result); err != nil {
 | 
			
		||||
		log.Errorf("Error parsing ISP info: %s", err)
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	canvas := image.NewRGBA(image.Rectangle{
 | 
			
		||||
		Min: image.Point{},
 | 
			
		||||
		Max: image.Point{
 | 
			
		||||
			X: canvasWidth,
 | 
			
		||||
			Y: canvasHeight,
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
 | 
			
		||||
 | 
			
		||||
	drawer := &font.Drawer{
 | 
			
		||||
		Dst:  canvas,
 | 
			
		||||
		Face: labelFace,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	drawer.Src = colorLabel
 | 
			
		||||
 | 
			
		||||
	// labels
 | 
			
		||||
	p := drawer.MeasureString(labelPing)
 | 
			
		||||
	x := canvasWidth/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight/10)
 | 
			
		||||
	drawer.DrawString(labelPing)
 | 
			
		||||
 | 
			
		||||
	p = drawer.MeasureString(labelJitter)
 | 
			
		||||
	x = canvasWidth*3/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight/10)
 | 
			
		||||
	drawer.DrawString(labelJitter)
 | 
			
		||||
 | 
			
		||||
	p = drawer.MeasureString(labelDownload)
 | 
			
		||||
	x = canvasWidth/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight/2)
 | 
			
		||||
	drawer.DrawString(labelDownload)
 | 
			
		||||
 | 
			
		||||
	p = drawer.MeasureString(labelUpload)
 | 
			
		||||
	x = canvasWidth*3/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight/2)
 | 
			
		||||
	drawer.DrawString(labelUpload)
 | 
			
		||||
 | 
			
		||||
	drawer.Face = smallLabelFace
 | 
			
		||||
	drawer.Src = colorMeasure
 | 
			
		||||
	p = drawer.MeasureString(labelMbps)
 | 
			
		||||
	x = canvasWidth/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
 | 
			
		||||
	drawer.DrawString(labelMbps)
 | 
			
		||||
 | 
			
		||||
	p = drawer.MeasureString(labelMbps)
 | 
			
		||||
	x = canvasWidth*3/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
 | 
			
		||||
	drawer.DrawString(labelMbps)
 | 
			
		||||
 | 
			
		||||
	msLength := drawer.MeasureString(labelMS)
 | 
			
		||||
 | 
			
		||||
	// ping value
 | 
			
		||||
	drawer.Face = valueFace
 | 
			
		||||
	pingValue := strings.Split(record.Ping, ".")[0]
 | 
			
		||||
	p = drawer.MeasureString(pingValue)
 | 
			
		||||
 | 
			
		||||
	x = canvasWidth/4 - (p.Round()+msLength.Round())/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
 | 
			
		||||
	drawer.Src = colorPing
 | 
			
		||||
	drawer.DrawString(pingValue)
 | 
			
		||||
	x = x + p.Round()
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
 | 
			
		||||
	drawer.Src = colorMeasure
 | 
			
		||||
	drawer.Face = smallLabelFace
 | 
			
		||||
	drawer.DrawString(labelMS)
 | 
			
		||||
 | 
			
		||||
	// jitter value
 | 
			
		||||
	drawer.Face = valueFace
 | 
			
		||||
	jitterValue := strings.Split(record.Jitter, ".")[0]
 | 
			
		||||
	p = drawer.MeasureString(jitterValue)
 | 
			
		||||
	x = canvasWidth*3/4 - (p.Round()+msLength.Round())/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
 | 
			
		||||
	drawer.Src = colorJitter
 | 
			
		||||
	drawer.DrawString(jitterValue)
 | 
			
		||||
	drawer.Face = smallLabelFace
 | 
			
		||||
	x = x + p.Round()
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
 | 
			
		||||
	drawer.Src = colorMeasure
 | 
			
		||||
	drawer.DrawString(labelMS)
 | 
			
		||||
 | 
			
		||||
	// download value
 | 
			
		||||
	drawer.Face = valueFace
 | 
			
		||||
	p = drawer.MeasureString(record.Download)
 | 
			
		||||
	x = canvasWidth/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
 | 
			
		||||
	drawer.Src = colorDownload
 | 
			
		||||
	drawer.DrawString(record.Download)
 | 
			
		||||
 | 
			
		||||
	// upload value
 | 
			
		||||
	p = drawer.MeasureString(record.Upload)
 | 
			
		||||
	x = canvasWidth*3/4 - p.Round()/2
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
 | 
			
		||||
	drawer.Src = colorUpload
 | 
			
		||||
	drawer.DrawString(record.Upload)
 | 
			
		||||
 | 
			
		||||
	// watermark
 | 
			
		||||
	ctx := freetype.NewContext()
 | 
			
		||||
	ctx.SetFont(fontLight)
 | 
			
		||||
	ctx.SetFontSize(14)
 | 
			
		||||
	ctx.SetDPI(dpi)
 | 
			
		||||
	ctx.SetHinting(font.HintingFull)
 | 
			
		||||
 | 
			
		||||
	drawer.Face = watermarkFace
 | 
			
		||||
	drawer.Src = colorWatermark
 | 
			
		||||
	p = drawer.MeasureString(watermark)
 | 
			
		||||
	x = canvasWidth - p.Round() - 5
 | 
			
		||||
	drawer.Dot = freetype.Pt(x, canvasHeight-10)
 | 
			
		||||
	drawer.DrawString(watermark)
 | 
			
		||||
 | 
			
		||||
	// separator
 | 
			
		||||
	for i := canvas.Bounds().Min.X; i < canvas.Bounds().Max.X; i++ {
 | 
			
		||||
		canvas.Set(i, canvasHeight-ctx.PointToFixed(14).Round()-10, colorSeparator)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ISP info
 | 
			
		||||
	drawer.Face = orgFace
 | 
			
		||||
	drawer.Src = colorISP
 | 
			
		||||
	drawer.Dot = freetype.Pt(6, canvasHeight-ctx.PointToFixed(14).Round()-15)
 | 
			
		||||
	if result.RawISPInfo.Organization != "" {
 | 
			
		||||
		removeRegexp := regexp.MustCompile(`AS\d+\s`)
 | 
			
		||||
		org := removeRegexp.ReplaceAllString(result.RawISPInfo.Organization, "")
 | 
			
		||||
		if result.RawISPInfo.Country != "" {
 | 
			
		||||
			org += ", " + result.RawISPInfo.Country
 | 
			
		||||
		}
 | 
			
		||||
		drawer.DrawString(org)
 | 
			
		||||
	} else {
 | 
			
		||||
		drawer.DrawString(result.ProcessedString)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Disposition", "inline; filename="+uuid+".png")
 | 
			
		||||
	w.Header().Set("Content-Type", "image/png")
 | 
			
		||||
	if err := png.Encode(w, canvas); err != nil {
 | 
			
		||||
		log.Errorf("Failed to output image to HTTP client: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user