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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,4 @@ GeneratedArtifacts/
_Pvt_Extensions/
ModelManifest.xml
.vs/
.vscode/
3 changes: 3 additions & 0 deletions KeeAnywhere/KeeAnywhereExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public override bool Initialize(IPluginHost pluginHost)
_configService = new ConfigurationService(pluginHost);
_configService.Load();

// Persist OneDrive refresh-token rotations as they happen.
StorageProviders.OneDrive.OneDriveHelper.OnAccountChanged = _ => _configService.Save();

// Initialize CacheManager
_cacheManagerService = new CacheManagerService(_configService, _host);
_cacheManagerService.RegisterEvents();
Expand Down
15 changes: 7 additions & 8 deletions KeeAnywhere/OAuth2/OidcSystemBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,14 @@ public async Task<BrowserResult> InvokeAsync(BrowserOptions options, Cancellatio
var context = await listener.GetContextAsync();

string result;

//if (options.ResponseMode == IdentityModel.OidcClient.OidcClientOptions.AuthorizeResponseMode.Redirect)
//{
if (context.Request.HttpMethod == "POST" && context.Request.HasEntityBody)
{
result = ProcessFormPost(context.Request);
}
else
{
result = context.Request.Url.Query;
//}
//else
//{
// result = ProcessFormPost(context.Request);
//}
}

await SendResponse(context.Response);

Expand Down
5 changes: 5 additions & 0 deletions KeeAnywhere/StorageProviders/Box/BoxHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public static async Task<BoxClient> GetClient(AccountConfiguration account)
return client;
}

public static void InvalidateCache(string accountId)
{
Cache.Remove(accountId);
}

public static BoxClient GetClient()
{
return GetClient((OAuthSession)null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ public async Task<Stream> Load(string path)

var file = await api.GetFileByPath(path, true);
if (file == null)
return null;
throw new FileNotFoundException("Google Drive: File not found.", path);

var stream = new MemoryStream();
var progress = await api.Files.Get(file.Id).DownloadAsync(stream);

if (progress.Status != DownloadStatus.Completed || progress.Exception != null)
return null;
if (progress.Status != DownloadStatus.Completed)
throw new InvalidOperationException("Google Drive download failed: " + progress.Status, progress.Exception);

stream.Seek(0, SeekOrigin.Begin);

Expand Down
71 changes: 45 additions & 26 deletions KeeAnywhere/StorageProviders/OneDrive/OneDriveApiExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Graph;
using Microsoft.Graph.Drives.Item.Items.Item;
using Microsoft.Graph.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -29,43 +30,61 @@ public static DriveItemItemRequestBuilder DriveItemFromStorageProviderItemId(thi
}

/// <summary>
/// Configures a Graph API request for a OneDrive drive based on a
/// path from a user's default drive. Accommodates top-level folders
/// which are links to remote, shared items.
/// Resolves a path under the user's default drive (or a remote
/// shared mount) to an Items[id] request builder.
/// </summary>
/// <param name="api">A Graph API request builder.</param>
/// <param name="path">
/// A URI path relative to the user's default drive.
/// </param>
/// <returns>
/// A task that yields a drive item request builder.
/// </returns>
/// <remarks>
/// An extra Web request has to be made to determine if the top
/// folder is remote or local. That's why this method is async.
/// Personal OneDrive does not reliably honor path-based item URLs
/// for content operations: the /:/path:/ form 404s on download, and
/// the itemWithPath(path='...') function form URL-encodes '/' as
/// %2F which Graph treats as a literal name. We walk the path one
/// segment at a time via Children listings so callers always get a
/// /drives/{drive}/items/{id} URL.
/// </remarks>
public async static Task<DriveItemItemRequestBuilder> DriveItemFromPathAsync(this GraphServiceClient api, string path)
{
// The top folder could be a shared folder, in which case it's
// on a different drive than the default. The path will use the
// name of the link in the user's root, which may be different
// from its actual (remote) name.
if (string.IsNullOrEmpty(path)) throw new ArgumentOutOfRangeException("path");
var parts = path.Split('/');
var rootItem = await api.Me.Drive.GetAsync();
var drive = await api.Me.Drive.GetAsync();

var driveId = drive.Id;
var currentId = (await api.Drives[driveId].Root.GetAsync()).Id;

if (parts.Length == 1)
foreach (var segment in parts)
{
return api.Drives[rootItem.Id].Root.ItemWithPath(parts[0]);
var match = await FindChildAsync(api, driveId, currentId, segment);
if (match == null)
throw new System.IO.FileNotFoundException("OneDrive: '" + segment + "' not found under '" + path + "'");

if (match.RemoteItem != null)
{
driveId = match.RemoteItem.ParentReference.DriveId;
currentId = match.RemoteItem.Id;
}
else
{
currentId = match.Id;
}
}

var topFolder = await api.Drives[rootItem.Id].Root.ItemWithPath(Uri.EscapeDataString(parts[0])).GetAsync();
var driveId = topFolder.RemoteItem == null ? topFolder.ParentReference.DriveId : topFolder.RemoteItem.ParentReference.DriveId;
var topFolderId = topFolder.RemoteItem == null ? topFolder.Id : topFolder.RemoteItem.Id;
// The top folder's apparent name can be different from its
// actual name, so don't use the name as part of the path at all.
// We have the id, so navigate from there instead.
return api.Drives[driveId].Items[topFolderId].ItemWithPath(Uri.EscapeDataString(string.Join("/",parts.Skip(1))));
return api.Drives[driveId].Items[currentId];
}

// Microsoft Graph paginates Children at 200 items per page by default.
// Walk pages until we find the named child or exhaust the listing.
private static async Task<DriveItem> FindChildAsync(GraphServiceClient api, string driveId, string parentId, string name)
{
var page = await api.Drives[driveId].Items[parentId].Children.GetAsync();
while (page != null)
{
var match = page.Value.FirstOrDefault(c => c.Name == name);
if (match != null) return match;
if (string.IsNullOrEmpty(page.OdataNextLink)) return null;
page = await api.Drives[driveId].Items[parentId].Children
.WithUrl(page.OdataNextLink)
.GetAsync();
}
return null;
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using IdentityModel.OidcClient.Results;
using KeeAnywhere.Configuration;
using KeeAnywhere.OAuth2;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
Expand All @@ -14,37 +15,57 @@ namespace KeeAnywhere.StorageProviders.OneDrive
{
public class OneDriveAuthenticationProvider : IAuthenticationProvider
{
private OidcFlow _flow;
private string _refreshToken;
private readonly OidcFlow _flow;
private readonly AccountConfiguration _account;
private readonly Action<AccountConfiguration> _onAccountChanged;
// Serializes concurrent Graph requests so the refresh + rotation-persist runs once.
private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);
private RefreshTokenResult _token;

public OneDriveAuthenticationProvider(OidcFlow flow, string refreshToken)
public OneDriveAuthenticationProvider(OidcFlow flow, AccountConfiguration account, Action<AccountConfiguration> onAccountChanged)
{
_flow = flow;
_refreshToken = refreshToken;
_account = account;
_onAccountChanged = onAccountChanged;
}

public async Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object> additionalAuthenticationContext = null, CancellationToken cancellationToken = default(CancellationToken))
{
var token = _token;

if (token == null || _token.IsError || _token.AccessTokenExpiration <= DateTime.Now)
await _refreshLock.WaitAsync(cancellationToken);
try
{
token = await _flow.RefreshTokenAsync(_refreshToken);
var token = _token;

if (token.IsError)
if (token == null || token.IsError || token.AccessTokenExpiration <= DateTime.UtcNow)
{
_token = null;
throw new ServiceException(token.Error);
token = await _flow.RefreshTokenAsync(_account.Secret);

if (token.IsError)
{
_token = null;
var detail = string.IsNullOrEmpty(token.ErrorDescription) ? token.Error : token.Error + ": " + token.ErrorDescription;
throw new ServiceException(detail);
}

// Microsoft rotates the refresh token on every refresh; persist it or the stored secret eventually drifts to invalid_grant.
if (!string.IsNullOrEmpty(token.RefreshToken) && token.RefreshToken != _account.Secret)
{
_account.Secret = token.RefreshToken;
if (_onAccountChanged != null) _onAccountChanged(_account);
}

_token = token;
}

_token = token;
var accessToken = token.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
{
request.Headers.Add("Authorization", new AuthenticationHeaderValue(CoreConstants.Headers.Bearer, accessToken).ToString());
}
}

var accessToken = token.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
finally
{
request.Headers.Add("Authorization", new AuthenticationHeaderValue(CoreConstants.Headers.Bearer, accessToken).ToString());
_refreshLock.Release();
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion KeeAnywhere/StorageProviders/OneDrive/OneDriveHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ never the real production keys.

private static readonly IDictionary<string, GraphServiceClient> Cache = new Dictionary<string, GraphServiceClient>();

// Invoked by OneDriveAuthenticationProvider after a refresh-token
// rotation. Wired by KeeAnywhereExt to ConfigurationService.Save().
public static Action<AccountConfiguration> OnAccountChanged;

public static OidcFlow CreateOidcFlow()
{
return new OidcFlow(StorageType.OneDrive, Authority, OneDriveClientId, null, Scopes)
Expand All @@ -51,7 +55,7 @@ public static GraphServiceClient GetApi(AccountConfiguration account)
{
if (Cache.ContainsKey(account.Id)) return Cache[account.Id];

var authProvider = new OneDriveAuthenticationProvider(CreateOidcFlow(), account.Secret);
var authProvider = new OneDriveAuthenticationProvider(CreateOidcFlow(), account, OnAccountChanged);

//var httpProvider = new HttpProvider(ProxyTools.CreateHttpClientHandler(), true)
//{
Expand All @@ -70,5 +74,10 @@ public static GraphServiceClient GetApi(AccountConfiguration account)

return api;
}

public static void InvalidateCache(string accountId)
{
Cache.Remove(accountId);
}
}
}
4 changes: 3 additions & 1 deletion KeeAnywhere/StorageProviders/StorageDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ namespace KeeAnywhere.StorageProviders
{
public class StorageDescriptor
{
public StorageDescriptor(StorageType type, string friendlyName, string scheme, Func<AccountConfiguration, IStorageProvider> providerFactory, Func<IStorageConfigurator> configuratorFactory, Image smallImage)
public StorageDescriptor(StorageType type, string friendlyName, string scheme, Func<AccountConfiguration, IStorageProvider> providerFactory, Func<IStorageConfigurator> configuratorFactory, Image smallImage, Action<string> invalidateCacheAction = null)
{
Type = type;
FriendlyName = friendlyName;
Scheme = scheme;
ProviderFactory = providerFactory;
ConfiguratorFactory = configuratorFactory;
SmallImage = smallImage;
InvalidateCacheAction = invalidateCacheAction;
}

public StorageType Type { get; private set; }
Expand All @@ -22,5 +23,6 @@ public StorageDescriptor(StorageType type, string friendlyName, string scheme, F
public Func<AccountConfiguration, IStorageProvider> ProviderFactory { get; private set; }
public Func<IStorageConfigurator> ConfiguratorFactory { get; private set; }
public Image SmallImage { get; private set; }
public Action<string> InvalidateCacheAction { get; private set; }
}
}
4 changes: 2 additions & 2 deletions KeeAnywhere/StorageProviders/StorageRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ static StorageRegistry()
d.Add(new StorageDescriptor(StorageType.AmazonS3, "Amazon S3", "s3", account => new AmazonS3StorageProvider(account), () => new AmazonS3StorageConfigurator(), PluginResources.AmazonS3_16x16));
d.Add(new StorageDescriptor(StorageType.AzureBlob, "Azure Blob Storage", "azureblob", account => new AzureBlobStorageProvider(account), () => new AzureStorageConfigurator(StorageType.AzureBlob), PluginResources.Azure_16x16));
d.Add(new StorageDescriptor(StorageType.AzureFile, "Azure File Storage", "azurefile", account => new AzureFileStorageProvider(account), () => new AzureStorageConfigurator(StorageType.AzureFile), PluginResources.Azure_16x16));
if (!isUnix) d.Add(new StorageDescriptor(StorageType.Box, "Box", "box", account => new BoxStorageProvider(account), () => new BoxStorageConfigurator(), PluginResources.Box_16x16));
if (!isUnix) d.Add(new StorageDescriptor(StorageType.Box, "Box", "box", account => new BoxStorageProvider(account), () => new BoxStorageConfigurator(), PluginResources.Box_16x16, BoxHelper.InvalidateCache));
d.Add(new StorageDescriptor(StorageType.Dropbox, "Dropbox", "dropbox", account => new DropboxStorageProvider(account), () => new DropboxStorageConfigurator(false), PluginResources.Dropbox_16x16));
d.Add(new StorageDescriptor(StorageType.DropboxRestricted, "Dropbox (restricted)", "dropbox-r", account => new DropboxStorageProvider(account), () => new DropboxStorageConfigurator(true), PluginResources.Dropbox_16x16));
d.Add(new StorageDescriptor(StorageType.GoogleCloudStorage, "Google Cloud Storage", "gs", account => new GoogleCloudStorageProvider(account), () => new GoogleCloudStorageConfigurator(), PluginResources.GoogleCloudStorage_16x16));
d.Add(new StorageDescriptor(StorageType.GoogleDrive, "Google Drive", "gdrive", account => new GoogleDriveStorageProvider(account), () => new GoogleDriveStorageConfigurator(false), PluginResources.GoogleDrive_16x16));
d.Add(new StorageDescriptor(StorageType.GoogleDriveRestricted, "Google Drive (restricted)", "gdrive-r", account => new GoogleDriveStorageProvider(account), () => new GoogleDriveStorageConfigurator(true), PluginResources.GoogleDrive_16x16));
d.Add(new StorageDescriptor(StorageType.HiDrive, "HiDrive", "hidrive", account => new HiDriveStorageProvider(account), () => new HiDriveStorageConfigurator(), PluginResources.HiDrive_16x16));
d.Add(new StorageDescriptor(StorageType.OneDrive, "OneDrive", "onedrive", account => new OneDriveStorageProvider(account), () => new OneDriveStorageConfigurator(), PluginResources.OneDrive_16x16));
d.Add(new StorageDescriptor(StorageType.OneDrive, "OneDrive", "onedrive", account => new OneDriveStorageProvider(account), () => new OneDriveStorageConfigurator(), PluginResources.OneDrive_16x16, OneDriveHelper.InvalidateCache));

Descriptors = d.ToArray();
}
Expand Down
9 changes: 9 additions & 0 deletions KeeAnywhere/UIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public async Task<AccountConfiguration> CreateOrUpdateAccount(StorageType type)

//existingAccount.Name = newAccount.Name;
existingAccount.Secret = newAccount.Secret;
InvalidateProviderCache(existingAccount);

return existingAccount;
}
Expand Down Expand Up @@ -91,10 +92,18 @@ public async Task CheckOrUpdateAccount(AccountConfiguration account)
else
{
account.Secret = newAccount.Secret;
InvalidateProviderCache(account);
MessageService.ShowInfo("Re-Authorization succeeded!");
}
}

private static void InvalidateProviderCache(AccountConfiguration account)
{
var descriptor = StorageRegistry.Descriptors.FirstOrDefault(_ => _.Type == account.Type);
if (descriptor != null && descriptor.InvalidateCacheAction != null)
descriptor.InvalidateCacheAction(account.Id);
}

public void ShowDonationDialog()
{
var lastShown = _configService.PluginConfiguration.DonationDialogLastShown;
Expand Down