mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 10:58:57 +00:00
This adds a "token manager" which handles storing and checking expired tokens, used for both sessions and CSRF tokens. It removes the old, corresponding functionality for CSRFs which saved things in a file. The result is less crap in the state directory, and active login sessions now survive a Syncthing restart (this really annoyed me). It also adds a boolean on login to create a longer-lived session cookie, which is now possible and useful. Thus we can remain logged in over browser restarts, which was also annoying... :) <img width="1001" alt="Screenshot 2023-12-12 at 09 56 34" src="https://github.com/syncthing/syncthing/assets/125426/55cb20c8-78fc-453e-825d-655b94c8623b"> Best viewed with whitespace-insensitive diff, as a bunch of the auth functions became methods instead of closures which changed indentation.
This commit is contained in:
parent
17df4b8634
commit
aa901790b9
@ -862,6 +862,7 @@ func cleanConfigDirectory() {
|
|||||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||||
"tmp-index-sorter.*": time.Minute, // these should never exist on startup
|
"tmp-index-sorter.*": time.Minute, // these should never exist on startup
|
||||||
"support-bundle-*": 30 * 24 * time.Hour, // keep old support bundle zip or folder for a month
|
"support-bundle-*": 30 * 24 * time.Hour, // keep old support bundle zip or folder for a month
|
||||||
|
"csrftokens.txt": 0, // deprecated, remove immediately
|
||||||
}
|
}
|
||||||
|
|
||||||
for pat, dur := range patterns {
|
for pat, dur := range patterns {
|
||||||
|
@ -359,6 +359,12 @@
|
|||||||
<input id="password" class="form-control" type="password" name="password" ng-model="login.password" ng-trim="false" autocomplete="current-password" />
|
<input id="password" class="form-control" type="password" name="password" ng-model="login.password" ng-trim="false" autocomplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="login.stayLoggedIn" > <span translate>Stay logged in</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9 login-form-messages">
|
<div class="col-md-9 login-form-messages">
|
||||||
<p ng-if="login.errors.badLogin" class="text-danger" translate>
|
<p ng-if="login.errors.badLogin" class="text-danger" translate>
|
||||||
|
@ -103,6 +103,7 @@ angular.module('syncthing.core')
|
|||||||
$http.post(authUrlbase + '/password', {
|
$http.post(authUrlbase + '/password', {
|
||||||
username: $scope.login.username,
|
username: $scope.login.username,
|
||||||
password: $scope.login.password,
|
password: $scope.login.password,
|
||||||
|
stayLoggedIn: $scope.login.stayLoggedIn,
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
location.reload();
|
location.reload();
|
||||||
}).catch(function (response) {
|
}).catch(function (response) {
|
||||||
@ -3602,7 +3603,7 @@ angular.module('syncthing.core')
|
|||||||
return n.match !== "";
|
return n.match !== "";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// The showModal and hideModal functions are a bandaid for a Bootstrap
|
// The showModal and hideModal functions are a bandaid for a Bootstrap
|
||||||
// bug (see https://github.com/twbs/bootstrap/issues/3902) that causes
|
// bug (see https://github.com/twbs/bootstrap/issues/3902) that causes
|
||||||
// multiple consecutively shown or hidden modals to overlap which leads
|
// multiple consecutively shown or hidden modals to overlap which leads
|
||||||
|
@ -91,6 +91,7 @@ type service struct {
|
|||||||
startupErr error
|
startupErr error
|
||||||
listenerAddr net.Addr
|
listenerAddr net.Addr
|
||||||
exitChan chan *svcutil.FatalErr
|
exitChan chan *svcutil.FatalErr
|
||||||
|
miscDB *db.NamespacedKV
|
||||||
|
|
||||||
guiErrors logger.Recorder
|
guiErrors logger.Recorder
|
||||||
systemLog logger.Recorder
|
systemLog logger.Recorder
|
||||||
@ -104,7 +105,7 @@ type Service interface {
|
|||||||
WaitForStart() error
|
WaitForStart() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool) Service {
|
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.NamespacedKV) Service {
|
||||||
return &service{
|
return &service{
|
||||||
id: id,
|
id: id,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -127,6 +128,7 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
|
|||||||
configChanged: make(chan struct{}),
|
configChanged: make(chan struct{}),
|
||||||
startedOnce: make(chan struct{}),
|
startedOnce: make(chan struct{}),
|
||||||
exitChan: make(chan *svcutil.FatalErr, 1),
|
exitChan: make(chan *svcutil.FatalErr, 1),
|
||||||
|
miscDB: miscDB,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +366,7 @@ func (s *service) Serve(ctx context.Context) error {
|
|||||||
|
|
||||||
// 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.
|
||||||
var handler http.Handler = newCsrfManager(s.id.Short().String(), "/rest", guiCfg, mux, locations.Get(locations.CsrfTokens))
|
var handler http.Handler = newCsrfManager(s.id.Short().String(), "/rest", guiCfg, mux, s.miscDB)
|
||||||
|
|
||||||
// Add our version and ID as a header to responses
|
// Add our version and ID as a header to responses
|
||||||
handler = withDetailsMiddleware(s.id, handler)
|
handler = withDetailsMiddleware(s.id, handler)
|
||||||
@ -372,12 +374,13 @@ func (s *service) Serve(ctx context.Context) error {
|
|||||||
// Wrap everything in basic auth, if user/password is set.
|
// Wrap everything in basic auth, if user/password is set.
|
||||||
if guiCfg.IsAuthEnabled() {
|
if guiCfg.IsAuthEnabled() {
|
||||||
sessionCookieName := "sessionid-" + s.id.Short().String()
|
sessionCookieName := "sessionid-" + s.id.Short().String()
|
||||||
handler = basicAuthAndSessionMiddleware(sessionCookieName, s.id.Short().String(), guiCfg, s.cfg.LDAP(), handler, s.evLogger)
|
authMW := newBasicAuthAndSessionMiddleware(sessionCookieName, s.id.Short().String(), guiCfg, s.cfg.LDAP(), handler, s.evLogger, s.miscDB)
|
||||||
handlePasswordAuth := passwordAuthHandler(sessionCookieName, guiCfg, s.cfg.LDAP(), s.evLogger)
|
handler = authMW
|
||||||
restMux.Handler(http.MethodPost, "/rest/noauth/auth/password", handlePasswordAuth)
|
|
||||||
|
restMux.Handler(http.MethodPost, "/rest/noauth/auth/password", http.HandlerFunc(authMW.passwordAuthHandler))
|
||||||
|
|
||||||
// Logout is a no-op without a valid session cookie, so /noauth/ is fine here
|
// Logout is a no-op without a valid session cookie, so /noauth/ is fine here
|
||||||
restMux.Handler(http.MethodPost, "/rest/noauth/auth/logout", handleLogout(sessionCookieName))
|
restMux.Handler(http.MethodPost, "/rest/noauth/auth/logout", http.HandlerFunc(authMW.handleLogout))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to HTTPS if we are supposed to
|
// Redirect to HTTPS if we are supposed to
|
||||||
|
@ -16,15 +16,16 @@ import (
|
|||||||
|
|
||||||
ldap "github.com/go-ldap/ldap/v3"
|
ldap "github.com/go-ldap/ldap/v3"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/rand"
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
sessions = make(map[string]bool)
|
maxSessionLifetime = 7 * 24 * time.Hour
|
||||||
sessionsMut = sync.NewMutex()
|
maxActiveSessions = 25
|
||||||
|
randomTokenLength = 64
|
||||||
)
|
)
|
||||||
|
|
||||||
func emitLoginAttempt(success bool, username, address string, evLogger events.Logger) {
|
func emitLoginAttempt(success bool, username, address string, evLogger events.Logger) {
|
||||||
@ -78,75 +79,91 @@ func isNoAuthPath(path string) bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func basicAuthAndSessionMiddleware(cookieName, shortID string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler {
|
type basicAuthAndSessionMiddleware struct {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
cookieName string
|
||||||
if hasValidAPIKeyHeader(r, guiCfg) {
|
shortID string
|
||||||
next.ServeHTTP(w, r)
|
guiCfg config.GUIConfiguration
|
||||||
return
|
ldapCfg config.LDAPConfiguration
|
||||||
}
|
next http.Handler
|
||||||
|
evLogger events.Logger
|
||||||
for _, cookie := range r.Cookies() {
|
tokens *tokenManager
|
||||||
// We iterate here since there may, historically, be multiple
|
|
||||||
// cookies with the same name but different path. Any "old" ones
|
|
||||||
// won't match an existing session and will be ignored, then
|
|
||||||
// later removed on logout or when timing out.
|
|
||||||
if cookie.Name == cookieName {
|
|
||||||
sessionsMut.Lock()
|
|
||||||
_, ok := sessions[cookie.Value]
|
|
||||||
sessionsMut.Unlock()
|
|
||||||
if ok {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to Basic auth if provided
|
|
||||||
if username, ok := attemptBasicAuth(r, guiCfg, ldapCfg, evLogger); ok {
|
|
||||||
createSession(cookieName, username, guiCfg, evLogger, w, r)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception for static assets and REST calls that don't require authentication.
|
|
||||||
if isNoAuthPath(r.URL.Path) {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some browsers don't send the Authorization request header unless prompted by a 401 response.
|
|
||||||
// This enables https://user:pass@localhost style URLs to keep working.
|
|
||||||
if guiCfg.SendBasicAuthPrompt {
|
|
||||||
unauthorized(w, shortID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
forbidden(w)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func passwordAuthHandler(cookieName string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, evLogger events.Logger) http.Handler {
|
func newBasicAuthAndSessionMiddleware(cookieName, shortID string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger, miscDB *db.NamespacedKV) *basicAuthAndSessionMiddleware {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return &basicAuthAndSessionMiddleware{
|
||||||
var req struct {
|
cookieName: cookieName,
|
||||||
Username string
|
shortID: shortID,
|
||||||
Password string
|
guiCfg: guiCfg,
|
||||||
}
|
ldapCfg: ldapCfg,
|
||||||
if err := unmarshalTo(r.Body, &req); err != nil {
|
next: next,
|
||||||
l.Debugln("Failed to parse username and password:", err)
|
evLogger: evLogger,
|
||||||
http.Error(w, "Failed to parse username and password.", http.StatusBadRequest)
|
tokens: newTokenManager("sessions", miscDB, maxSessionLifetime, maxActiveSessions),
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth(req.Username, req.Password, guiCfg, ldapCfg) {
|
func (m *basicAuthAndSessionMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
createSession(cookieName, req.Username, guiCfg, evLogger, w, r)
|
if hasValidAPIKeyHeader(r, m.guiCfg) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
m.next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emitLoginAttempt(false, req.Username, r.RemoteAddr, evLogger)
|
for _, cookie := range r.Cookies() {
|
||||||
antiBruteForceSleep()
|
// We iterate here since there may, historically, be multiple
|
||||||
forbidden(w)
|
// cookies with the same name but different path. Any "old" ones
|
||||||
})
|
// won't match an existing session and will be ignored, then
|
||||||
|
// later removed on logout or when timing out.
|
||||||
|
if cookie.Name == m.cookieName {
|
||||||
|
if m.tokens.Check(cookie.Value) {
|
||||||
|
m.next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to Basic auth if provided
|
||||||
|
if username, ok := attemptBasicAuth(r, m.guiCfg, m.ldapCfg, m.evLogger); ok {
|
||||||
|
m.createSession(username, false, w, r)
|
||||||
|
m.next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception for static assets and REST calls that don't require authentication.
|
||||||
|
if isNoAuthPath(r.URL.Path) {
|
||||||
|
m.next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some browsers don't send the Authorization request header unless prompted by a 401 response.
|
||||||
|
// This enables https://user:pass@localhost style URLs to keep working.
|
||||||
|
if m.guiCfg.SendBasicAuthPrompt {
|
||||||
|
unauthorized(w, m.shortID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *basicAuthAndSessionMiddleware) passwordAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
StayLoggedIn bool
|
||||||
|
}
|
||||||
|
if err := unmarshalTo(r.Body, &req); err != nil {
|
||||||
|
l.Debugln("Failed to parse username and password:", err)
|
||||||
|
http.Error(w, "Failed to parse username and password.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth(req.Username, req.Password, m.guiCfg, m.ldapCfg) {
|
||||||
|
m.createSession(req.Username, req.StayLoggedIn, w, r)
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emitLoginAttempt(false, req.Username, r.RemoteAddr, m.evLogger)
|
||||||
|
antiBruteForceSleep()
|
||||||
|
forbidden(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, evLogger events.Logger) (string, bool) {
|
func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, evLogger events.Logger) (string, bool) {
|
||||||
@ -172,11 +189,8 @@ func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg c
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSession(cookieName string, username string, guiCfg config.GUIConfiguration, evLogger events.Logger, w http.ResponseWriter, r *http.Request) {
|
func (m *basicAuthAndSessionMiddleware) createSession(username string, persistent bool, w http.ResponseWriter, r *http.Request) {
|
||||||
sessionid := rand.String(32)
|
sessionid := m.tokens.New()
|
||||||
sessionsMut.Lock()
|
|
||||||
sessions[sessionid] = true
|
|
||||||
sessionsMut.Unlock()
|
|
||||||
|
|
||||||
// Best effort detection of whether the connection is HTTPS --
|
// Best effort detection of whether the connection is HTTPS --
|
||||||
// either directly to us, or as used by the client towards a reverse
|
// either directly to us, or as used by the client towards a reverse
|
||||||
@ -186,45 +200,45 @@ func createSession(cookieName string, username string, guiCfg config.GUIConfigur
|
|||||||
strings.Contains(strings.ToLower(r.Header.Get("forwarded")), "proto=https")
|
strings.Contains(strings.ToLower(r.Header.Get("forwarded")), "proto=https")
|
||||||
// If the connection is HTTPS, or *should* be HTTPS, set the Secure
|
// If the connection is HTTPS, or *should* be HTTPS, set the Secure
|
||||||
// bit in cookies.
|
// bit in cookies.
|
||||||
useSecureCookie := connectionIsHTTPS || guiCfg.UseTLS()
|
useSecureCookie := connectionIsHTTPS || m.guiCfg.UseTLS()
|
||||||
|
|
||||||
|
maxAge := 0
|
||||||
|
if persistent {
|
||||||
|
maxAge = int(maxSessionLifetime.Seconds())
|
||||||
|
}
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieName,
|
Name: m.cookieName,
|
||||||
Value: sessionid,
|
Value: sessionid,
|
||||||
// In HTTP spec Max-Age <= 0 means delete immediately,
|
// In HTTP spec Max-Age <= 0 means delete immediately,
|
||||||
// but in http.Cookie MaxAge = 0 means unspecified (session) and MaxAge < 0 means delete immediately
|
// but in http.Cookie MaxAge = 0 means unspecified (session) and MaxAge < 0 means delete immediately
|
||||||
MaxAge: 0,
|
MaxAge: maxAge,
|
||||||
Secure: useSecureCookie,
|
Secure: useSecureCookie,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
|
|
||||||
emitLoginAttempt(true, username, r.RemoteAddr, evLogger)
|
emitLoginAttempt(true, username, r.RemoteAddr, m.evLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogout(cookieName string) http.Handler {
|
func (m *basicAuthAndSessionMiddleware) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
for _, cookie := range r.Cookies() {
|
||||||
for _, cookie := range r.Cookies() {
|
// We iterate here since there may, historically, be multiple
|
||||||
// We iterate here since there may, historically, be multiple
|
// cookies with the same name but different path. We drop them
|
||||||
// cookies with the same name but different path. We drop them
|
// all.
|
||||||
// all.
|
if cookie.Name == m.cookieName {
|
||||||
if cookie.Name == cookieName {
|
m.tokens.Delete(cookie.Value)
|
||||||
sessionsMut.Lock()
|
|
||||||
delete(sessions, cookie.Value)
|
|
||||||
sessionsMut.Unlock()
|
|
||||||
|
|
||||||
// Delete the cookie
|
// Delete the cookie
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieName,
|
Name: m.cookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: cookie.Secure,
|
Secure: cookie.Secure,
|
||||||
Path: cookie.Path,
|
Path: cookie.Path,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func auth(username string, password string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration) bool {
|
func auth(username string, password string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration) bool {
|
||||||
|
@ -8,8 +8,12 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
|
"github.com/syncthing/syncthing/lib/db/backend"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
var guiCfg config.GUIConfiguration
|
var guiCfg config.GUIConfiguration
|
||||||
@ -110,3 +114,76 @@ func TestEscapeForLDAPDN(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockClock struct {
|
||||||
|
now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockClock) Now() time.Time {
|
||||||
|
c.now = c.now.Add(1) // time always ticks by at least 1 ns
|
||||||
|
return c.now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockClock) wind(t time.Duration) {
|
||||||
|
c.now = c.now.Add(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenManager(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
|
||||||
|
kdb := db.NewNamespacedKV(mdb, "test")
|
||||||
|
clock := &mockClock{now: time.Now()}
|
||||||
|
|
||||||
|
// Token manager keeps up to three tokens with a validity time of 24 hours.
|
||||||
|
tm := newTokenManager("testTokens", kdb, 24*time.Hour, 3)
|
||||||
|
tm.timeNow = clock.Now
|
||||||
|
|
||||||
|
// Create three tokens
|
||||||
|
t0 := tm.New()
|
||||||
|
t1 := tm.New()
|
||||||
|
t2 := tm.New()
|
||||||
|
|
||||||
|
// Check that the tokens are valid
|
||||||
|
if !tm.Check(t0) {
|
||||||
|
t.Errorf("token %q should be valid", t0)
|
||||||
|
}
|
||||||
|
if !tm.Check(t1) {
|
||||||
|
t.Errorf("token %q should be valid", t1)
|
||||||
|
}
|
||||||
|
if !tm.Check(t2) {
|
||||||
|
t.Errorf("token %q should be valid", t2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a fourth token
|
||||||
|
t3 := tm.New()
|
||||||
|
// It should be valid
|
||||||
|
if !tm.Check(t3) {
|
||||||
|
t.Errorf("token %q should be valid", t3)
|
||||||
|
}
|
||||||
|
// But the first token should have been removed
|
||||||
|
if tm.Check(t0) {
|
||||||
|
t.Errorf("token %q should be invalid", t0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wind the clock by 12 hours
|
||||||
|
clock.wind(12 * time.Hour)
|
||||||
|
// The second token should still be valid (and checking it will give it more life)
|
||||||
|
if !tm.Check(t1) {
|
||||||
|
t.Errorf("token %q should be valid", t1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wind the clock by 12 hours
|
||||||
|
clock.wind(12 * time.Hour)
|
||||||
|
// The second token should still be valid
|
||||||
|
if !tm.Check(t1) {
|
||||||
|
t.Errorf("token %q should be valid", t1)
|
||||||
|
}
|
||||||
|
// But the third and fourth tokens should have expired
|
||||||
|
if tm.Check(t2) {
|
||||||
|
t.Errorf("token %q should be invalid", t2)
|
||||||
|
}
|
||||||
|
if tm.Check(t3) {
|
||||||
|
t.Errorf("token %q should be invalid", t3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,33 +7,24 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/rand"
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxCsrfTokens = 25
|
const (
|
||||||
|
maxCSRFTokenLifetime = time.Hour
|
||||||
|
maxActiveCSRFTokens = 25
|
||||||
|
)
|
||||||
|
|
||||||
type csrfManager struct {
|
type csrfManager struct {
|
||||||
// tokens is a list of valid tokens. It is sorted so that the most
|
|
||||||
// recently used token is first in the list. New tokens are added to the front
|
|
||||||
// of the list (as it is the most recently used at that time). The list is
|
|
||||||
// pruned to a maximum of maxCsrfTokens, throwing away the least recently used
|
|
||||||
// tokens.
|
|
||||||
tokens []string
|
|
||||||
tokensMut sync.Mutex
|
|
||||||
|
|
||||||
unique string
|
unique string
|
||||||
prefix string
|
prefix string
|
||||||
apiKeyValidator apiKeyValidator
|
apiKeyValidator apiKeyValidator
|
||||||
next http.Handler
|
next http.Handler
|
||||||
saveLocation string
|
tokens *tokenManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiKeyValidator interface {
|
type apiKeyValidator interface {
|
||||||
@ -43,17 +34,14 @@ type apiKeyValidator interface {
|
|||||||
// 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 newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, saveLocation string) *csrfManager {
|
func newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, miscDB *db.NamespacedKV) *csrfManager {
|
||||||
m := &csrfManager{
|
m := &csrfManager{
|
||||||
tokensMut: sync.NewMutex(),
|
|
||||||
tokens: make([]string, 0, maxCsrfTokens),
|
|
||||||
unique: unique,
|
unique: unique,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
apiKeyValidator: apiKeyValidator,
|
apiKeyValidator: apiKeyValidator,
|
||||||
next: next,
|
next: next,
|
||||||
saveLocation: saveLocation,
|
tokens: newTokenManager("csrfTokens", miscDB, maxCSRFTokenLifetime, maxActiveCSRFTokens),
|
||||||
}
|
}
|
||||||
m.load()
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,11 +66,11 @@ func (m *csrfManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// and set a CSRF cookie if there isn't already a valid one.
|
// and set a CSRF cookie if there isn't already a valid one.
|
||||||
if !strings.HasPrefix(r.URL.Path, m.prefix) {
|
if !strings.HasPrefix(r.URL.Path, m.prefix) {
|
||||||
cookie, err := r.Cookie("CSRF-Token-" + m.unique)
|
cookie, err := r.Cookie("CSRF-Token-" + m.unique)
|
||||||
if err != nil || !m.validToken(cookie.Value) {
|
if err != nil || !m.tokens.Check(cookie.Value) {
|
||||||
l.Debugln("new CSRF cookie in response to request for", r.URL)
|
l.Debugln("new CSRF cookie in response to request for", r.URL)
|
||||||
cookie = &http.Cookie{
|
cookie = &http.Cookie{
|
||||||
Name: "CSRF-Token-" + m.unique,
|
Name: "CSRF-Token-" + m.unique,
|
||||||
Value: m.newToken(),
|
Value: m.tokens.New(),
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
}
|
}
|
||||||
@ -99,7 +87,7 @@ func (m *csrfManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Verify the CSRF token
|
// Verify the CSRF token
|
||||||
token := r.Header.Get("X-CSRF-Token-" + m.unique)
|
token := r.Header.Get("X-CSRF-Token-" + m.unique)
|
||||||
if !m.validToken(token) {
|
if !m.tokens.Check(token) {
|
||||||
http.Error(w, "CSRF Error", http.StatusForbidden)
|
http.Error(w, "CSRF Error", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -107,78 +95,6 @@ func (m *csrfManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
m.next.ServeHTTP(w, r)
|
m.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *csrfManager) validToken(token string) bool {
|
|
||||||
m.tokensMut.Lock()
|
|
||||||
defer m.tokensMut.Unlock()
|
|
||||||
for i, t := range m.tokens {
|
|
||||||
if t == token {
|
|
||||||
if i > 0 {
|
|
||||||
// Move this token to the head of the list. Copy the tokens at
|
|
||||||
// the front one step to the right and then replace the token
|
|
||||||
// at the head.
|
|
||||||
copy(m.tokens[1:], m.tokens[:i])
|
|
||||||
m.tokens[0] = token
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *csrfManager) newToken() string {
|
|
||||||
token := rand.String(32)
|
|
||||||
|
|
||||||
m.tokensMut.Lock()
|
|
||||||
defer m.tokensMut.Unlock()
|
|
||||||
|
|
||||||
if len(m.tokens) < maxCsrfTokens {
|
|
||||||
m.tokens = append(m.tokens, "")
|
|
||||||
}
|
|
||||||
copy(m.tokens[1:], m.tokens)
|
|
||||||
m.tokens[0] = token
|
|
||||||
|
|
||||||
m.save()
|
|
||||||
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *csrfManager) save() {
|
|
||||||
// We're ignoring errors in here. It's not super critical and there's
|
|
||||||
// nothing relevant we can do about them anyway...
|
|
||||||
|
|
||||||
if m.saveLocation == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := osutil.CreateAtomic(m.saveLocation)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range m.tokens {
|
|
||||||
fmt.Fprintln(f, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *csrfManager) load() {
|
|
||||||
if m.saveLocation == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(m.saveLocation)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
s := bufio.NewScanner(f)
|
|
||||||
for s.Scan() {
|
|
||||||
m.tokens = append(m.tokens, s.Text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasValidAPIKeyHeader(r *http.Request, validator apiKeyValidator) bool {
|
func hasValidAPIKeyHeader(r *http.Request, validator apiKeyValidator) bool {
|
||||||
if key := r.Header.Get("X-API-Key"); validator.IsValidAPIKey(key) {
|
if key := r.Header.Get("X-API-Key"); validator.IsValidAPIKey(key) {
|
||||||
return true
|
return true
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -29,6 +28,8 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/build"
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
connmocks "github.com/syncthing/syncthing/lib/connections/mocks"
|
connmocks "github.com/syncthing/syncthing/lib/connections/mocks"
|
||||||
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
|
"github.com/syncthing/syncthing/lib/db/backend"
|
||||||
discovermocks "github.com/syncthing/syncthing/lib/discover/mocks"
|
discovermocks "github.com/syncthing/syncthing/lib/discover/mocks"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
|
eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
|
||||||
@ -72,71 +73,6 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCSRFToken(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
max := 10 * maxCsrfTokens
|
|
||||||
int := 5
|
|
||||||
if testing.Short() {
|
|
||||||
max = 1 + maxCsrfTokens
|
|
||||||
int = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
m := newCsrfManager("unique", "prefix", config.GUIConfiguration{}, nil, "")
|
|
||||||
|
|
||||||
t1 := m.newToken()
|
|
||||||
t2 := m.newToken()
|
|
||||||
|
|
||||||
t3 := m.newToken()
|
|
||||||
if !m.validToken(t3) {
|
|
||||||
t.Fatal("t3 should be valid")
|
|
||||||
}
|
|
||||||
|
|
||||||
valid := make(map[string]struct{}, maxCsrfTokens)
|
|
||||||
for _, token := range m.tokens {
|
|
||||||
valid[token] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < max; i++ {
|
|
||||||
if i%int == 0 {
|
|
||||||
// t1 and t2 should remain valid by virtue of us checking them now
|
|
||||||
// and then.
|
|
||||||
if !m.validToken(t1) {
|
|
||||||
t.Fatal("t1 should be valid at iteration", i)
|
|
||||||
}
|
|
||||||
if !m.validToken(t2) {
|
|
||||||
t.Fatal("t2 should be valid at iteration", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.tokens) == maxCsrfTokens {
|
|
||||||
// We're about to add a token, which will remove the last token
|
|
||||||
// from m.tokens.
|
|
||||||
delete(valid, m.tokens[len(m.tokens)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// The newly generated token is always valid
|
|
||||||
t4 := m.newToken()
|
|
||||||
if !m.validToken(t4) {
|
|
||||||
t.Fatal("t4 should be valid at iteration", i)
|
|
||||||
}
|
|
||||||
valid[t4] = struct{}{}
|
|
||||||
|
|
||||||
v := make(map[string]struct{}, maxCsrfTokens)
|
|
||||||
for _, token := range m.tokens {
|
|
||||||
v[token] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(v, valid) {
|
|
||||||
t.Fatalf("want valid tokens %v, got %v", valid, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.validToken(t3) {
|
|
||||||
t.Fatal("t3 should have expired by now")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStopAfterBrokenConfig(t *testing.T) {
|
func TestStopAfterBrokenConfig(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@ -148,7 +84,9 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
w := config.Wrap("/dev/null", cfg, protocol.LocalDeviceID, events.NoopLogger)
|
w := config.Wrap("/dev/null", cfg, protocol.LocalDeviceID, events.NoopLogger)
|
||||||
|
|
||||||
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
|
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
|
||||||
|
kdb := db.NewMiscDataNamespace(mdb)
|
||||||
|
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
|
||||||
defer os.Remove(token)
|
defer os.Remove(token)
|
||||||
|
|
||||||
srv.started = make(chan string)
|
srv.started = make(chan string)
|
||||||
@ -926,7 +864,9 @@ func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
|
|||||||
|
|
||||||
// Instantiate the API service
|
// Instantiate the API service
|
||||||
urService := ur.New(cfg, m, connections, false)
|
urService := ur.New(cfg, m, connections, false)
|
||||||
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, mockedSummary, errorLog, systemLog, false).(*service)
|
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
|
||||||
|
kdb := db.NewMiscDataNamespace(mdb)
|
||||||
|
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, mockedSummary, errorLog, systemLog, false, kdb).(*service)
|
||||||
defer os.Remove(token)
|
defer os.Remove(token)
|
||||||
svc.started = addrChan
|
svc.started = addrChan
|
||||||
|
|
||||||
@ -1467,7 +1407,9 @@ func TestEventMasks(t *testing.T) {
|
|||||||
cfg := newMockedConfig()
|
cfg := newMockedConfig()
|
||||||
defSub := new(eventmocks.BufferedSubscription)
|
defSub := new(eventmocks.BufferedSubscription)
|
||||||
diskSub := new(eventmocks.BufferedSubscription)
|
diskSub := new(eventmocks.BufferedSubscription)
|
||||||
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false).(*service)
|
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
|
||||||
|
kdb := db.NewMiscDataNamespace(mdb)
|
||||||
|
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
|
||||||
defer os.Remove(token)
|
defer os.Remove(token)
|
||||||
|
|
||||||
if mask := svc.getEventMask(""); mask != DefaultEventMask {
|
if mask := svc.getEventMask(""); mask != DefaultEventMask {
|
||||||
|
137
lib/api/tokenmanager.go
Normal file
137
lib/api/tokenmanager.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (C) 2024 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tokenManager struct {
|
||||||
|
key string
|
||||||
|
miscDB *db.NamespacedKV
|
||||||
|
lifetime time.Duration
|
||||||
|
maxItems int
|
||||||
|
|
||||||
|
timeNow func() time.Time // can be overridden for testing
|
||||||
|
|
||||||
|
mut sync.Mutex
|
||||||
|
tokens *TokenSet
|
||||||
|
saveTimer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager {
|
||||||
|
tokens := &TokenSet{
|
||||||
|
Tokens: make(map[string]int64),
|
||||||
|
}
|
||||||
|
if bs, ok, _ := miscDB.Bytes(key); ok {
|
||||||
|
_ = tokens.Unmarshal(bs) // best effort
|
||||||
|
}
|
||||||
|
return &tokenManager{
|
||||||
|
key: key,
|
||||||
|
miscDB: miscDB,
|
||||||
|
lifetime: lifetime,
|
||||||
|
maxItems: maxItems,
|
||||||
|
timeNow: time.Now,
|
||||||
|
mut: sync.NewMutex(),
|
||||||
|
tokens: tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check returns true if the token is valid, and updates the token's expiry
|
||||||
|
// time. The token is removed if it is expired.
|
||||||
|
func (m *tokenManager) Check(token string) bool {
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
expires, ok := m.tokens.Tokens[token]
|
||||||
|
if ok {
|
||||||
|
if expires < m.timeNow().UnixNano() {
|
||||||
|
// The token is expired.
|
||||||
|
m.saveLocked() // removes expired tokens
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the token further life.
|
||||||
|
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
|
||||||
|
m.saveLocked()
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new token and returns it.
|
||||||
|
func (m *tokenManager) New() string {
|
||||||
|
token := rand.String(randomTokenLength)
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano()
|
||||||
|
m.saveLocked()
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a token.
|
||||||
|
func (m *tokenManager) Delete(token string) {
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
delete(m.tokens.Tokens, token)
|
||||||
|
m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *tokenManager) saveLocked() {
|
||||||
|
// Remove expired tokens.
|
||||||
|
now := m.timeNow().UnixNano()
|
||||||
|
for token, expiry := range m.tokens.Tokens {
|
||||||
|
if expiry < now {
|
||||||
|
delete(m.tokens.Tokens, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a limit on the number of tokens, remove the oldest ones.
|
||||||
|
if m.maxItems > 0 && len(m.tokens.Tokens) > m.maxItems {
|
||||||
|
// Sort the tokens by expiry time, oldest first.
|
||||||
|
type tokenExpiry struct {
|
||||||
|
token string
|
||||||
|
expiry int64
|
||||||
|
}
|
||||||
|
var tokens []tokenExpiry
|
||||||
|
for token, expiry := range m.tokens.Tokens {
|
||||||
|
tokens = append(tokens, tokenExpiry{token, expiry})
|
||||||
|
}
|
||||||
|
slices.SortFunc(tokens, func(i, j tokenExpiry) int {
|
||||||
|
return int(i.expiry - j.expiry)
|
||||||
|
})
|
||||||
|
// Remove the oldest tokens.
|
||||||
|
for _, token := range tokens[:len(tokens)-m.maxItems] {
|
||||||
|
delete(m.tokens.Tokens, token.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postpone saving until one second of inactivity.
|
||||||
|
if m.saveTimer == nil {
|
||||||
|
m.saveTimer = time.AfterFunc(time.Second, m.scheduledSave)
|
||||||
|
} else {
|
||||||
|
m.saveTimer.Reset(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *tokenManager) scheduledSave() {
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
m.saveTimer = nil
|
||||||
|
|
||||||
|
bs, _ := m.tokens.Marshal() // can't fail
|
||||||
|
_ = m.miscDB.PutBytes(m.key, bs) // can fail, but what are we going to do?
|
||||||
|
}
|
411
lib/api/tokenset.pb.go
Normal file
411
lib/api/tokenset.pb.go
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||||
|
// source: lib/api/tokenset.proto
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
fmt "fmt"
|
||||||
|
proto "github.com/gogo/protobuf/proto"
|
||||||
|
io "io"
|
||||||
|
math "math"
|
||||||
|
math_bits "math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||||
|
|
||||||
|
type TokenSet struct {
|
||||||
|
// token -> expiry time (epoch nanoseconds)
|
||||||
|
Tokens map[string]int64 `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens" xml:"token" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TokenSet) Reset() { *m = TokenSet{} }
|
||||||
|
func (m *TokenSet) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*TokenSet) ProtoMessage() {}
|
||||||
|
func (*TokenSet) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_9ea8707737c33b38, []int{0}
|
||||||
|
}
|
||||||
|
func (m *TokenSet) XXX_Unmarshal(b []byte) error {
|
||||||
|
return m.Unmarshal(b)
|
||||||
|
}
|
||||||
|
func (m *TokenSet) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
if deterministic {
|
||||||
|
return xxx_messageInfo_TokenSet.Marshal(b, m, deterministic)
|
||||||
|
} else {
|
||||||
|
b = b[:cap(b)]
|
||||||
|
n, err := m.MarshalToSizedBuffer(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b[:n], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *TokenSet) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_TokenSet.Merge(m, src)
|
||||||
|
}
|
||||||
|
func (m *TokenSet) XXX_Size() int {
|
||||||
|
return m.ProtoSize()
|
||||||
|
}
|
||||||
|
func (m *TokenSet) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_TokenSet.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_TokenSet proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*TokenSet)(nil), "api.TokenSet")
|
||||||
|
proto.RegisterMapType((map[string]int64)(nil), "api.TokenSet.TokensEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("lib/api/tokenset.proto", fileDescriptor_9ea8707737c33b38) }
|
||||||
|
|
||||||
|
var fileDescriptor_9ea8707737c33b38 = []byte{
|
||||||
|
// 260 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xcb, 0xc9, 0x4c, 0xd2,
|
||||||
|
0x4f, 0x2c, 0xc8, 0xd4, 0x2f, 0xc9, 0xcf, 0x4e, 0xcd, 0x2b, 0x4e, 0x2d, 0xd1, 0x2b, 0x28, 0xca,
|
||||||
|
0x2f, 0xc9, 0x17, 0x62, 0x4e, 0x2c, 0xc8, 0x54, 0x3a, 0xce, 0xc8, 0xc5, 0x11, 0x02, 0x12, 0x0f,
|
||||||
|
0x4e, 0x2d, 0x11, 0x0a, 0xe0, 0x62, 0x83, 0xa8, 0x91, 0x60, 0x54, 0x60, 0xd6, 0xe0, 0x36, 0x92,
|
||||||
|
0xd4, 0x4b, 0x2c, 0xc8, 0xd4, 0x83, 0x49, 0x43, 0x18, 0xc5, 0xae, 0x79, 0x25, 0x45, 0x95, 0x4e,
|
||||||
|
0xb2, 0x27, 0xee, 0xc9, 0x33, 0xbc, 0xba, 0x27, 0x0f, 0xd5, 0xf0, 0xe9, 0x9e, 0x3c, 0x77, 0x45,
|
||||||
|
0x6e, 0x8e, 0x95, 0x12, 0x98, 0xab, 0x14, 0x04, 0x15, 0x96, 0xca, 0xe4, 0xe2, 0x46, 0xd2, 0x25,
|
||||||
|
0xa4, 0xc6, 0xc5, 0x9c, 0x9d, 0x5a, 0x29, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0xe9, 0x24, 0xf2, 0xea,
|
||||||
|
0x9e, 0x3c, 0x88, 0xfb, 0xe9, 0x9e, 0x3c, 0x27, 0x58, 0x6f, 0x76, 0x6a, 0xa5, 0x52, 0x10, 0x48,
|
||||||
|
0x44, 0x48, 0x8f, 0x8b, 0xb5, 0x2c, 0x31, 0xa7, 0x34, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0xd9,
|
||||||
|
0x49, 0xe2, 0xd5, 0x3d, 0x79, 0x88, 0x00, 0xdc, 0x1e, 0x30, 0x4f, 0x29, 0x08, 0x22, 0x6a, 0xc5,
|
||||||
|
0x64, 0xc1, 0xe8, 0xe4, 0x71, 0xe2, 0xa1, 0x1c, 0xc3, 0x85, 0x87, 0x72, 0x0c, 0x27, 0x1e, 0xc9,
|
||||||
|
0x31, 0x5e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x82, 0xc7, 0x72, 0x8c, 0x17, 0x1e,
|
||||||
|
0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0xa5, 0x96, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97,
|
||||||
|
0x9c, 0x9f, 0xab, 0x5f, 0x5c, 0x99, 0x97, 0x5c, 0x92, 0x91, 0x99, 0x97, 0x8e, 0xc4, 0x82, 0x86,
|
||||||
|
0x53, 0x12, 0x1b, 0x38, 0x7c, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xfe, 0x25, 0x31, 0x49,
|
||||||
|
0x39, 0x01, 0x00, 0x00,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TokenSet) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.ProtoSize()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TokenSet) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
size := m.ProtoSize()
|
||||||
|
return m.MarshalToSizedBuffer(dAtA[:size])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TokenSet) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||||
|
i := len(dAtA)
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.Tokens) > 0 {
|
||||||
|
for k := range m.Tokens {
|
||||||
|
v := m.Tokens[k]
|
||||||
|
baseI := i
|
||||||
|
i = encodeVarintTokenset(dAtA, i, uint64(v))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x10
|
||||||
|
i -= len(k)
|
||||||
|
copy(dAtA[i:], k)
|
||||||
|
i = encodeVarintTokenset(dAtA, i, uint64(len(k)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
i = encodeVarintTokenset(dAtA, i, uint64(baseI-i))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(dAtA) - i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeVarintTokenset(dAtA []byte, offset int, v uint64) int {
|
||||||
|
offset -= sovTokenset(v)
|
||||||
|
base := offset
|
||||||
|
for v >= 1<<7 {
|
||||||
|
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||||
|
v >>= 7
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
func (m *TokenSet) ProtoSize() (n int) {
|
||||||
|
if m == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.Tokens) > 0 {
|
||||||
|
for k, v := range m.Tokens {
|
||||||
|
_ = k
|
||||||
|
_ = v
|
||||||
|
mapEntrySize := 1 + len(k) + sovTokenset(uint64(len(k))) + 1 + sovTokenset(uint64(v))
|
||||||
|
n += mapEntrySize + 1 + sovTokenset(uint64(mapEntrySize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func sovTokenset(x uint64) (n int) {
|
||||||
|
return (math_bits.Len64(x|1) + 6) / 7
|
||||||
|
}
|
||||||
|
func sozTokenset(x uint64) (n int) {
|
||||||
|
return sovTokenset(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||||
|
}
|
||||||
|
func (m *TokenSet) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: TokenSet: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: TokenSet: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Tokens", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if m.Tokens == nil {
|
||||||
|
m.Tokens = make(map[string]int64)
|
||||||
|
}
|
||||||
|
var mapkey string
|
||||||
|
var mapvalue int64
|
||||||
|
for iNdEx < postIndex {
|
||||||
|
entryPreIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
if fieldNum == 1 {
|
||||||
|
var stringLenmapkey uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
stringLenmapkey |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intStringLenmapkey := int(stringLenmapkey)
|
||||||
|
if intStringLenmapkey < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
postStringIndexmapkey := iNdEx + intStringLenmapkey
|
||||||
|
if postStringIndexmapkey < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
if postStringIndexmapkey > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
mapkey = string(dAtA[iNdEx:postStringIndexmapkey])
|
||||||
|
iNdEx = postStringIndexmapkey
|
||||||
|
} else if fieldNum == 2 {
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
mapvalue |= int64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iNdEx = entryPreIndex
|
||||||
|
skippy, err := skipTokenset(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > postIndex {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Tokens[mapkey] = mapvalue
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipTokenset(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
|
return ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func skipTokenset(dAtA []byte) (n int, err error) {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
depth := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch wireType {
|
||||||
|
case 0:
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx++
|
||||||
|
if dAtA[iNdEx-1] < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
iNdEx += 8
|
||||||
|
case 2:
|
||||||
|
var length int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowTokenset
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
length |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if length < 0 {
|
||||||
|
return 0, ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
iNdEx += length
|
||||||
|
case 3:
|
||||||
|
depth++
|
||||||
|
case 4:
|
||||||
|
if depth == 0 {
|
||||||
|
return 0, ErrUnexpectedEndOfGroupTokenset
|
||||||
|
}
|
||||||
|
depth--
|
||||||
|
case 5:
|
||||||
|
iNdEx += 4
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||||
|
}
|
||||||
|
if iNdEx < 0 {
|
||||||
|
return 0, ErrInvalidLengthTokenset
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
return iNdEx, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidLengthTokenset = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||||
|
ErrIntOverflowTokenset = fmt.Errorf("proto: integer overflow")
|
||||||
|
ErrUnexpectedEndOfGroupTokenset = fmt.Errorf("proto: unexpected end of group")
|
||||||
|
)
|
@ -29,7 +29,6 @@ const (
|
|||||||
HTTPSKeyFile LocationEnum = "httpsKeyFile"
|
HTTPSKeyFile LocationEnum = "httpsKeyFile"
|
||||||
Database LocationEnum = "database"
|
Database LocationEnum = "database"
|
||||||
LogFile LocationEnum = "logFile"
|
LogFile LocationEnum = "logFile"
|
||||||
CsrfTokens LocationEnum = "csrfTokens"
|
|
||||||
PanicLog LocationEnum = "panicLog"
|
PanicLog LocationEnum = "panicLog"
|
||||||
AuditLog LocationEnum = "auditLog"
|
AuditLog LocationEnum = "auditLog"
|
||||||
GUIAssets LocationEnum = "guiAssets"
|
GUIAssets LocationEnum = "guiAssets"
|
||||||
@ -121,7 +120,6 @@ var locationTemplates = map[LocationEnum]string{
|
|||||||
HTTPSKeyFile: "${config}/https-key.pem",
|
HTTPSKeyFile: "${config}/https-key.pem",
|
||||||
Database: "${data}/" + LevelDBDir,
|
Database: "${data}/" + LevelDBDir,
|
||||||
LogFile: "${data}/syncthing.log", // --logfile on Windows
|
LogFile: "${data}/syncthing.log", // --logfile on Windows
|
||||||
CsrfTokens: "${data}/csrftokens.txt",
|
|
||||||
PanicLog: "${data}/panic-%{timestamp}.log",
|
PanicLog: "${data}/panic-%{timestamp}.log",
|
||||||
AuditLog: "${data}/audit-%{timestamp}.log",
|
AuditLog: "${data}/audit-%{timestamp}.log",
|
||||||
GUIAssets: "${config}/gui",
|
GUIAssets: "${config}/gui",
|
||||||
@ -170,7 +168,6 @@ func PrettyPaths() string {
|
|||||||
fmt.Fprintf(&b, "Database location:\n\t%s\n\n", Get(Database))
|
fmt.Fprintf(&b, "Database location:\n\t%s\n\n", Get(Database))
|
||||||
fmt.Fprintf(&b, "Log file:\n\t%s\n\n", Get(LogFile))
|
fmt.Fprintf(&b, "Log file:\n\t%s\n\n", Get(LogFile))
|
||||||
fmt.Fprintf(&b, "GUI override directory:\n\t%s\n\n", Get(GUIAssets))
|
fmt.Fprintf(&b, "GUI override directory:\n\t%s\n\n", Get(GUIAssets))
|
||||||
fmt.Fprintf(&b, "CSRF tokens file:\n\t%s\n\n", Get(CsrfTokens))
|
|
||||||
fmt.Fprintf(&b, "Default sync folder directory:\n\t%s\n\n", Get(DefFolder))
|
fmt.Fprintf(&b, "Default sync folder directory:\n\t%s\n\n", Get(DefFolder))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
@ -305,7 +305,7 @@ func (a *App) startup() error {
|
|||||||
|
|
||||||
// GUI
|
// GUI
|
||||||
|
|
||||||
if err := a.setupGUI(m, defaultSub, diskSub, discoveryManager, connectionsService, usageReportingSvc, errors, systemLog); err != nil {
|
if err := a.setupGUI(m, defaultSub, diskSub, discoveryManager, connectionsService, usageReportingSvc, errors, systemLog, miscDB); err != nil {
|
||||||
l.Warnln("Failed starting API:", err)
|
l.Warnln("Failed starting API:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -407,7 +407,7 @@ func (a *App) stopWithErr(stopReason svcutil.ExitStatus, err error) svcutil.Exit
|
|||||||
return a.exitStatus
|
return a.exitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, errors, systemLog logger.Recorder) error {
|
func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, errors, systemLog logger.Recorder, miscDB *db.NamespacedKV) error {
|
||||||
guiCfg := a.cfg.GUI()
|
guiCfg := a.cfg.GUI()
|
||||||
|
|
||||||
if !guiCfg.Enabled {
|
if !guiCfg.Enabled {
|
||||||
@ -421,7 +421,7 @@ func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscri
|
|||||||
summaryService := model.NewFolderSummaryService(a.cfg, m, a.myID, a.evLogger)
|
summaryService := model.NewFolderSummaryService(a.cfg, m, a.myID, a.evLogger)
|
||||||
a.mainService.Add(summaryService)
|
a.mainService.Add(summaryService)
|
||||||
|
|
||||||
apiSvc := api.New(a.myID, a.cfg, locations.Get(locations.GUIAssets), tlsDefaultCommonName, m, defaultSub, diskSub, a.evLogger, discoverer, connectionsService, urService, summaryService, errors, systemLog, a.opts.NoUpgrade)
|
apiSvc := api.New(a.myID, a.cfg, locations.Get(locations.GUIAssets), tlsDefaultCommonName, m, defaultSub, diskSub, a.evLogger, discoverer, connectionsService, urService, summaryService, errors, systemLog, a.opts.NoUpgrade, miscDB)
|
||||||
a.mainService.Add(apiSvc)
|
a.mainService.Add(apiSvc)
|
||||||
|
|
||||||
if err := apiSvc.WaitForStart(); err != nil {
|
if err := apiSvc.WaitForStart(); err != nil {
|
||||||
|
@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
// Inception, go generate calls the script itself that then deals with generation.
|
// Inception, go generate calls the script itself that then deals with generation.
|
||||||
// This is only done because go:generate does not support wildcards in paths.
|
// This is only done because go:generate does not support wildcards in paths.
|
||||||
//go:generate go run generate.go lib/protocol lib/config lib/fs lib/db lib/discover
|
//go:generate go run generate.go lib/protocol lib/config lib/fs lib/db lib/discover lib/api
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
for _, path := range os.Args[1:] {
|
for _, path := range os.Args[1:] {
|
||||||
|
8
proto/lib/api/tokenset.proto
Normal file
8
proto/lib/api/tokenset.proto
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package api;
|
||||||
|
|
||||||
|
message TokenSet {
|
||||||
|
// token -> expiry time (epoch nanoseconds)
|
||||||
|
map<string, int64> tokens = 1;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user