1
0
Fork 0
mirror of https://github.com/Luzifer/vault-otp-ui.git synced 2024-11-09 16:50:05 +00:00

Implement pre-fetching for next codes

to provide a more seamless experience

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2018-03-23 12:32:03 +01:00
parent c120d8864d
commit 34214190f8
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
5 changed files with 326 additions and 179 deletions

View file

@ -1,5 +1,12 @@
currentTimeout = 0 currentTimeout = 0
clipboard = undefined clipboard = null
preFetch = null
fetchInProgress = false
serverConnectionError = false
iterationCurrent = 'current'
iterationNext = 'next'
# document-ready function to start Javascript processing # document-ready function to start Javascript processing
$ -> $ ->
@ -38,20 +45,38 @@ delay = (delayMSecs, fkt) ->
window.setTimeout fkt, delayMSecs window.setTimeout fkt, delayMSecs
# fetchCodes contacts the backend to receive JSON containing current codes # fetchCodes contacts the backend to receive JSON containing current codes
fetchCodes = () -> fetchCodes = (iteration) ->
if fetchInProgress
return
fetchInProgress = true
if iteration == iterationCurrent
successFunc = updateCodes
else
successFunc = updatePreFetch
if iteration == iterationCurrent and preFetch != null
data = preFetch
preFetch = null
successFunc data
return
$.ajax $.ajax
url: 'codes.json', url: "codes.json?it=#{iteration}",
success: updateCodes, success: successFunc,
dataType: 'json', dataType: 'json',
error: () -> error: () ->
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? I will retry in a few seconds.', 5000 fetchInProgress = false
delay 5000, fetchCodes createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0
serverConnectionError = true
statusCode: statusCode:
401: () -> 401: () ->
window.location.reload() window.location.reload()
500: () -> 500: () ->
createAlert 'danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000 fetchInProgress = false
delay 2000, fetchCodes createAlert 'danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0
serverConnectionError = true
# filterChange is called when changing the filter field and matches the # filterChange is called when changing the filter field and matches the
# titles of all shown entries. Those not matching the given regular expression # titles of all shown entries. Those not matching the given regular expression
@ -71,7 +96,7 @@ initializeApplication = () ->
$('#keylist').empty() $('#keylist').empty()
$('#filter').bind 'keyup', filterChange $('#filter').bind 'keyup', filterChange
tick 500, refreshTimerProgress tick 500, refreshTimerProgress
fetchCodes() fetchCodes iterationCurrent
# refreshTimerProgress updates the top progressbar to display the # refreshTimerProgress updates the top progressbar to display the
# remaining time until the one-time-passwords changes # remaining time until the one-time-passwords changes
@ -79,6 +104,10 @@ refreshTimerProgress = () ->
secondsLeft = timeLeft() secondsLeft = timeLeft()
$('#timer').css 'width', "#{secondsLeft / 30 * 100}%" $('#timer').css 'width', "#{secondsLeft / 30 * 100}%"
if secondsLeft < 10 and preFetch == null and not serverConnectionError
# Do a pre-fetch to provide a seamless experience
fetchCodes iterationNext
# tick is a convenience wrapper to swap parameters of setInterval # tick is a convenience wrapper to swap parameters of setInterval
tick = (delay, fkt) -> tick = (delay, fkt) ->
window.setInterval fkt, delay window.setInterval fkt, delay
@ -108,4 +137,10 @@ updateCodes = (data) ->
filterChange() filterChange()
delay timeLeft()*1000, fetchCodes delay timeLeft()*1000, ->
fetchCodes iterationCurrent
fetchInProgress = false
updatePreFetch = (data) ->
preFetch = data
fetchInProgress = false

View file

@ -1,10 +1,20 @@
// Generated by CoffeeScript 1.12.4 // Generated by CoffeeScript 1.12.4
(function() { (function() {
var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, filterChange, initializeApplication, refreshTimerProgress, tick, timeLeft, updateCodes; var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, fetchInProgress, filterChange, initializeApplication, iterationCurrent, iterationNext, preFetch, refreshTimerProgress, serverConnectionError, tick, timeLeft, updateCodes, updatePreFetch;
currentTimeout = 0; currentTimeout = 0;
clipboard = void 0; clipboard = null;
preFetch = null;
fetchInProgress = false;
serverConnectionError = false;
iterationCurrent = 'current';
iterationNext = 'next';
$(function() { $(function() {
if ($('body').hasClass('state-signedin')) { if ($('body').hasClass('state-signedin')) {
@ -41,22 +51,40 @@
return window.setTimeout(fkt, delayMSecs); return window.setTimeout(fkt, delayMSecs);
}; };
fetchCodes = function() { fetchCodes = function(iteration) {
var data, successFunc;
if (fetchInProgress) {
return;
}
fetchInProgress = true;
if (iteration === iterationCurrent) {
successFunc = updateCodes;
} else {
successFunc = updatePreFetch;
}
if (iteration === iterationCurrent && preFetch !== null) {
data = preFetch;
preFetch = null;
successFunc(data);
return;
}
return $.ajax({ return $.ajax({
url: 'codes.json', url: "codes.json?it=" + iteration,
success: updateCodes, success: successFunc,
dataType: 'json', dataType: 'json',
error: function() { error: function() {
createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? I will retry in a few seconds.', 5000); fetchInProgress = false;
return delay(5000, fetchCodes); createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0);
return serverConnectionError = true;
}, },
statusCode: { statusCode: {
401: function() { 401: function() {
return window.location.reload(); return window.location.reload();
}, },
500: function() { 500: function() {
createAlert('danger', 'Oops.', 'The server responded with an internal error. I will retry in a few seconds.', 2000); fetchInProgress = false;
return delay(2000, fetchCodes); createAlert('danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0);
return serverConnectionError = true;
} }
} }
}); });
@ -78,13 +106,16 @@
$('#keylist').empty(); $('#keylist').empty();
$('#filter').bind('keyup', filterChange); $('#filter').bind('keyup', filterChange);
tick(500, refreshTimerProgress); tick(500, refreshTimerProgress);
return fetchCodes(); return fetchCodes(iterationCurrent);
}; };
refreshTimerProgress = function() { refreshTimerProgress = function() {
var secondsLeft; var secondsLeft;
secondsLeft = timeLeft(); secondsLeft = timeLeft();
return $('#timer').css('width', (secondsLeft / 30 * 100) + "%"); $('#timer').css('width', (secondsLeft / 30 * 100) + "%");
if (secondsLeft < 10 && preFetch === null && !serverConnectionError) {
return fetchCodes(iterationNext);
}
}; };
tick = function(delay, fkt) { tick = function(delay, fkt) {
@ -115,7 +146,15 @@
} }
}); });
filterChange(); filterChange();
return delay(timeLeft() * 1000, fetchCodes); delay(timeLeft() * 1000, function() {
return fetchCodes(iterationCurrent);
});
return fetchInProgress = false;
};
updatePreFetch = function(data) {
preFetch = data;
return fetchInProgress = false;
}; };
}).call(this); }).call(this);

369
assets.go

File diff suppressed because one or more lines are too long

10
main.go
View file

@ -207,7 +207,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
} }
log.WithFields(log.Fields{"token": hashSecret(tok)}).Debugf("Checked / renewed token") log.WithFields(log.Fields{"token": hashSecret(tok)}).Debugf("Checked / renewed token")
tokens, err := getSecretsFromVault(tok) pointOfTime := time.Now()
if r.URL.Query().Get("it") == "next" {
pointOfTime = pointOfTime.Add(time.Duration(30-(pointOfTime.Second()%30)) * time.Second)
}
tokens, err := getSecretsFromVault(tok, pointOfTime)
if err != nil { if err != nil {
log.Errorf("Unable to fetch codes: %s", err) log.Errorf("Unable to fetch codes: %s", err)
http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError) http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError)
@ -221,13 +226,12 @@ func handleCodesJSON(res http.ResponseWriter, r *http.Request) {
return return
} }
n := time.Now()
result := struct { result := struct {
Tokens []*token `json:"tokens"` Tokens []*token `json:"tokens"`
NextWrap time.Time `json:"next_wrap"` NextWrap time.Time `json:"next_wrap"`
}{ }{
Tokens: tokens, Tokens: tokens,
NextWrap: n.Add(time.Duration(30-(n.Second()%30)) * time.Second), NextWrap: pointOfTime.Add(time.Duration(30-(pointOfTime.Second()%30)) * time.Second),
} }
res.Header().Set("Content-Type", "application/json") res.Header().Set("Content-Type", "application/json")

View file

@ -79,7 +79,7 @@ func useOrRenewToken(tok, accessToken string) (string, error) {
} }
} }
func getSecretsFromVault(tok string) ([]*token, error) { func getSecretsFromVault(tok string, pointOfTime time.Time) ([]*token, error) {
client, err := api.NewClient(&api.Config{ client, err := api.NewClient(&api.Config{
Address: cfg.Vault.Address, Address: cfg.Vault.Address,
}) })
@ -112,7 +112,7 @@ func getSecretsFromVault(tok string) ([]*token, error) {
case key := <-scanPool: case key := <-scanPool:
go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg) go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg)
case key := <-keyPoolChan: case key := <-keyPoolChan:
go fetchTokenFromKey(client, key, respChan, wg) go fetchTokenFromKey(client, key, respChan, wg, pointOfTime)
case t := <-respChan: case t := <-respChan:
resp = append(resp, t) resp = append(resp, t)
case <-done: case <-done:
@ -159,7 +159,7 @@ func scanKeyForSubKeys(client *api.Client, key string, subKeyChan, tokenKeyChan
} }
} }
func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *sync.WaitGroup) { func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *sync.WaitGroup, pointOfTime time.Time) {
defer wg.Done() defer wg.Done()
data, err := client.Logical().Read(k) data, err := client.Logical().Read(k)
@ -182,7 +182,7 @@ func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *s
switch k { switch k {
case cfg.Vault.SecretField: case cfg.Vault.SecretField:
tok.Secret = v.(string) tok.Secret = v.(string)
tok.GenerateCode(time.Now()) tok.GenerateCode(pointOfTime)
case "code": case "code":
tok.Code = v.(string) tok.Code = v.(string)
case "name": case "name":