main.go 34 KB

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