diff options
Diffstat (limited to 'Duplicati/Library/Backend')
5 files changed, 103 insertions, 49 deletions
diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj index 9cdfd56bf..07d34fd01 100644 --- a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj +++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj @@ -36,11 +36,15 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> + <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\..\..\..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> <Compile Include="Jottacloud.cs" /> + <Compile Include="JottacloudAuthHelper.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Strings.cs" /> </ItemGroup> @@ -65,9 +69,14 @@ <Project>{D63E53E4-A458-4C2F-914D-92F715F58ACF}</Project> <Name>Duplicati.Library.Common</Name> </ProjectReference> + <ProjectReference Include="..\OAuthHelper\Duplicati.Library.OAuthHelper.csproj"> + <Project>{d4c37c33-5e73-4b56-b2c3-dc4a6baa36bb}</Project> + <Name>Duplicati.Library.OAuthHelper</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <None Include="Duplicati.snk" /> + <None Include="packages.config" /> </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 9f297abf2..2ebfcc7c1 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -31,9 +31,9 @@ namespace Duplicati.Library.Backend // This class is instantiated dynamically in the BackendLoader. public class Jottacloud : IBackend, IStreamingBackend { + private const string AUTHID_OPTION = "authid"; private const string JFS_ROOT = "https://jfs.jottacloud.com/jfs"; private const string JFS_ROOT_UPLOAD = "https://up.jottacloud.com/jfs"; // Separate host for uploading files - private const string API_VERSION = "2.4"; // Hard coded per 09. March 2017. private const string JFS_BUILTIN_DEVICE = "Jotta"; // The built-in device used for the built-in Sync and Archive mount points. private static readonly string JFS_DEFAULT_BUILTIN_MOUNT_POINT = "Archive"; // When using the built-in device we pick this mount point as our default. private static readonly string JFS_DEFAULT_CUSTOM_MOUNT_POINT = "Duplicati"; // When custom device is specified then we pick this mount point as our default. @@ -51,7 +51,6 @@ namespace Duplicati.Library.Backend private readonly string m_url_device; private readonly string m_url; private readonly string m_url_upload; - private readonly System.Net.NetworkCredential m_userInfo; private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE]; private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb"; @@ -59,6 +58,8 @@ namespace Duplicati.Library.Backend private readonly int m_threads; private readonly long m_chunksize; + private readonly JottacloudAuthHelper m_oauth; + /// <summary> /// The default maximum number of concurrent connections allowed by a ServicePoint object is 2. /// It should be increased to allow multiple download threads. @@ -132,40 +133,21 @@ namespace Duplicati.Library.Backend m_mountPoint = JFS_DEFAULT_CUSTOM_MOUNT_POINT; // Set a suitable default mount point for custom (backup) devices. } + string authid = null; + if (options.ContainsKey(AUTHID_OPTION)) + authid = options[AUTHID_OPTION]; + m_oauth = new JottacloudAuthHelper(authid); + // Build URL var u = new Utility.Uri(url); m_path = u.HostAndPath; // Host and path of "jottacloud://folder/subfolder" is "folder/subfolder", so the actual folder path within the mount point. if (string.IsNullOrEmpty(m_path)) // Require a folder. Actually it is possible to store files directly on the root level of the mount point, but that does not seem to be a good option. throw new UserInformationException(Strings.Jottacloud.NoPathError, "JottaNoPath"); m_path = Util.AppendDirSeparator(m_path, "/"); - if (!string.IsNullOrEmpty(u.Username)) - { - m_userInfo = new System.Net.NetworkCredential(); - m_userInfo.UserName = u.Username; - if (!string.IsNullOrEmpty(u.Password)) - m_userInfo.Password = u.Password; - else if (options.ContainsKey("auth-password")) - m_userInfo.Password = options["auth-password"]; - } - else - { - if (options.ContainsKey("auth-username")) - { - m_userInfo = new System.Net.NetworkCredential(); - m_userInfo.UserName = options["auth-username"]; - if (options.ContainsKey("auth-password")) - m_userInfo.Password = options["auth-password"]; - } - } - if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.UserName)) - throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername"); - if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.Password)) - throw new UserInformationException(Strings.Jottacloud.NoPasswordError, "JottaNoPassword"); - if (m_userInfo != null) // Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code - m_userInfo.Domain = ""; - m_url_device = JFS_ROOT + "/" + m_userInfo.UserName + "/" + m_device; + + m_url_device = JFS_ROOT + "/" + m_oauth.Username + "/" + m_device; m_url = m_url_device + "/" + m_mountPoint + "/" + m_path; - m_url_upload = JFS_ROOT_UPLOAD + "/" + m_userInfo.UserName + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url. + m_url_upload = JFS_ROOT_UPLOAD + "/" + m_oauth.Username + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url. m_threads = int.Parse(options.ContainsKey(JFS_THREADS) ? options[JFS_THREADS] : JFS_DEFAULT_THREADS); @@ -186,7 +168,7 @@ namespace Duplicati.Library.Backend m_chunksize = chunksize; } - #region IBackend Members +#region IBackend Members public string DisplayName { @@ -329,8 +311,7 @@ namespace Duplicati.Library.Backend get { return new List<ICommandLineArgument>(new ICommandLineArgument[] { - new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.DescriptionAuthPasswordShort, Strings.Jottacloud.DescriptionAuthPasswordLong), - new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionAuthUsernameShort, Strings.Jottacloud.DescriptionAuthUsernameLong), + new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.AuthidShort, Strings.Jottacloud.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("jottacloud"))), new CommandLineArgument(JFS_DEVICE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionDeviceShort, Strings.Jottacloud.DescriptionDeviceLong(JFS_MOUNT_POINT_OPTION)), new CommandLineArgument(JFS_MOUNT_POINT_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionMountPointShort, Strings.Jottacloud.DescriptionMountPointLong(JFS_DEVICE_OPTION)), new CommandLineArgument(JFS_THREADS, CommandLineArgument.ArgumentType.Integer, Strings.Jottacloud.ThreadsShort, Strings.Jottacloud.ThreadsLong, JFS_DEFAULT_THREADS), @@ -368,26 +349,19 @@ namespace Duplicati.Library.Backend } } - #endregion +#endregion - #region IDisposable Members +#region IDisposable Members public void Dispose() { } - #endregion +#endregion private System.Net.HttpWebRequest CreateRequest(string method, string url, string queryparams) { - System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams)); - req.Method = method; - req.Credentials = m_userInfo; - req.PreAuthenticate = true; // We need this under Mono for some reason, and it appears some servers require this as well - req.KeepAlive = false; - req.UserAgent = "Duplicati Jottacloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - req.Headers.Add("x-jftp-version", API_VERSION); - return req; + return m_oauth.CreateRequest(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams), method); } private System.Net.HttpWebRequest CreateRequest(string method, string remotename, string queryparams, bool upload) diff --git a/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs new file mode 100644 index 000000000..d8a5d2f48 --- /dev/null +++ b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs @@ -0,0 +1,70 @@ +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.Interface; +using Newtonsoft.Json; + +namespace Duplicati.Library.Backend +{ + public class JottacloudAuthHelper : OAuthHelper + { + private const string USERINFO_URL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/userinfo"; + private string m_username; + + public JottacloudAuthHelper(string accessToken) + : base(accessToken, "jottacloud") + { + base.AutoAuthHeader = true; + + var userinfo = GetJSONData<UserInfo>(USERINFO_URL); + if (userinfo == null || string.IsNullOrEmpty(userinfo.Username)) + throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername"); + m_username = userinfo.Username; + } + + public string Username + { + get + { + return m_username; + } + } + + private class UserInfo + { + [JsonProperty("sub")] + public string Subject { get; set; } + [JsonProperty("email_verified")] + public bool EmailVerified { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("realm")] + public string Realm { get; set; } + [JsonProperty("preferred_username")] + public string PreferredUsername { get; set; } // The numeric internal username, same as Username + [JsonProperty("given_name")] + public string GivenName { get; set; } + [JsonProperty("family_name")] + public string FamilyName { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("username")] + public string Username { get; set; } // The numeric internal username, same as PreferredUsername + } + } + +} + diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs index 08ba011b8..010050022 100644 --- a/Duplicati/Library/Backend/Jottacloud/Strings.cs +++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs @@ -4,15 +4,12 @@ namespace Duplicati.Library.Backend.Strings { { public static string DisplayName { get { return LC.L(@"Jottacloud"); } } public static string Description { get { return LC.L(@"This backend can read and write data to Jottacloud using it's REST protocol. Allowed format is ""jottacloud://folder/subfolder""."); } } - public static string NoUsernameError { get { return LC.L(@"No username given"); } } - public static string NoPasswordError { get { return LC.L(@"No password given"); } } + public static string AuthidShort { get { return LC.L(@"The authorization code"); } } + public static string AuthidLong(string url) { return LC.L(@"The authorization token retrieved from {0}", url); } + public static string NoUsernameError { get { return LC.L(@"No username found"); } } public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } } public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } } public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } } - public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } } - public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } } - public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } } - public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } } public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } } public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); } public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } } diff --git a/Duplicati/Library/Backend/Jottacloud/packages.config b/Duplicati/Library/Backend/Jottacloud/packages.config new file mode 100644 index 000000000..a75532f5f --- /dev/null +++ b/Duplicati/Library/Backend/Jottacloud/packages.config @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" /> +</packages>
\ No newline at end of file |