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  }