mirror of
https://github.com/octoleo/syncthing.git
synced 2025-01-22 22:58:25 +00:00
cmd/ursrv: Refactor to use CLI options, fewer global vars
This commit is contained in:
parent
b2886f11b1
commit
bf61e485a6
@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
_ "github.com/lib/pq" // PostgreSQL driver
|
_ "github.com/lib/pq" // PostgreSQL driver
|
||||||
"github.com/oschwald/geoip2-golang"
|
"github.com/oschwald/geoip2-golang"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@ -34,14 +35,17 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
UseHTTP bool `env:"UR_USE_HTTP"`
|
||||||
|
Debug bool `env:"UR_DEBUG"`
|
||||||
|
KeyFile string `env:"UR_KEY_FILE" default:"key.pem"`
|
||||||
|
CertFile string `env:"UR_CRT_FILE" default:"crt.pem"`
|
||||||
|
DBConn string `env:"UR_DB_URL" default:"postgres://user:password@localhost/ur?sslmode=disable"`
|
||||||
|
Listen string `env:"UR_LISTEN" default:"0.0.0.0:8443"`
|
||||||
|
GeoIPPath string `env:"UR_GEOIP" default:"GeoLite2-City.mmdb"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
useHTTP = os.Getenv("UR_USE_HTTP") != ""
|
|
||||||
debug = os.Getenv("UR_DEBUG") != ""
|
|
||||||
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")
|
|
||||||
geoIPPath = getEnvDefault("UR_GEOIP", "GeoLite2-City.mmdb")
|
|
||||||
tpl *template.Template
|
tpl *template.Template
|
||||||
compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
|
compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
|
||||||
progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"}
|
progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"}
|
||||||
@ -159,6 +163,9 @@ func main() {
|
|||||||
log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
|
log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
var cli CLI
|
||||||
|
kong.Parse(&cli)
|
||||||
|
|
||||||
// Template
|
// Template
|
||||||
|
|
||||||
fd, err := os.Open("static/index.html")
|
fd, err := os.Open("static/index.html")
|
||||||
@ -174,7 +181,7 @@ func main() {
|
|||||||
|
|
||||||
// DB
|
// DB
|
||||||
|
|
||||||
db, err := sql.Open("postgres", dbConn)
|
db, err := sql.Open("postgres", cli.DBConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("database:", err)
|
log.Fatalln("database:", err)
|
||||||
}
|
}
|
||||||
@ -186,11 +193,11 @@ func main() {
|
|||||||
// TLS & Listening
|
// TLS & Listening
|
||||||
|
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
if useHTTP {
|
if cli.UseHTTP {
|
||||||
listener, err = net.Listen("tcp", listenAddr)
|
listener, err = net.Listen("tcp", cli.Listen)
|
||||||
} else {
|
} else {
|
||||||
var cert tls.Certificate
|
var cert tls.Certificate
|
||||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
cert, err = tls.LoadX509KeyPair(cli.CertFile, cli.KeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("tls:", err)
|
log.Fatalln("tls:", err)
|
||||||
}
|
}
|
||||||
@ -199,81 +206,89 @@ func main() {
|
|||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
SessionTicketsDisabled: true,
|
SessionTicketsDisabled: true,
|
||||||
}
|
}
|
||||||
listener, err = tls.Listen("tcp", listenAddr, cfg)
|
listener, err = tls.Listen("tcp", cli.Listen, cfg)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("listen:", err)
|
log.Fatalln("listen:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := http.Server{
|
srv := &server{
|
||||||
|
db: db,
|
||||||
|
debug: cli.Debug,
|
||||||
|
geoIPPath: cli.GeoIPPath,
|
||||||
|
}
|
||||||
|
http.HandleFunc("/", srv.rootHandler)
|
||||||
|
http.HandleFunc("/newdata", srv.newDataHandler)
|
||||||
|
http.HandleFunc("/summary.json", srv.summaryHandler)
|
||||||
|
http.HandleFunc("/movement.json", srv.movementHandler)
|
||||||
|
http.HandleFunc("/performance.json", srv.performanceHandler)
|
||||||
|
http.HandleFunc("/blockstats.json", srv.blockStatsHandler)
|
||||||
|
http.HandleFunc("/locations.json", srv.locationsHandler)
|
||||||
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||||
|
|
||||||
|
go srv.cacheRefresher()
|
||||||
|
|
||||||
|
httpSrv := http.Server{
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
}
|
}
|
||||||
|
err = httpSrv.Serve(listener)
|
||||||
http.HandleFunc("/", withDB(db, rootHandler))
|
|
||||||
http.HandleFunc("/newdata", withDB(db, newDataHandler))
|
|
||||||
http.HandleFunc("/summary.json", withDB(db, summaryHandler))
|
|
||||||
http.HandleFunc("/movement.json", withDB(db, movementHandler))
|
|
||||||
http.HandleFunc("/performance.json", withDB(db, performanceHandler))
|
|
||||||
http.HandleFunc("/blockstats.json", withDB(db, blockStatsHandler))
|
|
||||||
http.HandleFunc("/locations.json", withDB(db, locationsHandler))
|
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
|
||||||
|
|
||||||
go cacheRefresher(db)
|
|
||||||
|
|
||||||
err = srv.Serve(listener)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("https:", err)
|
log.Fatalln("https:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type server struct {
|
||||||
|
debug bool
|
||||||
|
db *sql.DB
|
||||||
|
geoIPPath string
|
||||||
|
|
||||||
|
cacheMut sync.Mutex
|
||||||
cachedIndex []byte
|
cachedIndex []byte
|
||||||
cachedLocations []byte
|
cachedLocations []byte
|
||||||
cacheTime time.Time
|
cacheTime time.Time
|
||||||
cacheMut sync.Mutex
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const maxCacheTime = 15 * time.Minute
|
const maxCacheTime = 15 * time.Minute
|
||||||
|
|
||||||
func cacheRefresher(db *sql.DB) {
|
func (s *server) cacheRefresher() {
|
||||||
ticker := time.NewTicker(maxCacheTime - time.Minute)
|
ticker := time.NewTicker(maxCacheTime - time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for ; true; <-ticker.C {
|
for ; true; <-ticker.C {
|
||||||
cacheMut.Lock()
|
s.cacheMut.Lock()
|
||||||
if err := refreshCacheLocked(db); err != nil {
|
if err := s.refreshCacheLocked(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
cacheMut.Unlock()
|
s.cacheMut.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshCacheLocked(db *sql.DB) error {
|
func (s *server) refreshCacheLocked() error {
|
||||||
rep := getReport(db)
|
rep := getReport(s.db, s.geoIPPath)
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := tpl.Execute(buf, rep)
|
err := tpl.Execute(buf, rep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cachedIndex = buf.Bytes()
|
s.cachedIndex = buf.Bytes()
|
||||||
cacheTime = time.Now()
|
s.cacheTime = time.Now()
|
||||||
|
|
||||||
locs := rep["locations"].(map[location]int)
|
locs := rep["locations"].(map[location]int)
|
||||||
wlocs := make([]weightedLocation, 0, len(locs))
|
wlocs := make([]weightedLocation, 0, len(locs))
|
||||||
for loc, w := range locs {
|
for loc, w := range locs {
|
||||||
wlocs = append(wlocs, weightedLocation{loc, w})
|
wlocs = append(wlocs, weightedLocation{loc, w})
|
||||||
}
|
}
|
||||||
cachedLocations, _ = json.Marshal(wlocs)
|
s.cachedLocations, _ = json.Marshal(wlocs)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
func (s *server) rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||||
cacheMut.Lock()
|
s.cacheMut.Lock()
|
||||||
defer cacheMut.Unlock()
|
defer s.cacheMut.Unlock()
|
||||||
|
|
||||||
if time.Since(cacheTime) > maxCacheTime {
|
if time.Since(s.cacheTime) > maxCacheTime {
|
||||||
if err := refreshCacheLocked(db); err != nil {
|
if err := s.refreshCacheLocked(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, "Template Error", http.StatusInternalServerError)
|
http.Error(w, "Template Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -281,19 +296,19 @@ func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write(cachedIndex)
|
w.Write(s.cachedIndex)
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "Not found", 404)
|
http.Error(w, "Not found", 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationsHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
func (s *server) locationsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
cacheMut.Lock()
|
s.cacheMut.Lock()
|
||||||
defer cacheMut.Unlock()
|
defer s.cacheMut.Unlock()
|
||||||
|
|
||||||
if time.Since(cacheTime) > maxCacheTime {
|
if time.Since(s.cacheTime) > maxCacheTime {
|
||||||
if err := refreshCacheLocked(db); err != nil {
|
if err := s.refreshCacheLocked(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, "Template Error", http.StatusInternalServerError)
|
http.Error(w, "Template Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -301,10 +316,10 @@ func locationsHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.Write(cachedLocations)
|
w.Write(s.cachedLocations)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
func (s *server) newDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
addr := r.Header.Get("X-Forwarded-For")
|
addr := r.Header.Get("X-Forwarded-For")
|
||||||
@ -330,7 +345,7 @@ func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
bs, _ := io.ReadAll(lr)
|
bs, _ := io.ReadAll(lr)
|
||||||
if err := json.Unmarshal(bs, &rep); err != nil {
|
if err := json.Unmarshal(bs, &rep); err != nil {
|
||||||
log.Println("decode:", err)
|
log.Println("decode:", err)
|
||||||
if debug {
|
if s.debug {
|
||||||
log.Printf("%s", bs)
|
log.Printf("%s", bs)
|
||||||
}
|
}
|
||||||
http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
|
http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
|
||||||
@ -339,21 +354,21 @@ func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := rep.Validate(); err != nil {
|
if err := rep.Validate(); err != nil {
|
||||||
log.Println("validate:", err)
|
log.Println("validate:", err)
|
||||||
if debug {
|
if s.debug {
|
||||||
log.Printf("%#v", rep)
|
log.Printf("%#v", rep)
|
||||||
}
|
}
|
||||||
http.Error(w, "Validation Error", http.StatusInternalServerError)
|
http.Error(w, "Validation Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := insertReport(db, rep); err != nil {
|
if err := insertReport(s.db, rep); err != nil {
|
||||||
if err.Error() == `pq: duplicate key value violates unique constraint "uniqueidjsonindex"` {
|
if err.Error() == `pq: duplicate key value violates unique constraint "uniqueidjsonindex"` {
|
||||||
// We already have a report today for the same unique ID; drop
|
// We already have a report today for the same unique ID; drop
|
||||||
// this one without complaining.
|
// this one without complaining.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("insert:", err)
|
log.Println("insert:", err)
|
||||||
if debug {
|
if s.debug {
|
||||||
log.Printf("%#v", rep)
|
log.Printf("%#v", rep)
|
||||||
}
|
}
|
||||||
http.Error(w, "Database Error", http.StatusInternalServerError)
|
http.Error(w, "Database Error", http.StatusInternalServerError)
|
||||||
@ -361,16 +376,16 @@ func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
func (s *server) summaryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
min, _ := strconv.Atoi(r.URL.Query().Get("min"))
|
min, _ := strconv.Atoi(r.URL.Query().Get("min"))
|
||||||
s, err := getSummary(db, min)
|
sum, err := getSummary(s.db, min)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("summaryHandler:", err)
|
log.Println("summaryHandler:", err)
|
||||||
http.Error(w, "Database Error", http.StatusInternalServerError)
|
http.Error(w, "Database Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := s.MarshalJSON()
|
bs, err := sum.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("summaryHandler:", err)
|
log.Println("summaryHandler:", err)
|
||||||
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
||||||
@ -381,15 +396,15 @@ func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(bs)
|
w.Write(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func movementHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
func (s *server) movementHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
s, err := getMovement(db)
|
mov, err := getMovement(s.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("movementHandler:", err)
|
log.Println("movementHandler:", err)
|
||||||
http.Error(w, "Database Error", http.StatusInternalServerError)
|
http.Error(w, "Database Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := json.Marshal(s)
|
bs, err := json.Marshal(mov)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("movementHandler:", err)
|
log.Println("movementHandler:", err)
|
||||||
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
||||||
@ -400,15 +415,15 @@ func movementHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.Write(bs)
|
w.Write(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func performanceHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
func (s *server) performanceHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
s, err := getPerformance(db)
|
perf, err := getPerformance(s.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("performanceHandler:", err)
|
log.Println("performanceHandler:", err)
|
||||||
http.Error(w, "Database Error", http.StatusInternalServerError)
|
http.Error(w, "Database Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := json.Marshal(s)
|
bs, err := json.Marshal(perf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("performanceHandler:", err)
|
log.Println("performanceHandler:", err)
|
||||||
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
||||||
@ -419,15 +434,15 @@ func performanceHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.Write(bs)
|
w.Write(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func blockStatsHandler(db *sql.DB, w http.ResponseWriter, _ *http.Request) {
|
func (s *server) blockStatsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
s, err := getBlockStats(db)
|
blocks, err := getBlockStats(s.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("blockStatsHandler:", err)
|
log.Println("blockStatsHandler:", err)
|
||||||
http.Error(w, "Database Error", http.StatusInternalServerError)
|
http.Error(w, "Database Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := json.Marshal(s)
|
bs, err := json.Marshal(blocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("blockStatsHandler:", err)
|
log.Println("blockStatsHandler:", err)
|
||||||
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
|
||||||
@ -513,7 +528,7 @@ type weightedLocation struct {
|
|||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReport(db *sql.DB) map[string]interface{} {
|
func getReport(db *sql.DB, geoIPPath string) map[string]interface{} {
|
||||||
geoip, err := geoip2.Open(geoIPPath)
|
geoip, err := geoip2.Open(geoIPPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("opening geoip db", err)
|
log.Println("opening geoip db", err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user