main.go 34 KB

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