main.go 34 KB

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