main.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  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/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-.*[email protected]"), "Google Play"},
  46. {regexp.MustCompile("[email protected]"), "GitHub"},
  47. {regexp.MustCompile("[email protected]"), "APT"},
  48. {regexp.MustCompile("[email protected]"), "Docker Hub"},
  49. {regexp.MustCompile("[email protected]"), "GitHub"},
  50. {regexp.MustCompile("[email protected]"), "Snapcraft"},
  51. {regexp.MustCompile("android-.*vagrant@basebox-stretch64"), "F-Droid"},
  52. {regexp.MustCompile("builduser@(archlinux|svetlemodry)"), "Arch (3rd party)"},
  53. {regexp.MustCompile("[email protected]"), "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 := ioutil.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, r *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, _ := ioutil.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, r *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, r *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, r *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.Replace(natType, "unknown", "Unknown", -1)
  626. natType = strings.Replace(natType, "Symetric", "Symmetric", -1)
  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. add(featureGroups["Folder"]["v3"], "Conflicts", "Disabled", rep.FolderUsesV3.ConflictsDisabled)
  644. add(featureGroups["Folder"]["v3"], "Conflicts", "Unlimited", rep.FolderUsesV3.ConflictsUnlimited)
  645. add(featureGroups["Folder"]["v3"], "Conflicts", "Limited", rep.FolderUsesV3.ConflictsOther)
  646. for key, value := range rep.FolderUsesV3.PullOrder {
  647. add(featureGroups["Folder"]["v3"], "Pull Order", prettyCase(key), value)
  648. }
  649. totals["GUI"] += rep.GUIStats.Enabled
  650. inc(features["GUI"]["v3"], "Auth Enabled", rep.GUIStats.UseAuth)
  651. inc(features["GUI"]["v3"], "TLS Enabled", rep.GUIStats.UseTLS)
  652. inc(features["GUI"]["v3"], "Insecure Admin Access", rep.GUIStats.InsecureAdminAccess)
  653. inc(features["GUI"]["v3"], "Skip Host check", rep.GUIStats.InsecureSkipHostCheck)
  654. inc(features["GUI"]["v3"], "Allow Frame loading", rep.GUIStats.InsecureAllowFrameLoading)
  655. add(featureGroups["GUI"]["v3"], "Listen address", "Local", rep.GUIStats.ListenLocal)
  656. add(featureGroups["GUI"]["v3"], "Listen address", "Unspecified", rep.GUIStats.ListenUnspecified)
  657. add(featureGroups["GUI"]["v3"], "Listen address", "Other", rep.GUIStats.Enabled-rep.GUIStats.ListenLocal-rep.GUIStats.ListenUnspecified)
  658. for theme, count := range rep.GUIStats.Theme {
  659. add(featureGroups["GUI"]["v3"], "Theme", prettyCase(theme), count)
  660. }
  661. for transport, count := range rep.TransportStats {
  662. add(featureGroups["Connection"]["v3"], "Transport", strings.Title(transport), count)
  663. if strings.HasSuffix(transport, "4") {
  664. add(featureGroups["Connection"]["v3"], "IP version", "IPv4", count)
  665. } else if strings.HasSuffix(transport, "6") {
  666. add(featureGroups["Connection"]["v3"], "IP version", "IPv6", count)
  667. } else {
  668. add(featureGroups["Connection"]["v3"], "IP version", "Unknown", count)
  669. }
  670. }
  671. }
  672. }
  673. var categories []category
  674. categories = append(categories, category{
  675. Values: statsForInts(totFiles),
  676. Descr: "Files Managed per Device",
  677. })
  678. categories = append(categories, category{
  679. Values: statsForInts(maxFiles),
  680. Descr: "Files in Largest Folder",
  681. })
  682. categories = append(categories, category{
  683. Values: statsForInt64s(totMiB),
  684. Descr: "Data Managed per Device",
  685. Unit: "B",
  686. Type: NumberBinary,
  687. })
  688. categories = append(categories, category{
  689. Values: statsForInt64s(maxMiB),
  690. Descr: "Data in Largest Folder",
  691. Unit: "B",
  692. Type: NumberBinary,
  693. })
  694. categories = append(categories, category{
  695. Values: statsForInts(numDevices),
  696. Descr: "Number of Devices in Cluster",
  697. })
  698. categories = append(categories, category{
  699. Values: statsForInts(numFolders),
  700. Descr: "Number of Folders Configured",
  701. })
  702. categories = append(categories, category{
  703. Values: statsForInt64s(memoryUsage),
  704. Descr: "Memory Usage",
  705. Unit: "B",
  706. Type: NumberBinary,
  707. })
  708. categories = append(categories, category{
  709. Values: statsForInt64s(memorySize),
  710. Descr: "System Memory",
  711. Unit: "B",
  712. Type: NumberBinary,
  713. })
  714. categories = append(categories, category{
  715. Values: statsForFloats(sha256Perf),
  716. Descr: "SHA-256 Hashing Performance",
  717. Unit: "B/s",
  718. Type: NumberBinary,
  719. })
  720. categories = append(categories, category{
  721. Values: statsForInts(numCPU),
  722. Descr: "Number of CPU cores",
  723. })
  724. categories = append(categories, category{
  725. Values: statsForInts(uptime),
  726. Descr: "Uptime (v3)",
  727. Type: NumberDuration,
  728. })
  729. reportFeatures := make(map[string][]feature)
  730. for featureType, versions := range features {
  731. var featureList []feature
  732. for version, featureMap := range versions {
  733. // We count totals of the given feature type, for example number of
  734. // folders or devices, if that doesn't exist, we work out percentage
  735. // against the total of the version reports. Things like "Various"
  736. // never have counts.
  737. total, ok := totals[featureType]
  738. if !ok {
  739. total = reports[version]
  740. }
  741. for key, count := range featureMap {
  742. featureList = append(featureList, feature{
  743. Key: key,
  744. Version: version,
  745. Count: count,
  746. Pct: (100 * float64(count)) / float64(total),
  747. })
  748. }
  749. }
  750. sort.Sort(sort.Reverse(sortableFeatureList(featureList)))
  751. reportFeatures[featureType] = featureList
  752. }
  753. reportFeatureGroups := make(map[string][]featureGroup)
  754. for featureType, versions := range featureGroups {
  755. var featureList []featureGroup
  756. for version, featureMap := range versions {
  757. for key, counts := range featureMap {
  758. featureList = append(featureList, featureGroup{
  759. Key: key,
  760. Version: version,
  761. Counts: counts,
  762. })
  763. }
  764. }
  765. reportFeatureGroups[featureType] = featureList
  766. }
  767. var countryList []feature
  768. for country, count := range countries {
  769. countryList = append(countryList, feature{
  770. Key: country,
  771. Count: count,
  772. Pct: (100 * float64(count)) / float64(countriesTotal),
  773. })
  774. sort.Sort(sort.Reverse(sortableFeatureList(countryList)))
  775. }
  776. r := make(map[string]interface{})
  777. r["features"] = reportFeatures
  778. r["featureGroups"] = reportFeatureGroups
  779. r["nodes"] = nodes
  780. r["versionNodes"] = reports
  781. r["categories"] = categories
  782. r["versions"] = group(byVersion, analyticsFor(versions, 2000), 10)
  783. r["versionPenetrations"] = penetrationLevels(analyticsFor(versions, 2000), []float64{50, 75, 90, 95})
  784. r["platforms"] = group(byPlatform, analyticsFor(platforms, 2000), 10)
  785. r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 5)
  786. r["builders"] = analyticsFor(builders, 12)
  787. r["distributions"] = analyticsFor(distributions, len(knownDistributions))
  788. r["featureOrder"] = featureOrder
  789. r["locations"] = locations
  790. r["contries"] = countryList
  791. return r
  792. }
  793. var (
  794. plusRe = regexp.MustCompile(`(\+.*|\.dev\..*)$`)
  795. plusStr = "(+dev)"
  796. )
  797. // transformVersion returns a version number formatted correctly, with all
  798. // development versions aggregated into one.
  799. func transformVersion(v string) string {
  800. if v == "unknown-dev" {
  801. return v
  802. }
  803. if !strings.HasPrefix(v, "v") {
  804. v = "v" + v
  805. }
  806. v = plusRe.ReplaceAllString(v, " "+plusStr)
  807. return v
  808. }
  809. type summary struct {
  810. versions map[string]int // version string to count index
  811. max map[string]int // version string to max users per day
  812. rows map[string][]int // date to list of counts
  813. }
  814. func newSummary() summary {
  815. return summary{
  816. versions: make(map[string]int),
  817. max: make(map[string]int),
  818. rows: make(map[string][]int),
  819. }
  820. }
  821. func (s *summary) setCount(date, version string, count int) {
  822. idx, ok := s.versions[version]
  823. if !ok {
  824. idx = len(s.versions)
  825. s.versions[version] = idx
  826. }
  827. if s.max[version] < count {
  828. s.max[version] = count
  829. }
  830. row := s.rows[date]
  831. if len(row) <= idx {
  832. old := row
  833. row = make([]int, idx+1)
  834. copy(row, old)
  835. s.rows[date] = row
  836. }
  837. row[idx] = count
  838. }
  839. func (s *summary) MarshalJSON() ([]byte, error) {
  840. var versions []string
  841. for v := range s.versions {
  842. versions = append(versions, v)
  843. }
  844. sort.Strings(versions)
  845. var filtered []string
  846. for _, v := range versions {
  847. if s.max[v] > 50 {
  848. filtered = append(filtered, v)
  849. }
  850. }
  851. versions = filtered
  852. headerRow := []interface{}{"Day"}
  853. for _, v := range versions {
  854. headerRow = append(headerRow, v)
  855. }
  856. var table [][]interface{}
  857. table = append(table, headerRow)
  858. var dates []string
  859. for k := range s.rows {
  860. dates = append(dates, k)
  861. }
  862. sort.Strings(dates)
  863. for _, date := range dates {
  864. row := []interface{}{date}
  865. for _, ver := range versions {
  866. idx := s.versions[ver]
  867. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  868. row = append(row, s.rows[date][idx])
  869. } else {
  870. row = append(row, nil)
  871. }
  872. }
  873. table = append(table, row)
  874. }
  875. return json.Marshal(table)
  876. }
  877. // filter removes versions that never reach the specified min count.
  878. func (s *summary) filter(min int) {
  879. // We cheat and just remove the versions from the "index" and leave the
  880. // data points alone. The version index is used to build the table when
  881. // we do the serialization, so at that point the data points are
  882. // filtered out as well.
  883. for ver := range s.versions {
  884. if s.max[ver] < min {
  885. delete(s.versions, ver)
  886. delete(s.max, ver)
  887. }
  888. }
  889. }
  890. func getSummary(db *sql.DB, min int) (summary, error) {
  891. s := newSummary()
  892. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '2 year'::INTERVAL;`)
  893. if err != nil {
  894. return summary{}, err
  895. }
  896. defer rows.Close()
  897. for rows.Next() {
  898. var day time.Time
  899. var ver string
  900. var num int
  901. err := rows.Scan(&day, &ver, &num)
  902. if err != nil {
  903. return summary{}, err
  904. }
  905. if ver == "v0.0" {
  906. // ?
  907. continue
  908. }
  909. // SUPER UGLY HACK to avoid having to do sorting properly
  910. if len(ver) == 4 && strings.HasPrefix(ver, "v0.") { // v0.x
  911. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  912. }
  913. s.setCount(day.Format("2006-01-02"), ver, num)
  914. }
  915. s.filter(min)
  916. return s, nil
  917. }
  918. func getMovement(db *sql.DB) ([][]interface{}, error) {
  919. rows, err := db.Query(`SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '2 year'::INTERVAL ORDER BY Day`)
  920. if err != nil {
  921. return nil, err
  922. }
  923. defer rows.Close()
  924. res := [][]interface{}{
  925. {"Day", "Joined", "Left", "Bounced"},
  926. }
  927. for rows.Next() {
  928. var day time.Time
  929. var added, removed, bounced int
  930. err := rows.Scan(&day, &added, &removed, &bounced)
  931. if err != nil {
  932. return nil, err
  933. }
  934. row := []interface{}{day.Format("2006-01-02"), added, -removed, bounced}
  935. if removed == 0 {
  936. row[2] = nil
  937. }
  938. if bounced == 0 {
  939. row[3] = nil
  940. }
  941. res = append(res, row)
  942. }
  943. return res, nil
  944. }
  945. func getPerformance(db *sql.DB) ([][]interface{}, error) {
  946. rows, err := db.Query(`SELECT Day, TotFiles, TotMiB, SHA256Perf, MemorySize, MemoryUsageMiB FROM Performance WHERE Day > '2014-06-20'::TIMESTAMP ORDER BY Day`)
  947. if err != nil {
  948. return nil, err
  949. }
  950. defer rows.Close()
  951. res := [][]interface{}{
  952. {"Day", "TotFiles", "TotMiB", "SHA256Perf", "MemorySize", "MemoryUsageMiB"},
  953. }
  954. for rows.Next() {
  955. var day time.Time
  956. var sha256Perf float64
  957. var totFiles, totMiB, memorySize, memoryUsage int
  958. err := rows.Scan(&day, &totFiles, &totMiB, &sha256Perf, &memorySize, &memoryUsage)
  959. if err != nil {
  960. return nil, err
  961. }
  962. row := []interface{}{day.Format("2006-01-02"), totFiles, totMiB, float64(int(sha256Perf*10)) / 10, memorySize, memoryUsage}
  963. res = append(res, row)
  964. }
  965. return res, nil
  966. }
  967. func getBlockStats(db *sql.DB) ([][]interface{}, error) {
  968. rows, err := db.Query(`SELECT Day, Reports, Pulled, Renamed, Reused, CopyOrigin, CopyOriginShifted, CopyElsewhere FROM BlockStats WHERE Day > '2017-10-23'::TIMESTAMP ORDER BY Day`)
  969. if err != nil {
  970. return nil, err
  971. }
  972. defer rows.Close()
  973. res := [][]interface{}{
  974. {"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)"},
  975. }
  976. blocksToGb := float64(8 * 1024)
  977. for rows.Next() {
  978. var day time.Time
  979. var reports, pulled, renamed, reused, copyOrigin, copyOriginShifted, copyElsewhere float64
  980. err := rows.Scan(&day, &reports, &pulled, &renamed, &reused, &copyOrigin, &copyOriginShifted, &copyElsewhere)
  981. if err != nil {
  982. return nil, err
  983. }
  984. row := []interface{}{
  985. day.Format("2006-01-02"),
  986. reports,
  987. pulled / blocksToGb,
  988. renamed / blocksToGb,
  989. reused / blocksToGb,
  990. copyOrigin / blocksToGb,
  991. copyOriginShifted / blocksToGb,
  992. copyElsewhere / blocksToGb,
  993. }
  994. res = append(res, row)
  995. }
  996. return res, nil
  997. }
  998. type sortableFeatureList []feature
  999. func (l sortableFeatureList) Len() int {
  1000. return len(l)
  1001. }
  1002. func (l sortableFeatureList) Swap(a, b int) {
  1003. l[a], l[b] = l[b], l[a]
  1004. }
  1005. func (l sortableFeatureList) Less(a, b int) bool {
  1006. if l[a].Pct != l[b].Pct {
  1007. return l[a].Pct < l[b].Pct
  1008. }
  1009. return l[a].Key > l[b].Key
  1010. }
  1011. func prettyCase(input string) string {
  1012. output := ""
  1013. for i, runeValue := range input {
  1014. if i == 0 {
  1015. runeValue = unicode.ToUpper(runeValue)
  1016. } else if unicode.IsUpper(runeValue) {
  1017. output += " "
  1018. }
  1019. output += string(runeValue)
  1020. }
  1021. return output
  1022. }