mirror of https://github.com/keeweb/keeweb.git
fix #1359: fixed Google Drive login issues in desktop apps
This commit is contained in:
parent
61d83319d4
commit
7ccf38f0df
|
@ -1,4 +1,5 @@
|
|||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
|
||||
const ChooserAppKey = 'qp7ctun6qt5n9d6';
|
||||
|
||||
|
@ -26,8 +27,8 @@ DropboxChooser.prototype.choose = function() {
|
|||
};
|
||||
|
||||
DropboxChooser.prototype.buildUrl = function() {
|
||||
const urlParams = {
|
||||
origin: encodeURIComponent(window.location.protocol + '//' + window.location.host),
|
||||
return UrlFormat.makeUrl('https://www.dropbox.com/chooser', {
|
||||
origin: window.location.protocol + '//' + window.location.host,
|
||||
'app_key': AppSettingsModel.dropboxAppKey || ChooserAppKey,
|
||||
'link_type': 'direct',
|
||||
trigger: 'js',
|
||||
|
@ -36,13 +37,7 @@ DropboxChooser.prototype.buildUrl = function() {
|
|||
folderselect: 'false',
|
||||
iframe: 'false',
|
||||
version: 2
|
||||
};
|
||||
return (
|
||||
'https://www.dropbox.com/chooser?' +
|
||||
Object.keys(urlParams)
|
||||
.map(key => key + '=' + urlParams[key])
|
||||
.join('&')
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
DropboxChooser.prototype.onMessage = function(e) {
|
||||
|
|
|
@ -10,7 +10,8 @@ const Timeouts = {
|
|||
RedrawInactiveWindow: 50,
|
||||
PopupWaitTime: 1000,
|
||||
AutoUpdatePluginsAfterStart: 500,
|
||||
LinkDownloadRevoke: 10 * 1000 * 60
|
||||
LinkDownloadRevoke: 10 * 1000 * 60,
|
||||
DefaultHttpRequest: 60000
|
||||
};
|
||||
|
||||
export { Timeouts };
|
||||
|
|
|
@ -211,18 +211,14 @@ class StorageDropbox extends StorageBase {
|
|||
const host = args.host || 'api';
|
||||
let headers;
|
||||
let data = args.data;
|
||||
let dataType;
|
||||
if (args.apiArg) {
|
||||
headers = {
|
||||
'Dropbox-API-Arg': this._encodeJsonHttpHeader(JSON.stringify(args.apiArg))
|
||||
};
|
||||
if (args.data) {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
}
|
||||
} else if (args.data) {
|
||||
data = JSON.stringify(data);
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
dataType = 'application/json';
|
||||
}
|
||||
this._xhr({
|
||||
url: `https://${host}.dropboxapi.com/2/${args.method}`,
|
||||
|
@ -230,6 +226,7 @@ class StorageDropbox extends StorageBase {
|
|||
responseType: args.responseType || 'json',
|
||||
headers,
|
||||
data,
|
||||
dataType,
|
||||
statuses: args.statuses || undefined,
|
||||
success: args.success,
|
||||
error: (e, xhr) => {
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { StorageBase } from 'storage/storage-base';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const GDriveClientId = {
|
||||
Local: '783608538594-36tkdh8iscrq8t8dq87gghubnhivhjp5.apps.googleusercontent.com',
|
||||
Production: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com'
|
||||
Production: '847548101761-koqkji474gp3i2gn3k5omipbfju7pbt1.apps.googleusercontent.com',
|
||||
Desktop: '847548101761-h2pcl2p6m1tssnlqm0vrm33crlveccbr.apps.googleusercontent.com'
|
||||
};
|
||||
const GDriveClientSecret = {
|
||||
// They are not really secrets and are supposed to be embedded in the app code according to
|
||||
// the official guide: https://developers.google.com/identity/protocols/oauth2#installed
|
||||
// The process results in a client ID and, in some cases, a client secret,
|
||||
// which you embed in the source code of your application.
|
||||
// (In this context, the client secret is obviously not treated as a secret.)
|
||||
Desktop: 'nTSCiqXtUNmURIIdASaC1TJK'
|
||||
};
|
||||
const NewFileIdPrefix = 'NewFile:';
|
||||
|
||||
|
@ -95,46 +105,48 @@ class StorageGDrive extends StorageBase {
|
|||
const ts = this.logger.ts();
|
||||
const isNew = path.lastIndexOf(NewFileIdPrefix, 0) === 0;
|
||||
let url;
|
||||
let dataType;
|
||||
let dataIsMultipart = false;
|
||||
if (isNew) {
|
||||
url =
|
||||
this._baseUrlUpload +
|
||||
'/files?uploadType=multipart&fields=id,headRevisionId';
|
||||
const fileName = path.replace(NewFileIdPrefix, '') + '.kdbx';
|
||||
const boundry = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
|
||||
data = new Blob(
|
||||
[
|
||||
'--',
|
||||
boundry,
|
||||
'\r\n',
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
'\r\n\r\n',
|
||||
JSON.stringify({ name: fileName }),
|
||||
'\r\n',
|
||||
'--',
|
||||
boundry,
|
||||
'\r\n',
|
||||
'Content-Type: application/octet-stream',
|
||||
'\r\n\r\n',
|
||||
data,
|
||||
'\r\n',
|
||||
'--',
|
||||
boundry,
|
||||
'--',
|
||||
'\r\n'
|
||||
],
|
||||
{ type: 'multipart/related; boundary="' + boundry + '"' }
|
||||
);
|
||||
const boundary = 'b' + Date.now() + 'x' + Math.round(Math.random() * 1000000);
|
||||
data = [
|
||||
'--',
|
||||
boundary,
|
||||
'\r\n',
|
||||
'Content-Type: application/json; charset=UTF-8',
|
||||
'\r\n\r\n',
|
||||
JSON.stringify({ name: fileName }),
|
||||
'\r\n',
|
||||
'--',
|
||||
boundary,
|
||||
'\r\n',
|
||||
'Content-Type: application/octet-stream',
|
||||
'\r\n\r\n',
|
||||
data,
|
||||
'\r\n',
|
||||
'--',
|
||||
boundary,
|
||||
'--',
|
||||
'\r\n'
|
||||
];
|
||||
dataType = 'multipart/related; boundary="' + boundary + '"';
|
||||
dataIsMultipart = true;
|
||||
} else {
|
||||
url =
|
||||
this._baseUrlUpload +
|
||||
'/files/{id}?uploadType=media&fields=headRevisionId'.replace('{id}', path);
|
||||
data = new Blob([data], { type: 'application/octet-stream' });
|
||||
}
|
||||
this._xhr({
|
||||
url,
|
||||
method: isNew ? 'POST' : 'PATCH',
|
||||
responseType: 'json',
|
||||
data,
|
||||
dataType,
|
||||
dataIsMultipart,
|
||||
success: response => {
|
||||
this.logger.debug('Saved', path, this.logger.ts(ts));
|
||||
const newRev = response.headRevisionId;
|
||||
|
@ -239,20 +251,32 @@ class StorageGDrive extends StorageBase {
|
|||
|
||||
_getOAuthConfig() {
|
||||
let clientId = this.appSettings.gdriveClientId;
|
||||
let clientSecret;
|
||||
if (!clientId) {
|
||||
clientId =
|
||||
location.origin.indexOf('localhost') >= 0
|
||||
? GDriveClientId.Local
|
||||
: GDriveClientId.Production;
|
||||
if (Features.isDesktop) {
|
||||
clientId = GDriveClientId.Desktop;
|
||||
clientSecret = GDriveClientSecret.Desktop;
|
||||
} else {
|
||||
clientId =
|
||||
location.origin.indexOf('localhost') >= 0
|
||||
? GDriveClientId.Local
|
||||
: GDriveClientId.Production;
|
||||
}
|
||||
}
|
||||
return {
|
||||
scope: 'https://www.googleapis.com/auth/drive',
|
||||
url: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
tokenUrl: 'https://oauth2.googleapis.com/token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
width: 600,
|
||||
height: 400
|
||||
};
|
||||
}
|
||||
|
||||
_useLocalOAuthRedirectListener() {
|
||||
return Features.isDesktop;
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageGDrive };
|
||||
|
|
|
@ -121,7 +121,7 @@ class StorageOneDrive extends StorageBase {
|
|||
method: 'PUT',
|
||||
responseType: 'json',
|
||||
headers: rev ? { 'If-Match': rev } : null,
|
||||
data: new Blob([data], { type: 'application/octet-stream' }),
|
||||
data,
|
||||
statuses: [200, 201, 412],
|
||||
success: (response, xhr) => {
|
||||
rev = response.eTag;
|
||||
|
@ -213,7 +213,8 @@ class StorageOneDrive extends StorageBase {
|
|||
method: 'POST',
|
||||
responseType: 'json',
|
||||
statuses: [200, 204],
|
||||
data: new Blob([data], { type: 'application/json' }),
|
||||
data,
|
||||
dataType: 'application/json',
|
||||
success: () => {
|
||||
this.logger.debug('Made dir', path, this.logger.ts(ts));
|
||||
return callback && callback();
|
||||
|
|
|
@ -3,6 +3,11 @@ import { Links } from 'const/links';
|
|||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { StorageOAuthListener } from 'storage/storage-oauth-listener';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { omitEmpty } from 'util/fn';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
const MaxRequestRetries = 3;
|
||||
|
||||
|
@ -37,19 +42,30 @@ class StorageBase {
|
|||
}
|
||||
|
||||
_xhr(config) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
if (config.responseType) {
|
||||
xhr.responseType = config.responseType;
|
||||
}
|
||||
const statuses = config.statuses || [200];
|
||||
xhr.addEventListener('load', () => {
|
||||
if (statuses.indexOf(xhr.status) >= 0) {
|
||||
return config.success && config.success(xhr.response, xhr);
|
||||
if (config.data) {
|
||||
if (!config.dataType) {
|
||||
config.dataType = 'application/octet-stream';
|
||||
}
|
||||
if (xhr.status === 401 && this._oauthToken) {
|
||||
this._oauthRefreshToken(err => {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
'Content-Type': config.dataType
|
||||
};
|
||||
}
|
||||
if (this._oauthToken && !config.skipAuth) {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
'Authorization': 'Bearer ' + this._oauthToken.accessToken
|
||||
};
|
||||
}
|
||||
this._httpRequest(config, response => {
|
||||
const statuses = config.statuses || [200];
|
||||
if (statuses.indexOf(response.status) >= 0) {
|
||||
return config.success && config.success(response.response, response);
|
||||
}
|
||||
if (response.status === 401 && this._oauthToken) {
|
||||
this._oauthGetNewToken(err => {
|
||||
if (err) {
|
||||
return config.error && config.error('unauthorized', xhr);
|
||||
return config.error && config.error('unauthorized', response);
|
||||
} else {
|
||||
config.tryNum = (config.tryNum || 0) + 1;
|
||||
if (config.tryNum >= MaxRequestRetries) {
|
||||
|
@ -57,16 +73,35 @@ class StorageBase {
|
|||
'Too many authorize attempts, fail request',
|
||||
config.url
|
||||
);
|
||||
return config.error && config.error('unauthorized', xhr);
|
||||
return config.error && config.error('unauthorized', response);
|
||||
}
|
||||
this.logger.info('Repeat request, try #' + config.tryNum, config.url);
|
||||
this._xhr(config);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return config.error && config.error('http status ' + xhr.status, xhr);
|
||||
return config.error && config.error('http status ' + response.status, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_httpRequest(config, onLoad) {
|
||||
const httpRequest = Launcher ? this._httpRequestLauncher : this._httpRequestWeb;
|
||||
httpRequest(config, onLoad);
|
||||
}
|
||||
|
||||
_httpRequestWeb(config, onLoad) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
if (config.responseType) {
|
||||
xhr.responseType = config.responseType;
|
||||
}
|
||||
xhr.addEventListener('load', () => {
|
||||
onLoad({
|
||||
status: xhr.status,
|
||||
response: xhr.response,
|
||||
getResponseHeader: name => xhr.getResponseHeader(name)
|
||||
});
|
||||
});
|
||||
xhr.addEventListener('error', () => {
|
||||
return config.error && config.error('network error', xhr);
|
||||
});
|
||||
|
@ -74,22 +109,68 @@ class StorageBase {
|
|||
return config.error && config.error('timeout', xhr);
|
||||
});
|
||||
xhr.open(config.method || 'GET', config.url);
|
||||
if (this._oauthToken && !config.skipAuth) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + this._oauthToken.accessToken);
|
||||
}
|
||||
if (config.headers) {
|
||||
for (const [key, value] of Object.entries(config.headers)) {
|
||||
xhr.setRequestHeader(key, value);
|
||||
}
|
||||
}
|
||||
let data = config.data;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
data = new Uint8Array(data);
|
||||
if (data) {
|
||||
if (!config.dataIsMultipart) {
|
||||
data = [data];
|
||||
}
|
||||
data = new Blob(data, { type: config.dataType });
|
||||
}
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
_openPopup(url, title, width, height) {
|
||||
_httpRequestLauncher(config, onLoad) {
|
||||
const https = Launcher.req('https');
|
||||
const req = https.request(config.url, {
|
||||
method: config.method || 'GET',
|
||||
headers: config.headers,
|
||||
timeout: Timeouts.DefaultHttpRequest
|
||||
});
|
||||
req.on('response', res => {
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
let response = Buffer.concat(chunks);
|
||||
if (config.responseType === 'json') {
|
||||
response = JSON.parse(response.toString('utf8'));
|
||||
} else {
|
||||
response = response.buffer.slice(
|
||||
response.byteOffset,
|
||||
response.byteOffset + response.length
|
||||
);
|
||||
}
|
||||
onLoad({
|
||||
status: res.statusCode,
|
||||
response,
|
||||
getResponseHeader: name => res.headers[name.toLowerCase()]
|
||||
});
|
||||
});
|
||||
});
|
||||
req.on('error', () => {
|
||||
return config.error && config.error('network error', {});
|
||||
});
|
||||
req.on('timeout', () => {
|
||||
req.abort();
|
||||
return config.error && config.error('timeout', {});
|
||||
});
|
||||
if (config.data) {
|
||||
let data;
|
||||
if (config.dataIsMultipart) {
|
||||
data = Buffer.concat(config.data.map(chunk => Buffer.from(chunk)));
|
||||
} else {
|
||||
data = Buffer.from(config.data);
|
||||
}
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
}
|
||||
|
||||
_openPopup(url, title, width, height, extras) {
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
|
||||
|
||||
|
@ -121,7 +202,7 @@ class StorageBase {
|
|||
.map(key => key + '=' + settings[key])
|
||||
.join(',');
|
||||
|
||||
return window.open(url, title, settings);
|
||||
return window.open(url, title, settings, extras);
|
||||
}
|
||||
|
||||
_getOauthRedirectUrl() {
|
||||
|
@ -143,12 +224,37 @@ class StorageBase {
|
|||
this._oauthToken = oldToken;
|
||||
return callback();
|
||||
}
|
||||
const url =
|
||||
opts.url +
|
||||
'?client_id={cid}&scope={scope}&response_type=token&redirect_uri={url}'
|
||||
.replace('{cid}', encodeURIComponent(opts.clientId))
|
||||
.replace('{scope}', encodeURIComponent(opts.scope))
|
||||
.replace('{url}', encodeURIComponent(this._getOauthRedirectUrl()));
|
||||
|
||||
if (oldToken && oldToken.refreshToken) {
|
||||
return this._oauthExchangeRefreshToken(callback);
|
||||
}
|
||||
|
||||
if (this._useLocalOAuthRedirectListener()) {
|
||||
return StorageOAuthListener.listen()
|
||||
.then(listener => {
|
||||
const url = UrlFormat.makeUrl(opts.url, {
|
||||
'client_id': opts.clientId,
|
||||
'scope': opts.scope,
|
||||
'state': listener.state,
|
||||
'redirect_uri': listener.redirectUri,
|
||||
'response_type': 'code',
|
||||
'code_challenge': listener.codeChallenge,
|
||||
'code_challenge_method': 'S256'
|
||||
});
|
||||
Launcher.openLink(url);
|
||||
callback('browser-auth-started');
|
||||
listener.callback = code => this._oauthCodeReceived(code, listener);
|
||||
})
|
||||
.catch(err => callback(err));
|
||||
}
|
||||
|
||||
const url = UrlFormat.makeUrl(opts.url, {
|
||||
'client_id': opts.clientId,
|
||||
'scope': opts.scope,
|
||||
'response_type': 'token',
|
||||
'redirect_uri': this._getOauthRedirectUrl()
|
||||
});
|
||||
|
||||
this.logger.debug('OAuth: popup opened');
|
||||
const popupWindow = this._openPopup(url, 'OAuth', opts.width, opts.height);
|
||||
if (!popupWindow) {
|
||||
|
@ -203,21 +309,26 @@ class StorageBase {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
return omitEmpty({
|
||||
dt: Date.now() - 60 * 1000,
|
||||
tokenType: data.token_type,
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
authenticationToken: data.authentication_token,
|
||||
expiresIn: +data.expires_in,
|
||||
scope: data.scope,
|
||||
userId: data.user_id
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_oauthRefreshToken(callback) {
|
||||
_oauthGetNewToken(callback) {
|
||||
this._oauthToken.expired = true;
|
||||
this.runtimeData[this.name + 'OAuthToken'] = this._oauthToken;
|
||||
this._oauthAuthorize(callback);
|
||||
if (this._oauthToken.refreshToken) {
|
||||
this._oauthExchangeRefreshToken(callback);
|
||||
} else {
|
||||
this._oauthAuthorize(callback);
|
||||
}
|
||||
}
|
||||
|
||||
_oauthRevokeToken(url) {
|
||||
|
@ -243,6 +354,73 @@ class StorageBase {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_useLocalOAuthRedirectListener() {
|
||||
return false;
|
||||
}
|
||||
|
||||
_oauthCodeReceived(code, listener) {
|
||||
this.logger.debug('OAuth code received');
|
||||
Launcher.showMainWindow();
|
||||
const config = this._getOAuthConfig();
|
||||
this._xhr({
|
||||
url: config.tokenUrl,
|
||||
method: 'POST',
|
||||
responseType: 'json',
|
||||
skipAuth: true,
|
||||
data: JSON.stringify({
|
||||
'client_id': config.clientId,
|
||||
'client_secret': config.clientSecret,
|
||||
'grant_type': 'authorization_code',
|
||||
code,
|
||||
'code_verifier': listener.codeVerifier,
|
||||
'redirect_uri': listener.redirectUri
|
||||
}),
|
||||
dataType: 'application/json',
|
||||
success: response => {
|
||||
this.logger.debug('OAuth code exchanged');
|
||||
this._oauthProcessReturn(response);
|
||||
},
|
||||
error: err => {
|
||||
this.logger.error('Error exchanging OAuth code', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_oauthExchangeRefreshToken(callback) {
|
||||
this.logger.debug('Exchanging refresh token');
|
||||
const { refreshToken } = this.runtimeData[this.name + 'OAuthToken'];
|
||||
const config = this._getOAuthConfig();
|
||||
this._xhr({
|
||||
url: config.tokenUrl,
|
||||
method: 'POST',
|
||||
responseType: 'json',
|
||||
skipAuth: true,
|
||||
data: JSON.stringify({
|
||||
'client_id': config.clientId,
|
||||
'client_secret': config.clientSecret,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refreshToken
|
||||
}),
|
||||
dataType: 'application/json',
|
||||
success: response => {
|
||||
this.logger.debug('Refresh token exchanged');
|
||||
this._oauthProcessReturn({
|
||||
'refresh_token': refreshToken,
|
||||
...response
|
||||
});
|
||||
callback();
|
||||
},
|
||||
error: (err, xhr) => {
|
||||
if (xhr.status === 400) {
|
||||
delete this.runtimeData[this.name + 'OAuthToken'];
|
||||
this._oauthToken = null;
|
||||
}
|
||||
this.logger.error('Error exchanging refresh token', err);
|
||||
callback && callback('Error exchanging refresh token');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageBase };
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { noop } from 'util/fn';
|
||||
|
||||
const DefaultPort = 48149;
|
||||
const logger = new Logger('storage-oauth-listener');
|
||||
|
||||
const StorageOAuthListener = {
|
||||
server: null,
|
||||
|
||||
listen() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.server) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
const listener = {
|
||||
callback: noop,
|
||||
state: Math.round(Math.random() * Date.now()).toString()
|
||||
};
|
||||
|
||||
const http = Launcher.req('http');
|
||||
const server = http.createServer((req, resp) => {
|
||||
resp.writeHead(204);
|
||||
resp.end();
|
||||
this.handleResult(req.url, listener);
|
||||
});
|
||||
|
||||
const port = DefaultPort;
|
||||
logger.info(`Starting OAuth listener on port ${port}...`);
|
||||
server.listen(port);
|
||||
server.on('error', err => {
|
||||
logger.error('Failed to start OAuth listener', err);
|
||||
reject('Failed to start OAuth listener: ' + err);
|
||||
server.close();
|
||||
});
|
||||
server.on('listening', () => {
|
||||
this.server = server;
|
||||
listener.redirectUri = `http://127.0.0.1:${port}/oauth-result`;
|
||||
this._setCodeVerifier(listener);
|
||||
resolve(listener);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
logger.info('OAuth listener stopped');
|
||||
}
|
||||
},
|
||||
|
||||
handleResult(url, listener) {
|
||||
logger.info('OAuth result with code received');
|
||||
this.stop();
|
||||
url = new URL(url, 'http://localhost');
|
||||
const state = url.searchParams.get('state');
|
||||
if (!state) {
|
||||
logger.info('OAuth result has no state');
|
||||
return;
|
||||
}
|
||||
if (state !== listener.state) {
|
||||
logger.info('OAuth result has bad state');
|
||||
return;
|
||||
}
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
logger.info('OAuth result has no code');
|
||||
return;
|
||||
}
|
||||
listener.callback(code);
|
||||
},
|
||||
|
||||
_setCodeVerifier(listener) {
|
||||
const crypto = Launcher.req('crypto');
|
||||
|
||||
listener.codeVerifier = crypto.randomBytes(50).toString('hex');
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(listener.codeVerifier);
|
||||
|
||||
listener.codeChallenge = hash
|
||||
.digest('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
};
|
||||
|
||||
export { StorageOAuthListener };
|
|
@ -44,6 +44,18 @@ export function omit(obj, props) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function omitEmpty(obj) {
|
||||
if (!obj) {
|
||||
return obj;
|
||||
}
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
if (value) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function mapObject(obj, fn) {
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
result[key] = fn(value);
|
||||
|
|
|
@ -22,6 +22,13 @@ const UrlFormat = {
|
|||
|
||||
fileToDir(url) {
|
||||
return url.replace(this.lastPartRegex, '') || '/';
|
||||
},
|
||||
|
||||
makeUrl(base, args) {
|
||||
const queryString = Object.entries(args)
|
||||
.map(([key, value]) => key + '=' + encodeURIComponent(value))
|
||||
.join('&');
|
||||
return base + '?' + queryString;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -735,6 +735,9 @@ class OpenView extends View {
|
|||
this.busy = false;
|
||||
if (err || !files) {
|
||||
err = err ? err.toString() : '';
|
||||
if (err === 'browser-auth-started') {
|
||||
return;
|
||||
}
|
||||
if (err.lastIndexOf('OAuth', 0) !== 0 && !Alerts.alertDisplayed) {
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Release notes
|
||||
-------------
|
||||
##### v1.13.0 (TBD)
|
||||
`-` #1359: fixed Google Drive login issues in desktop apps
|
||||
`+` #1341: auto-lock the app on screen lock on Windows
|
||||
`+` #1065: PORTABLE_EXECUTABLE_DIR environment variable
|
||||
`*` #1397: Segoe UI font on Windows
|
||||
|
|
Loading…
Reference in New Issue