mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-23 11:28:59 +00:00
Merge remote-tracking branch 'origin/pr/646'
* origin/pr/646: Add session support (fixes #611)
This commit is contained in:
commit
728289ee3a
@ -5,13 +5,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -44,7 +41,6 @@ var (
|
|||||||
configInSync = true
|
configInSync = true
|
||||||
guiErrors = []guiError{}
|
guiErrors = []guiError{}
|
||||||
guiErrorsMut sync.Mutex
|
guiErrorsMut sync.Mutex
|
||||||
apiKey string
|
|
||||||
modt = time.Now().UTC().Format(http.TimeFormat)
|
modt = time.Now().UTC().Format(http.TimeFormat)
|
||||||
eventSub *events.BufferedSubscription
|
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
|
// The GET handlers
|
||||||
getRestMux := http.NewServeMux()
|
getRestMux := http.NewServeMux()
|
||||||
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
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
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||||
// protected, other requests will grant cookies.
|
// protected, other requests will grant cookies.
|
||||||
handler := csrfMiddleware("/rest", mux)
|
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
||||||
|
|
||||||
// Add our version as a header to responses
|
// Add our version as a header to responses
|
||||||
handler = withVersionMiddleware(handler)
|
handler = withVersionMiddleware(handler)
|
||||||
|
|
||||||
// Wrap everything in basic auth, if user/password is set.
|
// Wrap everything in basic auth, if user/password is set.
|
||||||
if len(cfg.User) > 0 {
|
if len(cfg.User) > 0 {
|
||||||
handler = basicAuthMiddleware(cfg.User, cfg.Password, handler)
|
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
go http.Serve(listener, 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)
|
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 {
|
func embeddedStatic(assetDir string) http.Handler {
|
||||||
assets := auto.Assets()
|
assets := auto.Assets()
|
||||||
|
|
||||||
|
95
cmd/syncthing/gui_auth.go
Executable file
95
cmd/syncthing/gui_auth.go
Executable file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
@ -25,10 +25,11 @@ var csrfMut sync.Mutex
|
|||||||
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
|
// 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
|
// the request with 403. For / and /index.html, set a new CSRF cookie if none
|
||||||
// is currently set.
|
// 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Allow requests carrying a valid API key
|
// 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)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -76,13 +77,7 @@ func validCsrfToken(token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newCsrfToken() string {
|
func newCsrfToken() string {
|
||||||
bs := make([]byte, 30)
|
token := randomString(30)
|
||||||
_, err := rand.Reader.Read(bs)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
token := base64.StdEncoding.EncodeToString(bs)
|
|
||||||
|
|
||||||
csrfMut.Lock()
|
csrfMut.Lock()
|
||||||
csrfTokens = append(csrfTokens, token)
|
csrfTokens = append(csrfTokens, token)
|
||||||
@ -134,3 +129,13 @@ func loadCsrfTokens() {
|
|||||||
csrfTokens = append(csrfTokens, s.Text())
|
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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user