telemetry.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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 IPInfoResponse `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 Record(w http.ResponseWriter, r *http.Request) {
  115. ipAddr, _, _ := net.SplitHostPort(r.RemoteAddr)
  116. userAgent := r.UserAgent()
  117. language := r.Header.Get("Accept-Language")
  118. ispInfo := r.FormValue("ispinfo")
  119. download := r.FormValue("dl")
  120. upload := r.FormValue("ul")
  121. ping := r.FormValue("ping")
  122. jitter := r.FormValue("jitter")
  123. logs := r.FormValue("log")
  124. extra := r.FormValue("extra")
  125. if config.LoadedConfig().RedactIP {
  126. ipAddr = "0.0.0.0"
  127. ipv4Regex.ReplaceAllString(ispInfo, "0.0.0.0")
  128. ipv4Regex.ReplaceAllString(logs, "0.0.0.0")
  129. ipv6Regex.ReplaceAllString(ispInfo, "0.0.0.0")
  130. ipv6Regex.ReplaceAllString(logs, "0.0.0.0")
  131. hostnameRegex.ReplaceAllString(ispInfo, `"hostname":"REDACTED"`)
  132. hostnameRegex.ReplaceAllString(logs, `"hostname":"REDACTED"`)
  133. }
  134. var record schema.TelemetryData
  135. record.IPAddress = ipAddr
  136. if ispInfo == "" {
  137. record.ISPInfo = "{}"
  138. } else {
  139. record.ISPInfo = ispInfo
  140. }
  141. record.Extra = extra
  142. record.UserAgent = userAgent
  143. record.Language = language
  144. record.Download = download
  145. record.Upload = upload
  146. record.Ping = ping
  147. record.Jitter = jitter
  148. record.Log = logs
  149. t := time.Now()
  150. entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
  151. uuid := ulid.MustNew(ulid.Timestamp(t), entropy)
  152. record.UUID = uuid.String()
  153. err := database.DB.Insert(&record)
  154. if err != nil {
  155. log.Errorf("Error inserting into database: %s", err)
  156. w.WriteHeader(http.StatusInternalServerError)
  157. return
  158. }
  159. if _, err := w.Write([]byte("id " + uuid.String())); err != nil {
  160. log.Errorf("Error writing ID to telemetry request: %s", err)
  161. w.WriteHeader(http.StatusInternalServerError)
  162. }
  163. }
  164. func DrawPNG(w http.ResponseWriter, r *http.Request) {
  165. uuid := r.FormValue("id")
  166. record, err := database.DB.FetchByUUID(uuid)
  167. if err != nil {
  168. log.Errorf("Error querying database: %s", err)
  169. w.WriteHeader(http.StatusInternalServerError)
  170. return
  171. }
  172. var result Result
  173. if err := json.Unmarshal([]byte(record.ISPInfo), &result); err != nil {
  174. log.Errorf("Error parsing ISP info: %s", err)
  175. w.WriteHeader(http.StatusInternalServerError)
  176. return
  177. }
  178. canvas := image.NewRGBA(image.Rectangle{
  179. Min: image.Point{},
  180. Max: image.Point{
  181. X: canvasWidth,
  182. Y: canvasHeight,
  183. },
  184. })
  185. draw.Draw(canvas, canvas.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
  186. drawer := &font.Drawer{
  187. Dst: canvas,
  188. Face: labelFace,
  189. }
  190. drawer.Src = colorLabel
  191. // labels
  192. p := drawer.MeasureString(labelPing)
  193. x := canvasWidth/4 - p.Round()/2
  194. drawer.Dot = freetype.Pt(x, canvasHeight/10)
  195. drawer.DrawString(labelPing)
  196. p = drawer.MeasureString(labelJitter)
  197. x = canvasWidth*3/4 - p.Round()/2
  198. drawer.Dot = freetype.Pt(x, canvasHeight/10)
  199. drawer.DrawString(labelJitter)
  200. p = drawer.MeasureString(labelDownload)
  201. x = canvasWidth/4 - p.Round()/2
  202. drawer.Dot = freetype.Pt(x, canvasHeight/2)
  203. drawer.DrawString(labelDownload)
  204. p = drawer.MeasureString(labelUpload)
  205. x = canvasWidth*3/4 - p.Round()/2
  206. drawer.Dot = freetype.Pt(x, canvasHeight/2)
  207. drawer.DrawString(labelUpload)
  208. drawer.Face = smallLabelFace
  209. drawer.Src = colorMeasure
  210. p = drawer.MeasureString(labelMbps)
  211. x = canvasWidth/4 - p.Round()/2
  212. drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
  213. drawer.DrawString(labelMbps)
  214. p = drawer.MeasureString(labelMbps)
  215. x = canvasWidth*3/4 - p.Round()/2
  216. drawer.Dot = freetype.Pt(x, canvasHeight*8/10)
  217. drawer.DrawString(labelMbps)
  218. msLength := drawer.MeasureString(labelMS)
  219. // ping value
  220. drawer.Face = valueFace
  221. pingValue := strings.Split(record.Ping, ".")[0]
  222. p = drawer.MeasureString(pingValue)
  223. x = canvasWidth/4 - (p.Round()+msLength.Round())/2
  224. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  225. drawer.Src = colorPing
  226. drawer.DrawString(pingValue)
  227. x = x + p.Round()
  228. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  229. drawer.Src = colorMeasure
  230. drawer.Face = smallLabelFace
  231. drawer.DrawString(labelMS)
  232. // jitter value
  233. drawer.Face = valueFace
  234. jitterValue := strings.Split(record.Jitter, ".")[0]
  235. p = drawer.MeasureString(jitterValue)
  236. x = canvasWidth*3/4 - (p.Round()+msLength.Round())/2
  237. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  238. drawer.Src = colorJitter
  239. drawer.DrawString(jitterValue)
  240. drawer.Face = smallLabelFace
  241. x = x + p.Round()
  242. drawer.Dot = freetype.Pt(x, canvasHeight*11/40)
  243. drawer.Src = colorMeasure
  244. drawer.DrawString(labelMS)
  245. // download value
  246. drawer.Face = valueFace
  247. p = drawer.MeasureString(record.Download)
  248. x = canvasWidth/4 - p.Round()/2
  249. drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
  250. drawer.Src = colorDownload
  251. drawer.DrawString(record.Download)
  252. // upload value
  253. p = drawer.MeasureString(record.Upload)
  254. x = canvasWidth*3/4 - p.Round()/2
  255. drawer.Dot = freetype.Pt(x, canvasHeight*27/40)
  256. drawer.Src = colorUpload
  257. drawer.DrawString(record.Upload)
  258. // watermark
  259. ctx := freetype.NewContext()
  260. ctx.SetFont(fontLight)
  261. ctx.SetFontSize(14)
  262. ctx.SetDPI(dpi)
  263. ctx.SetHinting(font.HintingFull)
  264. drawer.Face = watermarkFace
  265. drawer.Src = colorWatermark
  266. p = drawer.MeasureString(watermark)
  267. x = canvasWidth - p.Round() - 5
  268. drawer.Dot = freetype.Pt(x, canvasHeight-10)
  269. drawer.DrawString(watermark)
  270. // separator
  271. for i := canvas.Bounds().Min.X; i < canvas.Bounds().Max.X; i++ {
  272. canvas.Set(i, canvasHeight-ctx.PointToFixed(14).Round()-10, colorSeparator)
  273. }
  274. // ISP info
  275. drawer.Face = orgFace
  276. drawer.Src = colorISP
  277. drawer.Dot = freetype.Pt(6, canvasHeight-ctx.PointToFixed(14).Round()-15)
  278. removeRegexp := regexp.MustCompile(`AS\d+\s`)
  279. org := removeRegexp.ReplaceAllString(result.RawISPInfo.Organization, "")
  280. if result.RawISPInfo.Country != "" {
  281. org += ", " + result.RawISPInfo.Country
  282. }
  283. drawer.DrawString(org)
  284. w.Header().Set("Content-Disposition", "inline; filename="+uuid+".png")
  285. w.Header().Set("Content-Type", "image/png")
  286. if err := png.Encode(w, canvas); err != nil {
  287. log.Errorf("Failed to output image to HTTP client: %s", err)
  288. }
  289. }