2014-06-11 23:40:54 +00:00
package main
import (
2015-03-04 12:02:53 +00:00
"bytes"
2014-10-13 09:18:13 +00:00
"crypto/tls"
2015-03-04 10:31:46 +00:00
"database/sql"
2014-06-11 23:40:54 +00:00
"encoding/json"
"fmt"
2014-06-28 07:46:03 +00:00
"html/template"
2014-06-11 23:40:54 +00:00
"io"
2014-06-28 07:46:03 +00:00
"io/ioutil"
2014-06-11 23:40:54 +00:00
"log"
"net/http"
"os"
2014-06-28 07:46:03 +00:00
"regexp"
2015-05-21 06:52:19 +00:00
"sort"
2014-06-28 07:46:03 +00:00
"strings"
2014-06-11 23:40:54 +00:00
"sync"
"time"
2015-03-04 10:31:46 +00:00
_ "github.com/lib/pq"
2014-06-11 23:40:54 +00:00
)
var (
2016-05-30 07:52:38 +00:00
keyFile = getEnvDefault ( "UR_KEY_FILE" , "key.pem" )
certFile = getEnvDefault ( "UR_CRT_FILE" , "crt.pem" )
dbConn = getEnvDefault ( "UR_DB_URL" , "postgres://user:password@localhost/ur?sslmode=disable" )
listenAddr = getEnvDefault ( "UR_LISTEN" , "0.0.0.0:8443" )
tpl * template . Template
compilerRe = regexp . MustCompile ( ` \(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+) ` )
2014-06-11 23:40:54 +00:00
)
2014-06-28 07:46:03 +00:00
var funcs = map [ string ] interface { } {
"commatize" : commatize ,
2014-06-28 09:24:25 +00:00
"number" : number ,
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
func getEnvDefault ( key , def string ) string {
if val := os . Getenv ( key ) ; val != "" {
return val
}
return def
}
type report struct {
Received time . Time // Only from DB
UniqueID string
Version string
LongVersion string
Platform string
NumFolders int
NumDevices int
TotFiles int
FolderMaxFiles int
TotMiB int
FolderMaxMiB int
MemoryUsageMiB int
SHA256Perf float64
MemorySize int
2015-09-10 12:03:34 +00:00
// v2 fields
URVersion int
NumCPU int
FolderUses struct {
2016-02-15 11:50:00 +00:00
ReadOnly int
IgnorePerms int
IgnoreDelete int
AutoNormalize int
SimpleVersioning int
ExternalVersioning int
StaggeredVersioning int
TrashcanVersioning int
2015-09-10 12:03:34 +00:00
}
DeviceUses struct {
Introducer int
CustomCertName int
CompressAlways int
CompressMetadata int
CompressNever int
DynamicAddr int
StaticAddr int
}
Announce struct {
GlobalEnabled bool
LocalEnabled bool
DefaultServersDNS int
DefaultServersIP int
OtherServers int
}
Relays struct {
Enabled bool
DefaultServers int
OtherServers int
}
UsesRateLimit bool
UpgradeAllowedManual bool
UpgradeAllowedAuto bool
2017-10-14 17:58:13 +00:00
// v3 fields
Uptime int
NATType string
BlockStats struct {
Total int
Renamed int
Reused int
Pulled int
CopyOrigin int
CopyOriginShifted int
CopyElsewhere int
}
TransportStats struct {
TCP int
Relay int
KCP int
}
IgnoreStats struct {
Lines int
Inverts int
Folded int
Deletable int
Rooted int
Includes int
EscapedIncludes int
DoubleStars int
Stars int
}
2015-09-10 12:03:34 +00:00
// Generated
2015-03-04 10:31:46 +00:00
Date string
}
func ( r * report ) Validate ( ) error {
if r . UniqueID == "" || r . Version == "" || r . Platform == "" {
return fmt . Errorf ( "missing required field" )
}
if len ( r . Date ) != 8 {
return fmt . Errorf ( "date not initialized" )
}
return nil
}
2015-09-10 14:11:36 +00:00
func ( r * report ) FieldPointers ( ) [ ] interface { } {
// All the fields of the report, in the same order as the database fields.
return [ ] interface { } {
& r . Received , & r . UniqueID , & r . Version , & r . LongVersion , & r . Platform ,
& r . NumFolders , & r . NumDevices , & r . TotFiles , & r . FolderMaxFiles ,
& r . TotMiB , & r . FolderMaxMiB , & r . MemoryUsageMiB , & r . SHA256Perf ,
2017-10-14 17:58:13 +00:00
& r . MemorySize , & r . Date ,
// V2
& r . URVersion , & r . NumCPU , & r . FolderUses . ReadOnly , & r . FolderUses . IgnorePerms ,
& r . FolderUses . IgnoreDelete , & r . FolderUses . AutoNormalize , & r . DeviceUses . Introducer ,
2015-09-10 14:11:36 +00:00
& r . DeviceUses . CustomCertName , & r . DeviceUses . CompressAlways ,
& r . DeviceUses . CompressMetadata , & r . DeviceUses . CompressNever ,
& r . DeviceUses . DynamicAddr , & r . DeviceUses . StaticAddr ,
& r . Announce . GlobalEnabled , & r . Announce . LocalEnabled ,
& r . Announce . DefaultServersDNS , & r . Announce . DefaultServersIP ,
& r . Announce . OtherServers , & r . Relays . Enabled , & r . Relays . DefaultServers ,
& r . Relays . OtherServers , & r . UsesRateLimit , & r . UpgradeAllowedManual ,
2017-10-14 17:58:13 +00:00
& r . UpgradeAllowedAuto , & r . FolderUses . SimpleVersioning ,
& r . FolderUses . ExternalVersioning , & r . FolderUses . StaggeredVersioning ,
& r . FolderUses . TrashcanVersioning ,
// V3
& r . Uptime , & r . NATType , & r . BlockStats . Total , & r . BlockStats . Renamed ,
& r . BlockStats . Reused , & r . BlockStats . Pulled , & r . BlockStats . CopyOrigin ,
& r . BlockStats . CopyOriginShifted , & r . BlockStats . CopyElsewhere ,
& r . TransportStats . TCP , & r . TransportStats . Relay , & r . TransportStats . KCP ,
& r . IgnoreStats . Lines , & r . IgnoreStats . Inverts , & r . IgnoreStats . Folded ,
& r . IgnoreStats . Deletable , & r . IgnoreStats . Rooted , & r . IgnoreStats . Includes ,
& r . IgnoreStats . EscapedIncludes , & r . IgnoreStats . DoubleStars , & r . IgnoreStats . Stars ,
2016-02-15 11:50:00 +00:00
}
2015-09-10 14:11:36 +00:00
}
func ( r * report ) FieldNames ( ) [ ] string {
// The database fields that back this struct in PostgreSQL
return [ ] string {
// V1
"Received" ,
"UniqueID" ,
"Version" ,
"LongVersion" ,
"Platform" ,
"NumFolders" ,
"NumDevices" ,
"TotFiles" ,
"FolderMaxFiles" ,
"TotMiB" ,
"FolderMaxMiB" ,
"MemoryUsageMiB" ,
"SHA256Perf" ,
"MemorySize" ,
"Date" ,
// V2
"ReportVersion" ,
"NumCPU" ,
"FolderRO" ,
"FolderIgnorePerms" ,
"FolderIgnoreDelete" ,
"FolderAutoNormalize" ,
"DeviceIntroducer" ,
"DeviceCustomCertName" ,
"DeviceCompressAlways" ,
"DeviceCompressMetadata" ,
"DeviceCompressNever" ,
"DeviceDynamicAddr" ,
"DeviceStaticAddr" ,
"AnnounceGlobalEnabled" ,
"AnnounceLocalEnabled" ,
"AnnounceDefaultServersDNS" ,
"AnnounceDefaultServersIP" ,
"AnnounceOtherServers" ,
"RelayEnabled" ,
"RelayDefaultServers" ,
"RelayOtherServers" ,
"RateLimitEnabled" ,
"UpgradeAllowedManual" ,
"UpgradeAllowedAuto" ,
2016-02-15 11:50:00 +00:00
// v0.12.19+
"FolderSimpleVersioning" ,
"FolderExternalVersioning" ,
"FolderStaggeredVersioning" ,
"FolderTrashcanVersioning" ,
2017-10-14 17:58:13 +00:00
// V3
"Uptime" ,
"NATType" ,
"BlocksTotal" ,
"BlocksRenamed" ,
"BlocksReused" ,
"BlocksPulled" ,
"BlocksCopyOrigin" ,
"BlocksCopyOriginShifted" ,
"BlocksCopyElsewhere" ,
"TransportTCP" ,
"TransportRelay" ,
"TransportKCP" ,
"IgnoreLines" ,
"IgnoreInverts" ,
"IgnoreFolded" ,
"IgnoreDeletable" ,
"IgnoreRooted" ,
"IgnoreIncludes" ,
"IgnoreEscapedIncludes" ,
"IgnoreDoubleStars" ,
"IgnoreStars" ,
2015-09-10 14:11:36 +00:00
}
2015-09-10 12:03:34 +00:00
}
2015-03-04 10:31:46 +00:00
func setupDB ( db * sql . DB ) error {
_ , err := db . Exec ( ` CREATE TABLE IF NOT EXISTS Reports (
Received TIMESTAMP NOT NULL ,
UniqueID VARCHAR ( 32 ) NOT NULL ,
Version VARCHAR ( 32 ) NOT NULL ,
LongVersion VARCHAR ( 256 ) NOT NULL ,
Platform VARCHAR ( 32 ) NOT NULL ,
NumFolders INTEGER NOT NULL ,
NumDevices INTEGER NOT NULL ,
TotFiles INTEGER NOT NULL ,
FolderMaxFiles INTEGER NOT NULL ,
TotMiB INTEGER NOT NULL ,
FolderMaxMiB INTEGER NOT NULL ,
MemoryUsageMiB INTEGER NOT NULL ,
SHA256Perf DOUBLE PRECISION NOT NULL ,
MemorySize INTEGER NOT NULL ,
Date VARCHAR ( 8 ) NOT NULL
) ` )
if err != nil {
return err
}
2015-09-10 12:03:34 +00:00
var t string
2015-03-04 10:31:46 +00:00
row := db . QueryRow ( ` SELECT 'UniqueIDIndex'::regclass ` )
2015-09-10 12:03:34 +00:00
if err := row . Scan ( & t ) ; err != nil {
if _ , err = db . Exec ( ` CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID) ` ) ; err != nil {
return err
}
2015-03-04 10:31:46 +00:00
}
row = db . QueryRow ( ` SELECT 'ReceivedIndex'::regclass ` )
2015-09-10 12:03:34 +00:00
if err := row . Scan ( & t ) ; err != nil {
if _ , err = db . Exec ( ` CREATE INDEX ReceivedIndex ON Reports (Received) ` ) ; err != nil {
return err
}
2015-03-04 10:31:46 +00:00
}
2017-10-14 17:58:13 +00:00
// V2
2015-09-10 12:03:34 +00:00
row = db . QueryRow ( ` SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'reportversion' ` )
if err := row . Scan ( & t ) ; err != nil {
// The ReportVersion column doesn't exist; add the new columns.
_ , err = db . Exec ( ` ALTER TABLE Reports
ADD COLUMN ReportVersion INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN NumCPU INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderRO INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderIgnorePerms INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderIgnoreDelete INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderAutoNormalize INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceIntroducer INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCustomCertName INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressAlways INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressMetadata INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressNever INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceDynamicAddr INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceStaticAddr INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceGlobalEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN AnnounceLocalEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN AnnounceDefaultServersDNS INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceDefaultServersIP INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceOtherServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RelayEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN RelayDefaultServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RelayOtherServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RateLimitEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN UpgradeAllowedManual BOOLEAN NOT NULL DEFAULT FALSE ,
2016-02-15 11:50:00 +00:00
ADD COLUMN UpgradeAllowedAuto BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN FolderSimpleVersioning INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderExternalVersioning INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderStaggeredVersioning INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderTrashcanVersioning INTEGER NOT NULL DEFAULT 0
2015-09-10 12:03:34 +00:00
` )
if err != nil {
return err
}
}
row = db . QueryRow ( ` SELECT 'ReportVersionIndex'::regclass ` )
if err := row . Scan ( & t ) ; err != nil {
if _ , err = db . Exec ( ` CREATE INDEX ReportVersionIndex ON Reports (ReportVersion) ` ) ; err != nil {
return err
}
}
2017-10-14 17:58:13 +00:00
// V3
row = db . QueryRow ( ` SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'uptime' ` )
if err := row . Scan ( & t ) ; err != nil {
// The Uptime column doesn't exist; add the new columns.
_ , err = db . Exec ( ` ALTER TABLE Reports
ADD COLUMN Uptime INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN NATType VARCHAR ( 32 ) NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksTotal INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksRenamed INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksReused INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksPulled INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksCopyOrigin INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksCopyOriginShifted INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN BlocksCopyElsewhere INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN TransportTCP INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN TransportRelay INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN TransportKCP INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreLines INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreInverts INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreFolded INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreDeletable INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreRooted INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreIncludes INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreEscapedIncludes INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreDoubleStars INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN IgnoreStars INTEGER NOT NULL DEFAULT 0
` )
if err != nil {
return err
}
}
2015-09-10 12:03:34 +00:00
return nil
2015-03-04 10:31:46 +00:00
}
func insertReport ( db * sql . DB , r report ) error {
2015-09-10 14:11:36 +00:00
r . Received = time . Now ( ) . UTC ( )
fields := r . FieldPointers ( )
2015-09-10 12:03:34 +00:00
params := make ( [ ] string , len ( fields ) )
for i := range params {
params [ i ] = fmt . Sprintf ( "$%d" , i + 1 )
}
2015-09-10 14:11:36 +00:00
query := "INSERT INTO Reports (" + strings . Join ( r . FieldNames ( ) , ", " ) + ") VALUES (" + strings . Join ( params , ", " ) + ")"
2015-09-10 12:03:34 +00:00
_ , err := db . Exec ( query , fields ... )
2015-03-04 10:31:46 +00:00
return err
}
type withDBFunc func ( * sql . DB , http . ResponseWriter , * http . Request )
func withDB ( db * sql . DB , f withDBFunc ) http . HandlerFunc {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
f ( db , w , r )
} )
}
2014-06-11 23:40:54 +00:00
func main ( ) {
2015-03-04 10:31:46 +00:00
log . SetFlags ( log . Ltime | log . Ldate )
2014-12-07 14:48:48 +00:00
log . SetOutput ( os . Stdout )
2015-03-04 10:31:46 +00:00
// Template
2014-06-11 23:40:54 +00:00
2014-06-28 07:46:03 +00:00
fd , err := os . Open ( "static/index.html" )
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "template:" , err )
2014-06-28 07:46:03 +00:00
}
bs , err := ioutil . ReadAll ( fd )
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "template:" , err )
2014-06-28 07:46:03 +00:00
}
fd . Close ( )
tpl = template . Must ( template . New ( "index.html" ) . Funcs ( funcs ) . Parse ( string ( bs ) ) )
2015-03-04 10:31:46 +00:00
// DB
2014-06-11 23:40:54 +00:00
2015-03-04 10:31:46 +00:00
db , err := sql . Open ( "postgres" , dbConn )
2014-10-13 09:18:13 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "database:" , err )
}
err = setupDB ( db )
if err != nil {
log . Fatalln ( "database:" , err )
}
// TLS
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
log . Fatalln ( "tls:" , err )
2014-10-13 09:18:13 +00:00
}
cfg := & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
SessionTicketsDisabled : true ,
}
2015-03-04 10:31:46 +00:00
// HTTPS
listener , err := tls . Listen ( "tcp" , listenAddr , cfg )
2014-10-13 09:18:13 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "https:" , err )
2014-10-13 09:18:13 +00:00
}
srv := http . Server {
ReadTimeout : 5 * time . Second ,
WriteTimeout : 5 * time . Second ,
}
2015-03-04 10:31:46 +00:00
http . HandleFunc ( "/" , withDB ( db , rootHandler ) )
http . HandleFunc ( "/newdata" , withDB ( db , newDataHandler ) )
2015-05-21 06:52:19 +00:00
http . HandleFunc ( "/summary.json" , withDB ( db , summaryHandler ) )
2015-07-15 11:23:13 +00:00
http . HandleFunc ( "/movement.json" , withDB ( db , movementHandler ) )
2016-09-06 18:15:18 +00:00
http . HandleFunc ( "/performance.json" , withDB ( db , performanceHandler ) )
2015-03-04 10:31:46 +00:00
http . Handle ( "/static/" , http . StripPrefix ( "/static/" , http . FileServer ( http . Dir ( "static" ) ) ) )
2014-10-13 09:18:13 +00:00
err = srv . Serve ( listener )
2014-06-11 23:40:54 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "https:" , err )
2014-06-11 23:40:54 +00:00
}
}
2015-03-04 12:02:53 +00:00
var (
cacheData [ ] byte
cacheTime time . Time
cacheMut sync . Mutex
)
const maxCacheTime = 5 * 60 * time . Second
2015-03-04 10:31:46 +00:00
func rootHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
2014-06-28 07:46:03 +00:00
if r . URL . Path == "/" || r . URL . Path == "/index.html" {
2015-03-04 12:02:53 +00:00
cacheMut . Lock ( )
defer cacheMut . Unlock ( )
if time . Since ( cacheTime ) > maxCacheTime {
rep := getReport ( db )
buf := new ( bytes . Buffer )
err := tpl . Execute ( buf , rep )
if err != nil {
log . Println ( err )
http . Error ( w , "Template Error" , http . StatusInternalServerError )
return
}
cacheData = buf . Bytes ( )
cacheTime = time . Now ( )
}
2014-06-28 07:46:03 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2015-03-04 12:02:53 +00:00
w . Write ( cacheData )
2014-06-28 07:46:03 +00:00
} else {
http . Error ( w , "Not found" , 404 )
2015-03-04 12:02:53 +00:00
return
2014-06-28 07:46:03 +00:00
}
}
2015-03-04 10:31:46 +00:00
func newDataHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
defer r . Body . Close ( )
2014-06-28 07:46:03 +00:00
2015-03-04 10:31:46 +00:00
var rep report
rep . Date = time . Now ( ) . UTC ( ) . Format ( "20060102" )
2014-06-11 23:40:54 +00:00
lr := & io . LimitedReader { R : r . Body , N : 10240 }
2015-03-04 10:31:46 +00:00
if err := json . NewDecoder ( lr ) . Decode ( & rep ) ; err != nil {
log . Println ( "json decode:" , err )
http . Error ( w , "JSON Decode Error" , http . StatusInternalServerError )
2014-06-11 23:40:54 +00:00
return
}
2015-03-04 10:31:46 +00:00
if err := rep . Validate ( ) ; err != nil {
log . Println ( "validate:" , err )
log . Printf ( "%#v" , rep )
http . Error ( w , "Validation Error" , http . StatusInternalServerError )
2014-06-11 23:40:54 +00:00
return
}
2014-06-16 09:14:01 +00:00
2015-03-04 10:31:46 +00:00
if err := insertReport ( db , rep ) ; err != nil {
log . Println ( "insert:" , err )
log . Printf ( "%#v" , rep )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
2014-06-16 09:14:01 +00:00
}
}
2015-05-21 06:52:19 +00:00
func summaryHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
s , err := getSummary ( db )
if err != nil {
log . Println ( "summaryHandler:" , err )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
}
bs , err := s . MarshalJSON ( )
if err != nil {
log . Println ( "summaryHandler:" , err )
http . Error ( w , "JSON Encode Error" , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Write ( bs )
}
2015-07-15 11:23:13 +00:00
func movementHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
s , err := getMovement ( db )
if err != nil {
log . Println ( "movementHandler:" , err )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
}
bs , err := json . Marshal ( s )
if err != nil {
log . Println ( "movementHandler:" , err )
http . Error ( w , "JSON Encode Error" , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Write ( bs )
}
2016-09-06 18:15:18 +00:00
func performanceHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
s , err := getPerformance ( db )
if err != nil {
log . Println ( "performanceHandler:" , err )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
}
bs , err := json . Marshal ( s )
if err != nil {
log . Println ( "performanceHandler:" , err )
http . Error ( w , "JSON Encode Error" , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Write ( bs )
}
2014-06-28 09:24:25 +00:00
type category struct {
Values [ 4 ] float64
Key string
Descr string
Unit string
Binary bool
}
2015-09-11 08:56:32 +00:00
type feature struct {
Key string
2016-06-09 09:55:05 +00:00
Pct float64
2015-09-11 08:56:32 +00:00
}
2015-03-04 12:02:53 +00:00
func getReport ( db * sql . DB ) map [ string ] interface { } {
2014-06-28 07:46:03 +00:00
nodes := 0
var versions [ ] string
var platforms [ ] string
2015-03-04 10:31:46 +00:00
var numFolders [ ] int
var numDevices [ ] int
2014-06-28 07:46:03 +00:00
var totFiles [ ] int
var maxFiles [ ] int
var totMiB [ ] int
var maxMiB [ ] int
var memoryUsage [ ] int
var sha256Perf [ ] float64
var memorySize [ ] int
2015-05-21 06:52:19 +00:00
var compilers [ ] string
var builders [ ] string
2014-06-11 23:40:54 +00:00
2015-09-11 08:56:32 +00:00
v2Reports := 0
2016-06-09 09:55:05 +00:00
features := map [ string ] float64 {
2015-09-11 08:56:32 +00:00
"Rate limiting" : 0 ,
"Upgrades allowed (automatic)" : 0 ,
"Upgrades allowed (manual)" : 0 ,
"Folders, automatic normalization" : 0 ,
"Folders, ignore deletes" : 0 ,
"Folders, ignore permissions" : 0 ,
"Folders, master mode" : 0 ,
2016-02-15 11:50:00 +00:00
"Folders, simple versioning" : 0 ,
"Folders, external versioning" : 0 ,
"Folders, staggered versioning" : 0 ,
"Folders, trashcan versioning" : 0 ,
2015-09-11 08:56:32 +00:00
"Devices, compress always" : 0 ,
"Devices, compress metadata" : 0 ,
"Devices, compress nothing" : 0 ,
"Devices, custom certificate" : 0 ,
"Devices, dynamic addresses" : 0 ,
"Devices, static addresses" : 0 ,
"Devices, introducer" : 0 ,
"Relaying, enabled" : 0 ,
"Relaying, default relays" : 0 ,
"Relaying, other relays" : 0 ,
"Discovery, global enabled" : 0 ,
"Discovery, local enabled" : 0 ,
"Discovery, default servers (using DNS)" : 0 ,
"Discovery, default servers (using IP)" : 0 ,
"Discovery, other servers" : 0 ,
}
var numCPU [ ] int
2015-09-10 14:11:36 +00:00
var rep report
2015-09-10 13:55:25 +00:00
2015-09-10 14:11:36 +00:00
rows , err := db . Query ( ` SELECT ` + strings . Join ( rep . FieldNames ( ) , "," ) + ` FROM Reports WHERE Received > now() - '1 day'::INTERVAL ` )
2015-03-04 10:31:46 +00:00
if err != nil {
log . Println ( "sql:" , err )
return nil
}
defer rows . Close ( )
for rows . Next ( ) {
2015-09-10 14:11:36 +00:00
err := rows . Scan ( rep . FieldPointers ( ) ... )
2015-03-04 10:31:46 +00:00
2014-06-28 07:46:03 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Println ( "sql:" , err )
return nil
2014-06-28 07:46:03 +00:00
}
2014-06-11 23:40:54 +00:00
2014-06-28 07:46:03 +00:00
nodes ++
versions = append ( versions , transformVersion ( rep . Version ) )
platforms = append ( platforms , rep . Platform )
2015-05-21 06:52:19 +00:00
if m := compilerRe . FindStringSubmatch ( rep . LongVersion ) ; len ( m ) == 3 {
compilers = append ( compilers , m [ 1 ] )
builders = append ( builders , m [ 2 ] )
}
2015-03-04 10:31:46 +00:00
if rep . NumFolders > 0 {
numFolders = append ( numFolders , rep . NumFolders )
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
if rep . NumDevices > 0 {
numDevices = append ( numDevices , rep . NumDevices )
2014-06-28 07:46:03 +00:00
}
if rep . TotFiles > 0 {
totFiles = append ( totFiles , rep . TotFiles )
}
2015-03-04 10:31:46 +00:00
if rep . FolderMaxFiles > 0 {
maxFiles = append ( maxFiles , rep . FolderMaxFiles )
2014-06-28 07:46:03 +00:00
}
if rep . TotMiB > 0 {
2014-06-28 09:24:25 +00:00
totMiB = append ( totMiB , rep . TotMiB * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
if rep . FolderMaxMiB > 0 {
maxMiB = append ( maxMiB , rep . FolderMaxMiB * ( 1 << 20 ) )
2014-06-11 23:40:54 +00:00
}
2014-06-28 07:46:03 +00:00
if rep . MemoryUsageMiB > 0 {
2014-06-28 09:24:25 +00:00
memoryUsage = append ( memoryUsage , rep . MemoryUsageMiB * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
if rep . SHA256Perf > 0 {
2014-06-28 09:24:25 +00:00
sha256Perf = append ( sha256Perf , rep . SHA256Perf * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
if rep . MemorySize > 0 {
2014-06-28 09:24:25 +00:00
memorySize = append ( memorySize , rep . MemorySize * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
2015-09-11 08:56:32 +00:00
if rep . URVersion >= 2 {
v2Reports ++
numCPU = append ( numCPU , rep . NumCPU )
if rep . UsesRateLimit {
features [ "Rate limiting" ] ++
}
if rep . UpgradeAllowedAuto {
features [ "Upgrades allowed (automatic)" ] ++
}
if rep . UpgradeAllowedManual {
features [ "Upgrades allowed (manual)" ] ++
}
if rep . FolderUses . AutoNormalize > 0 {
features [ "Folders, automatic normalization" ] ++
}
if rep . FolderUses . IgnoreDelete > 0 {
features [ "Folders, ignore deletes" ] ++
}
if rep . FolderUses . IgnorePerms > 0 {
features [ "Folders, ignore permissions" ] ++
}
if rep . FolderUses . ReadOnly > 0 {
features [ "Folders, master mode" ] ++
}
2016-02-15 11:50:00 +00:00
if rep . FolderUses . SimpleVersioning > 0 {
features [ "Folders, simple versioning" ] ++
}
if rep . FolderUses . ExternalVersioning > 0 {
features [ "Folders, external versioning" ] ++
}
if rep . FolderUses . StaggeredVersioning > 0 {
features [ "Folders, staggered versioning" ] ++
}
if rep . FolderUses . TrashcanVersioning > 0 {
features [ "Folders, trashcan versioning" ] ++
}
2015-09-11 08:56:32 +00:00
if rep . DeviceUses . CompressAlways > 0 {
features [ "Devices, compress always" ] ++
}
if rep . DeviceUses . CompressMetadata > 0 {
features [ "Devices, compress metadata" ] ++
}
if rep . DeviceUses . CompressNever > 0 {
features [ "Devices, compress nothing" ] ++
}
if rep . DeviceUses . CustomCertName > 0 {
features [ "Devices, custom certificate" ] ++
}
if rep . DeviceUses . DynamicAddr > 0 {
features [ "Devices, dynamic addresses" ] ++
}
if rep . DeviceUses . StaticAddr > 0 {
features [ "Devices, static addresses" ] ++
}
if rep . DeviceUses . Introducer > 0 {
features [ "Devices, introducer" ] ++
}
if rep . Relays . Enabled {
features [ "Relaying, enabled" ] ++
}
if rep . Relays . DefaultServers > 0 {
features [ "Relaying, default relays" ] ++
}
if rep . Relays . OtherServers > 0 {
features [ "Relaying, other relays" ] ++
}
if rep . Announce . GlobalEnabled {
features [ "Discovery, global enabled" ] ++
}
if rep . Announce . LocalEnabled {
features [ "Discovery, local enabled" ] ++
}
if rep . Announce . DefaultServersDNS > 0 {
features [ "Discovery, default servers (using DNS)" ] ++
}
if rep . Announce . DefaultServersIP > 0 {
features [ "Discovery, default servers (using IP)" ] ++
}
if rep . Announce . DefaultServersIP > 0 {
features [ "Discovery, other servers" ] ++
}
}
2014-06-28 07:46:03 +00:00
}
2014-06-28 09:24:25 +00:00
var categories [ ] category
categories = append ( categories , category {
Values : statsForInts ( totFiles ) ,
2015-02-15 11:00:15 +00:00
Descr : "Files Managed per Device" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( maxFiles ) ,
2015-02-15 11:00:15 +00:00
Descr : "Files in Largest Folder" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( totMiB ) ,
2015-02-15 11:00:15 +00:00
Descr : "Data Managed per Device" ,
2014-06-28 09:24:25 +00:00
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForInts ( maxMiB ) ,
2015-02-15 11:00:15 +00:00
Descr : "Data in Largest Folder" ,
2014-06-28 09:24:25 +00:00
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
2015-03-04 10:31:46 +00:00
Values : statsForInts ( numDevices ) ,
2015-02-15 11:00:15 +00:00
Descr : "Number of Devices in Cluster" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
2015-03-04 10:31:46 +00:00
Values : statsForInts ( numFolders ) ,
2015-02-15 11:00:15 +00:00
Descr : "Number of Folders Configured" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( memoryUsage ) ,
Descr : "Memory Usage" ,
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForInts ( memorySize ) ,
Descr : "System Memory" ,
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForFloats ( sha256Perf ) ,
Descr : "SHA-256 Hashing Performance" ,
Unit : "B/s" ,
Binary : true ,
} )
2015-09-11 08:56:32 +00:00
categories = append ( categories , category {
Values : statsForInts ( numCPU ) ,
Descr : "Number of CPU cores" ,
} )
var featureList [ ] feature
var featureNames [ ] string
for key := range features {
featureNames = append ( featureNames , key )
}
sort . Strings ( featureNames )
2015-09-30 06:29:41 +00:00
if v2Reports > 0 {
for _ , key := range featureNames {
featureList = append ( featureList , feature {
Key : key ,
2016-06-09 09:55:05 +00:00
Pct : ( 100 * features [ key ] ) / float64 ( v2Reports ) ,
2015-09-30 06:29:41 +00:00
} )
}
sort . Sort ( sort . Reverse ( sortableFeatureList ( featureList ) ) )
2015-09-11 08:56:32 +00:00
}
2014-06-28 07:46:03 +00:00
r := make ( map [ string ] interface { } )
r [ "nodes" ] = nodes
2015-09-11 08:56:32 +00:00
r [ "v2nodes" ] = v2Reports
2014-06-28 07:46:03 +00:00
r [ "categories" ] = categories
2016-05-30 07:52:38 +00:00
r [ "versions" ] = group ( byVersion , analyticsFor ( versions , 2000 ) , 5 )
r [ "platforms" ] = group ( byPlatform , analyticsFor ( platforms , 2000 ) , 5 )
2016-06-07 06:12:32 +00:00
r [ "compilers" ] = group ( byCompiler , analyticsFor ( compilers , 2000 ) , 3 )
2015-05-21 06:52:19 +00:00
r [ "builders" ] = analyticsFor ( builders , 12 )
2015-09-11 08:56:32 +00:00
r [ "features" ] = featureList
2014-06-28 07:46:03 +00:00
return r
}
2014-06-11 23:40:54 +00:00
func ensureDir ( dir string , mode int ) {
fi , err := os . Stat ( dir )
if os . IsNotExist ( err ) {
os . MkdirAll ( dir , 0700 )
} else if mode >= 0 && err == nil && int ( fi . Mode ( ) & 0777 ) != mode {
os . Chmod ( dir , os . FileMode ( mode ) )
}
}
2014-06-28 07:46:03 +00:00
2017-03-04 14:24:24 +00:00
var plusRe = regexp . MustCompile ( ` \+.*$ ` )
2014-06-28 07:46:03 +00:00
// transformVersion returns a version number formatted correctly, with all
// development versions aggregated into one.
func transformVersion ( v string ) string {
2014-08-07 12:55:13 +00:00
if v == "unknown-dev" {
return v
}
2014-06-28 07:46:03 +00:00
if ! strings . HasPrefix ( v , "v" ) {
v = "v" + v
}
2017-03-04 14:24:24 +00:00
v = plusRe . ReplaceAllString ( v , " (+dev)" )
2014-12-09 15:56:47 +00:00
2014-06-28 07:46:03 +00:00
return v
}
2015-05-21 06:52:19 +00:00
type summary struct {
versions map [ string ] int // version string to count index
2016-07-08 08:00:07 +00:00
max map [ string ] int // version string to max users per day
2015-05-21 06:52:19 +00:00
rows map [ string ] [ ] int // date to list of counts
}
func newSummary ( ) summary {
return summary {
versions : make ( map [ string ] int ) ,
2016-07-08 08:00:07 +00:00
max : make ( map [ string ] int ) ,
2015-05-21 06:52:19 +00:00
rows : make ( map [ string ] [ ] int ) ,
}
}
func ( s * summary ) setCount ( date , version string , count int ) {
idx , ok := s . versions [ version ]
if ! ok {
idx = len ( s . versions )
s . versions [ version ] = idx
}
2016-07-08 08:00:07 +00:00
if s . max [ version ] < count {
s . max [ version ] = count
}
2015-05-21 06:52:19 +00:00
row := s . rows [ date ]
if len ( row ) <= idx {
old := row
row = make ( [ ] int , idx + 1 )
copy ( row , old )
s . rows [ date ] = row
}
row [ idx ] = count
}
func ( s * summary ) MarshalJSON ( ) ( [ ] byte , error ) {
var versions [ ] string
for v := range s . versions {
versions = append ( versions , v )
}
sort . Strings ( versions )
2016-07-08 08:00:07 +00:00
var filtered [ ] string
for _ , v := range versions {
if s . max [ v ] > 50 {
filtered = append ( filtered , v )
}
}
versions = filtered
2015-05-21 06:52:19 +00:00
headerRow := [ ] interface { } { "Day" }
for _ , v := range versions {
headerRow = append ( headerRow , v )
}
var table [ ] [ ] interface { }
table = append ( table , headerRow )
var dates [ ] string
for k := range s . rows {
dates = append ( dates , k )
}
sort . Strings ( dates )
for _ , date := range dates {
row := [ ] interface { } { date }
for _ , ver := range versions {
idx := s . versions [ ver ]
2015-06-15 08:46:48 +00:00
if len ( s . rows [ date ] ) > idx && s . rows [ date ] [ idx ] > 0 {
2015-05-21 06:52:19 +00:00
row = append ( row , s . rows [ date ] [ idx ] )
} else {
2015-06-15 08:46:48 +00:00
row = append ( row , nil )
2015-05-21 06:52:19 +00:00
}
}
table = append ( table , row )
}
return json . Marshal ( table )
}
func getSummary ( db * sql . DB ) ( summary , error ) {
s := newSummary ( )
2016-12-07 14:22:25 +00:00
rows , err := db . Query ( ` SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '2 year'::INTERVAL; ` )
2015-05-21 06:52:19 +00:00
if err != nil {
return summary { } , err
}
defer rows . Close ( )
for rows . Next ( ) {
var day time . Time
var ver string
var num int
err := rows . Scan ( & day , & ver , & num )
if err != nil {
return summary { } , err
}
if ver == "v0.0" {
// ?
continue
}
// SUPER UGLY HACK to avoid having to do sorting properly
if len ( ver ) == 4 { // v0.x
ver = ver [ : 3 ] + "0" + ver [ 3 : ] // now v0.0x
}
s . setCount ( day . Format ( "2006-01-02" ) , ver , num )
}
return s , nil
}
2015-07-15 11:23:13 +00:00
func getMovement ( db * sql . DB ) ( [ ] [ ] interface { } , error ) {
2016-12-07 14:22:25 +00:00
rows , err := db . Query ( ` SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '2 year'::INTERVAL ORDER BY Day ` )
2015-07-15 11:23:13 +00:00
if err != nil {
return nil , err
}
defer rows . Close ( )
res := [ ] [ ] interface { } {
2015-07-15 11:45:33 +00:00
{ "Day" , "Joined" , "Left" , "Bounced" } ,
2015-07-15 11:23:13 +00:00
}
for rows . Next ( ) {
var day time . Time
2015-07-15 11:45:33 +00:00
var added , removed , bounced int
err := rows . Scan ( & day , & added , & removed , & bounced )
2015-07-15 11:23:13 +00:00
if err != nil {
return nil , err
}
2015-07-15 11:45:33 +00:00
row := [ ] interface { } { day . Format ( "2006-01-02" ) , added , - removed , bounced }
2015-07-15 11:23:13 +00:00
if removed == 0 {
row [ 2 ] = nil
}
2015-07-15 11:45:33 +00:00
if bounced == 0 {
row [ 3 ] = nil
}
2015-07-15 11:23:13 +00:00
res = append ( res , row )
}
return res , nil
}
2015-09-30 06:29:41 +00:00
2016-09-06 18:15:18 +00:00
func getPerformance ( db * sql . DB ) ( [ ] [ ] interface { } , error ) {
rows , err := db . Query ( ` SELECT Day, TotFiles, TotMiB, SHA256Perf, MemorySize, MemoryUsageMiB FROM Performance WHERE Day > '2014-06-20'::TIMESTAMP ORDER BY Day ` )
if err != nil {
return nil , err
}
defer rows . Close ( )
res := [ ] [ ] interface { } {
{ "Day" , "TotFiles" , "TotMiB" , "SHA256Perf" , "MemorySize" , "MemoryUsageMiB" } ,
}
for rows . Next ( ) {
var day time . Time
var sha256Perf float64
var totFiles , totMiB , memorySize , memoryUsage int
err := rows . Scan ( & day , & totFiles , & totMiB , & sha256Perf , & memorySize , & memoryUsage )
if err != nil {
return nil , err
}
row := [ ] interface { } { day . Format ( "2006-01-02" ) , totFiles , totMiB , float64 ( int ( sha256Perf * 10 ) ) / 10 , memorySize , memoryUsage }
res = append ( res , row )
}
return res , nil
}
2015-09-30 06:29:41 +00:00
type sortableFeatureList [ ] feature
func ( l sortableFeatureList ) Len ( ) int {
return len ( l )
}
func ( l sortableFeatureList ) Swap ( a , b int ) {
l [ a ] , l [ b ] = l [ b ] , l [ a ]
}
func ( l sortableFeatureList ) Less ( a , b int ) bool {
2016-02-15 11:50:00 +00:00
if l [ a ] . Pct != l [ b ] . Pct {
return l [ a ] . Pct < l [ b ] . Pct
}
return l [ a ] . Key > l [ b ] . Key
2015-09-30 06:29:41 +00:00
}