gui.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. // Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
  2. // Use of this source code is governed by an MIT-style license that can be
  3. // found in the LICENSE file.
  4. package main
  5. import (
  6. "bytes"
  7. "encoding/base64"
  8. "encoding/json"
  9. "fmt"
  10. "io/ioutil"
  11. "log"
  12. "math/rand"
  13. "mime"
  14. "net"
  15. "net/http"
  16. "path/filepath"
  17. "reflect"
  18. "runtime"
  19. "sync"
  20. "time"
  21. "crypto/tls"
  22. "code.google.com/p/go.crypto/bcrypt"
  23. "github.com/calmh/syncthing/auto"
  24. "github.com/calmh/syncthing/config"
  25. "github.com/calmh/syncthing/logger"
  26. "github.com/calmh/syncthing/model"
  27. "github.com/codegangsta/martini"
  28. "github.com/vitrun/qart/qr"
  29. )
  30. type guiError struct {
  31. Time time.Time
  32. Error string
  33. }
  34. var (
  35. configInSync = true
  36. guiErrors = []guiError{}
  37. guiErrorsMut sync.Mutex
  38. static func(http.ResponseWriter, *http.Request, *log.Logger)
  39. apiKey string
  40. )
  41. const (
  42. unchangedPassword = "--password-unchanged--"
  43. )
  44. func init() {
  45. l.AddHandler(logger.LevelWarn, showGuiError)
  46. }
  47. func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
  48. var listener net.Listener
  49. var err error
  50. if cfg.UseTLS {
  51. cert, err := loadCert(confDir, "https-")
  52. if err != nil {
  53. l.Infoln("Loading HTTPS certificate:", err)
  54. l.Infoln("Creating new HTTPS certificate", err)
  55. newCertificate(confDir, "https-")
  56. cert, err = loadCert(confDir, "https-")
  57. }
  58. if err != nil {
  59. return err
  60. }
  61. tlsCfg := &tls.Config{
  62. Certificates: []tls.Certificate{cert},
  63. ServerName: "syncthing",
  64. }
  65. listener, err = tls.Listen("tcp", cfg.Address, tlsCfg)
  66. if err != nil {
  67. return err
  68. }
  69. } else {
  70. listener, err = net.Listen("tcp", cfg.Address)
  71. if err != nil {
  72. return err
  73. }
  74. }
  75. if len(assetDir) > 0 {
  76. static = martini.Static(assetDir).(func(http.ResponseWriter, *http.Request, *log.Logger))
  77. } else {
  78. static = embeddedStatic()
  79. }
  80. router := martini.NewRouter()
  81. router.Get("/", getRoot)
  82. router.Get("/rest/version", restGetVersion)
  83. router.Get("/rest/model", restGetModel)
  84. router.Get("/rest/need", restGetNeed)
  85. router.Get("/rest/connections", restGetConnections)
  86. router.Get("/rest/config", restGetConfig)
  87. router.Get("/rest/config/sync", restGetConfigInSync)
  88. router.Get("/rest/system", restGetSystem)
  89. router.Get("/rest/errors", restGetErrors)
  90. router.Get("/rest/discovery", restGetDiscovery)
  91. router.Get("/rest/report", restGetReport)
  92. router.Get("/qr/:text", getQR)
  93. router.Post("/rest/config", restPostConfig)
  94. router.Post("/rest/restart", restPostRestart)
  95. router.Post("/rest/reset", restPostReset)
  96. router.Post("/rest/shutdown", restPostShutdown)
  97. router.Post("/rest/error", restPostError)
  98. router.Post("/rest/error/clear", restClearErrors)
  99. router.Post("/rest/discovery/hint", restPostDiscoveryHint)
  100. mr := martini.New()
  101. mr.Use(csrfMiddleware)
  102. if len(cfg.User) > 0 && len(cfg.Password) > 0 {
  103. mr.Use(basic(cfg.User, cfg.Password))
  104. }
  105. mr.Use(static)
  106. mr.Use(martini.Recovery())
  107. mr.Use(restMiddleware)
  108. mr.Action(router.Handle)
  109. mr.Map(m)
  110. apiKey = cfg.APIKey
  111. loadCsrfTokens()
  112. go http.Serve(listener, mr)
  113. return nil
  114. }
  115. func getRoot(w http.ResponseWriter, r *http.Request) {
  116. r.URL.Path = "/index.html"
  117. static(w, r, nil)
  118. }
  119. func restMiddleware(w http.ResponseWriter, r *http.Request) {
  120. if len(r.URL.Path) >= 6 && r.URL.Path[:6] == "/rest/" {
  121. w.Header().Set("Cache-Control", "no-cache")
  122. }
  123. }
  124. func restGetVersion() string {
  125. return Version
  126. }
  127. func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
  128. var qs = r.URL.Query()
  129. var repo = qs.Get("repo")
  130. var res = make(map[string]interface{})
  131. for _, cr := range cfg.Repositories {
  132. if cr.ID == repo {
  133. res["invalid"] = cr.Invalid
  134. break
  135. }
  136. }
  137. globalFiles, globalDeleted, globalBytes := m.GlobalSize(repo)
  138. res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes
  139. localFiles, localDeleted, localBytes := m.LocalSize(repo)
  140. res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
  141. needFiles, needBytes := m.NeedSize(repo)
  142. res["needFiles"], res["needBytes"] = needFiles, needBytes
  143. res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
  144. res["state"] = m.State(repo)
  145. w.Header().Set("Content-Type", "application/json")
  146. json.NewEncoder(w).Encode(res)
  147. }
  148. func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
  149. var qs = r.URL.Query()
  150. var repo = qs.Get("repo")
  151. files := m.NeedFilesRepo(repo)
  152. w.Header().Set("Content-Type", "application/json")
  153. json.NewEncoder(w).Encode(files)
  154. }
  155. func restGetConnections(m *model.Model, w http.ResponseWriter) {
  156. var res = m.ConnectionStats()
  157. w.Header().Set("Content-Type", "application/json")
  158. json.NewEncoder(w).Encode(res)
  159. }
  160. func restGetConfig(w http.ResponseWriter) {
  161. encCfg := cfg
  162. if encCfg.GUI.Password != "" {
  163. encCfg.GUI.Password = unchangedPassword
  164. }
  165. json.NewEncoder(w).Encode(encCfg)
  166. }
  167. func restPostConfig(req *http.Request, m *model.Model) {
  168. var newCfg config.Configuration
  169. err := json.NewDecoder(req.Body).Decode(&newCfg)
  170. if err != nil {
  171. l.Warnln(err)
  172. } else {
  173. if newCfg.GUI.Password == "" {
  174. // Leave it empty
  175. } else if newCfg.GUI.Password == unchangedPassword {
  176. newCfg.GUI.Password = cfg.GUI.Password
  177. } else {
  178. hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
  179. if err != nil {
  180. l.Warnln(err)
  181. } else {
  182. newCfg.GUI.Password = string(hash)
  183. }
  184. }
  185. // Figure out if any changes require a restart
  186. if len(cfg.Repositories) != len(newCfg.Repositories) {
  187. configInSync = false
  188. } else {
  189. om := cfg.RepoMap()
  190. nm := newCfg.RepoMap()
  191. for id := range om {
  192. if !reflect.DeepEqual(om[id], nm[id]) {
  193. configInSync = false
  194. break
  195. }
  196. }
  197. }
  198. if len(cfg.Nodes) != len(newCfg.Nodes) {
  199. configInSync = false
  200. } else {
  201. om := cfg.NodeMap()
  202. nm := newCfg.NodeMap()
  203. for k := range om {
  204. if _, ok := nm[k]; !ok {
  205. configInSync = false
  206. break
  207. }
  208. }
  209. }
  210. if newCfg.Options.UREnabled && !cfg.Options.UREnabled {
  211. // UR was enabled
  212. cfg.Options.UREnabled = true
  213. cfg.Options.URDeclined = false
  214. cfg.Options.URAccepted = usageReportVersion
  215. // Set the corresponding options in newCfg so we don't trigger the restart check if this was the only option change
  216. newCfg.Options.URDeclined = false
  217. newCfg.Options.URAccepted = usageReportVersion
  218. err := sendUsageReport(m)
  219. if err != nil {
  220. l.Infoln("Usage report:", err)
  221. }
  222. go usageReportingLoop(m)
  223. } else if !newCfg.Options.UREnabled && cfg.Options.UREnabled {
  224. // UR was disabled
  225. cfg.Options.UREnabled = false
  226. cfg.Options.URDeclined = true
  227. cfg.Options.URAccepted = 0
  228. // Set the corresponding options in newCfg so we don't trigger the restart check if this was the only option change
  229. newCfg.Options.URDeclined = true
  230. newCfg.Options.URAccepted = 0
  231. stopUsageReporting()
  232. } else {
  233. cfg.Options.URDeclined = newCfg.Options.URDeclined
  234. }
  235. if !reflect.DeepEqual(cfg.Options, newCfg.Options) {
  236. configInSync = false
  237. }
  238. // Activate and save
  239. cfg = newCfg
  240. saveConfig()
  241. }
  242. }
  243. func restGetConfigInSync(w http.ResponseWriter) {
  244. json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
  245. }
  246. func restPostRestart(w http.ResponseWriter) {
  247. flushResponse(`{"ok": "restarting"}`, w)
  248. go restart()
  249. }
  250. func restPostReset(w http.ResponseWriter) {
  251. flushResponse(`{"ok": "resetting repos"}`, w)
  252. resetRepositories()
  253. go restart()
  254. }
  255. func restPostShutdown(w http.ResponseWriter) {
  256. flushResponse(`{"ok": "shutting down"}`, w)
  257. go shutdown()
  258. }
  259. func flushResponse(s string, w http.ResponseWriter) {
  260. w.Write([]byte(s + "\n"))
  261. f := w.(http.Flusher)
  262. f.Flush()
  263. }
  264. var cpuUsagePercent [10]float64 // The last ten seconds
  265. var cpuUsageLock sync.RWMutex
  266. func restGetSystem(w http.ResponseWriter) {
  267. var m runtime.MemStats
  268. runtime.ReadMemStats(&m)
  269. res := make(map[string]interface{})
  270. res["myID"] = myID
  271. res["goroutines"] = runtime.NumGoroutine()
  272. res["alloc"] = m.Alloc
  273. res["sys"] = m.Sys
  274. res["tilde"] = expandTilde("~")
  275. if cfg.Options.GlobalAnnEnabled && discoverer != nil {
  276. res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
  277. }
  278. cpuUsageLock.RLock()
  279. var cpusum float64
  280. for _, p := range cpuUsagePercent {
  281. cpusum += p
  282. }
  283. cpuUsageLock.RUnlock()
  284. res["cpuPercent"] = cpusum / 10
  285. w.Header().Set("Content-Type", "application/json")
  286. json.NewEncoder(w).Encode(res)
  287. }
  288. func restGetErrors(w http.ResponseWriter) {
  289. guiErrorsMut.Lock()
  290. json.NewEncoder(w).Encode(guiErrors)
  291. guiErrorsMut.Unlock()
  292. }
  293. func restPostError(req *http.Request) {
  294. bs, _ := ioutil.ReadAll(req.Body)
  295. req.Body.Close()
  296. showGuiError(0, string(bs))
  297. }
  298. func restClearErrors() {
  299. guiErrorsMut.Lock()
  300. guiErrors = []guiError{}
  301. guiErrorsMut.Unlock()
  302. }
  303. func showGuiError(l logger.LogLevel, err string) {
  304. guiErrorsMut.Lock()
  305. guiErrors = append(guiErrors, guiError{time.Now(), err})
  306. if len(guiErrors) > 5 {
  307. guiErrors = guiErrors[len(guiErrors)-5:]
  308. }
  309. guiErrorsMut.Unlock()
  310. }
  311. func restPostDiscoveryHint(r *http.Request) {
  312. var qs = r.URL.Query()
  313. var node = qs.Get("node")
  314. var addr = qs.Get("addr")
  315. if len(node) != 0 && len(addr) != 0 && discoverer != nil {
  316. discoverer.Hint(node, []string{addr})
  317. }
  318. }
  319. func restGetDiscovery(w http.ResponseWriter) {
  320. json.NewEncoder(w).Encode(discoverer.All())
  321. }
  322. func restGetReport(w http.ResponseWriter, m *model.Model) {
  323. json.NewEncoder(w).Encode(reportData(m))
  324. }
  325. func getQR(w http.ResponseWriter, params martini.Params) {
  326. code, err := qr.Encode(params["text"], qr.M)
  327. if err != nil {
  328. http.Error(w, "Invalid", 500)
  329. return
  330. }
  331. w.Header().Set("Content-Type", "image/png")
  332. w.Write(code.PNG())
  333. }
  334. func basic(username string, passhash string) http.HandlerFunc {
  335. return func(res http.ResponseWriter, req *http.Request) {
  336. if validAPIKey(req.Header.Get("X-API-Key")) {
  337. return
  338. }
  339. error := func() {
  340. time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
  341. res.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
  342. http.Error(res, "Not Authorized", http.StatusUnauthorized)
  343. }
  344. hdr := req.Header.Get("Authorization")
  345. if len(hdr) < len("Basic ") || hdr[:6] != "Basic " {
  346. error()
  347. return
  348. }
  349. hdr = hdr[6:]
  350. bs, err := base64.StdEncoding.DecodeString(hdr)
  351. if err != nil {
  352. error()
  353. return
  354. }
  355. fields := bytes.SplitN(bs, []byte(":"), 2)
  356. if len(fields) != 2 {
  357. error()
  358. return
  359. }
  360. if string(fields[0]) != username {
  361. error()
  362. return
  363. }
  364. if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil {
  365. error()
  366. return
  367. }
  368. }
  369. }
  370. func validAPIKey(k string) bool {
  371. return len(apiKey) > 0 && k == apiKey
  372. }
  373. func embeddedStatic() func(http.ResponseWriter, *http.Request, *log.Logger) {
  374. var modt = time.Now().UTC().Format(http.TimeFormat)
  375. return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
  376. file := req.URL.Path
  377. if file[0] == '/' {
  378. file = file[1:]
  379. }
  380. bs, ok := auto.Assets[file]
  381. if !ok {
  382. return
  383. }
  384. mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
  385. if len(mtype) != 0 {
  386. res.Header().Set("Content-Type", mtype)
  387. }
  388. res.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
  389. res.Header().Set("Last-Modified", modt)
  390. res.Write(bs)
  391. }
  392. }