Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions browser/html/framed.doc.html
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,9 @@
post({'MessageId': messageId, 'Values': values});
}

function reset_access_token(accesstoken) {
function reset_access_token(accesstoken, ttl) {
post({'MessageId': 'Reset_Access_Token',
'Values': { 'token': accesstoken, }
'Values': { 'token': accesstoken, 'ttl': ttl }
});
}

Expand Down Expand Up @@ -597,9 +597,14 @@ <h3>Various other messages to post</h3>

<form class="vbox framed">
<h3>New Access-Token</h3>
<label for="new-access-token"><b>Access Token:</b></label>
<textarea name="new-access-token" id="new-access-token" value="" rows="1" cols="30">123456789AA</textarea>
<label for="new-access-token-ttl"><b>Access Token TTL (millis since epoch when the token will expire):</b></label>
<textarea name="new-access-token-ttl" id="new-access-token-ttl" value="" rows="1" cols="30"></textarea>
<div>
<button onclick="reset_access_token(document.getElementById('new-access-token').value); return false;">Reset Access-Token</button>
<button
onclick="reset_access_token(document.getElementById('new-access-token').value, document.getElementById('new-access-token-ttl').value); return false;">Reset
Access-Token</button>
</div>

<h3>User State</h3>
Expand Down
9 changes: 5 additions & 4 deletions browser/js/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class InitializerBase {
window.hexifyUrl = false;
window.versionPath = "";
window.accessToken = element.dataset.accessToken;
window.accessTokenTTL = element.dataset.accessTokenTtl;
window.accessTokenTTL = element.dataset.accessTokenTtl || '0';
window.noAuthHeader = element.dataset.noAuthHeader;
window.accessHeader = element.dataset.accessHeader;
window.postMessageOriginExt = "";
Expand Down Expand Up @@ -1917,7 +1917,7 @@ function showWelcomeSVG() {
global.webserver = global.webserver.replace(/\/*$/, ''); // Remove trailing slash.
}

var docParams, wopiParams;
var docParams = '', wopiParams;
var filePath = global.coolParams.get('file_path');
global.wopiSrc = global.coolParams.get('WOPISrc');
if (global.wopiSrc != '') {
Expand Down Expand Up @@ -1981,7 +1981,8 @@ function showWelcomeSVG() {
wopiSrc += '&RouteToken=' + global.routeToken;
}

return root + '/ws' + wopiSrc + '&' + docParams;
var separator = wopiSrc ? '&' : (docParams ? '?' : '');
return root + '/ws' + wopiSrc + separator + docParams;
};

// Form a valid WS URL to the host with the given path and
Expand Down Expand Up @@ -2041,7 +2042,7 @@ function showWelcomeSVG() {
global.socket = new global.FakeWebSocket();
global.TheFakeWebSocket = global.socket;
} else {
if (global.enableExperimentalFeatures) {
if (global.enableExperimentalFeatures && global.wopiSrc) {
var websocketURI = global.makeWopiCoolWsUrl(global.makeWsUrl('/cool'), docParams);
} else {
// The URL may already contain a query (e.g., 'http://server.tld/foo/wopi/files/bar?desktop=baz') - then just append more params
Expand Down
63 changes: 45 additions & 18 deletions browser/src/app/Socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Socket {
private _inLayerTransaction: boolean;
private _slurpDuringTransaction: boolean;
private _accessTokenExpireTimeout: TimeoutHdl | undefined;
private _accessTokenExpireWarningCount: number = 0;
private _reconnecting: boolean;
private _slurpTimer: TimeoutHdl | undefined;
private _renderEventTimer: TimeoutHdl | undefined;
Expand Down Expand Up @@ -236,8 +237,8 @@ class Socket {
}

private getWebSocketBaseURI(map: MapInterface): string {
if (window.enableExperimentalFeatures) {
// Use the new Cool WS URL.
if (window.enableExperimentalFeatures && map.options.wopiSrc) {
// Use the new Cool WS URL for WOPI documents.
return window.makeWopiCoolWsUrl(
window.makeWsUrl('/cool'),
$.param(map.options.docParams),
Expand Down Expand Up @@ -296,22 +297,25 @@ class Socket {

this._connectCount++;
this._faultInjection();
if (
map.options.docParams.access_token &&
parseInt(map.options.docParams.access_token_ttl as string)
) {
this.resetTokenExpiryTimer();

// process messages for early socket connection
this._emptyQueue();
}

public resetTokenExpiryTimer(): void {
clearTimeout(this._accessTokenExpireTimeout); // Always clear the old timer.
this._accessTokenExpireWarningCount = 0;
const ttl = parseInt(
this._map.options.docParams.access_token_ttl as string,
);
if (this._map.options.docParams.access_token && ttl) {
const tokenExpiryWarning = 900 * 1000; // Warn when 15 minutes remain
clearTimeout(this._accessTokenExpireTimeout);
this._accessTokenExpireTimeout = setTimeout(
this._sessionExpiredWarning.bind(this),
parseInt(map.options.docParams.access_token_ttl as string) -
Date.now() -
tokenExpiryWarning,
ttl - Date.now() - tokenExpiryWarning,
);
}

// process messages for early socket connection
this._emptyQueue();
}

public close(code?: number, reason?: string): void {
Expand Down Expand Up @@ -654,11 +658,27 @@ class Socket {
const timerepr = dateTime.toLocaleDateString(String.locale, dateOptions);
this._map.fire('warn', { msg: expirymsg.replace('{time}', timerepr) });

// If user still doesn't refresh the session, warn again periodically
this._accessTokenExpireTimeout = setTimeout(
this._sessionExpiredWarning.bind(this),
120 * 1000,
);
// Notify the host so it can refresh the token programmatically.
const remainingMs =
parseInt(this._map.options.docParams.access_token_ttl as string) -
Date.now();
this._map.fire('postMessage', {
msgId: 'App_TokenExpiring',
args: {
Timeout: Math.max(remainingMs, 0),
},
});

// If user still doesn't refresh the session, warn again periodically.
// Cap at 10 retries (~20 minutes, i.e. ~5 minutes after the access_token
// expires) so we don't spam the host indefinitely.
this._accessTokenExpireWarningCount++;
if (this._accessTokenExpireWarningCount < 10) {
this._accessTokenExpireTimeout = setTimeout(
this._sessionExpiredWarning.bind(this),
120 * 1000,
);
}
}

public setUnloading(): void {
Expand Down Expand Up @@ -1142,6 +1162,13 @@ class Socket {
} else if (textMsg.startsWith('migrate:') && window.indirectSocket) {
this._onMigrateMsg(textMsg);
return;
} else if (textMsg.startsWith('tokenexpired')) {
// Server got a 401 on save. Ask the host for a fresh token.
this._map.fire('postMessage', {
msgId: 'App_TokenExpired',
args: {},
});
return;
} else if (textMsg.startsWith('close: ')) {
this._onCloseMsg(textMsg);
return;
Expand Down
9 changes: 8 additions & 1 deletion browser/src/map/handler/Map.WOPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,14 @@ window.L.Map.WOPI = window.L.Handler.extend({
this._postViewsMessage('Get_Views_Resp');
}
else if (msg.MessageId === 'Reset_Access_Token') {
app.socket.sendMessage('resetaccesstoken ' + msg.Values.token);
if (msg.Values) {
// No ttl implies no expiry tracking, matching the legacy
// single-arg form of the resetaccesstoken protocol command.
var ttl = msg.Values.ttl ? msg.Values['ttl'] : '0';
app.socket.sendMessage('resetaccesstoken ' + msg.Values.token + ' ' + ttl);
this._map.options.docParams.access_token_ttl = ttl;
app.socket.resetTokenExpiryTimer();
}
}
else if (msg.MessageId === 'Action_Save') {
var dontTerminateEdit = msg.Values && msg.Values['DontTerminateEdit'];
Expand Down
48 changes: 47 additions & 1 deletion common/Authorization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
#include <config.h>

#include "Authorization.hpp"

#include <common/ConfigUtil.hpp>
#include <common/Log.hpp>
#include <common/NumUtil.hpp>
#include <common/StringVector.hpp>
#include <common/Uri.hpp>

Expand Down Expand Up @@ -98,9 +101,31 @@ void Authorization::authorizeRequest(Poco::Net::HTTPRequest& request) const
}
}

Authorization::duration Authorization::adjustExpiryEpoch(duration rawExpiryEpoch)
{
if (rawExpiryEpoch == duration::zero())
{
// No TTL provided; apply default lifetime if configured.
CONFIG_STATIC const int defaultLifetimeMins =
ConfigUtil::getInt("storage.wopi.access_token.default_lifetime_mins", 0);
if (defaultLifetimeMins > 0)
{
const auto expiry = std::chrono::duration_cast<duration>(
std::chrono::system_clock::now().time_since_epoch() +
std::chrono::minutes(defaultLifetimeMins));
LOG_TRC("No access_token_ttl provided, using default lifetime of "
<< defaultLifetimeMins << "m, expiry at " << expiry);
return expiry;
}
}

return rawExpiryEpoch;
}

Authorization Authorization::create(const Poco::URI& uri)
{
bool noHeader = false;
duration rawExpiryEpoch = duration::zero();
Authorization::Type type = Authorization::Type::None;
std::string decoded;
for (const auto& param : uri.getQueryParameters())
Expand All @@ -119,10 +144,31 @@ Authorization Authorization::create(const Poco::URI& uri)
noHeader = true;
}
}
else if (param.first == "access_token_ttl")
{
rawExpiryEpoch = duration(NumUtil::u64FromString(param.second, 0));
}
}

if (!decoded.empty())
return Authorization(type, std::move(decoded), noHeader);
{
Authorization auth(type, std::move(decoded), noHeader);
if (type == Authorization::Type::Token)
{
const duration adjusted = adjustExpiryEpoch(rawExpiryEpoch);
if (adjusted > duration::zero())
{
auth.setExpiryEpoch(adjusted);
}
}
else if (rawExpiryEpoch > duration::zero())
{
LOG_WRN("Ignoring invalid access_token_ttl with ["
<< name(type) << "] authorization type in uri [" << uri.toString() << ']');
}

return auth;
}

return Authorization();
}
Expand Down
Loading
Loading