github.com/kayoticsully/syncthing@v0.8.9-0.20140724133906-c45a2fdc03f8/cmd/syncthing/gui.go (about) 1 // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file). 2 // All rights reserved. Use of this source code is governed by an MIT-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io/ioutil" 13 "log" 14 "math/rand" 15 "mime" 16 "net" 17 "net/http" 18 "os" 19 "path/filepath" 20 "reflect" 21 "runtime" 22 "strconv" 23 "strings" 24 "sync" 25 "time" 26 27 "crypto/tls" 28 "code.google.com/p/go.crypto/bcrypt" 29 "github.com/calmh/syncthing/auto" 30 "github.com/calmh/syncthing/config" 31 "github.com/calmh/syncthing/events" 32 "github.com/calmh/syncthing/logger" 33 "github.com/calmh/syncthing/model" 34 "github.com/calmh/syncthing/protocol" 35 "github.com/vitrun/qart/qr" 36 ) 37 38 type guiError struct { 39 Time time.Time 40 Error string 41 } 42 43 var ( 44 configInSync = true 45 guiErrors = []guiError{} 46 guiErrorsMut sync.Mutex 47 static func(http.ResponseWriter, *http.Request, *log.Logger) 48 apiKey string 49 modt = time.Now().UTC().Format(http.TimeFormat) 50 eventSub = events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000) 51 ) 52 53 const ( 54 unchangedPassword = "--password-unchanged--" 55 ) 56 57 func init() { 58 l.AddHandler(logger.LevelWarn, showGuiError) 59 } 60 61 func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error { 62 var listener net.Listener 63 var err error 64 if cfg.UseTLS { 65 cert, err := loadCert(confDir, "https-") 66 if err != nil { 67 l.Infoln("Loading HTTPS certificate:", err) 68 l.Infoln("Creating new HTTPS certificate") 69 newCertificate(confDir, "https-") 70 cert, err = loadCert(confDir, "https-") 71 } 72 if err != nil { 73 return err 74 } 75 tlsCfg := &tls.Config{ 76 Certificates: []tls.Certificate{cert}, 77 ServerName: "syncthing", 78 } 79 listener, err = tls.Listen("tcp", cfg.Address, tlsCfg) 80 if err != nil { 81 return err 82 } 83 } else { 84 listener, err = net.Listen("tcp", cfg.Address) 85 if err != nil { 86 return err 87 } 88 } 89 90 apiKey = cfg.APIKey 91 loadCsrfTokens() 92 93 // The GET handlers 94 getRestMux := http.NewServeMux() 95 getRestMux.HandleFunc("/rest/version", restGetVersion) 96 getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel)) 97 getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion)) 98 getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed)) 99 getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections)) 100 getRestMux.HandleFunc("/rest/config", restGetConfig) 101 getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync) 102 getRestMux.HandleFunc("/rest/system", restGetSystem) 103 getRestMux.HandleFunc("/rest/errors", restGetErrors) 104 getRestMux.HandleFunc("/rest/discovery", restGetDiscovery) 105 getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport)) 106 getRestMux.HandleFunc("/rest/events", restGetEvents) 107 getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade) 108 getRestMux.HandleFunc("/rest/nodeid", restGetNodeID) 109 110 // The POST handlers 111 postRestMux := http.NewServeMux() 112 postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig)) 113 postRestMux.HandleFunc("/rest/restart", restPostRestart) 114 postRestMux.HandleFunc("/rest/reset", restPostReset) 115 postRestMux.HandleFunc("/rest/shutdown", restPostShutdown) 116 postRestMux.HandleFunc("/rest/error", restPostError) 117 postRestMux.HandleFunc("/rest/error/clear", restClearErrors) 118 postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint) 119 postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride)) 120 postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade) 121 122 // A handler that splits requests between the two above and disables 123 // caching 124 restMux := noCacheMiddleware(getPostHandler(getRestMux, postRestMux)) 125 126 // The main routing handler 127 mux := http.NewServeMux() 128 mux.Handle("/rest/", restMux) 129 mux.HandleFunc("/qr/", getQR) 130 131 // Serve compiled in assets unless an asset directory was set (for development) 132 mux.Handle("/", embeddedStatic(assetDir)) 133 134 // Wrap everything in CSRF protection. The /rest prefix should be 135 // protected, other requests will grant cookies. 136 handler := csrfMiddleware("/rest", mux) 137 138 // Wrap everything in basic auth, if user/password is set. 139 if len(cfg.User) > 0 { 140 handler = basicAuthMiddleware(cfg.User, cfg.Password, handler) 141 } 142 143 go http.Serve(listener, handler) 144 return nil 145 } 146 147 func getPostHandler(get, post http.Handler) http.Handler { 148 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 149 switch r.Method { 150 case "GET": 151 get.ServeHTTP(w, r) 152 case "POST": 153 post.ServeHTTP(w, r) 154 default: 155 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 156 } 157 }) 158 } 159 160 func noCacheMiddleware(h http.Handler) http.Handler { 161 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 162 w.Header().Set("Cache-Control", "no-cache") 163 h.ServeHTTP(w, r) 164 }) 165 } 166 167 func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc { 168 return func(w http.ResponseWriter, r *http.Request) { 169 h(m, w, r) 170 } 171 } 172 173 func restGetVersion(w http.ResponseWriter, r *http.Request) { 174 w.Write([]byte(Version)) 175 } 176 177 func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) { 178 var qs = r.URL.Query() 179 var repo = qs.Get("repo") 180 var res = make(map[string]interface{}) 181 182 res["version"] = m.LocalVersion(repo) 183 184 w.Header().Set("Content-Type", "application/json; charset=utf-8") 185 json.NewEncoder(w).Encode(res) 186 } 187 188 func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) { 189 var qs = r.URL.Query() 190 var repo = qs.Get("repo") 191 var res = make(map[string]interface{}) 192 193 for _, cr := range cfg.Repositories { 194 if cr.ID == repo { 195 res["invalid"] = cr.Invalid 196 break 197 } 198 } 199 200 globalFiles, globalDeleted, globalBytes := m.GlobalSize(repo) 201 res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes 202 203 localFiles, localDeleted, localBytes := m.LocalSize(repo) 204 res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes 205 206 needFiles, needBytes := m.NeedSize(repo) 207 res["needFiles"], res["needBytes"] = needFiles, needBytes 208 209 res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes 210 211 res["state"], res["stateChanged"] = m.State(repo) 212 res["version"] = m.LocalVersion(repo) 213 214 w.Header().Set("Content-Type", "application/json; charset=utf-8") 215 json.NewEncoder(w).Encode(res) 216 } 217 218 func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) { 219 var qs = r.URL.Query() 220 var repo = qs.Get("repo") 221 m.Override(repo) 222 } 223 224 func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) { 225 var qs = r.URL.Query() 226 var repo = qs.Get("repo") 227 228 files := m.NeedFilesRepo(repo) 229 230 w.Header().Set("Content-Type", "application/json; charset=utf-8") 231 json.NewEncoder(w).Encode(files) 232 } 233 234 func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request) { 235 var res = m.ConnectionStats() 236 w.Header().Set("Content-Type", "application/json; charset=utf-8") 237 json.NewEncoder(w).Encode(res) 238 } 239 240 func restGetConfig(w http.ResponseWriter, r *http.Request) { 241 encCfg := cfg 242 if encCfg.GUI.Password != "" { 243 encCfg.GUI.Password = unchangedPassword 244 } 245 w.Header().Set("Content-Type", "application/json; charset=utf-8") 246 json.NewEncoder(w).Encode(encCfg) 247 } 248 249 func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) { 250 var newCfg config.Configuration 251 err := json.NewDecoder(r.Body).Decode(&newCfg) 252 if err != nil { 253 l.Warnln(err) 254 } else { 255 if newCfg.GUI.Password == "" { 256 // Leave it empty 257 } else if newCfg.GUI.Password == unchangedPassword { 258 newCfg.GUI.Password = cfg.GUI.Password 259 } else { 260 hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0) 261 if err != nil { 262 l.Warnln(err) 263 } else { 264 newCfg.GUI.Password = string(hash) 265 } 266 } 267 268 // Figure out if any changes require a restart 269 270 if len(cfg.Repositories) != len(newCfg.Repositories) { 271 configInSync = false 272 } else { 273 om := cfg.RepoMap() 274 nm := newCfg.RepoMap() 275 for id := range om { 276 if !reflect.DeepEqual(om[id], nm[id]) { 277 configInSync = false 278 break 279 } 280 } 281 } 282 283 if len(cfg.Nodes) != len(newCfg.Nodes) { 284 configInSync = false 285 } else { 286 om := cfg.NodeMap() 287 nm := newCfg.NodeMap() 288 for k := range om { 289 if _, ok := nm[k]; !ok { 290 configInSync = false 291 break 292 } 293 } 294 } 295 296 if newCfg.Options.URAccepted > cfg.Options.URAccepted { 297 // UR was enabled 298 newCfg.Options.URAccepted = usageReportVersion 299 err := sendUsageReport(m) 300 if err != nil { 301 l.Infoln("Usage report:", err) 302 } 303 go usageReportingLoop(m) 304 } else if newCfg.Options.URAccepted < cfg.Options.URAccepted { 305 // UR was disabled 306 newCfg.Options.URAccepted = -1 307 stopUsageReporting() 308 } 309 310 if !reflect.DeepEqual(cfg.Options, newCfg.Options) || !reflect.DeepEqual(cfg.GUI, newCfg.GUI) { 311 configInSync = false 312 } 313 314 // Activate and save 315 316 cfg = newCfg 317 saveConfig() 318 } 319 } 320 321 func restGetConfigInSync(w http.ResponseWriter, r *http.Request) { 322 w.Header().Set("Content-Type", "application/json; charset=utf-8") 323 json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync}) 324 } 325 326 func restPostRestart(w http.ResponseWriter, r *http.Request) { 327 flushResponse(`{"ok": "restarting"}`, w) 328 go restart() 329 } 330 331 func restPostReset(w http.ResponseWriter, r *http.Request) { 332 flushResponse(`{"ok": "resetting repos"}`, w) 333 resetRepositories() 334 go restart() 335 } 336 337 func restPostShutdown(w http.ResponseWriter, r *http.Request) { 338 flushResponse(`{"ok": "shutting down"}`, w) 339 go shutdown() 340 } 341 342 func flushResponse(s string, w http.ResponseWriter) { 343 w.Write([]byte(s + "\n")) 344 f := w.(http.Flusher) 345 f.Flush() 346 } 347 348 var cpuUsagePercent [10]float64 // The last ten seconds 349 var cpuUsageLock sync.RWMutex 350 351 func restGetSystem(w http.ResponseWriter, r *http.Request) { 352 var m runtime.MemStats 353 runtime.ReadMemStats(&m) 354 355 res := make(map[string]interface{}) 356 res["myID"] = myID.String() 357 res["goroutines"] = runtime.NumGoroutine() 358 res["alloc"] = m.Alloc 359 res["sys"] = m.Sys 360 res["tilde"] = expandTilde("~") 361 if cfg.Options.GlobalAnnEnabled && discoverer != nil { 362 res["extAnnounceOK"] = discoverer.ExtAnnounceOK() 363 } 364 cpuUsageLock.RLock() 365 var cpusum float64 366 for _, p := range cpuUsagePercent { 367 cpusum += p 368 } 369 cpuUsageLock.RUnlock() 370 res["cpuPercent"] = cpusum / 10 371 372 w.Header().Set("Content-Type", "application/json; charset=utf-8") 373 json.NewEncoder(w).Encode(res) 374 } 375 376 func restGetErrors(w http.ResponseWriter, r *http.Request) { 377 w.Header().Set("Content-Type", "application/json; charset=utf-8") 378 guiErrorsMut.Lock() 379 json.NewEncoder(w).Encode(guiErrors) 380 guiErrorsMut.Unlock() 381 } 382 383 func restPostError(w http.ResponseWriter, r *http.Request) { 384 bs, _ := ioutil.ReadAll(r.Body) 385 r.Body.Close() 386 showGuiError(0, string(bs)) 387 } 388 389 func restClearErrors(w http.ResponseWriter, r *http.Request) { 390 guiErrorsMut.Lock() 391 guiErrors = []guiError{} 392 guiErrorsMut.Unlock() 393 } 394 395 func showGuiError(l logger.LogLevel, err string) { 396 guiErrorsMut.Lock() 397 guiErrors = append(guiErrors, guiError{time.Now(), err}) 398 if len(guiErrors) > 5 { 399 guiErrors = guiErrors[len(guiErrors)-5:] 400 } 401 guiErrorsMut.Unlock() 402 } 403 404 func restPostDiscoveryHint(w http.ResponseWriter, r *http.Request) { 405 var qs = r.URL.Query() 406 var node = qs.Get("node") 407 var addr = qs.Get("addr") 408 if len(node) != 0 && len(addr) != 0 && discoverer != nil { 409 discoverer.Hint(node, []string{addr}) 410 } 411 } 412 413 func restGetDiscovery(w http.ResponseWriter, r *http.Request) { 414 json.NewEncoder(w).Encode(discoverer.All()) 415 } 416 417 func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) { 418 w.Header().Set("Content-Type", "application/json; charset=utf-8") 419 json.NewEncoder(w).Encode(reportData(m)) 420 } 421 422 func restGetEvents(w http.ResponseWriter, r *http.Request) { 423 qs := r.URL.Query() 424 ts := qs.Get("since") 425 since, _ := strconv.Atoi(ts) 426 427 w.Header().Set("Content-Type", "application/json; charset=utf-8") 428 json.NewEncoder(w).Encode(eventSub.Since(since, nil)) 429 } 430 431 func restGetUpgrade(w http.ResponseWriter, r *http.Request) { 432 rel, err := currentRelease() 433 if err != nil { 434 http.Error(w, err.Error(), 500) 435 return 436 } 437 res := make(map[string]interface{}) 438 res["running"] = Version 439 res["latest"] = rel.Tag 440 res["newer"] = compareVersions(rel.Tag, Version) == 1 441 442 w.Header().Set("Content-Type", "application/json; charset=utf-8") 443 json.NewEncoder(w).Encode(res) 444 } 445 446 func restGetNodeID(w http.ResponseWriter, r *http.Request) { 447 qs := r.URL.Query() 448 idStr := qs.Get("id") 449 id, err := protocol.NodeIDFromString(idStr) 450 w.Header().Set("Content-Type", "application/json; charset=utf-8") 451 if err == nil { 452 json.NewEncoder(w).Encode(map[string]string{ 453 "id": id.String(), 454 }) 455 } else { 456 json.NewEncoder(w).Encode(map[string]string{ 457 "error": err.Error(), 458 }) 459 } 460 } 461 462 func restPostUpgrade(w http.ResponseWriter, r *http.Request) { 463 err := upgrade() 464 if err != nil { 465 l.Warnln(err) 466 http.Error(w, err.Error(), 500) 467 return 468 } 469 470 restPostRestart(w, r) 471 } 472 473 func getQR(w http.ResponseWriter, r *http.Request) { 474 r.ParseForm() 475 text := r.FormValue("text") 476 code, err := qr.Encode(text, qr.M) 477 if err != nil { 478 http.Error(w, "Invalid", 500) 479 return 480 } 481 482 w.Header().Set("Content-Type", "image/png") 483 w.Write(code.PNG()) 484 } 485 486 func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler { 487 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 488 if validAPIKey(r.Header.Get("X-API-Key")) { 489 next.ServeHTTP(w, r) 490 return 491 } 492 493 error := func() { 494 time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond) 495 w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") 496 http.Error(w, "Not Authorized", http.StatusUnauthorized) 497 } 498 499 hdr := r.Header.Get("Authorization") 500 if !strings.HasPrefix(hdr, "Basic ") { 501 error() 502 return 503 } 504 505 hdr = hdr[6:] 506 bs, err := base64.StdEncoding.DecodeString(hdr) 507 if err != nil { 508 error() 509 return 510 } 511 512 fields := bytes.SplitN(bs, []byte(":"), 2) 513 if len(fields) != 2 { 514 error() 515 return 516 } 517 518 if string(fields[0]) != username { 519 error() 520 return 521 } 522 523 if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil { 524 error() 525 return 526 } 527 528 next.ServeHTTP(w, r) 529 }) 530 } 531 532 func validAPIKey(k string) bool { 533 return len(apiKey) > 0 && k == apiKey 534 } 535 536 func embeddedStatic(assetDir string) http.Handler { 537 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 538 file := r.URL.Path 539 540 if file[0] == '/' { 541 file = file[1:] 542 } 543 544 if len(file) == 0 { 545 file = "index.html" 546 } 547 548 if assetDir != "" { 549 p := filepath.Join(assetDir, filepath.FromSlash(file)) 550 _, err := os.Stat(p) 551 if err == nil { 552 http.ServeFile(w, r, p) 553 return 554 } 555 } 556 557 bs, ok := auto.Assets[file] 558 if !ok { 559 http.NotFound(w, r) 560 return 561 } 562 563 mtype := mime.TypeByExtension(filepath.Ext(r.URL.Path)) 564 if len(mtype) != 0 { 565 w.Header().Set("Content-Type", mtype) 566 } 567 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs))) 568 w.Header().Set("Last-Modified", modt) 569 570 w.Write(bs) 571 }) 572 }