gui.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "io/ioutil"
  7. "log"
  8. "math/rand"
  9. "net"
  10. "net/http"
  11. "runtime"
  12. "sync"
  13. "time"
  14. "code.google.com/p/go.crypto/bcrypt"
  15. "github.com/calmh/syncthing/config"
  16. "github.com/calmh/syncthing/logger"
  17. "github.com/calmh/syncthing/model"
  18. "github.com/codegangsta/martini"
  19. )
  20. type guiError struct {
  21. Time time.Time
  22. Error string
  23. }
  24. var (
  25. configInSync = true
  26. guiErrors = []guiError{}
  27. guiErrorsMut sync.Mutex
  28. static = embeddedStatic()
  29. staticFunc = static.(func(http.ResponseWriter, *http.Request, *log.Logger))
  30. )
  31. const (
  32. unchangedPassword = "--password-unchanged--"
  33. )
  34. func init() {
  35. l.AddHandler(logger.LevelWarn, showGuiError)
  36. }
  37. func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
  38. listener, err := net.Listen("tcp", cfg.Address)
  39. if err != nil {
  40. return err
  41. }
  42. router := martini.NewRouter()
  43. router.Get("/", getRoot)
  44. router.Get("/rest/version", restGetVersion)
  45. router.Get("/rest/model", restGetModel)
  46. router.Get("/rest/need", restGetNeed)
  47. router.Get("/rest/connections", restGetConnections)
  48. router.Get("/rest/config", restGetConfig)
  49. router.Get("/rest/config/sync", restGetConfigInSync)
  50. router.Get("/rest/system", restGetSystem)
  51. router.Get("/rest/errors", restGetErrors)
  52. router.Get("/rest/discovery", restGetDiscovery)
  53. router.Post("/rest/config", restPostConfig)
  54. router.Post("/rest/restart", restPostRestart)
  55. router.Post("/rest/reset", restPostReset)
  56. router.Post("/rest/shutdown", restPostShutdown)
  57. router.Post("/rest/error", restPostError)
  58. router.Post("/rest/error/clear", restClearErrors)
  59. router.Post("/rest/discovery/hint", restPostDiscoveryHint)
  60. mr := martini.New()
  61. if len(cfg.User) > 0 && len(cfg.Password) > 0 {
  62. mr.Use(basic(cfg.User, cfg.Password))
  63. }
  64. mr.Use(static)
  65. mr.Use(martini.Recovery())
  66. mr.Use(restMiddleware)
  67. mr.Action(router.Handle)
  68. mr.Map(m)
  69. go http.Serve(listener, mr)
  70. return nil
  71. }
  72. func getRoot(w http.ResponseWriter, r *http.Request) {
  73. r.URL.Path = "/index.html"
  74. staticFunc(w, r, nil)
  75. }
  76. func restMiddleware(w http.ResponseWriter, r *http.Request) {
  77. if len(r.URL.Path) >= 6 && r.URL.Path[:6] == "/rest/" {
  78. w.Header().Set("Cache-Control", "no-cache")
  79. }
  80. }
  81. func restGetVersion() string {
  82. return Version
  83. }
  84. func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
  85. var qs = r.URL.Query()
  86. var repo = qs.Get("repo")
  87. var res = make(map[string]interface{})
  88. for _, cr := range cfg.Repositories {
  89. if cr.ID == repo {
  90. res["invalid"] = cr.Invalid
  91. break
  92. }
  93. }
  94. globalFiles, globalDeleted, globalBytes := m.GlobalSize(repo)
  95. res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes
  96. localFiles, localDeleted, localBytes := m.LocalSize(repo)
  97. res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
  98. needFiles, needBytes := m.NeedSize(repo)
  99. res["needFiles"], res["needBytes"] = needFiles, needBytes
  100. res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
  101. res["state"] = m.State(repo)
  102. w.Header().Set("Content-Type", "application/json")
  103. json.NewEncoder(w).Encode(res)
  104. }
  105. func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
  106. var qs = r.URL.Query()
  107. var repo = qs.Get("repo")
  108. files := m.NeedFilesRepo(repo)
  109. w.Header().Set("Content-Type", "application/json")
  110. json.NewEncoder(w).Encode(files)
  111. }
  112. func restGetConnections(m *model.Model, w http.ResponseWriter) {
  113. var res = m.ConnectionStats()
  114. w.Header().Set("Content-Type", "application/json")
  115. json.NewEncoder(w).Encode(res)
  116. }
  117. func restGetConfig(w http.ResponseWriter) {
  118. encCfg := cfg
  119. if encCfg.GUI.Password != "" {
  120. encCfg.GUI.Password = unchangedPassword
  121. }
  122. json.NewEncoder(w).Encode(encCfg)
  123. }
  124. func restPostConfig(req *http.Request) {
  125. var prevPassHash = cfg.GUI.Password
  126. err := json.NewDecoder(req.Body).Decode(&cfg)
  127. if err != nil {
  128. l.Warnln(err)
  129. } else {
  130. if cfg.GUI.Password == "" {
  131. // Leave it empty
  132. } else if cfg.GUI.Password != unchangedPassword {
  133. hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
  134. if err != nil {
  135. l.Warnln(err)
  136. } else {
  137. cfg.GUI.Password = string(hash)
  138. }
  139. } else {
  140. cfg.GUI.Password = prevPassHash
  141. }
  142. saveConfig()
  143. configInSync = false
  144. }
  145. }
  146. func restGetConfigInSync(w http.ResponseWriter) {
  147. json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
  148. }
  149. func restPostRestart(w http.ResponseWriter) {
  150. flushResponse(`{"ok": "restarting"}`, w)
  151. go restart()
  152. }
  153. func restPostReset(w http.ResponseWriter) {
  154. flushResponse(`{"ok": "resetting repos"}`, w)
  155. resetRepositories()
  156. go restart()
  157. }
  158. func restPostShutdown(w http.ResponseWriter) {
  159. flushResponse(`{"ok": "shutting down"}`, w)
  160. go shutdown()
  161. }
  162. func flushResponse(s string, w http.ResponseWriter) {
  163. w.Write([]byte(s + "\n"))
  164. f := w.(http.Flusher)
  165. f.Flush()
  166. }
  167. var cpuUsagePercent [10]float64 // The last ten seconds
  168. var cpuUsageLock sync.RWMutex
  169. func restGetSystem(w http.ResponseWriter) {
  170. var m runtime.MemStats
  171. runtime.ReadMemStats(&m)
  172. res := make(map[string]interface{})
  173. res["myID"] = myID
  174. res["goroutines"] = runtime.NumGoroutine()
  175. res["alloc"] = m.Alloc
  176. res["sys"] = m.Sys
  177. if cfg.Options.GlobalAnnEnabled && discoverer != nil {
  178. res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
  179. }
  180. cpuUsageLock.RLock()
  181. var cpusum float64
  182. for _, p := range cpuUsagePercent {
  183. cpusum += p
  184. }
  185. cpuUsageLock.RUnlock()
  186. res["cpuPercent"] = cpusum / 10
  187. w.Header().Set("Content-Type", "application/json")
  188. json.NewEncoder(w).Encode(res)
  189. }
  190. func restGetErrors(w http.ResponseWriter) {
  191. guiErrorsMut.Lock()
  192. json.NewEncoder(w).Encode(guiErrors)
  193. guiErrorsMut.Unlock()
  194. }
  195. func restPostError(req *http.Request) {
  196. bs, _ := ioutil.ReadAll(req.Body)
  197. req.Body.Close()
  198. showGuiError(0, string(bs))
  199. }
  200. func restClearErrors() {
  201. guiErrorsMut.Lock()
  202. guiErrors = []guiError{}
  203. guiErrorsMut.Unlock()
  204. }
  205. func showGuiError(l logger.LogLevel, err string) {
  206. guiErrorsMut.Lock()
  207. guiErrors = append(guiErrors, guiError{time.Now(), err})
  208. if len(guiErrors) > 5 {
  209. guiErrors = guiErrors[len(guiErrors)-5:]
  210. }
  211. guiErrorsMut.Unlock()
  212. }
  213. func restPostDiscoveryHint(r *http.Request) {
  214. var qs = r.URL.Query()
  215. var node = qs.Get("node")
  216. var addr = qs.Get("addr")
  217. if len(node) != 0 && len(addr) != 0 && discoverer != nil {
  218. discoverer.Hint(node, []string{addr})
  219. }
  220. }
  221. func restGetDiscovery(w http.ResponseWriter) {
  222. json.NewEncoder(w).Encode(discoverer.All())
  223. }
  224. func basic(username string, passhash string) http.HandlerFunc {
  225. return func(res http.ResponseWriter, req *http.Request) {
  226. error := func() {
  227. time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
  228. res.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
  229. http.Error(res, "Not Authorized", http.StatusUnauthorized)
  230. }
  231. hdr := req.Header.Get("Authorization")
  232. if len(hdr) < len("Basic ") || hdr[:6] != "Basic " {
  233. error()
  234. return
  235. }
  236. hdr = hdr[6:]
  237. bs, err := base64.StdEncoding.DecodeString(hdr)
  238. if err != nil {
  239. error()
  240. return
  241. }
  242. fields := bytes.SplitN(bs, []byte(":"), 2)
  243. if len(fields) != 2 {
  244. error()
  245. return
  246. }
  247. if string(fields[0]) != username {
  248. error()
  249. return
  250. }
  251. if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil {
  252. error()
  253. return
  254. }
  255. }
  256. }