From 4e608b116a3b132701522a06c1d2f0617e6385b4 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Mon, 1 Sep 2014 21:51:44 +0100 Subject: [PATCH] Add session support (fixes #611) --- cmd/syncthing/gui.go | 61 +------------------------ cmd/syncthing/gui_auth.go | 95 +++++++++++++++++++++++++++++++++++++++ cmd/syncthing/gui_csrf.go | 23 ++++++---- 3 files changed, 111 insertions(+), 68 deletions(-) create mode 100755 cmd/syncthing/gui_auth.go diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index c159e0045..cd7eaffdc 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -5,13 +5,10 @@ package main import ( - "bytes" "crypto/tls" - "encoding/base64" "encoding/json" "fmt" "io/ioutil" - "math/rand" "mime" "net" "net/http" @@ -44,7 +41,6 @@ var ( configInSync = true guiErrors = []guiError{} guiErrorsMut sync.Mutex - apiKey string modt = time.Now().UTC().Format(http.TimeFormat) eventSub *events.BufferedSubscription ) @@ -88,9 +84,6 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro } } - apiKey = cfg.APIKey - loadCsrfTokens() - // The GET handlers getRestMux := http.NewServeMux() getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion)) @@ -141,14 +134,14 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro // Wrap everything in CSRF protection. The /rest prefix should be // protected, other requests will grant cookies. - handler := csrfMiddleware("/rest", mux) + handler := csrfMiddleware("/rest", cfg.APIKey, mux) // Add our version as a header to responses handler = withVersionMiddleware(handler) // Wrap everything in basic auth, if user/password is set. if len(cfg.User) > 0 { - handler = basicAuthMiddleware(cfg.User, cfg.Password, handler) + handler = basicAuthAndSessionMiddleware(cfg, handler) } go http.Serve(listener, handler) @@ -600,56 +593,6 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(comp) } -func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if validAPIKey(r.Header.Get("X-API-Key")) { - next.ServeHTTP(w, r) - return - } - - error := func() { - time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond) - w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") - http.Error(w, "Not Authorized", http.StatusUnauthorized) - } - - hdr := r.Header.Get("Authorization") - if !strings.HasPrefix(hdr, "Basic ") { - error() - return - } - - hdr = hdr[6:] - bs, err := base64.StdEncoding.DecodeString(hdr) - if err != nil { - error() - return - } - - fields := bytes.SplitN(bs, []byte(":"), 2) - if len(fields) != 2 { - error() - return - } - - if string(fields[0]) != username { - error() - return - } - - if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil { - error() - return - } - - next.ServeHTTP(w, r) - }) -} - -func validAPIKey(k string) bool { - return len(apiKey) > 0 && k == apiKey -} - func embeddedStatic(assetDir string) http.Handler { assets := auto.Assets() diff --git a/cmd/syncthing/gui_auth.go b/cmd/syncthing/gui_auth.go new file mode 100755 index 000000000..ced11436a --- /dev/null +++ b/cmd/syncthing/gui_auth.go @@ -0,0 +1,95 @@ +// 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. + +package main + +import ( + "bufio" + "bytes" + "encoding/base64" + "fmt" + "math/rand" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "code.google.com/p/go.crypto/bcrypt" + "github.com/syncthing/syncthing/config" + "github.com/syncthing/syncthing/osutil" +) + +var ( + sessions = make(map[string]bool) + sessionsMut sync.Mutex +) + +func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey { + next.ServeHTTP(w, r) + return + } + + cookie, err := r.Cookie("sessionid") + if err == nil && cookie != nil { + sessionsMut.Lock() + _, ok := sessions[cookie.Value] + sessionsMut.Unlock() + if ok { + next.ServeHTTP(w, r) + return + } + } + + error := func() { + time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond) + w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") + http.Error(w, "Not Authorized", http.StatusUnauthorized) + } + + hdr := r.Header.Get("Authorization") + if !strings.HasPrefix(hdr, "Basic ") { + error() + return + } + + hdr = hdr[6:] + bs, err := base64.StdEncoding.DecodeString(hdr) + if err != nil { + error() + return + } + + fields := bytes.SplitN(bs, []byte(":"), 2) + if len(fields) != 2 { + error() + return + } + + if string(fields[0]) != cfg.User { + error() + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil { + error() + return + } + + sessionid := randomString(32) + sessionsMut.Lock() + sessions[sessionid] = true + sessionsMut.Unlock() + http.SetCookie(w, &http.Cookie{ + Name: "sessionid", + Value: sessionid, + MaxAge: 0, + }) + + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go index f5eaad833..f34a07287 100644 --- a/cmd/syncthing/gui_csrf.go +++ b/cmd/syncthing/gui_csrf.go @@ -25,10 +25,11 @@ var csrfMut sync.Mutex // Check for CSRF token on /rest/ URLs. If a correct one is not given, reject // the request with 403. For / and /index.html, set a new CSRF cookie if none // is currently set. -func csrfMiddleware(prefix string, next http.Handler) http.Handler { +func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler { + loadCsrfTokens() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Allow requests carrying a valid API key - if validAPIKey(r.Header.Get("X-API-Key")) { + if apiKey != "" && r.Header.Get("X-API-Key") == apiKey { next.ServeHTTP(w, r) return } @@ -76,13 +77,7 @@ func validCsrfToken(token string) bool { } func newCsrfToken() string { - bs := make([]byte, 30) - _, err := rand.Reader.Read(bs) - if err != nil { - l.Fatalln(err) - } - - token := base64.StdEncoding.EncodeToString(bs) + token := randomString(30) csrfMut.Lock() csrfTokens = append(csrfTokens, token) @@ -134,3 +129,13 @@ func loadCsrfTokens() { csrfTokens = append(csrfTokens, s.Text()) } } + +func randomString(len int) string { + bs := make([]byte, len) + _, err := rand.Reader.Read(bs) + if err != nil { + l.Fatalln(err) + } + + return base64.StdEncoding.EncodeToString(bs) +}