telemetry.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. package results
  2. import (
  3. "encoding/json"
  4. "image"
  5. "image/color"
  6. "image/draw"
  7. "image/png"
  8. "io/ioutil"
  9. "math/rand"
  10. "net"
  11. "net/http"
  12. "regexp"
  13. "strings"
  14. "time"
  15. "backend/config"
  16. "backend/database"
  17. "backend/database/schema"
  18. "github.com/golang/freetype"
  19. "github.com/golang/freetype/truetype"
  20. "github.com/oklog/ulid/v2"
  21. log "github.com/sirupsen/logrus"
  22. "golang.org/x/image/font"
  23. )
  24. const (
  25. watermark = "LibreSpeed"
  26. labelMS = " ms"
  27. labelMbps = "Mbps"
  28. labelPing = "Ping"
  29. labelJitter = "Jitter"
  30. labelDownload = "Download"
  31. labelUpload = "Upload"
  32. )
  33. var (
  34. 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]?)`)
  35. 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]))`)
  36. hostnameRegex = regexp.MustCompile(`"hostname":"([^\\\\"]|\\\\")*"`)
  37. fontLight, fontBold *truetype.Font
  38. labelFace, valueFace, smallLabelFace, orgFace, watermarkFace font.Face
  39. canvasWidth, canvasHeight = 800, 600
  40. dpi = 150.0
  41. colorLabel = image.NewUniform(color.RGBA{40, 40, 40, 255})
  42. colorDownload = image.NewUniform(color.RGBA{96, 96, 170, 255})
  43. colorUpload = image.NewUniform(color.RGBA{96, 96, 96, 255})
  44. colorPing = image.NewUniform(color.RGBA{170, 96, 96, 255})
  45. colorJitter = image.NewUniform(color.RGBA{170, 96, 96, 255})
  46. colorMeasure = image.NewUniform(color.RGBA{40, 40, 40, 255})
  47. colorISP = image.NewUniform(color.RGBA{40, 40, 40, 255})
  48. colorWatermark = image.NewUniform(color.RGBA{160, 160, 160, 255})
  49. colorSeparator = image.NewUniform(color.RGBA{192, 192, 192, 255})
  50. )
  51. type Result struct {
  52. ProcessedString string `json:"processedString"`
  53. RawISPInfo string `json:"rawIspInfo"`
  54. }
  55. type IPInfoResponse struct {
  56. IP string `json:"ip"`
  57. Hostname string `json:"hostname"`
  58. City string `json:"city"`
  59. Region string `json:"region"`
  60. Country string `json:"country"`
  61. Location string `json:"loc"`
  62. Organization string `json:"org"`
  63. Postal string `json:"postal"`
  64. Timezone string `json:"timezone"`
  65. Readme string `json:"readme"`
  66. }
  67. func init() {
  68. // changed to use Noto Sans instead of OpenSans, due to issue:
  69. // https://github.com/golang/freetype/issues/8
  70. if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Light.ttf"); err != nil {
  71. log.Fatalf("Error opening NotoSansDisplay-Light font: %s", err)
  72. } else {
  73. f, err := freetype.ParseFont(b)
  74. if err != nil {
  75. log.Fatalf("Error parsing NotoSansDisplay-Light font: %s", err)
  76. }
  77. fontLight = f
  78. }
  79. if b, err := ioutil.ReadFile("assets/NotoSansDisplay-Medium.ttf"); err != nil {
  80. log.Fatalf("Error opening NotoSansDisplay-Medium font: %s", err)
  81. } else {
  82. f, err := freetype.ParseFont(b)
  83. if err != nil {
  84. log.Fatalf("Error parsing NotoSansDisplay-Medium font: %s", err)
  85. }
  86. fontBold = f
  87. }
  88. labelFace = truetype.NewFace(fontBold, &truetype.Options{
  89. Size: 26,
  90. DPI: dpi,
  91. Hinting: font.HintingFull,
  92. })
  93. valueFace = truetype.NewFace(fontLight, &truetype.Options{
  94. Size: 36,
  95. DPI: dpi,
  96. Hinting: font.HintingFull,
  97. })
  98. smallLabelFace = truetype.NewFace(fontBold, &truetype.Options{
  99. Size: 20,
  100. DPI: dpi,
  101. Hinting: font.HintingFull,
  102. })
  103. orgFace = truetype.NewFace(fontBold, &truetype.Options{
  104. Size: 16,
  105. DPI: dpi,
  106. Hinting: font.HintingFull,
  107. })
  108. watermarkFace = truetype.NewFace(fontLight, &truetype.Options{
  109. Size: 14,
  110. DPI: dpi,
  111. Hinting: font.HintingFull,
  112. })
  113. }
  114. func (r *Result) GetISPInfo() (IPInfoResponse, error) {
  115. var ret IPInfoResponse
  116. var err error
  117. if r.RawISPInfo != "" {
  118. err = json.Unmarshal([]byte(r.RawISPInfo), &ret)
  119. } else {
  120. // if ISP info is not available (i.e. localhost testing), use ProcessedString as Organization
  121. ret.Organization = r.ProcessedString
  122. }
  123. return ret, err
  124. }
  125. func Record(w http.ResponseWriter, r *http.Request) {
  126. ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr)
  127. userAgent := r.UserAgent()
  128. language := r.Header.Get("Accept-Language")
  129. ispInfo := r.FormValue("ispinfo")
  130. download := r.FormValue("dl")
  131. upload := r.FormValue("ul")
  132. ping := r.FormValue("ping")
  133. jitter := r.FormValue("jitter")
  134. logs := r.FormValue("log")
  135. extra := r.FormValue("extra")
  136. if config.LoadedConfig().RedactIP {
  137. ipAddr = "0.0.0.0"
  138. ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0")
  139. ipv4Regex.ReplaceAllString(logs, "0.0.0.0")
  140. ipv6Regex.ReplaceAllString(ispInfo, "0.0.0.0")
  141. ipv6Regex.ReplaceAllString(logs, "0.0.0.0")
  142. hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`)
  143. hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`)
  144. }
  145. var record schema.TelemetryData
  146. record.IPAddress = ipAddr
  147. if ispInfo == "" {
  148. record.ISPInfo = "{}"
  149. } else {
  150. record.ISPInfo = ispInfo
  151. }
  152. record.Extra = extra
  153. record.UserAgent = userAgent
  154. record.Language = language
  155. record.Download = download
  156. record.Upload = upload
  157. record.Ping = ping
  158. record.Jitter = jitter
  159. record.Log = logs
  160. t := time.Now()
  161. entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
  162. uuid := ulid.MustNew(ulid.Timestamp(t), entropy)
  163. record.UUID = uuid.String()
  164. err := database.DB.Insert(&record)
  165. if err != nil {
  166. log.Errorf("Error inserting into database: %s", err)
  167. w.WriteHeader(http.StatusInternalServerError)
  168. return
  169. }
  170. if _, err := w.Write([]byte("id " + uuid.String())); err != nil {
  171. log.Errorf("Error writing ID to telemetry request: %s", err)
  172. w.WriteHeader(http.StatusInternalServerError)
  173. }
  174. }
  175. func DrawPNG(w http.ResponseWriter, r *http.Request) {
  176. uuid := r.FormValue("id")
  177. record, err := database.DB.FetchByUUID(uuid)
  178. if err != nil {
  179. log.Errorf("Error querying database: %s", err)
  180. w.WriteHeader(http.StatusInternalServerError)
  181. return
  182. }
  183. var result Result
  184. if err := json.Unmarshal([]byte(record.ISPInfo), &result); err != nil {
  185. log.Errorf("Error parsing ISP info: %s", err)
  186. w.WriteHeader(http.StatusInternalServerError)
  187. return
  188. }
  189. ispInfo, err := result.GetISPInfo()
  190. if err != nil {
  191. log.Errorf("Error parsing ISP info: %s", err)
  192. w.WriteHeader(http.StatusInternalServerError)
  193. return
  194. }
  195. canvas := image.NewRGBA(image.Rectangle{
  196. Min: image.Point{},
  197. Max: image.Point{
  198. X: canvasWidth,
  199. Y: canvasHeight,
  200. },
  201. })
  202. draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
  203. drawer := &font.Drawer{
  204. Dst: canvas,
  205. Face: labelFace,
  206. }
  207. drawer.Src = colorLabel
  208. // labels
  209. p := drawer.MeasureString(labelPing)
  210. x := canvasWidth/4 - p.Round()/2
  211. drawer.Dot = freetype.Pt(x, canvasHeight/10)
  212. drawer.DrawString(labelPing)
  213. p = drawer.MeasureString(labelJitter)
  214. x = canvasWidth*3/4 - p.Round()/2
  215. drawer.Dot = freetype.Pt(x, canvasHeight/10)
  216. drawer.DrawString(labelJitter)
  217. p = drawer.MeasureString(labelDownload)
  218. x = canvasWidth/4 - p.Round()/2
  219. drawer.Dot = freetype.Pt(x, canvasHeight/2)
  220. drawer.DrawString(labelDownload)
  221. p = drawer.MeasureString(labelUpload)
  222. x = canvasWidth*3/4 - p.Round()/2
  223. drawer.Dot = freetype.Pt(x, canvasHeight/2)
  224. drawer.DrawString(labelUpload)
  225. drawer.Face = smallLabelFace
  226. drawer.Src = colorMeasure
  227. p = drawer.MeasureString(labelMbps)
  228. x = canvasWidth/4 - p.Round()/2
  229. drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
  230. drawer.DrawString(labelMbps)
  231. p = drawer.MeasureString(labelMbps)
  232. x = canvasWidth*3/4 - p.Round()/2
  233. drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
  234. drawer.DrawString(labelMbps)
  235. msLength := drawer.MeasureString(labelMS)
  236. // ping value
  237. drawer.Face = valueFace
  238. pingValue := strings.Split(record.Ping, ".")[0]
  239. p = drawer.MeasureString(pingValue)
  240. x = canvasWidth/4 - (p.Round()+msLength.Round())/2
  241. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  242. drawer.Src = colorPing
  243. drawer.DrawString(pingValue)
  244. x = x + p.Round()
  245. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  246. drawer.Src = colorMeasure
  247. drawer.Face = smallLabelFace
  248. drawer.DrawString(labelMS)
  249. // jitter value
  250. drawer.Face = valueFace
  251. jitterValue := strings.Split(record.Jitter, ".")[0]
  252. p = drawer.MeasureString(jitterValue)
  253. x = canvasWidth*3/4 - (p.Round()+msLength.Round())/2
  254. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  255. drawer.Src = colorJitter
  256. drawer.DrawString(jitterValue)
  257. drawer.Face = smallLabelFace
  258. x = x + p.Round()
  259. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  260. drawer.Src = colorMeasure
  261. drawer.DrawString(labelMS)
  262. // download value
  263. drawer.Face = valueFace
  264. p = drawer.MeasureString(record.Download)
  265. x = canvasWidth/4 - p.Round()/2
  266. drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
  267. drawer.Src = colorDownload
  268. drawer.DrawString(record.Download)
  269. // upload value
  270. p = drawer.MeasureString(record.Upload)
  271. x = canvasWidth*3/4 - p.Round()/2
  272. drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
  273. drawer.Src = colorUpload
  274. drawer.DrawString(record.Upload)
  275. // watermark
  276. ctx := freetype.NewContext()
  277. ctx.SetFont(fontLight)
  278. ctx.SetFontSize(14)
  279. ctx.SetDPI(dpi)
  280. ctx.SetHinting(font.HintingFull)
  281. drawer.Face = watermarkFace
  282. drawer.Src = colorWatermark
  283. p = drawer.MeasureString(watermark)
  284. x = canvasWidth - p.Round() - 5
  285. drawer.Dot = freetype.Pt(x, canvasHeight-10)
  286. drawer.DrawString(watermark)
  287. // separator
  288. for i := canvas.Bounds().Min.X; i < canvas.Bounds().Max.X; i++ {
  289. canvas.Set(i, canvasHeight-ctx.PointToFixed(14).Round()-10, colorSeparator)
  290. }
  291. // ISP info
  292. drawer.Face = orgFace
  293. drawer.Src = colorISP
  294. drawer.Dot = freetype.Pt(6, canvasHeight-ctx.PointToFixed(14).Round()-15)
  295. removeRegexp := regexp.MustCompile(`AS\d+\s`)
  296. org := removeRegexp.ReplaceAllString(ispInfo.Organization, "")
  297. if ispInfo.Country != "" {
  298. org += ", " + ispInfo.Country
  299. }
  300. drawer.DrawString(org)
  301. w.Header().Set("Content-Disposition", "inline; filename="+uuid+".png")
  302. w.Header().Set("Content-Type", "image/png")
  303. if err := png.Encode(w, canvas); err != nil {
  304. log.Errorf("Failed to output image to HTTP client: %s", err)
  305. }
  306. }