serve.go 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135
  1. // Copyright (C) 2018 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package serve
  7. import (
  8. "bytes"
  9. "context"
  10. "database/sql"
  11. "embed"
  12. "encoding/json"
  13. "fmt"
  14. "html/template"
  15. "io"
  16. "log"
  17. "net"
  18. "net/http"
  19. "os"
  20. "regexp"
  21. "sort"
  22. "strconv"
  23. "strings"
  24. "sync"
  25. "time"
  26. "unicode"
  27. _ "github.com/lib/pq" // PostgreSQL driver
  28. "github.com/prometheus/client_golang/prometheus/promhttp"
  29. "golang.org/x/text/cases"
  30. "golang.org/x/text/language"
  31. "github.com/syncthing/syncthing/lib/geoip"
  32. "github.com/syncthing/syncthing/lib/upgrade"
  33. "github.com/syncthing/syncthing/lib/ur/contract"
  34. )
  35. type CLI struct {
  36. Debug bool `env:"UR_DEBUG"`
  37. DBConn string `env:"UR_DB_URL" default:"postgres://user:password@localhost/ur?sslmode=disable"`
  38. Listen string `env:"UR_LISTEN" default:"0.0.0.0:8080"`
  39. GeoIPLicenseKey string `env:"UR_GEOIP_LICENSE_KEY"`
  40. GeoIPAccountID int `env:"UR_GEOIP_ACCOUNT_ID"`
  41. }
  42. //go:embed static
  43. var statics embed.FS
  44. var (
  45. tpl *template.Template
  46. compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
  47. progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"}
  48. featureOrder = []string{"Various", "Folder", "Device", "Connection", "GUI"}
  49. knownVersions = []string{"v2", "v3"}
  50. knownDistributions = []distributionMatch{
  51. // Maps well known builders to the official distribution method that
  52. // they represent
  53. {regexp.MustCompile(`\steamcity@build\.syncthing\.net`), "GitHub"},
  54. {regexp.MustCompile(`\sjenkins@build\.syncthing\.net`), "GitHub"},
  55. {regexp.MustCompile(`\sbuilder@github\.syncthing\.net`), "GitHub"},
  56. {regexp.MustCompile(`\sdeb@build\.syncthing\.net`), "APT"},
  57. {regexp.MustCompile(`\sdebian@github\.syncthing\.net`), "APT"},
  58. {regexp.MustCompile(`\sdocker@syncthing\.net`), "Docker Hub"},
  59. {regexp.MustCompile(`\sdocker@build.syncthing\.net`), "Docker Hub"},
  60. {regexp.MustCompile(`\sdocker@github.syncthing\.net`), "Docker Hub"},
  61. {regexp.MustCompile(`\sandroid-builder@github\.syncthing\.net`), "Google Play"},
  62. {regexp.MustCompile(`\sandroid-.*teamcity@build\.syncthing\.net`), "Google Play"},
  63. {regexp.MustCompile(`\sandroid-.*vagrant@basebox-stretch64`), "F-Droid"},
  64. {regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"},
  65. {regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"},
  66. {regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"},
  67. {regexp.MustCompile(`@debian`), "Debian (3rd party)"},
  68. {regexp.MustCompile(`@fedora`), "Fedora (3rd party)"},
  69. {regexp.MustCompile(`\sbrew@`), "Homebrew (3rd party)"},
  70. {regexp.MustCompile(`\sroot@buildkitsandbox`), "LinuxServer.io (3rd party)"},
  71. {regexp.MustCompile(`\sports@freebsd`), "FreeBSD (3rd party)"},
  72. {regexp.MustCompile(`.`), "Others"},
  73. }
  74. )
  75. type distributionMatch struct {
  76. matcher *regexp.Regexp
  77. distribution string
  78. }
  79. var funcs = map[string]interface{}{
  80. "commatize": commatize,
  81. "number": number,
  82. "proportion": proportion,
  83. "counter": func() *counter {
  84. return &counter{}
  85. },
  86. "progressBarClassByIndex": func(a int) string {
  87. return progressBarClass[a%len(progressBarClass)]
  88. },
  89. "slice": func(numParts, whichPart int, input []feature) []feature {
  90. var part []feature
  91. perPart := (len(input) / numParts) + len(input)%2
  92. parts := make([][]feature, 0, numParts)
  93. for len(input) >= perPart {
  94. part, input = input[:perPart], input[perPart:]
  95. parts = append(parts, part)
  96. }
  97. if len(input) > 0 {
  98. parts = append(parts, input)
  99. }
  100. return parts[whichPart-1]
  101. },
  102. }
  103. func setupDB(db *sql.DB) error {
  104. _, err := db.Exec(`CREATE TABLE IF NOT EXISTS ReportsJson (
  105. Received TIMESTAMP NOT NULL,
  106. Report JSONB NOT NULL
  107. )`)
  108. if err != nil {
  109. return err
  110. }
  111. var t string
  112. if err := db.QueryRow(`SELECT 'UniqueIDJsonIndex'::regclass`).Scan(&t); err != nil {
  113. if _, err = db.Exec(`CREATE UNIQUE INDEX UniqueIDJsonIndex ON ReportsJson ((Report->>'date'), (Report->>'uniqueID'))`); err != nil {
  114. return err
  115. }
  116. }
  117. if err := db.QueryRow(`SELECT 'ReceivedJsonIndex'::regclass`).Scan(&t); err != nil {
  118. if _, err = db.Exec(`CREATE INDEX ReceivedJsonIndex ON ReportsJson (Received)`); err != nil {
  119. return err
  120. }
  121. }
  122. if err := db.QueryRow(`SELECT 'ReportVersionJsonIndex'::regclass`).Scan(&t); err != nil {
  123. if _, err = db.Exec(`CREATE INDEX ReportVersionJsonIndex ON ReportsJson (cast((Report->>'urVersion') as numeric))`); err != nil {
  124. return err
  125. }
  126. }
  127. return nil
  128. }
  129. func insertReport(db *sql.DB, r contract.Report) error {
  130. _, err := db.Exec("INSERT INTO ReportsJson (Report, Received) VALUES ($1, $2)", r, time.Now().UTC())
  131. return err
  132. }
  133. type withDBFunc func(*sql.DB, http.ResponseWriter, *http.Request)
  134. func withDB(db *sql.DB, f withDBFunc) http.HandlerFunc {
  135. return func(w http.ResponseWriter, r *http.Request) {
  136. f(db, w, r)
  137. }
  138. }
  139. func (cli *CLI) Run() error {
  140. // Template
  141. fd, err := statics.Open("static/index.html")
  142. if err != nil {
  143. log.Fatalln("template:", err)
  144. }
  145. bs, err := io.ReadAll(fd)
  146. if err != nil {
  147. log.Fatalln("template:", err)
  148. }
  149. fd.Close()
  150. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  151. // DB
  152. db, err := sql.Open("postgres", cli.DBConn)
  153. if err != nil {
  154. log.Fatalln("database:", err)
  155. }
  156. err = setupDB(db)
  157. if err != nil {
  158. log.Fatalln("database:", err)
  159. }
  160. // Listening
  161. listener, err := net.Listen("tcp", cli.Listen)
  162. if err != nil {
  163. log.Fatalln("listen:", err)
  164. }
  165. geoip, err := geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir())
  166. if err != nil {
  167. log.Fatalln("geoip:", err)
  168. }
  169. go geoip.Serve(context.TODO())
  170. srv := &server{
  171. db: db,
  172. debug: cli.Debug,
  173. geoip: geoip,
  174. }
  175. http.HandleFunc("/", srv.rootHandler)
  176. http.HandleFunc("/newdata", srv.newDataHandler)
  177. http.HandleFunc("/summary.json", srv.summaryHandler)
  178. http.HandleFunc("/performance.json", srv.performanceHandler)
  179. http.HandleFunc("/blockstats.json", srv.blockStatsHandler)
  180. http.HandleFunc("/locations.json", srv.locationsHandler)
  181. http.Handle("/metrics", promhttp.Handler())
  182. http.Handle("/static/", http.FileServer(http.FS(statics)))
  183. go srv.cacheRefresher()
  184. httpSrv := http.Server{
  185. ReadTimeout: 5 * time.Second,
  186. WriteTimeout: 15 * time.Second,
  187. }
  188. return httpSrv.Serve(listener)
  189. }
  190. type server struct {
  191. debug bool
  192. db *sql.DB
  193. geoip *geoip.Provider
  194. cacheMut sync.Mutex
  195. cachedIndex []byte
  196. cachedLocations []byte
  197. cacheTime time.Time
  198. }
  199. const maxCacheTime = 15 * time.Minute
  200. func (s *server) cacheRefresher() {
  201. ticker := time.NewTicker(maxCacheTime - time.Minute)
  202. defer ticker.Stop()
  203. for ; true; <-ticker.C {
  204. s.cacheMut.Lock()
  205. if err := s.refreshCacheLocked(); err != nil {
  206. log.Println(err)
  207. }
  208. s.cacheMut.Unlock()
  209. }
  210. }
  211. func (s *server) refreshCacheLocked() error {
  212. rep := getReport(s.db, s.geoip)
  213. buf := new(bytes.Buffer)
  214. err := tpl.Execute(buf, rep)
  215. if err != nil {
  216. return err
  217. }
  218. s.cachedIndex = buf.Bytes()
  219. s.cacheTime = time.Now()
  220. locs := rep["locations"].(map[location]int)
  221. wlocs := make([]weightedLocation, 0, len(locs))
  222. for loc, w := range locs {
  223. wlocs = append(wlocs, weightedLocation{loc, w})
  224. }
  225. s.cachedLocations, _ = json.Marshal(wlocs)
  226. return nil
  227. }
  228. func (s *server) rootHandler(w http.ResponseWriter, r *http.Request) {
  229. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  230. s.cacheMut.Lock()
  231. defer s.cacheMut.Unlock()
  232. if time.Since(s.cacheTime) > maxCacheTime {
  233. if err := s.refreshCacheLocked(); err != nil {
  234. log.Println(err)
  235. http.Error(w, "Template Error", http.StatusInternalServerError)
  236. return
  237. }
  238. }
  239. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  240. w.Write(s.cachedIndex)
  241. } else {
  242. http.Error(w, "Not found", 404)
  243. return
  244. }
  245. }
  246. func (s *server) locationsHandler(w http.ResponseWriter, _ *http.Request) {
  247. s.cacheMut.Lock()
  248. defer s.cacheMut.Unlock()
  249. if time.Since(s.cacheTime) > maxCacheTime {
  250. if err := s.refreshCacheLocked(); err != nil {
  251. log.Println(err)
  252. http.Error(w, "Template Error", http.StatusInternalServerError)
  253. return
  254. }
  255. }
  256. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  257. w.Write(s.cachedLocations)
  258. }
  259. func (s *server) newDataHandler(w http.ResponseWriter, r *http.Request) {
  260. version := "fail"
  261. defer func() {
  262. // Version is "fail", "duplicate", "v2", "v3", ...
  263. metricReportsTotal.WithLabelValues(version).Inc()
  264. }()
  265. defer r.Body.Close()
  266. addr := r.Header.Get("X-Forwarded-For")
  267. if addr != "" {
  268. addr = strings.Split(addr, ", ")[0]
  269. } else {
  270. addr = r.RemoteAddr
  271. }
  272. if host, _, err := net.SplitHostPort(addr); err == nil {
  273. addr = host
  274. }
  275. if net.ParseIP(addr) == nil {
  276. addr = ""
  277. }
  278. var rep contract.Report
  279. rep.Date = time.Now().UTC().Format("20060102")
  280. rep.Address = addr
  281. lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
  282. bs, _ := io.ReadAll(lr)
  283. if err := json.Unmarshal(bs, &rep); err != nil {
  284. log.Println("decode:", err)
  285. if s.debug {
  286. log.Printf("%s", bs)
  287. }
  288. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  289. return
  290. }
  291. if err := rep.Validate(); err != nil {
  292. log.Println("validate:", err)
  293. if s.debug {
  294. log.Printf("%#v", rep)
  295. }
  296. http.Error(w, "Validation Error", http.StatusInternalServerError)
  297. return
  298. }
  299. if err := insertReport(s.db, rep); err != nil {
  300. if err.Error() == `pq: duplicate key value violates unique constraint "uniqueidjsonindex"` {
  301. // We already have a report today for the same unique ID; drop
  302. // this one without complaining.
  303. version = "duplicate"
  304. return
  305. }
  306. log.Println("insert:", err)
  307. if s.debug {
  308. log.Printf("%#v", rep)
  309. }
  310. http.Error(w, "Database Error", http.StatusInternalServerError)
  311. return
  312. }
  313. version = fmt.Sprintf("v%d", rep.URVersion)
  314. }
  315. func (s *server) summaryHandler(w http.ResponseWriter, r *http.Request) {
  316. min, _ := strconv.Atoi(r.URL.Query().Get("min"))
  317. sum, err := getSummary(s.db, min)
  318. if err != nil {
  319. log.Println("summaryHandler:", err)
  320. http.Error(w, "Database Error", http.StatusInternalServerError)
  321. return
  322. }
  323. bs, err := sum.MarshalJSON()
  324. if err != nil {
  325. log.Println("summaryHandler:", err)
  326. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  327. return
  328. }
  329. w.Header().Set("Content-Type", "application/json")
  330. w.Write(bs)
  331. }
  332. func (s *server) performanceHandler(w http.ResponseWriter, _ *http.Request) {
  333. perf, err := getPerformance(s.db)
  334. if err != nil {
  335. log.Println("performanceHandler:", err)
  336. http.Error(w, "Database Error", http.StatusInternalServerError)
  337. return
  338. }
  339. bs, err := json.Marshal(perf)
  340. if err != nil {
  341. log.Println("performanceHandler:", err)
  342. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  343. return
  344. }
  345. w.Header().Set("Content-Type", "application/json")
  346. w.Write(bs)
  347. }
  348. func (s *server) blockStatsHandler(w http.ResponseWriter, _ *http.Request) {
  349. blocks, err := getBlockStats(s.db)
  350. if err != nil {
  351. log.Println("blockStatsHandler:", err)
  352. http.Error(w, "Database Error", http.StatusInternalServerError)
  353. return
  354. }
  355. bs, err := json.Marshal(blocks)
  356. if err != nil {
  357. log.Println("blockStatsHandler:", err)
  358. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  359. return
  360. }
  361. w.Header().Set("Content-Type", "application/json")
  362. w.Write(bs)
  363. }
  364. type category struct {
  365. Values [4]float64
  366. Key string
  367. Descr string
  368. Unit string
  369. Type NumberType
  370. }
  371. type feature struct {
  372. Key string
  373. Version string
  374. Count int
  375. Pct float64
  376. }
  377. type featureGroup struct {
  378. Key string
  379. Version string
  380. Counts map[string]int
  381. }
  382. // Used in the templates
  383. type counter struct {
  384. n int
  385. }
  386. func (c *counter) Current() int {
  387. return c.n
  388. }
  389. func (c *counter) Increment() string {
  390. c.n++
  391. return ""
  392. }
  393. func (c *counter) DrawTwoDivider() bool {
  394. return c.n != 0 && c.n%2 == 0
  395. }
  396. // add sets a key in a nested map, initializing things if needed as we go.
  397. func add(storage map[string]map[string]int, parent, child string, value int) {
  398. n, ok := storage[parent]
  399. if !ok {
  400. n = make(map[string]int)
  401. storage[parent] = n
  402. }
  403. n[child] += value
  404. }
  405. // inc makes sure that even for unused features, we initialize them in the
  406. // feature map. Furthermore, this acts as a helper that accepts booleans
  407. // to increment by one, or integers to increment by that integer.
  408. func inc(storage map[string]int, key string, i interface{}) {
  409. cv := storage[key]
  410. switch v := i.(type) {
  411. case bool:
  412. if v {
  413. cv++
  414. }
  415. case int:
  416. cv += v
  417. }
  418. storage[key] = cv
  419. }
  420. type location struct {
  421. Latitude float64 `json:"lat"`
  422. Longitude float64 `json:"lon"`
  423. }
  424. type weightedLocation struct {
  425. location
  426. Weight int `json:"weight"`
  427. }
  428. func getReport(db *sql.DB, geoip *geoip.Provider) map[string]interface{} {
  429. nodes := 0
  430. countriesTotal := 0
  431. var versions []string
  432. var platforms []string
  433. var numFolders []int
  434. var numDevices []int
  435. var totFiles []int
  436. var maxFiles []int
  437. var totMiB []int64
  438. var maxMiB []int64
  439. var memoryUsage []int64
  440. var sha256Perf []float64
  441. var memorySize []int64
  442. var uptime []int
  443. var compilers []string
  444. var builders []string
  445. var distributions []string
  446. locations := make(map[location]int)
  447. countries := make(map[string]int)
  448. reports := make(map[string]int)
  449. totals := make(map[string]int)
  450. // category -> version -> feature -> count
  451. features := make(map[string]map[string]map[string]int)
  452. // category -> version -> feature -> group -> count
  453. featureGroups := make(map[string]map[string]map[string]map[string]int)
  454. for _, category := range featureOrder {
  455. features[category] = make(map[string]map[string]int)
  456. featureGroups[category] = make(map[string]map[string]map[string]int)
  457. for _, version := range knownVersions {
  458. features[category][version] = make(map[string]int)
  459. featureGroups[category][version] = make(map[string]map[string]int)
  460. }
  461. }
  462. // Initialize some features that hide behind if conditions, and might not
  463. // be initialized.
  464. add(featureGroups["Various"]["v2"], "Upgrades", "Pre-release", 0)
  465. add(featureGroups["Various"]["v2"], "Upgrades", "Automatic", 0)
  466. add(featureGroups["Various"]["v2"], "Upgrades", "Manual", 0)
  467. add(featureGroups["Various"]["v2"], "Upgrades", "Disabled", 0)
  468. add(featureGroups["Various"]["v3"], "Temporary Retention", "Disabled", 0)
  469. add(featureGroups["Various"]["v3"], "Temporary Retention", "Custom", 0)
  470. add(featureGroups["Various"]["v3"], "Temporary Retention", "Default", 0)
  471. add(featureGroups["Connection"]["v3"], "IP version", "IPv4", 0)
  472. add(featureGroups["Connection"]["v3"], "IP version", "IPv6", 0)
  473. add(featureGroups["Connection"]["v3"], "IP version", "Unknown", 0)
  474. var numCPU []int
  475. var rep contract.Report
  476. rows, err := db.Query(`SELECT Received, Report FROM ReportsJson WHERE Received > now() - '1 day'::INTERVAL`)
  477. if err != nil {
  478. log.Println("sql:", err)
  479. return nil
  480. }
  481. defer rows.Close()
  482. for rows.Next() {
  483. err := rows.Scan(&rep.Received, &rep)
  484. if err != nil {
  485. log.Println("sql:", err)
  486. return nil
  487. }
  488. if geoip != nil && rep.Address != "" {
  489. if addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(rep.Address, "0")); err == nil {
  490. city, err := geoip.City(addr.IP)
  491. if err == nil {
  492. loc := location{
  493. Latitude: city.Location.Latitude,
  494. Longitude: city.Location.Longitude,
  495. }
  496. locations[loc]++
  497. countries[city.Country.Names["en"]]++
  498. countriesTotal++
  499. }
  500. }
  501. }
  502. nodes++
  503. versions = append(versions, transformVersion(rep.Version))
  504. platforms = append(platforms, rep.Platform)
  505. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  506. compilers = append(compilers, m[1])
  507. builders = append(builders, m[2])
  508. loop:
  509. for _, d := range knownDistributions {
  510. if d.matcher.MatchString(rep.LongVersion) {
  511. distributions = append(distributions, d.distribution)
  512. break loop
  513. }
  514. }
  515. }
  516. if rep.NumFolders > 0 {
  517. numFolders = append(numFolders, rep.NumFolders)
  518. }
  519. if rep.NumDevices > 0 {
  520. numDevices = append(numDevices, rep.NumDevices)
  521. }
  522. if rep.TotFiles > 0 {
  523. totFiles = append(totFiles, rep.TotFiles)
  524. }
  525. if rep.FolderMaxFiles > 0 {
  526. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  527. }
  528. if rep.TotMiB > 0 {
  529. totMiB = append(totMiB, int64(rep.TotMiB)*(1<<20))
  530. }
  531. if rep.FolderMaxMiB > 0 {
  532. maxMiB = append(maxMiB, int64(rep.FolderMaxMiB)*(1<<20))
  533. }
  534. if rep.MemoryUsageMiB > 0 {
  535. memoryUsage = append(memoryUsage, int64(rep.MemoryUsageMiB)*(1<<20))
  536. }
  537. if rep.SHA256Perf > 0 {
  538. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  539. }
  540. if rep.MemorySize > 0 {
  541. memorySize = append(memorySize, int64(rep.MemorySize)*(1<<20))
  542. }
  543. if rep.Uptime > 0 {
  544. uptime = append(uptime, rep.Uptime)
  545. }
  546. totals["Device"] += rep.NumDevices
  547. totals["Folder"] += rep.NumFolders
  548. if rep.URVersion >= 2 {
  549. reports["v2"]++
  550. numCPU = append(numCPU, rep.NumCPU)
  551. // Various
  552. inc(features["Various"]["v2"], "Rate limiting", rep.UsesRateLimit)
  553. if rep.UpgradeAllowedPre {
  554. add(featureGroups["Various"]["v2"], "Upgrades", "Pre-release", 1)
  555. } else if rep.UpgradeAllowedAuto {
  556. add(featureGroups["Various"]["v2"], "Upgrades", "Automatic", 1)
  557. } else if rep.UpgradeAllowedManual {
  558. add(featureGroups["Various"]["v2"], "Upgrades", "Manual", 1)
  559. } else {
  560. add(featureGroups["Various"]["v2"], "Upgrades", "Disabled", 1)
  561. }
  562. // Folders
  563. inc(features["Folder"]["v2"], "Automatic normalization", rep.FolderUses.AutoNormalize)
  564. inc(features["Folder"]["v2"], "Ignore deletes", rep.FolderUses.IgnoreDelete)
  565. inc(features["Folder"]["v2"], "Ignore permissions", rep.FolderUses.IgnorePerms)
  566. inc(features["Folder"]["v2"], "Mode, send only", rep.FolderUses.SendOnly)
  567. inc(features["Folder"]["v2"], "Mode, receive only", rep.FolderUses.ReceiveOnly)
  568. add(featureGroups["Folder"]["v2"], "Versioning", "Simple", rep.FolderUses.SimpleVersioning)
  569. add(featureGroups["Folder"]["v2"], "Versioning", "External", rep.FolderUses.ExternalVersioning)
  570. add(featureGroups["Folder"]["v2"], "Versioning", "Staggered", rep.FolderUses.StaggeredVersioning)
  571. add(featureGroups["Folder"]["v2"], "Versioning", "Trashcan", rep.FolderUses.TrashcanVersioning)
  572. add(featureGroups["Folder"]["v2"], "Versioning", "Disabled", rep.NumFolders-rep.FolderUses.SimpleVersioning-rep.FolderUses.ExternalVersioning-rep.FolderUses.StaggeredVersioning-rep.FolderUses.TrashcanVersioning)
  573. // Device
  574. inc(features["Device"]["v2"], "Custom certificate", rep.DeviceUses.CustomCertName)
  575. inc(features["Device"]["v2"], "Introducer", rep.DeviceUses.Introducer)
  576. add(featureGroups["Device"]["v2"], "Compress", "Always", rep.DeviceUses.CompressAlways)
  577. add(featureGroups["Device"]["v2"], "Compress", "Metadata", rep.DeviceUses.CompressMetadata)
  578. add(featureGroups["Device"]["v2"], "Compress", "Nothing", rep.DeviceUses.CompressNever)
  579. add(featureGroups["Device"]["v2"], "Addresses", "Dynamic", rep.DeviceUses.DynamicAddr)
  580. add(featureGroups["Device"]["v2"], "Addresses", "Static", rep.DeviceUses.StaticAddr)
  581. // Connections
  582. inc(features["Connection"]["v2"], "Relaying, enabled", rep.Relays.Enabled)
  583. inc(features["Connection"]["v2"], "Discovery, global enabled", rep.Announce.GlobalEnabled)
  584. inc(features["Connection"]["v2"], "Discovery, local enabled", rep.Announce.LocalEnabled)
  585. add(featureGroups["Connection"]["v2"], "Discovery", "Default servers (using DNS)", rep.Announce.DefaultServersDNS)
  586. add(featureGroups["Connection"]["v2"], "Relaying", "Default relays", rep.Relays.DefaultServers)
  587. add(featureGroups["Connection"]["v2"], "Relaying", "Other relays", rep.Relays.OtherServers)
  588. }
  589. if rep.URVersion >= 3 {
  590. reports["v3"]++
  591. inc(features["Various"]["v3"], "Custom LAN classification", rep.AlwaysLocalNets)
  592. inc(features["Various"]["v3"], "Ignore caching", rep.CacheIgnoredFiles)
  593. inc(features["Various"]["v3"], "Overwrite device names", rep.OverwriteRemoteDeviceNames)
  594. inc(features["Various"]["v3"], "Download progress disabled", !rep.ProgressEmitterEnabled)
  595. inc(features["Various"]["v3"], "Custom default path", rep.CustomDefaultFolderPath)
  596. inc(features["Various"]["v3"], "Custom traffic class", rep.CustomTrafficClass)
  597. inc(features["Various"]["v3"], "Custom temporary index threshold", rep.CustomTempIndexMinBlocks)
  598. inc(features["Various"]["v3"], "Weak hash enabled", rep.WeakHashEnabled)
  599. inc(features["Various"]["v3"], "LAN rate limiting", rep.LimitBandwidthInLan)
  600. inc(features["Various"]["v3"], "Custom release server", rep.CustomReleaseURL)
  601. inc(features["Various"]["v3"], "Restart after suspend", rep.RestartOnWakeup)
  602. inc(features["Various"]["v3"], "Custom stun servers", rep.CustomStunServers)
  603. inc(features["Various"]["v3"], "Ignore patterns", rep.IgnoreStats.Lines > 0)
  604. if rep.NATType != "" {
  605. natType := rep.NATType
  606. natType = strings.ReplaceAll(natType, "unknown", "Unknown")
  607. natType = strings.ReplaceAll(natType, "Symetric", "Symmetric")
  608. add(featureGroups["Various"]["v3"], "NAT Type", natType, 1)
  609. }
  610. if rep.TemporariesDisabled {
  611. add(featureGroups["Various"]["v3"], "Temporary Retention", "Disabled", 1)
  612. } else if rep.TemporariesCustom {
  613. add(featureGroups["Various"]["v3"], "Temporary Retention", "Custom", 1)
  614. } else {
  615. add(featureGroups["Various"]["v3"], "Temporary Retention", "Default", 1)
  616. }
  617. inc(features["Folder"]["v3"], "Scan progress disabled", rep.FolderUsesV3.ScanProgressDisabled)
  618. inc(features["Folder"]["v3"], "Disable sharing of partial files", rep.FolderUsesV3.DisableTempIndexes)
  619. inc(features["Folder"]["v3"], "Disable sparse files", rep.FolderUsesV3.DisableSparseFiles)
  620. inc(features["Folder"]["v3"], "Weak hash, always", rep.FolderUsesV3.AlwaysWeakHash)
  621. inc(features["Folder"]["v3"], "Weak hash, custom threshold", rep.FolderUsesV3.CustomWeakHashThreshold)
  622. inc(features["Folder"]["v3"], "Filesystem watcher", rep.FolderUsesV3.FsWatcherEnabled)
  623. inc(features["Folder"]["v3"], "Case sensitive FS", rep.FolderUsesV3.CaseSensitiveFS)
  624. inc(features["Folder"]["v3"], "Mode, receive encrypted", rep.FolderUsesV3.ReceiveEncrypted)
  625. add(featureGroups["Folder"]["v3"], "Conflicts", "Disabled", rep.FolderUsesV3.ConflictsDisabled)
  626. add(featureGroups["Folder"]["v3"], "Conflicts", "Unlimited", rep.FolderUsesV3.ConflictsUnlimited)
  627. add(featureGroups["Folder"]["v3"], "Conflicts", "Limited", rep.FolderUsesV3.ConflictsOther)
  628. for key, value := range rep.FolderUsesV3.PullOrder {
  629. add(featureGroups["Folder"]["v3"], "Pull Order", prettyCase(key), value)
  630. }
  631. for key, value := range rep.FolderUsesV3.CopyRangeMethod {
  632. add(featureGroups["Folder"]["v3"], "Copy Range Method", prettyCase(key), value)
  633. }
  634. inc(features["Device"]["v3"], "Untrusted", rep.DeviceUsesV3.Untrusted)
  635. totals["GUI"] += rep.GUIStats.Enabled
  636. inc(features["GUI"]["v3"], "Auth Enabled", rep.GUIStats.UseAuth)
  637. inc(features["GUI"]["v3"], "TLS Enabled", rep.GUIStats.UseTLS)
  638. inc(features["GUI"]["v3"], "Insecure Admin Access", rep.GUIStats.InsecureAdminAccess)
  639. inc(features["GUI"]["v3"], "Skip Host check", rep.GUIStats.InsecureSkipHostCheck)
  640. inc(features["GUI"]["v3"], "Allow Frame loading", rep.GUIStats.InsecureAllowFrameLoading)
  641. add(featureGroups["GUI"]["v3"], "Listen address", "Local", rep.GUIStats.ListenLocal)
  642. add(featureGroups["GUI"]["v3"], "Listen address", "Unspecified", rep.GUIStats.ListenUnspecified)
  643. add(featureGroups["GUI"]["v3"], "Listen address", "Other", rep.GUIStats.Enabled-rep.GUIStats.ListenLocal-rep.GUIStats.ListenUnspecified)
  644. for theme, count := range rep.GUIStats.Theme {
  645. add(featureGroups["GUI"]["v3"], "Theme", prettyCase(theme), count)
  646. }
  647. for transport, count := range rep.TransportStats {
  648. add(featureGroups["Connection"]["v3"], "Transport", cases.Title(language.English).String(transport), count)
  649. if strings.HasSuffix(transport, "4") {
  650. add(featureGroups["Connection"]["v3"], "IP version", "IPv4", count)
  651. } else if strings.HasSuffix(transport, "6") {
  652. add(featureGroups["Connection"]["v3"], "IP version", "IPv6", count)
  653. } else {
  654. add(featureGroups["Connection"]["v3"], "IP version", "Unknown", count)
  655. }
  656. }
  657. }
  658. }
  659. categories := []category{
  660. {
  661. Values: statsForInts(totFiles),
  662. Descr: "Files Managed per Device",
  663. }, {
  664. Values: statsForInts(maxFiles),
  665. Descr: "Files in Largest Folder",
  666. }, {
  667. Values: statsForInt64s(totMiB),
  668. Descr: "Data Managed per Device",
  669. Unit: "B",
  670. Type: NumberBinary,
  671. }, {
  672. Values: statsForInt64s(maxMiB),
  673. Descr: "Data in Largest Folder",
  674. Unit: "B",
  675. Type: NumberBinary,
  676. }, {
  677. Values: statsForInts(numDevices),
  678. Descr: "Number of Devices in Cluster",
  679. }, {
  680. Values: statsForInts(numFolders),
  681. Descr: "Number of Folders Configured",
  682. }, {
  683. Values: statsForInt64s(memoryUsage),
  684. Descr: "Memory Usage",
  685. Unit: "B",
  686. Type: NumberBinary,
  687. }, {
  688. Values: statsForInt64s(memorySize),
  689. Descr: "System Memory",
  690. Unit: "B",
  691. Type: NumberBinary,
  692. }, {
  693. Values: statsForFloats(sha256Perf),
  694. Descr: "SHA-256 Hashing Performance",
  695. Unit: "B/s",
  696. Type: NumberBinary,
  697. }, {
  698. Values: statsForInts(numCPU),
  699. Descr: "Number of CPU cores",
  700. }, {
  701. Values: statsForInts(uptime),
  702. Descr: "Uptime (v3)",
  703. Type: NumberDuration,
  704. },
  705. }
  706. reportFeatures := make(map[string][]feature)
  707. for featureType, versions := range features {
  708. var featureList []feature
  709. for version, featureMap := range versions {
  710. // We count totals of the given feature type, for example number of
  711. // folders or devices, if that doesn't exist, we work out percentage
  712. // against the total of the version reports. Things like "Various"
  713. // never have counts.
  714. total, ok := totals[featureType]
  715. if !ok {
  716. total = reports[version]
  717. }
  718. for key, count := range featureMap {
  719. featureList = append(featureList, feature{
  720. Key: key,
  721. Version: version,
  722. Count: count,
  723. Pct: (100 * float64(count)) / float64(total),
  724. })
  725. }
  726. }
  727. sort.Sort(sort.Reverse(sortableFeatureList(featureList)))
  728. reportFeatures[featureType] = featureList
  729. }
  730. reportFeatureGroups := make(map[string][]featureGroup)
  731. for featureType, versions := range featureGroups {
  732. var featureList []featureGroup
  733. for version, featureMap := range versions {
  734. for key, counts := range featureMap {
  735. featureList = append(featureList, featureGroup{
  736. Key: key,
  737. Version: version,
  738. Counts: counts,
  739. })
  740. }
  741. }
  742. reportFeatureGroups[featureType] = featureList
  743. }
  744. var countryList []feature
  745. for country, count := range countries {
  746. countryList = append(countryList, feature{
  747. Key: country,
  748. Count: count,
  749. Pct: (100 * float64(count)) / float64(countriesTotal),
  750. })
  751. sort.Sort(sort.Reverse(sortableFeatureList(countryList)))
  752. }
  753. r := make(map[string]interface{})
  754. r["features"] = reportFeatures
  755. r["featureGroups"] = reportFeatureGroups
  756. r["nodes"] = nodes
  757. r["versionNodes"] = reports
  758. r["categories"] = categories
  759. r["versions"] = group(byVersion, analyticsFor(versions, 2000), 5, 1.0)
  760. r["versionPenetrations"] = penetrationLevels(analyticsFor(versions, 2000), []float64{50, 75, 90, 95})
  761. r["platforms"] = group(byPlatform, analyticsFor(platforms, 2000), 10, 0.0)
  762. r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 5, 1.0)
  763. r["builders"] = analyticsFor(builders, 12)
  764. r["distributions"] = analyticsFor(distributions, len(knownDistributions))
  765. r["featureOrder"] = featureOrder
  766. r["locations"] = locations
  767. r["countries"] = countryList
  768. return r
  769. }
  770. var (
  771. plusRe = regexp.MustCompile(`(\+.*|\.dev\..*)$`)
  772. plusStr = "(+dev)"
  773. )
  774. // transformVersion returns a version number formatted correctly, with all
  775. // development versions aggregated into one.
  776. func transformVersion(v string) string {
  777. if v == "unknown-dev" {
  778. return v
  779. }
  780. if !strings.HasPrefix(v, "v") {
  781. v = "v" + v
  782. }
  783. v = plusRe.ReplaceAllString(v, " "+plusStr)
  784. return v
  785. }
  786. type summary struct {
  787. versions map[string]int // version string to count index
  788. max map[string]int // version string to max users per day
  789. rows map[string][]int // date to list of counts
  790. }
  791. func newSummary() summary {
  792. return summary{
  793. versions: make(map[string]int),
  794. max: make(map[string]int),
  795. rows: make(map[string][]int),
  796. }
  797. }
  798. func (s *summary) setCount(date, version string, count int) {
  799. idx, ok := s.versions[version]
  800. if !ok {
  801. idx = len(s.versions)
  802. s.versions[version] = idx
  803. }
  804. if s.max[version] < count {
  805. s.max[version] = count
  806. }
  807. row := s.rows[date]
  808. if len(row) <= idx {
  809. old := row
  810. row = make([]int, idx+1)
  811. copy(row, old)
  812. s.rows[date] = row
  813. }
  814. row[idx] = count
  815. }
  816. func (s *summary) MarshalJSON() ([]byte, error) {
  817. var versions []string
  818. for v := range s.versions {
  819. versions = append(versions, v)
  820. }
  821. sort.Slice(versions, func(a, b int) bool {
  822. return upgrade.CompareVersions(versions[a], versions[b]) < 0
  823. })
  824. var filtered []string
  825. for _, v := range versions {
  826. if s.max[v] > 50 {
  827. filtered = append(filtered, v)
  828. }
  829. }
  830. versions = filtered
  831. headerRow := []interface{}{"Day"}
  832. for _, v := range versions {
  833. headerRow = append(headerRow, v)
  834. }
  835. var table [][]interface{}
  836. table = append(table, headerRow)
  837. var dates []string
  838. for k := range s.rows {
  839. dates = append(dates, k)
  840. }
  841. sort.Strings(dates)
  842. for _, date := range dates {
  843. row := []interface{}{date}
  844. for _, ver := range versions {
  845. idx := s.versions[ver]
  846. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  847. row = append(row, s.rows[date][idx])
  848. } else {
  849. row = append(row, nil)
  850. }
  851. }
  852. table = append(table, row)
  853. }
  854. return json.Marshal(table)
  855. }
  856. // filter removes versions that never reach the specified min count.
  857. func (s *summary) filter(min int) {
  858. // We cheat and just remove the versions from the "index" and leave the
  859. // data points alone. The version index is used to build the table when
  860. // we do the serialization, so at that point the data points are
  861. // filtered out as well.
  862. for ver := range s.versions {
  863. if s.max[ver] < min {
  864. delete(s.versions, ver)
  865. delete(s.max, ver)
  866. }
  867. }
  868. }
  869. func getSummary(db *sql.DB, min int) (summary, error) {
  870. s := newSummary()
  871. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '3 year'::INTERVAL;`)
  872. if err != nil {
  873. return summary{}, err
  874. }
  875. defer rows.Close()
  876. for rows.Next() {
  877. var day time.Time
  878. var ver string
  879. var num int
  880. err := rows.Scan(&day, &ver, &num)
  881. if err != nil {
  882. return summary{}, err
  883. }
  884. if ver == "v0.0" {
  885. // ?
  886. continue
  887. }
  888. // SUPER UGLY HACK to avoid having to do sorting properly
  889. if len(ver) == 4 && strings.HasPrefix(ver, "v0.") { // v0.x
  890. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  891. }
  892. s.setCount(day.Format(time.DateOnly), ver, num)
  893. }
  894. s.filter(min)
  895. return s, nil
  896. }
  897. func getPerformance(db *sql.DB) ([][]interface{}, error) {
  898. rows, err := db.Query(`SELECT Day, TotFiles, TotMiB, SHA256Perf, MemorySize, MemoryUsageMiB FROM Performance WHERE Day > now() - '5 year'::INTERVAL ORDER BY Day`)
  899. if err != nil {
  900. return nil, err
  901. }
  902. defer rows.Close()
  903. res := [][]interface{}{
  904. {"Day", "TotFiles", "TotMiB", "SHA256Perf", "MemorySize", "MemoryUsageMiB"},
  905. }
  906. for rows.Next() {
  907. var day time.Time
  908. var sha256Perf float64
  909. var totFiles, totMiB, memorySize, memoryUsage int
  910. err := rows.Scan(&day, &totFiles, &totMiB, &sha256Perf, &memorySize, &memoryUsage)
  911. if err != nil {
  912. return nil, err
  913. }
  914. row := []interface{}{day.Format(time.DateOnly), totFiles, totMiB, float64(int(sha256Perf*10)) / 10, memorySize, memoryUsage}
  915. res = append(res, row)
  916. }
  917. return res, nil
  918. }
  919. func getBlockStats(db *sql.DB) ([][]interface{}, error) {
  920. rows, err := db.Query(`SELECT Day, Reports, Pulled, Renamed, Reused, CopyOrigin, CopyOriginShifted, CopyElsewhere FROM BlockStats WHERE Day > now() - '3 year'::INTERVAL ORDER BY Day`)
  921. if err != nil {
  922. return nil, err
  923. }
  924. defer rows.Close()
  925. res := [][]interface{}{
  926. {"Day", "Number of Reports", "Transferred (GiB)", "Saved by renaming files (GiB)", "Saved by resuming transfer (GiB)", "Saved by reusing data from old file (GiB)", "Saved by reusing shifted data from old file (GiB)", "Saved by reusing data from other files (GiB)"},
  927. }
  928. blocksToGb := float64(8 * 1024)
  929. for rows.Next() {
  930. var day time.Time
  931. var reports, pulled, renamed, reused, copyOrigin, copyOriginShifted, copyElsewhere float64
  932. err := rows.Scan(&day, &reports, &pulled, &renamed, &reused, &copyOrigin, &copyOriginShifted, &copyElsewhere)
  933. if err != nil {
  934. return nil, err
  935. }
  936. // Legacy bad data on certain days
  937. if reports <= 0 || pulled < 0 || renamed < 0 || reused < 0 || copyOrigin < 0 || copyOriginShifted < 0 || copyElsewhere < 0 {
  938. continue
  939. }
  940. row := []interface{}{
  941. day.Format(time.DateOnly),
  942. reports,
  943. pulled / blocksToGb,
  944. renamed / blocksToGb,
  945. reused / blocksToGb,
  946. copyOrigin / blocksToGb,
  947. copyOriginShifted / blocksToGb,
  948. copyElsewhere / blocksToGb,
  949. }
  950. res = append(res, row)
  951. }
  952. return res, nil
  953. }
  954. type sortableFeatureList []feature
  955. func (l sortableFeatureList) Len() int {
  956. return len(l)
  957. }
  958. func (l sortableFeatureList) Swap(a, b int) {
  959. l[a], l[b] = l[b], l[a]
  960. }
  961. func (l sortableFeatureList) Less(a, b int) bool {
  962. if l[a].Pct != l[b].Pct {
  963. return l[a].Pct < l[b].Pct
  964. }
  965. return l[a].Key > l[b].Key
  966. }
  967. func prettyCase(input string) string {
  968. output := ""
  969. for i, runeValue := range input {
  970. if i == 0 {
  971. runeValue = unicode.ToUpper(runeValue)
  972. } else if unicode.IsUpper(runeValue) {
  973. output += " "
  974. }
  975. output += string(runeValue)
  976. }
  977. return output
  978. }