2014-07-12 22:45:33 +00:00
|
|
|
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
|
|
|
// All rights reserved. Use of this source code is governed by an MIT-style
|
|
|
|
// license that can be found in the LICENSE file.
|
2014-06-01 20:50:14 +00:00
|
|
|
|
2014-03-02 22:58:14 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2014-08-21 22:45:40 +00:00
|
|
|
"crypto/tls"
|
2014-03-02 22:58:14 +00:00
|
|
|
"encoding/json"
|
2014-05-22 14:12:19 +00:00
|
|
|
"fmt"
|
2014-03-02 22:58:14 +00:00
|
|
|
"io/ioutil"
|
2014-05-22 14:12:19 +00:00
|
|
|
"mime"
|
2014-04-30 20:52:38 +00:00
|
|
|
"net"
|
2014-03-02 22:58:14 +00:00
|
|
|
"net/http"
|
2014-07-22 18:11:36 +00:00
|
|
|
"os"
|
2014-05-22 14:12:19 +00:00
|
|
|
"path/filepath"
|
2014-03-02 22:58:14 +00:00
|
|
|
"runtime"
|
2014-07-13 19:07:24 +00:00
|
|
|
"strconv"
|
2014-07-05 19:40:29 +00:00
|
|
|
"strings"
|
2014-03-02 22:58:14 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2014-04-19 11:33:51 +00:00
|
|
|
"code.google.com/p/go.crypto/bcrypt"
|
2014-08-01 14:35:37 +00:00
|
|
|
"github.com/syncthing/syncthing/auto"
|
|
|
|
"github.com/syncthing/syncthing/config"
|
|
|
|
"github.com/syncthing/syncthing/events"
|
|
|
|
"github.com/syncthing/syncthing/logger"
|
|
|
|
"github.com/syncthing/syncthing/model"
|
|
|
|
"github.com/syncthing/syncthing/protocol"
|
|
|
|
"github.com/syncthing/syncthing/upgrade"
|
2014-05-21 18:06:14 +00:00
|
|
|
"github.com/vitrun/qart/qr"
|
2014-03-02 22:58:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type guiError struct {
|
|
|
|
Time time.Time
|
|
|
|
Error string
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
configInSync = true
|
|
|
|
guiErrors = []guiError{}
|
|
|
|
guiErrorsMut sync.Mutex
|
2014-07-05 19:40:29 +00:00
|
|
|
modt = time.Now().UTC().Format(http.TimeFormat)
|
2014-07-29 09:06:52 +00:00
|
|
|
eventSub *events.BufferedSubscription
|
2014-03-02 22:58:14 +00:00
|
|
|
)
|
|
|
|
|
2014-05-15 00:08:56 +00:00
|
|
|
func init() {
|
|
|
|
l.AddHandler(logger.LevelWarn, showGuiError)
|
2014-08-05 11:14:04 +00:00
|
|
|
sub := events.Default.Subscribe(events.AllEvents)
|
2014-07-29 09:06:52 +00:00
|
|
|
eventSub = events.NewBufferedSubscription(sub, 1000)
|
2014-05-15 00:08:56 +00:00
|
|
|
}
|
|
|
|
|
2014-05-22 14:12:19 +00:00
|
|
|
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
2014-05-21 12:04:16 +00:00
|
|
|
var err error
|
2014-09-12 19:28:47 +00:00
|
|
|
|
|
|
|
cert, err := loadCert(confDir, "https-")
|
|
|
|
if err != nil {
|
|
|
|
l.Infoln("Loading HTTPS certificate:", err)
|
|
|
|
l.Infoln("Creating new HTTPS certificate")
|
|
|
|
newCertificate(confDir, "https-")
|
|
|
|
cert, err = loadCert(confDir, "https-")
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
Certificates: []tls.Certificate{cert},
|
|
|
|
ServerName: "syncthing",
|
2014-04-30 20:52:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-14 22:18:05 +00:00
|
|
|
rawListener, err := net.Listen("tcp", cfg.Address)
|
2014-09-12 19:28:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2014-04-30 20:52:38 +00:00
|
|
|
}
|
2014-09-14 22:18:05 +00:00
|
|
|
listener := &DowngradingListener{rawListener, tlsCfg}
|
2014-04-30 20:52:38 +00:00
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
// The GET handlers
|
|
|
|
getRestMux := http.NewServeMux()
|
2014-09-18 10:55:28 +00:00
|
|
|
getRestMux.HandleFunc("/rest/ping", restPing)
|
2014-07-29 09:06:52 +00:00
|
|
|
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
2014-07-05 19:40:29 +00:00
|
|
|
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
|
|
|
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
2014-07-29 09:06:52 +00:00
|
|
|
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
|
2014-07-05 19:40:29 +00:00
|
|
|
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
2014-07-29 09:06:52 +00:00
|
|
|
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
2014-07-13 19:07:24 +00:00
|
|
|
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
2014-07-26 20:30:29 +00:00
|
|
|
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
2014-07-29 09:06:52 +00:00
|
|
|
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
|
|
|
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
|
|
|
|
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
|
|
|
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
|
|
|
|
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
|
|
|
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
|
|
|
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
|
|
|
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
2014-08-21 22:46:34 +00:00
|
|
|
getRestMux.HandleFunc("/rest/stats/node", withModel(m, restGetNodeStats))
|
2014-07-05 19:40:29 +00:00
|
|
|
|
2014-07-29 11:01:27 +00:00
|
|
|
// Debug endpoints, not for general use
|
|
|
|
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
// The POST handlers
|
|
|
|
postRestMux := http.NewServeMux()
|
2014-09-18 10:55:28 +00:00
|
|
|
postRestMux.HandleFunc("/rest/ping", restPing)
|
2014-07-05 19:40:29 +00:00
|
|
|
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
2014-07-29 09:06:52 +00:00
|
|
|
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
2014-07-05 19:40:29 +00:00
|
|
|
postRestMux.HandleFunc("/rest/error", restPostError)
|
|
|
|
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
|
|
|
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
2014-07-29 09:06:52 +00:00
|
|
|
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
|
|
|
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
|
|
|
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
2014-07-14 08:45:29 +00:00
|
|
|
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
2014-08-11 18:20:01 +00:00
|
|
|
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
|
2014-07-05 19:40:29 +00:00
|
|
|
|
|
|
|
// A handler that splits requests between the two above and disables
|
|
|
|
// caching
|
|
|
|
restMux := noCacheMiddleware(getPostHandler(getRestMux, postRestMux))
|
|
|
|
|
|
|
|
// The main routing handler
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle("/rest/", restMux)
|
|
|
|
mux.HandleFunc("/qr/", getQR)
|
|
|
|
|
|
|
|
// Serve compiled in assets unless an asset directory was set (for development)
|
2014-07-22 18:11:36 +00:00
|
|
|
mux.Handle("/", embeddedStatic(assetDir))
|
2014-05-22 14:12:19 +00:00
|
|
|
|
2014-07-06 13:00:44 +00:00
|
|
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
|
|
|
// protected, other requests will grant cookies.
|
2014-09-01 20:51:44 +00:00
|
|
|
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
2014-06-04 19:20:07 +00:00
|
|
|
|
2014-08-31 10:59:20 +00:00
|
|
|
// Add our version as a header to responses
|
|
|
|
handler = withVersionMiddleware(handler)
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
// Wrap everything in basic auth, if user/password is set.
|
2014-09-13 21:06:25 +00:00
|
|
|
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
|
2014-09-01 20:51:44 +00:00
|
|
|
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
2014-07-05 19:40:29 +00:00
|
|
|
}
|
2014-04-30 20:52:38 +00:00
|
|
|
|
2014-09-14 22:18:05 +00:00
|
|
|
// Redirect to HTTPS if we are supposed to
|
|
|
|
if cfg.UseTLS {
|
|
|
|
handler = redirectToHTTPSMiddleware(handler)
|
|
|
|
}
|
2014-09-12 19:28:47 +00:00
|
|
|
|
2014-09-18 07:27:26 +00:00
|
|
|
go func() {
|
|
|
|
err := http.Serve(listener, handler)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}()
|
2014-04-30 20:52:38 +00:00
|
|
|
return nil
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func getPostHandler(get, post http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch r.Method {
|
|
|
|
case "GET":
|
|
|
|
get.ServeHTTP(w, r)
|
|
|
|
case "POST":
|
|
|
|
post.ServeHTTP(w, r)
|
|
|
|
default:
|
|
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
})
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-09-14 22:18:05 +00:00
|
|
|
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
2014-09-12 19:28:47 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2014-09-14 22:18:05 +00:00
|
|
|
// Add a generous access-control-allow-origin header since we may be
|
|
|
|
// redirecting REST requests over protocols
|
|
|
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
|
|
|
|
|
|
|
if r.TLS == nil {
|
|
|
|
// Redirect HTTP requests to HTTPS
|
|
|
|
r.URL.Host = r.Host
|
2014-09-12 19:28:47 +00:00
|
|
|
r.URL.Scheme = "https"
|
|
|
|
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
|
|
|
} else {
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func noCacheMiddleware(h http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2014-03-26 19:32:35 +00:00
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
2014-07-05 19:40:29 +00:00
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-08-31 10:59:20 +00:00
|
|
|
func withVersionMiddleware(h http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("X-Syncthing-Version", Version)
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
h(m, w, r)
|
2014-03-26 19:32:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-18 10:55:28 +00:00
|
|
|
func restPing(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"ping": "pong",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetVersion(w http.ResponseWriter, r *http.Request) {
|
2014-09-18 10:52:45 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"version": Version,
|
|
|
|
"longVersion": LongVersion,
|
|
|
|
"os": runtime.GOOS,
|
|
|
|
"arch": runtime.GOARCH,
|
|
|
|
})
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-07-29 09:06:52 +00:00
|
|
|
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var qs = r.URL.Query()
|
|
|
|
var repo = qs.Get("repo")
|
|
|
|
var nodeStr = qs.Get("node")
|
|
|
|
|
|
|
|
node, err := protocol.NodeIDFromString(nodeStr)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res := map[string]float64{
|
|
|
|
"completion": m.Completion(node, repo),
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-06-19 22:27:54 +00:00
|
|
|
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var qs = r.URL.Query()
|
|
|
|
var repo = qs.Get("repo")
|
|
|
|
var res = make(map[string]interface{})
|
|
|
|
|
2014-07-15 11:04:37 +00:00
|
|
|
res["version"] = m.LocalVersion(repo)
|
2014-06-19 22:27:54 +00:00
|
|
|
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-06-19 22:27:54 +00:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-05-15 03:26:55 +00:00
|
|
|
func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-04-23 08:04:07 +00:00
|
|
|
var qs = r.URL.Query()
|
|
|
|
var repo = qs.Get("repo")
|
2014-03-02 22:58:14 +00:00
|
|
|
var res = make(map[string]interface{})
|
|
|
|
|
2014-04-27 19:53:27 +00:00
|
|
|
for _, cr := range cfg.Repositories {
|
|
|
|
if cr.ID == repo {
|
|
|
|
res["invalid"] = cr.Invalid
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-04-09 20:03:30 +00:00
|
|
|
globalFiles, globalDeleted, globalBytes := m.GlobalSize(repo)
|
2014-03-02 22:58:14 +00:00
|
|
|
res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes
|
|
|
|
|
2014-04-09 20:03:30 +00:00
|
|
|
localFiles, localDeleted, localBytes := m.LocalSize(repo)
|
2014-03-02 22:58:14 +00:00
|
|
|
res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
|
|
|
|
|
2014-04-09 20:03:30 +00:00
|
|
|
needFiles, needBytes := m.NeedSize(repo)
|
|
|
|
res["needFiles"], res["needBytes"] = needFiles, needBytes
|
2014-03-02 22:58:14 +00:00
|
|
|
|
2014-04-09 20:03:30 +00:00
|
|
|
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
2014-03-02 22:58:14 +00:00
|
|
|
|
2014-07-17 11:38:36 +00:00
|
|
|
res["state"], res["stateChanged"] = m.State(repo)
|
2014-07-15 11:04:37 +00:00
|
|
|
res["version"] = m.LocalVersion(repo)
|
2014-04-14 07:58:17 +00:00
|
|
|
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 22:58:14 +00:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-06-16 08:47:02 +00:00
|
|
|
var qs = r.URL.Query()
|
|
|
|
var repo = qs.Get("repo")
|
2014-09-04 20:29:53 +00:00
|
|
|
go m.Override(repo)
|
2014-06-16 08:47:02 +00:00
|
|
|
}
|
|
|
|
|
2014-05-19 20:31:28 +00:00
|
|
|
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var qs = r.URL.Query()
|
|
|
|
var repo = qs.Get("repo")
|
|
|
|
|
2014-09-13 13:11:47 +00:00
|
|
|
files := m.NeedFilesRepoLimited(repo, 100, 2500) // max 100 files or 2500 blocks
|
2014-05-19 20:31:28 +00:00
|
|
|
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-05-19 20:31:28 +00:00
|
|
|
json.NewEncoder(w).Encode(files)
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-03-02 22:58:14 +00:00
|
|
|
var res = m.ConnectionStats()
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 22:58:14 +00:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-08-21 22:46:34 +00:00
|
|
|
func restGetNodeStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var res = m.NodeStatistics()
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-09-13 20:52:20 +00:00
|
|
|
json.NewEncoder(w).Encode(cfg)
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-05-24 19:00:47 +00:00
|
|
|
var newCfg config.Configuration
|
2014-07-05 19:40:29 +00:00
|
|
|
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
2014-03-02 22:58:14 +00:00
|
|
|
if err != nil {
|
2014-08-17 08:28:36 +00:00
|
|
|
l.Warnln("decoding posted config:", err)
|
2014-08-08 12:09:27 +00:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
2014-03-02 22:58:14 +00:00
|
|
|
} else {
|
2014-09-13 20:52:20 +00:00
|
|
|
if newCfg.GUI.Password != cfg.GUI.Password {
|
|
|
|
if newCfg.GUI.Password != "" {
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
|
|
|
if err != nil {
|
|
|
|
l.Warnln("bcrypting password:", err)
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
newCfg.GUI.Password = string(hash)
|
|
|
|
}
|
2014-04-19 11:33:51 +00:00
|
|
|
}
|
|
|
|
}
|
2014-06-07 02:00:46 +00:00
|
|
|
|
2014-09-20 16:42:09 +00:00
|
|
|
// Start or stop usage reporting as appropriate
|
2014-06-07 02:00:46 +00:00
|
|
|
|
2014-06-17 21:22:19 +00:00
|
|
|
if newCfg.Options.URAccepted > cfg.Options.URAccepted {
|
2014-06-11 18:04:23 +00:00
|
|
|
// UR was enabled
|
|
|
|
newCfg.Options.URAccepted = usageReportVersion
|
2014-06-11 23:05:00 +00:00
|
|
|
err := sendUsageReport(m)
|
|
|
|
if err != nil {
|
|
|
|
l.Infoln("Usage report:", err)
|
|
|
|
}
|
2014-06-11 18:04:23 +00:00
|
|
|
go usageReportingLoop(m)
|
2014-06-17 21:22:19 +00:00
|
|
|
} else if newCfg.Options.URAccepted < cfg.Options.URAccepted {
|
2014-06-11 18:04:23 +00:00
|
|
|
// UR was disabled
|
2014-06-17 21:22:19 +00:00
|
|
|
newCfg.Options.URAccepted = -1
|
2014-06-11 18:04:23 +00:00
|
|
|
stopUsageReporting()
|
|
|
|
}
|
|
|
|
|
2014-06-07 02:00:46 +00:00
|
|
|
// Activate and save
|
|
|
|
|
2014-09-20 16:42:09 +00:00
|
|
|
configInSync = !config.ChangeRequiresRestart(cfg, newCfg)
|
2014-09-06 12:11:18 +00:00
|
|
|
newCfg.Location = cfg.Location
|
|
|
|
newCfg.Save()
|
2014-05-24 19:00:47 +00:00
|
|
|
cfg = newCfg
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetConfigInSync(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 22:58:14 +00:00
|
|
|
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostRestart(w http.ResponseWriter, r *http.Request) {
|
2014-05-13 00:15:18 +00:00
|
|
|
flushResponse(`{"ok": "restarting"}`, w)
|
2014-04-03 20:10:51 +00:00
|
|
|
go restart()
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostReset(w http.ResponseWriter, r *http.Request) {
|
2014-05-13 00:15:18 +00:00
|
|
|
flushResponse(`{"ok": "resetting repos"}`, w)
|
2014-04-03 20:10:51 +00:00
|
|
|
resetRepositories()
|
|
|
|
go restart()
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostShutdown(w http.ResponseWriter, r *http.Request) {
|
2014-05-13 00:15:18 +00:00
|
|
|
flushResponse(`{"ok": "shutting down"}`, w)
|
2014-05-11 23:16:27 +00:00
|
|
|
go shutdown()
|
|
|
|
}
|
|
|
|
|
2014-05-13 00:15:18 +00:00
|
|
|
func flushResponse(s string, w http.ResponseWriter) {
|
|
|
|
w.Write([]byte(s + "\n"))
|
|
|
|
f := w.(http.Flusher)
|
|
|
|
f.Flush()
|
|
|
|
}
|
|
|
|
|
2014-04-14 10:02:40 +00:00
|
|
|
var cpuUsagePercent [10]float64 // The last ten seconds
|
2014-03-02 22:58:14 +00:00
|
|
|
var cpuUsageLock sync.RWMutex
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetSystem(w http.ResponseWriter, r *http.Request) {
|
2014-03-02 22:58:14 +00:00
|
|
|
var m runtime.MemStats
|
|
|
|
runtime.ReadMemStats(&m)
|
|
|
|
|
|
|
|
res := make(map[string]interface{})
|
2014-06-29 23:42:03 +00:00
|
|
|
res["myID"] = myID.String()
|
2014-03-02 22:58:14 +00:00
|
|
|
res["goroutines"] = runtime.NumGoroutine()
|
|
|
|
res["alloc"] = m.Alloc
|
2014-08-05 20:14:11 +00:00
|
|
|
res["sys"] = m.Sys - m.HeapReleased
|
2014-05-24 11:22:09 +00:00
|
|
|
res["tilde"] = expandTilde("~")
|
2014-04-30 20:17:43 +00:00
|
|
|
if cfg.Options.GlobalAnnEnabled && discoverer != nil {
|
2014-04-16 15:36:09 +00:00
|
|
|
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
|
|
|
}
|
2014-03-02 22:58:14 +00:00
|
|
|
cpuUsageLock.RLock()
|
2014-04-14 10:02:40 +00:00
|
|
|
var cpusum float64
|
|
|
|
for _, p := range cpuUsagePercent {
|
|
|
|
cpusum += p
|
|
|
|
}
|
2014-03-02 22:58:14 +00:00
|
|
|
cpuUsageLock.RUnlock()
|
2014-04-14 10:02:40 +00:00
|
|
|
res["cpuPercent"] = cpusum / 10
|
2014-03-02 22:58:14 +00:00
|
|
|
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 22:58:14 +00:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetErrors(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 22:58:14 +00:00
|
|
|
guiErrorsMut.Lock()
|
2014-09-18 10:49:59 +00:00
|
|
|
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
|
2014-03-02 22:58:14 +00:00
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostError(w http.ResponseWriter, r *http.Request) {
|
|
|
|
bs, _ := ioutil.ReadAll(r.Body)
|
|
|
|
r.Body.Close()
|
2014-05-15 00:08:56 +00:00
|
|
|
showGuiError(0, string(bs))
|
2014-03-02 22:58:14 +00:00
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restClearErrors(w http.ResponseWriter, r *http.Request) {
|
2014-04-16 14:30:49 +00:00
|
|
|
guiErrorsMut.Lock()
|
2014-05-17 11:54:11 +00:00
|
|
|
guiErrors = []guiError{}
|
2014-04-16 14:30:49 +00:00
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
|
|
|
|
2014-05-15 00:08:56 +00:00
|
|
|
func showGuiError(l logger.LogLevel, err string) {
|
2014-03-02 22:58:14 +00:00
|
|
|
guiErrorsMut.Lock()
|
|
|
|
guiErrors = append(guiErrors, guiError{time.Now(), err})
|
|
|
|
if len(guiErrors) > 5 {
|
|
|
|
guiErrors = guiErrors[len(guiErrors)-5:]
|
|
|
|
}
|
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
2014-04-19 11:33:51 +00:00
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restPostDiscoveryHint(w http.ResponseWriter, r *http.Request) {
|
2014-05-13 00:50:54 +00:00
|
|
|
var qs = r.URL.Query()
|
|
|
|
var node = qs.Get("node")
|
|
|
|
var addr = qs.Get("addr")
|
|
|
|
if len(node) != 0 && len(addr) != 0 && discoverer != nil {
|
|
|
|
discoverer.Hint(node, []string{addr})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetDiscovery(w http.ResponseWriter, r *http.Request) {
|
2014-05-13 01:08:55 +00:00
|
|
|
json.NewEncoder(w).Encode(discoverer.All())
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-06-22 15:26:31 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-06-11 18:04:23 +00:00
|
|
|
json.NewEncoder(w).Encode(reportData(m))
|
|
|
|
}
|
|
|
|
|
2014-07-13 19:07:24 +00:00
|
|
|
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
2014-07-29 09:06:52 +00:00
|
|
|
sinceStr := qs.Get("since")
|
|
|
|
limitStr := qs.Get("limit")
|
|
|
|
since, _ := strconv.Atoi(sinceStr)
|
|
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
|
|
|
2014-08-19 22:30:32 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
2014-08-19 22:18:28 +00:00
|
|
|
// Flush before blocking, to indicate that we've received the request
|
|
|
|
// and that it should not be retried.
|
|
|
|
f := w.(http.Flusher)
|
|
|
|
f.Flush()
|
|
|
|
|
2014-07-29 09:06:52 +00:00
|
|
|
evs := eventSub.Since(since, nil)
|
|
|
|
if 0 < limit && limit < len(evs) {
|
|
|
|
evs = evs[len(evs)-limit:]
|
|
|
|
}
|
2014-07-13 19:07:24 +00:00
|
|
|
|
2014-07-29 09:06:52 +00:00
|
|
|
json.NewEncoder(w).Encode(evs)
|
2014-07-13 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
2014-07-14 08:45:29 +00:00
|
|
|
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
|
2014-07-31 14:01:23 +00:00
|
|
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
2014-07-14 08:45:29 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res := make(map[string]interface{})
|
|
|
|
res["running"] = Version
|
|
|
|
res["latest"] = rel.Tag
|
2014-07-31 14:01:23 +00:00
|
|
|
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == 1
|
2014-07-14 08:45:29 +00:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-18 08:00:02 +00:00
|
|
|
func restGetNodeID(w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
|
|
|
idStr := qs.Get("id")
|
|
|
|
id, err := protocol.NodeIDFromString(idStr)
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
if err == nil {
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"id": id.String(),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"error": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-26 20:30:29 +00:00
|
|
|
func restGetLang(w http.ResponseWriter, r *http.Request) {
|
|
|
|
lang := r.Header.Get("Accept-Language")
|
|
|
|
var langs []string
|
|
|
|
for _, l := range strings.Split(lang, ",") {
|
2014-08-14 15:04:17 +00:00
|
|
|
parts := strings.SplitN(l, ";", 2)
|
2014-08-28 11:23:23 +00:00
|
|
|
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
|
2014-07-26 20:30:29 +00:00
|
|
|
}
|
2014-08-05 07:38:38 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-07-26 20:30:29 +00:00
|
|
|
json.NewEncoder(w).Encode(langs)
|
|
|
|
}
|
|
|
|
|
2014-07-14 08:45:29 +00:00
|
|
|
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
2014-07-31 14:01:23 +00:00
|
|
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
2014-07-14 08:45:29 +00:00
|
|
|
if err != nil {
|
2014-08-17 08:28:36 +00:00
|
|
|
l.Warnln("getting latest release:", err)
|
2014-07-14 08:45:29 +00:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-07-31 14:01:23 +00:00
|
|
|
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
2014-08-07 13:57:20 +00:00
|
|
|
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
2014-07-31 14:01:23 +00:00
|
|
|
if err != nil {
|
2014-08-17 08:28:36 +00:00
|
|
|
l.Warnln("upgrading:", err)
|
2014-07-31 14:01:23 +00:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-13 14:25:39 +00:00
|
|
|
flushResponse(`{"ok": "restarting"}`, w)
|
|
|
|
l.Infoln("Upgrading")
|
|
|
|
stop <- exitUpgrading
|
2014-07-31 14:01:23 +00:00
|
|
|
}
|
2014-07-14 08:45:29 +00:00
|
|
|
}
|
|
|
|
|
2014-08-11 18:20:01 +00:00
|
|
|
func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
|
|
|
repo := qs.Get("repo")
|
|
|
|
sub := qs.Get("sub")
|
|
|
|
err := m.ScanRepoSub(repo, sub)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 19:40:29 +00:00
|
|
|
func getQR(w http.ResponseWriter, r *http.Request) {
|
2014-08-04 20:53:37 +00:00
|
|
|
var qs = r.URL.Query()
|
|
|
|
var text = qs.Get("text")
|
2014-07-05 19:40:29 +00:00
|
|
|
code, err := qr.Encode(text, qr.M)
|
2014-05-21 18:06:14 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "Invalid", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
w.Write(code.PNG())
|
|
|
|
}
|
|
|
|
|
2014-07-29 11:01:27 +00:00
|
|
|
func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
tot := map[string]float64{}
|
|
|
|
count := map[string]float64{}
|
|
|
|
|
|
|
|
for _, repo := range cfg.Repositories {
|
|
|
|
for _, node := range repo.NodeIDs() {
|
|
|
|
nodeStr := node.String()
|
|
|
|
if m.ConnectedTo(node) {
|
|
|
|
tot[nodeStr] += m.Completion(node, repo.ID)
|
|
|
|
} else {
|
|
|
|
tot[nodeStr] = 0
|
|
|
|
}
|
|
|
|
count[nodeStr]++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
comp := map[string]int{}
|
|
|
|
for node := range tot {
|
|
|
|
comp[node] = int(tot[node] / count[node])
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(comp)
|
|
|
|
}
|
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
func embeddedStatic(assetDir string) http.Handler {
|
2014-09-02 11:07:33 +00:00
|
|
|
assets := auto.Assets()
|
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
file := r.URL.Path
|
2014-05-22 14:12:19 +00:00
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
if file[0] == '/' {
|
|
|
|
file = file[1:]
|
|
|
|
}
|
2014-05-22 14:12:19 +00:00
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
if len(file) == 0 {
|
|
|
|
file = "index.html"
|
|
|
|
}
|
2014-05-22 14:12:19 +00:00
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
if assetDir != "" {
|
|
|
|
p := filepath.Join(assetDir, filepath.FromSlash(file))
|
|
|
|
_, err := os.Stat(p)
|
|
|
|
if err == nil {
|
|
|
|
http.ServeFile(w, r, p)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2014-05-22 14:12:19 +00:00
|
|
|
|
2014-09-02 11:07:33 +00:00
|
|
|
bs, ok := assets[file]
|
2014-07-22 18:11:36 +00:00
|
|
|
if !ok {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-04 06:24:42 +00:00
|
|
|
mtype := mimeTypeForFile(file)
|
2014-07-22 18:11:36 +00:00
|
|
|
if len(mtype) != 0 {
|
|
|
|
w.Header().Set("Content-Type", mtype)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
|
|
|
w.Header().Set("Last-Modified", modt)
|
2014-07-05 19:40:29 +00:00
|
|
|
|
2014-07-22 18:11:36 +00:00
|
|
|
w.Write(bs)
|
|
|
|
})
|
2014-05-22 14:12:19 +00:00
|
|
|
}
|
2014-09-04 06:24:42 +00:00
|
|
|
|
|
|
|
func mimeTypeForFile(file string) string {
|
|
|
|
// We use a built in table of the common types since the system
|
|
|
|
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
|
|
|
// to the system.
|
|
|
|
ext := filepath.Ext(file)
|
|
|
|
switch ext {
|
|
|
|
case ".htm", ".html":
|
|
|
|
return "text/html"
|
|
|
|
case ".css":
|
|
|
|
return "text/css"
|
|
|
|
case ".js":
|
|
|
|
return "application/javascript"
|
|
|
|
case ".json":
|
|
|
|
return "application/json"
|
|
|
|
case ".png":
|
|
|
|
return "image/png"
|
|
|
|
case ".ttf":
|
|
|
|
return "application/x-font-ttf"
|
2014-09-04 06:47:23 +00:00
|
|
|
case ".woff":
|
|
|
|
return "application/x-font-woff"
|
2014-09-04 06:24:42 +00:00
|
|
|
default:
|
|
|
|
return mime.TypeByExtension(ext)
|
|
|
|
}
|
|
|
|
}
|