diff --git a/Duplicati/Library/Backend/SkyDrive/Duplicati.Library.Backend.SkyDrive.csproj b/Duplicati/Library/Backend/SkyDrive/Duplicati.Library.Backend.SkyDrive.csproj index 920031398..c1b7778a3 100644 --- a/Duplicati/Library/Backend/SkyDrive/Duplicati.Library.Backend.SkyDrive.csproj +++ b/Duplicati/Library/Backend/SkyDrive/Duplicati.Library.Backend.SkyDrive.csproj @@ -1,4 +1,4 @@ - + Debug @@ -12,6 +12,7 @@ Duplicati.Library.Backend.SkyDrive true Duplicati.snk + v2.0 512 @@ -61,6 +62,11 @@ True SkyDrive.resx + + True + True + SkyDriveSession.resx + True True @@ -90,6 +96,10 @@ ResXFileCodeGenerator SkyDrive.Designer.cs + + ResXFileCodeGenerator + SkyDriveSession.Designer.cs + ResXFileCodeGenerator SkyDriveUI.Designer.cs diff --git a/Duplicati/Library/Backend/SkyDrive/SkyDriveSession.cs b/Duplicati/Library/Backend/SkyDrive/SkyDriveSession.cs index 3e92bbe04..6542a0b17 100644 --- a/Duplicati/Library/Backend/SkyDrive/SkyDriveSession.cs +++ b/Duplicati/Library/Backend/SkyDrive/SkyDriveSession.cs @@ -1,964 +1,1009 @@ - using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; -using System.Net; -using System.Xml; -using Duplicati.Library.Interface; -using System.Web; - -namespace Duplicati.Library.Backend -{ - /// - /// Class that encapsulates the http protocol specifics for a SkyDrive request - /// - public class SkyDriveSession : IDisposable - { - /// - /// The main url for the SkyDrive service, used to query for root folders - /// - private static string SKYDRIVE_URL = "https://docs.live.net/SkyDocsService.svc"; - - /// - /// The url used to get a the url for authenticating the Passport - /// - private const string PASSPORT_LIST_URL = "https://nexus.passport.com/rdr/pprdr.asp"; - /// - /// The name of the http header that contains the login url - /// - private const string PASSPORT_URL_LIST_HEADER = "PassportURLs"; - /// - /// A regular expression for extracting the Passport authentication url from the header value - /// - private static readonly Regex PASSPORT_LOGIN_MATCH = new Regex("DALogin=(?[^,]+)"); - /// - /// A regular expression for reading the token generation status - /// - private static readonly Regex LOGIN_STATUS_MATCH = new Regex("da-status=(?[^,]+)"); - /// - /// A regular expression for reading the actual token result - /// - private static readonly Regex LOGIN_TOKEN_MATCH = new Regex("from-PP=(?[^,]+)"); - /// - /// A regular expressing for detecting a Passport nonce string, which can then be re-used with new nonce values - /// - private static readonly Regex NONCE_MATCH = new Regex("Passport1.4[^ ]* ct=(?[^,]+).*"); - - /// - /// A regular expression for identifying a CID as part of a query string - /// - private static readonly Regex CID_FINDER = new Regex("cid=(?[^&]+)"); - - /// - /// A template for generating Passport authorization requests - /// - private const string LOGIN_HEADER_TEMPLATE = "Passport1.4 sign-in={0},pwd={1},OrgVerb={2},OrgUrl={3},{4}"; - /// - /// A template for generating Passport authorization responses - /// - private const string TOKEN_FORMAT_TEMPLATE = "Passport1.4 from-PP={0}"; - /// - /// A template for creating a WLID authorization response - /// - private const string WLID_TOKEN_FORMAT_TEMPLATE = "WLID1.0 {0}"; - - /// - /// The name of the header that contains the authentication nonce - /// - private const string NONCE_HEADER = "WWW-Authenticate"; - /// - /// The name of the redirect header - /// - private const string REDIR_LOCATION_HEADER = "Location"; - /// - /// The name of the header that contains authorization information - /// - private const string AUTHORIZATION_HEADER = "Authorization"; - /// - /// The name of the header that contains the authorization token - /// - private const string AUTHENTIFICATION_RESULT_HEADER = "Authentication-Info"; - - /// - /// An xml SOAP request for getting basic account information - /// - private readonly string SOAP_GETACCOUNT_INFO_REQUEST = "" + - "" + - "" + - "" + USER_AGENT + "en-US" + - "v1.0" + - "true"; - - /// - /// A template for requesting file creation - /// - private const string CREATE_FILE_TEMPLATE = "{0}Documentfalse"; - /// - /// A template for requesting subfolder creation - /// - private const string CREATE_SUBFOLDER_TEMPLATE = "{0}Folderfalse"; - /// - /// A template for requesting root folder creation - /// - private const string CREATE_ROOTFOLDER_TEMPLATE = "{0}LibraryDocumentPrivate"; - - /// - /// The user agent applied to all requests - /// - private readonly static string USER_AGENT = "Duplicati SkyDrive client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); - - - /// - /// The Passport username - /// - private string m_username; - /// - /// The Passport password - /// - private string m_password; - /// - /// The name of the root folder - /// - private string m_rootfolder; - /// - /// The prefix (aka subfolder) path - /// - private string m_prefix; - - /// - /// The url used to authenticate against - /// - private string m_loginUrl; - /// - /// The CID of the folder this session is bound to - /// - private string m_folderCID; - /// - /// The passport token used to authticate WebDAV requests - /// - private string m_mainpassporttoken; - /// - /// The WLID token used to authenticate requests - /// - private string m_mainwlidtoken; - /// - /// The template used to generate nonce values - /// - private string m_noncetemplate; - /// - /// The CID of the user - /// - private string m_userCID; - /// - /// A list of filenames, used to obtain the CID of files - /// - private List m_filenameCache; - /// - /// A lookup table with all WEBDAV folders, key is the root foldername, value is the WEBDAV url - /// - private Dictionary m_webdav_rootFolderCache = null; - - /// - /// A lock used to prevent loading the loginUrl multiple times - /// - private static object _lock = new object(); - /// - /// The login url used to authenticate, kept static because it takes a few seconds to read, - /// and it appears to be completely fixed: https://login.live.com/login2.srf - /// - private static string _loginUrl = null; - - /// - /// Initializes a new instance of the class. - /// - /// The Passport username. - /// The Passport password. - /// The rootfolder. - /// The prefix (aka subfolder path). - /// if set to true missing folders will be created. - public SkyDriveSession(string username, string password, string rootfolder, string prefix, bool createFolders) - { - //Cache basic info - m_username = username; - m_password = password; - m_rootfolder = rootfolder; - m_prefix = prefix; - - //The call is sooo slow, and won't change often (if ever). - //Worst case scenario: the app needs to be restarted - if (string.IsNullOrEmpty(_loginUrl)) - lock(_lock) - if (string.IsNullOrEmpty(_loginUrl)) - _loginUrl = GetLoginUrl(); - - m_loginUrl = _loginUrl; - //Populate m_userCID, m_noncetemplate and m_webdav_rootFolderCache - SetupConnection(); - m_folderCID = GetFolderCID(rootfolder, prefix, createFolders); - } - - - /// - /// Gets the login URL. - /// - /// The login url - private string GetLoginUrl() - { - HttpWebRequest getUrls = (HttpWebRequest)WebRequest.Create(PASSPORT_LIST_URL); - getUrls.UserAgent = USER_AGENT; - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(getUrls)) - { - string header = resp.Headers[PASSPORT_URL_LIST_HEADER]; - if (string.IsNullOrEmpty(header)) - throw new Exception(Strings.SkyDrive.NoPassportLoginUrls); - string loginurl = PASSPORT_LOGIN_MATCH.Match(header).Groups["url"].Value; - - if (string.IsNullOrEmpty(loginurl)) - throw new Exception(Strings.SkyDrive.NoPassportLoginUrls); - - if (!loginurl.StartsWith("https://")) - loginurl = "https://" + loginurl; - - return loginurl; - } - } - - /// - /// Reads the user CID, generates the noncetemplate and populates the WEBDAV folder list - /// - public void SetupConnection() - { - m_webdav_rootFolderCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - HttpWebRequest listRequest = CreateGetWebAccountInfoRequest(); - - //We use the first request to extract the nonce template - if (string.IsNullOrEmpty(m_noncetemplate)) - { - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(listRequest)) - { - string nonce = resp.Headers[NONCE_HEADER]; - if (string.IsNullOrEmpty(nonce)) - throw new Exception(Strings.SkyDrive.RequestDidNotRequireAuthentificationError); - Match m = NONCE_MATCH.Match(nonce); - if (!m.Success) - throw new Exception(string.Format(Strings.SkyDrive.InvalidNonceError, nonce)); - - m_noncetemplate = - nonce.Substring(0, m.Groups["nonce"].Index) + - "{0}" + - nonce.Substring(m.Groups["nonce"].Index + m.Groups["nonce"].Length); - } - - listRequest = CreateGetWebAccountInfoRequest(); - } - - //Get the list of root folders, the user CID and the urls for webdav folders - XmlDocument doc = new XmlDocument(); - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(listRequest)) - using (System.IO.Stream s = resp.GetResponseStream()) - doc.Load(s); - - XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); - mgr.AddNamespace("ms", "http://schemas.microsoft.com/clouddocuments"); - - if (string.IsNullOrEmpty(m_userCID)) - { - //Extract the CID from the NewLibraryUrl node - XmlNode cidnode = doc.SelectSingleNode("//ms:NewLibraryUrl", mgr); - if (cidnode == null) - throw new Exception(Strings.SkyDrive.UnableToFindCIDError); - m_userCID = CID_FINDER.Match(cidnode.InnerText).Groups["cid"].Value; - if (string.IsNullOrEmpty(m_userCID)) - throw new Exception(Strings.SkyDrive.UnableToFindCIDError); - } - - //Load the list of WEBDAV urls - foreach (XmlNode n in doc.SelectNodes("//ms:Library", mgr)) - m_webdav_rootFolderCache.Add(n.SelectSingleNode("ms:DisplayName", mgr).InnerText, n.SelectSingleNode("ms:DavUrl", mgr).InnerText); - } - - /// - /// Gets the folder CID for the folder this session is bound to. - /// - /// The rootfolder. - /// The prefix. - /// if set to true missing folders will be created. - /// The folder CID - private string GetFolderCID(string rootfolder, string prefix, bool createFolder) - { - //Obtain a valid passport token for downloading with WebDAV - m_mainpassporttoken = GetAuthenticationToken("GET", RootUrl); - - //Assign the WLID1.0 token, which is the same as the Passport1.4 token, but with another prefix - m_mainwlidtoken = ConvertPassportTokenToWLIDToken(m_mainpassporttoken); - - string[] folders = (rootfolder + prefix).Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries); - TaggedFileEntry curfolder = null; - - //Do a recursive lookup on the folders, which is required to get the CID for the folder - foreach (string folder in folders) - { - if (string.IsNullOrEmpty(folder)) - continue; - - TaggedFileEntry newfolder = null; - foreach (TaggedFileEntry tfe in ListFolderItems(curfolder == null ? null : curfolder.CID)) - if (tfe.Name.Equals(folder, StringComparison.InvariantCultureIgnoreCase)) - { - newfolder = tfe; - break; - } - - if (newfolder == null && createFolder) - { - //Create the folder and re-list the contents - CreateFolder(folder, curfolder == null ? null : curfolder.CID); - foreach (TaggedFileEntry tfe in ListFolderItems(curfolder == null ? null : curfolder.CID)) - if (tfe.Name.Equals(folder, StringComparison.InvariantCultureIgnoreCase)) - { - newfolder = tfe; - break; - } - } - - //If the folder was not created, inform the caller - if (newfolder == null) - throw new Interface.FolderMissingException(); - - curfolder = newfolder; - } - - if (curfolder == null) - throw new FolderMissingException(); - - return curfolder.CID; - - } - - /// - /// Creates a WebAccountInfo request used to read basic account information. - /// - /// A configured request - private HttpWebRequest CreateGetWebAccountInfoRequest() - { - HttpWebRequest soapRequest = (HttpWebRequest)WebRequest.Create(SKYDRIVE_URL); - soapRequest.AllowAutoRedirect = false; - soapRequest.PreAuthenticate = false; - soapRequest.UserAgent = USER_AGENT; - soapRequest.Headers["SOAPAction"] = "GetWebAccountInfo"; - soapRequest.Headers["translate"] = "f"; - soapRequest.Method = "POST"; - soapRequest.ContentType = "text/xml; charset=utf-8"; - SetAuthenticationToken(soapRequest); - - byte[] data = System.Text.Encoding.UTF8.GetBytes(SOAP_GETACCOUNT_INFO_REQUEST); - using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(soapRequest)) - s.Write(data, 0, data.Length); - - return soapRequest; - } - - /// - /// Gets the root URL onto which WLID requests can be made. - /// - /// The root URL. - public string RootUrl - { - get - { - return String.Format( - "http://cid-{0}.users.api.live.net/Users({1})/Files", - m_userCID, - Int64.Parse(m_userCID, System.Globalization.NumberStyles.HexNumber)); - } - } - - /// - /// Gets the folder CID for the folder this session is bound to. - /// - /// The folder CID. - public string FolderCID - { - get { return m_folderCID; } - } - - /// - /// Gets the URL for the folder this session is bound to. - /// - /// The folder URL. - public string FolderUrl - { - get { return GetFolderUrl(m_folderCID); } - } - - /// - /// Gets the URL for a specific folder. - /// - /// The folder CID. - /// - private string GetFolderUrl(string folderCID) - { - return String.Format("{0}/Folders('{1}')", RootUrl, folderCID); - } - - /// - /// Gets the URL for a specific file. - /// - /// The file CID. - /// - private string GetFileUrl(string fileCID) - { - return String.Format("{0}/Files('{1}')", RootUrl, fileCID); - } - - - /// - /// Creates a nonce using the template. - /// - /// The http verb. - /// The http request URL. - /// The generated nonce - private string CreateNonce(string verb, string url) - { - //The nonce template seems to be statically defined as: - //"Passport1.4 ct={0},rver=6.1.6206.0,wp=MBI,lc=1033,id=250206" + using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Net; +using System.Xml; +using Duplicati.Library.Interface; +using System.Web; + +namespace Duplicati.Library.Backend +{ + /// + /// Class that encapsulates the http protocol specifics for a SkyDrive request + /// + public class SkyDriveSession : IDisposable + { + /// + /// The main url for the SkyDrive service, used to query for root folders + /// + private static string SKYDRIVE_URL = "https://docs.live.net/SkyDocsService.svc"; + + /// + /// The url used to get a the url for authenticating the Passport + /// + private const string PASSPORT_LIST_URL = "https://nexus.passport.com/rdr/pprdr.asp"; + /// + /// The name of the http header that contains the login url + /// + private const string PASSPORT_URL_LIST_HEADER = "PassportURLs"; + /// + /// A regular expression for extracting the Passport authentication url from the header value + /// + private static readonly Regex PASSPORT_LOGIN_MATCH = new Regex("DALogin=(?[^,]+)"); + /// + /// A regular expression for reading the token generation status + /// + private static readonly Regex LOGIN_STATUS_MATCH = new Regex("da-status=(?[^,]+)"); + /// + /// A regular expression for reading the actual token result + /// + private static readonly Regex LOGIN_TOKEN_MATCH = new Regex("from-PP=(?[^,]+)"); + /// + /// A regular expressing for detecting a Passport nonce string, which can then be re-used with new nonce values + /// + private static readonly Regex NONCE_MATCH = new Regex("Passport1.4[^ ]* ct=(?[^,]+).*"); + + /// + /// A regular expression for identifying a CID as part of a query string + /// + private static readonly Regex CID_FINDER = new Regex("cid=(?[^&]+)"); + + /// + /// A template for generating Passport authorization requests + /// + private const string LOGIN_HEADER_TEMPLATE = "Passport1.4 sign-in={0},pwd={1},OrgVerb={2},OrgUrl={3},{4}"; + /// + /// A template for generating Passport authorization responses + /// + private const string TOKEN_FORMAT_TEMPLATE = "Passport1.4 from-PP={0}"; + /// + /// A template for creating a WLID authorization response + /// + private const string WLID_TOKEN_FORMAT_TEMPLATE = "WLID1.0 {0}"; + + /// + /// The name of the header that contains the authentication nonce + /// + private const string NONCE_HEADER = "WWW-Authenticate"; + /// + /// The name of the redirect header + /// + private const string REDIR_LOCATION_HEADER = "Location"; + /// + /// The name of the header that contains authorization information + /// + private const string AUTHORIZATION_HEADER = "Authorization"; + /// + /// The name of the header that contains the authorization token + /// + private const string AUTHENTIFICATION_RESULT_HEADER = "Authentication-Info"; + + /// + /// An xml SOAP request for getting basic account information + /// + private readonly string SOAP_GETACCOUNT_INFO_REQUEST = "" + + "" + + "" + + "" + USER_AGENT + "en-US" + + "v1.0" + + "true"; + + /// + /// A template for requesting file creation + /// + private const string CREATE_FILE_TEMPLATE = "{0}Documentfalse"; + /// + /// A template for requesting subfolder creation + /// + private const string CREATE_SUBFOLDER_TEMPLATE = "{0}Folderfalse"; + /// + /// A template for requesting root folder creation + /// + private const string CREATE_ROOTFOLDER_TEMPLATE = "{0}LibraryDocumentPrivate"; + + /// + /// The user agent applied to all requests + /// + private readonly static string USER_AGENT = "Duplicati SkyDrive client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(); + + + /// + /// The Passport username + /// + private string m_username; + /// + /// The Passport password + /// + private string m_password; + /// + /// The name of the root folder + /// + private string m_rootfolder; + /// + /// The prefix (aka subfolder) path + /// + private string m_prefix; + + /// + /// The url used to authenticate against + /// + private string m_loginUrl; + /// + /// The CID of the folder this session is bound to + /// + private string m_folderCID; + /// + /// The passport token used to authticate WebDAV requests + /// + private string m_mainpassporttoken; + /// + /// The WLID token used to authenticate requests + /// + private string m_mainwlidtoken; + /// + /// The template used to generate nonce values + /// + private string m_noncetemplate; + /// + /// The CID of the user + /// + private string m_userCID; + /// + /// A list of filenames, used to obtain the CID of files + /// + private List m_filenameCache; + /// + /// A lookup table with all WEBDAV folders, key is the root foldername, value is the WEBDAV url + /// + private Dictionary m_webdav_rootFolderCache = null; + + /// + /// A lock used to prevent loading the loginUrl multiple times + /// + private static object _lock = new object(); + /// + /// The login url used to authenticate, kept static because it takes a few seconds to read, + /// and it appears to be completely fixed: https://login.live.com/login2.srf + /// + private static string _loginUrl = null; + + /// + /// Initializes a new instance of the class. + /// + /// The Passport username. + /// The Passport password. + /// The rootfolder. + /// The prefix (aka subfolder path). + /// if set to true missing folders will be created. + public SkyDriveSession(string username, string password, string rootfolder, string prefix, bool createFolders) + { + //Cache basic info + m_username = username; + m_password = password; + m_rootfolder = rootfolder; + m_prefix = prefix; + + //The call is sooo slow, and won't change often (if ever). + //Worst case scenario: the app needs to be restarted + if (string.IsNullOrEmpty(_loginUrl)) + lock(_lock) + if (string.IsNullOrEmpty(_loginUrl)) + _loginUrl = GetLoginUrl(); + + m_loginUrl = _loginUrl; + //Populate m_userCID, m_noncetemplate and m_webdav_rootFolderCache + SetupConnection(); + m_folderCID = GetFolderCID(rootfolder, prefix, createFolders); + } + + + /// + /// Gets the login URL. + /// + /// The login url + private string GetLoginUrl() + { + HttpWebRequest getUrls = (HttpWebRequest)WebRequest.Create(PASSPORT_LIST_URL); + getUrls.UserAgent = USER_AGENT; + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(getUrls)) + { + string header = resp.Headers[PASSPORT_URL_LIST_HEADER]; + if (string.IsNullOrEmpty(header)) + throw new Exception(Strings.SkyDrive.NoPassportLoginUrls); + string loginurl = PASSPORT_LOGIN_MATCH.Match(header).Groups["url"].Value; + + if (string.IsNullOrEmpty(loginurl)) + throw new Exception(Strings.SkyDrive.NoPassportLoginUrls); + + if (!loginurl.StartsWith("https://")) + loginurl = "https://" + loginurl; + + return loginurl; + } + } + + /// + /// Reads the user CID, generates the noncetemplate and populates the WEBDAV folder list + /// + public void SetupConnection() + { + m_webdav_rootFolderCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + HttpWebRequest listRequest = CreateGetWebAccountInfoRequest(); + + //We use the first request to extract the nonce template + if (string.IsNullOrEmpty(m_noncetemplate)) + { + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(listRequest)) + { + string nonce = resp.Headers[NONCE_HEADER]; + if (string.IsNullOrEmpty(nonce)) + throw new Exception(Strings.SkyDrive.RequestDidNotRequireAuthentificationError); + Match m = NONCE_MATCH.Match(nonce); + if (!m.Success) + throw new Exception(string.Format(Strings.SkyDrive.InvalidNonceError, nonce)); + + m_noncetemplate = + nonce.Substring(0, m.Groups["nonce"].Index) + + "{0}" + + nonce.Substring(m.Groups["nonce"].Index + m.Groups["nonce"].Length); + } + + listRequest = CreateGetWebAccountInfoRequest(); + } + + //Get the list of root folders, the user CID and the urls for webdav folders + XmlDocument doc = new XmlDocument(); + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(listRequest)) + using (System.IO.Stream s = resp.GetResponseStream()) + doc.Load(s); + + XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); + mgr.AddNamespace("ms", "http://schemas.microsoft.com/clouddocuments"); + + if (string.IsNullOrEmpty(m_userCID)) + { + //Extract the CID from the NewLibraryUrl node + XmlNode cidnode = doc.SelectSingleNode("//ms:NewLibraryUrl", mgr); + if (cidnode == null) + throw new Exception(Strings.SkyDrive.UnableToFindCIDError); + m_userCID = CID_FINDER.Match(cidnode.InnerText).Groups["cid"].Value; + if (string.IsNullOrEmpty(m_userCID)) + throw new Exception(Strings.SkyDrive.UnableToFindCIDError); + } + + //Load the list of WEBDAV urls + foreach (XmlNode n in doc.SelectNodes("//ms:Library", mgr)) + m_webdav_rootFolderCache.Add(n.SelectSingleNode("ms:DisplayName", mgr).InnerText, n.SelectSingleNode("ms:DavUrl", mgr).InnerText); + } + + /// + /// Gets the folder CID for the folder this session is bound to. + /// + /// The rootfolder. + /// The prefix. + /// if set to true missing folders will be created. + /// The folder CID + private string GetFolderCID(string rootfolder, string prefix, bool createFolder) + { + //Obtain a valid passport token for downloading with WebDAV + m_mainpassporttoken = GetAuthenticationToken("GET", RootUrl); + + //Assign the WLID1.0 token, which is the same as the Passport1.4 token, but with another prefix + m_mainwlidtoken = ConvertPassportTokenToWLIDToken(m_mainpassporttoken); + + string[] folders = (rootfolder + prefix).Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries); + TaggedFileEntry curfolder = null; + + //Do a recursive lookup on the folders, which is required to get the CID for the folder + foreach (string folder in folders) + { + if (string.IsNullOrEmpty(folder)) + continue; + + TaggedFileEntry newfolder = null; + foreach (TaggedFileEntry tfe in ListFolderItems(curfolder == null ? null : curfolder.CID)) + if (tfe.Name.Equals(folder, StringComparison.InvariantCultureIgnoreCase)) + { + newfolder = tfe; + break; + } + + if (newfolder == null && createFolder) + { + //Create the folder and re-list the contents + CreateFolder(folder, curfolder == null ? null : curfolder.CID); + foreach (TaggedFileEntry tfe in ListFolderItems(curfolder == null ? null : curfolder.CID)) + if (tfe.Name.Equals(folder, StringComparison.InvariantCultureIgnoreCase)) + { + newfolder = tfe; + break; + } + } + + //If the folder was not created, inform the caller + if (newfolder == null) + throw new Interface.FolderMissingException(); + + curfolder = newfolder; + } + + if (curfolder == null) + throw new FolderMissingException(); + + return curfolder.CID; + + } + + /// + /// Creates a WebAccountInfo request used to read basic account information. + /// + /// A configured request + private HttpWebRequest CreateGetWebAccountInfoRequest() + { + HttpWebRequest soapRequest = (HttpWebRequest)WebRequest.Create(SKYDRIVE_URL); + soapRequest.AllowAutoRedirect = false; + soapRequest.PreAuthenticate = false; + soapRequest.UserAgent = USER_AGENT; + soapRequest.Headers["SOAPAction"] = "GetWebAccountInfo"; + soapRequest.Headers["translate"] = "f"; + soapRequest.Method = "POST"; + soapRequest.ContentType = "text/xml; charset=utf-8"; + SetAuthenticationToken(soapRequest); + + byte[] data = System.Text.Encoding.UTF8.GetBytes(SOAP_GETACCOUNT_INFO_REQUEST); + using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(soapRequest)) + s.Write(data, 0, data.Length); + + return soapRequest; + } + + /// + /// Gets the root URL onto which WLID requests can be made. + /// + /// The root URL. + public string RootUrl + { + get + { + return String.Format( + "http://cid-{0}.users.api.live.net/Users({1})/Files", + m_userCID, + Int64.Parse(m_userCID, System.Globalization.NumberStyles.HexNumber)); + } + } + + /// + /// Gets the folder CID for the folder this session is bound to. + /// + /// The folder CID. + public string FolderCID + { + get { return m_folderCID; } + } + + /// + /// Gets the URL for the folder this session is bound to. + /// + /// The folder URL. + public string FolderUrl + { + get { return GetFolderUrl(m_folderCID); } + } + + /// + /// Gets the URL for a specific folder. + /// + /// The folder CID. + /// + private string GetFolderUrl(string folderCID) + { + return String.Format("{0}/Folders('{1}')", RootUrl, folderCID); + } + + /// + /// Gets the URL for a specific file. + /// + /// The file CID. + /// + private string GetFileUrl(string fileCID) + { + return String.Format("{0}/Files('{1}')", RootUrl, fileCID); + } + + + /// + /// Creates a nonce using the template. + /// + /// The http verb. + /// The http request URL. + /// The generated nonce + private string CreateNonce(string verb, string url) + { + //The nonce template seems to be statically defined as: + //"Passport1.4 ct={0},rver=6.1.6206.0,wp=MBI,lc=1033,id=250206" // but just to be sure, we pick it up from the server - return String.Format(m_noncetemplate, (int)Math.Floor((DateTime.Now - Library.Utility.Utility.EPOCH).TotalSeconds)); - } - - /// - /// Sets the WLID authentication token on the request. - /// - /// The req to assign the WLID token to - private void SetWlidAuthenticationToken(HttpWebRequest req) - { - req.Headers[AUTHORIZATION_HEADER] = m_mainwlidtoken; - //Hmm, magic header value, without it all requests are denied - req.Headers["AppId"] = "1140851978"; - req.UserAgent = USER_AGENT; - } - - /// - /// Generates and sets the Passport authentication token on the request. - /// - /// The request to set the token on. - private void SetAuthenticationToken(HttpWebRequest req) - { - string s = GetAuthenticationToken(req.Method, req.RequestUri.ToString()); - if (!string.IsNullOrEmpty(s)) - req.Headers[AUTHORIZATION_HEADER] = s; - req.UserAgent = USER_AGENT; - } - - /// - /// Gets a Passport authentication token for a verb and url. - /// - /// The http verb. - /// The http request URL. - /// - private string GetAuthenticationToken(string verb, string url) - { - if (string.IsNullOrEmpty(m_noncetemplate)) - return null; - - //Get a authentification token - HttpWebRequest getToken = (HttpWebRequest)WebRequest.Create(m_loginUrl); - getToken.UserAgent = USER_AGENT; - getToken.Headers[AUTHORIZATION_HEADER] = string.Format(LOGIN_HEADER_TEMPLATE, HttpUtility.UrlEncode(m_username), HttpUtility.UrlEncode(m_password), verb, url, CreateNonce(verb, url)); - - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(getToken)) - { - string token_header = resp.Headers[AUTHENTIFICATION_RESULT_HEADER]; - string redir = resp.Headers[REDIR_LOCATION_HEADER]; - - string status = LOGIN_STATUS_MATCH.Match(token_header).Groups["status"].Value; - string token = LOGIN_TOKEN_MATCH.Match(token_header).Groups["token"].Value; - - if (status != "success") - throw new Exception(string.Format(Strings.SkyDrive.LoginFailedError, status)); - if (string.IsNullOrEmpty(token)) - throw new Exception(Strings.SkyDrive.NoTokenError); - - return string.Format(TOKEN_FORMAT_TEMPLATE, token); - } - } - - /// - /// Converts the Passport token to a WLID token. - /// - /// The passport token. - /// The WLID token - private static string ConvertPassportTokenToWLIDToken(string passporttoken) - { - string basetoken = LOGIN_TOKEN_MATCH.Match(passporttoken).Groups["token"].Value; - if (basetoken.StartsWith("'") && basetoken.EndsWith("'")) - basetoken = basetoken.Substring(1, basetoken.Length - 2); - return string.Format(WLID_TOKEN_FORMAT_TEMPLATE, basetoken); - } - - /// - /// Creates a new folder. - /// - /// The name of the folder to create. - /// The parent cid. If this is null, a new root folder will be created. - public void CreateFolder(string name, string parentCid) - { - HttpWebRequest req; - if (string.IsNullOrEmpty(parentCid)) - req = (HttpWebRequest)WebRequest.Create(RootUrl); - else - req = (HttpWebRequest)WebRequest.Create(GetFolderUrl(parentCid)); - req.Method = WebRequestMethods.Http.Post; - req.Accept = "application/atom+xml"; - req.ContentType = req.Accept; - - SetWlidAuthenticationToken(req); - - byte[] data; - if (string.IsNullOrEmpty(parentCid)) - data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_ROOTFOLDER_TEMPLATE, name)); - else - data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_SUBFOLDER_TEMPLATE, name)); - - req.ContentLength = data.Length; - - using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) - s.Write(data, 0, data.Length); - - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically - throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); - } - - //If we added a root folder, update the webdab list - if (string.IsNullOrEmpty(parentCid)) - SetupConnection(); - } - - private TaggedFileEntry ParseXmlAtomEntry(XmlNode node, XmlNamespaceManager mgr) - { - if (mgr == null) - mgr = new XmlNamespaceManager(node.OwnerDocument.NameTable); - - if (!mgr.HasNamespace("live")) - mgr.AddNamespace("live", "http://api.live.com/schemas"); - if (!mgr.HasNamespace("atom")) - mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); - - XmlNode title = node.SelectSingleNode("atom:title", mgr); - XmlNode id = node.SelectSingleNode("live:resourceId", mgr); - XmlNode url = node.SelectSingleNode("atom:id", mgr); - - if (title == null || id == null || url == null) - return null; - - XmlNode updated = node.SelectSingleNode("atom:updated", mgr); - XmlNode size = node.SelectSingleNode("live:size", mgr); - XmlNode type = node.SelectSingleNode("live:type", mgr); - - string u = url.InnerText; - XmlNode content = node.SelectSingleNode("atom:content", mgr); - if (content != null) content = content.Attributes["src"]; - if (content != null) u = content.Value; - - string editLink = null; - string altLink = null; - - foreach (XmlNode n in node.SelectNodes("atom:link", mgr)) - if (n.Attributes["rel"] != null && n.Attributes["href"] != null) - { - if (n.Attributes["rel"].Value == "edit-media") - editLink = n.Attributes["href"].Value; - else if (n.Attributes["rel"].Value == "alternate") - altLink = n.Attributes["href"].Value; - } - - TaggedFileEntry tf = new TaggedFileEntry(title.InnerText, u, id.InnerText, altLink, editLink); - try { tf.LastAccess = tf.LastModification = DateTime.Parse(updated.InnerText); } - catch { } - - try { tf.Size = long.Parse(size.InnerText); } - catch { } - - try { tf.IsFolder = type.InnerText.Equals("Library", StringComparison.InvariantCultureIgnoreCase) || type.InnerText.Equals("Folder", StringComparison.InvariantCultureIgnoreCase); } - catch { } - - return tf; - } - - private List ParseXmlAtomDocument(XmlDocument doc) - { - List results = new List(); - XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); - mgr.AddNamespace("live", "http://api.live.com/schemas"); - mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); - - foreach (XmlNode n in doc.SelectNodes("//atom:entry", mgr)) - { - TaggedFileEntry tf = ParseXmlAtomEntry(n, mgr); - if (tf != null) - results.Add(tf); - } - - return results; - } - - /// - /// Lists the items in a specific folder. - /// - /// The parent folder CID. If this is null, the root folder items are listed - /// A list of elements in the folder - public List ListFolderItems(string parentCid) - { - HttpWebRequest req; - if (string.IsNullOrEmpty(parentCid)) - req = (HttpWebRequest)WebRequest.Create(RootUrl); - else - req = (HttpWebRequest)WebRequest.Create(GetFolderUrl(parentCid)); - - req.Accept = "application/atom+xml"; - req.ContentType = req.Accept; - req.ContentLength = 0; - SetWlidAuthenticationToken(req); - - XmlDocument doc = new XmlDocument(); - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) - using (System.IO.Stream s = resp.GetResponseStream()) - doc.Load(s); - - List results = ParseXmlAtomDocument(doc); - - //Cache the results if we listed the folder that the session is bound to - if (m_folderCID != null && m_folderCID.Equals(parentCid)) - m_filenameCache = new List(results); - - return results; - - } - - /// - /// Returns a WEBDAV url for a given file - /// - /// The name of the file to generate an URL for - /// The WEBDAV URL for the file - private string GetWebDavUrl(string remotename) - { - if (!m_webdav_rootFolderCache.ContainsKey(m_rootfolder)) - throw new FolderMissingException(); - - return m_webdav_rootFolderCache[m_rootfolder] + m_prefix + remotename; - } - - /// - /// Looks for a file and returns the file information, including the CID - /// - /// The name of the file to look for - /// The file information or null - private TaggedFileEntry GetFileInfoFromCache(string name) - { - if (m_filenameCache == null) - return GetFileInfo(name); - - foreach (TaggedFileEntry f in m_filenameCache) - { - if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - return f; - } - - return null; - - } - - /// - /// Looks for a file and returns the file information, including the CID - /// - /// The name of the file to look for - /// The file information or null - private TaggedFileEntry GetFileInfo(string name) - { - //Look in the cache first - if (m_filenameCache != null) - foreach (TaggedFileEntry f in m_filenameCache) - { - if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - return f; - } - - //No match, perform a new list request - foreach (TaggedFileEntry f in ListFolderItems(m_folderCID)) - { - if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - return f; - } - - return null; - } - - /// - /// Deletes a file. - /// - /// The remote file name. - /// The server response - public void DeleteFile(string remotename) - { - try - { - TaggedFileEntry fileinfo = GetFileInfo(remotename); - if (fileinfo == null) - throw new System.IO.FileNotFoundException(); //TODO: Should throw 404? - - //First we need the content url - HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetFileUrl(fileinfo.CID)); - req.AllowAutoRedirect = false; - req.Method = "DELETE"; - req.KeepAlive = false; - //Delete uses the default timeout - //req.Timeout = System.Threading.Timeout.Infinite; - SetWlidAuthenticationToken(req); - - using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically - throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); - m_filenameCache.Remove(fileinfo); - } - } - catch - { - m_filenameCache = null; - throw; - } - } - - /// - /// Uploads a file. - /// - /// The remote file name. - /// The data that represents the file. - /// The server response - public void UploadFile(string remotename, System.IO.Stream data) - { - TaggedFileEntry fileinfo = null; - - //This is done slightly odd, because we do not want to - // re-issue the file listing if it can be avoided - if (m_filenameCache == null) - fileinfo = GetFileInfoFromCache(remotename); - else - { - foreach (TaggedFileEntry f in m_filenameCache) - if (f.Name.Equals(remotename, StringComparison.InvariantCultureIgnoreCase)) - { - fileinfo = f; - break; - } - } - - if (fileinfo != null && fileinfo.EditUrl == null) - { - try { this.DeleteFile(remotename); } - catch { } - - fileinfo = GetFileInfo(remotename); - if (fileinfo != null && fileinfo.EditUrl == null) - throw new Exception(string.Format(Strings.SkyDrive.FileIsReadOnlyError, remotename)); - } - - HttpWebRequest req; - if (fileinfo == null) - { - try - { - string boundary = String.Format("---------------------------{0:x}", DateTime.Now.Ticks); - - req = (HttpWebRequest)WebRequest.Create(FolderUrl); - - req.Method = WebRequestMethods.Http.Post; - req.ContentType = "multipart/related; type=\"application/atom+xml\"; boundary=\"" + boundary + "\""; - req.Accept = "application/atom+xml"; - req.AllowAutoRedirect = false; - req.KeepAlive = false; - - //We only depend on the ReadWriteTimeout - req.Timeout = System.Threading.Timeout.Infinite; - SetWlidAuthenticationToken(req); - - byte[] createRequest = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_FILE_TEMPLATE, HttpUtility.HtmlEncode(remotename))); - - byte[] newLine = Encoding.UTF8.GetBytes("\r\n"); - byte[] lastBoundary = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); - - - byte[] xmlHeader = System.Text.Encoding.UTF8.GetBytes( - "--" + boundary + "\r\n" + - "MIME-Version: 1.0" + "\r\n" + - "Content-Type: application/atom+xml" + "\r\n\r\n"); - - byte[] dataHeader = System.Text.Encoding.UTF8.GetBytes( - "--" + boundary + "\r\n" + - "MIME-Version: 1.0" + "\r\n" + - "Content-Type: application/octet-stream" + "\r\n\r\n"); - - - try { req.ContentLength = xmlHeader.Length + createRequest.Length + newLine.Length + dataHeader.Length + data.Length + newLine.Length + lastBoundary.Length; } - catch { } - - using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) - { - s.Write(xmlHeader, 0, xmlHeader.Length); - s.Write(createRequest, 0, createRequest.Length); - s.Write(newLine, 0, newLine.Length); - s.Write(dataHeader, 0, dataHeader.Length); - Utility.Utility.CopyStream(data, s); - s.Write(newLine, 0, newLine.Length); - s.Write(lastBoundary, 0, lastBoundary.Length); - } - - using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically - throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); - - //If all goes well, we should now get an atom entry describing the new element - System.Xml.XmlDocument xml = new XmlDocument(); - using (System.IO.Stream s = resp.GetResponseStream()) - xml.Load(s); - - TaggedFileEntry tf = ParseXmlAtomEntry(xml["entry"], null); - if (tf == null) - throw new Exception(string.Format(Strings.SkyDrive.UploadFailedError, remotename)); - - if (fileinfo != null) - m_filenameCache.Remove(fileinfo); - m_filenameCache.Add(tf); - } - } - catch - { - m_filenameCache = null; - throw; - } - } - else - { - //Overwrite is simpler - req = (HttpWebRequest)WebRequest.Create(fileinfo.EditUrl); - req.Method = WebRequestMethods.Http.Put; - req.ContentLength = data.Length; - req.ContentType = "application/octet-stream"; - req.AllowAutoRedirect = false; - req.KeepAlive = false; - - //We only depend on the ReadWriteTimeout - req.Timeout = System.Threading.Timeout.Infinite; - SetWlidAuthenticationToken(req); - - using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) - Utility.Utility.CopyStream(data, s); - - using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) - { - int code = (int)resp.StatusCode; - if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically - throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); - } - } - } - - /// - /// Downloads a file. - /// - /// The remote file name. - /// The server response - public HttpWebResponse DownloadFile(string remotename) - { - TaggedFileEntry fileinfo = GetFileInfo(remotename); - if (fileinfo == null) - throw new System.IO.FileNotFoundException(); //TODO: Should throw 404? - - //This method is much preferred because it allows us to use non-url-friendly characters - //For some reason it will not accept the WLID token :( - //I also tried creating WLID and passport tokens for the URL and - // the hostnames, but everything redirects. - //The browser redirects to an auth page that sets a WLSRDAuth - // cookie, but I cannot find any information on that - - /*HttpWebRequest req = (HttpWebRequest)WebRequest.Create(fileinfo.Url); - req.Method = WebRequestMethods.Http.Get; - req.AllowAutoRedirect = false; - req.KeepAlive = false; - //We only depend on the ReadWriteTimeout - req.Timeout = System.Threading.Timeout.Infinite; - SetWlidAuthenticationToken(req); - */ - - //This uses a WebDAV hack to download files via WebDAV, - //I have not yet figured out how to encode special characters, - // but it works with -_. and that should be enough for Duplicati - HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetWebDavUrl(remotename)); - req.Method = WebRequestMethods.Http.Get; - req.AllowAutoRedirect = false; - req.KeepAlive = false; - //We only depend on the ReadWriteTimeout - req.Timeout = System.Threading.Timeout.Infinite; - req.UserAgent = USER_AGENT; - req.Headers[AUTHORIZATION_HEADER] = m_mainpassporttoken; - - HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req); - int code = (int)resp.StatusCode; - - //For some reason Mono does not throw this automatically - if (code < 200 || code >= 300) - { - //if (resp.StatusCode == HttpStatusCode.Moved || resp.StatusCode == HttpStatusCode.MovedPermanently) - using (resp) - throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); - } - - return resp; - } - - - //For some reason this always throws 403, perhaps some locking is required? - /*private HttpWebResponse Upload_webdav(string remotename, System.IO.Stream data) - { - HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetWebDavUrl(remotename)); - req.AllowAutoRedirect = false; - req.Method = "PUT"; - req.ContentType = "application/octet-stream"; - req.ContentLength = data.Length; - //We only depend on the ReadWriteTimeout - req.Timeout = System.Threading.Timeout.Infinite; - SetAuthenticationToken(req); - - using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) - Utility.Utility.CopyStream(data, s); - - return (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req); - - }*/ - - #region IDisposable Members - - public void Dispose() - { - m_username = null; - m_password = null; - m_rootfolder = null; - m_prefix = null; - m_folderCID = null; - m_mainwlidtoken = null; - m_noncetemplate = null; - m_userCID = null; - m_filenameCache = null; - m_webdav_rootFolderCache = null; - } - - #endregion - } -} + return String.Format(m_noncetemplate, (int)Math.Floor((DateTime.Now - Library.Utility.Utility.EPOCH).TotalSeconds)); + } + + /// + /// Sets the WLID authentication token on the request. + /// + /// The req to assign the WLID token to + private void SetWlidAuthenticationToken(HttpWebRequest req) + { + req.Headers[AUTHORIZATION_HEADER] = m_mainwlidtoken; + //Hmm, magic header value, without it all requests are denied + req.Headers["AppId"] = "1140851978"; + req.UserAgent = USER_AGENT; + } + + /// + /// Generates and sets the Passport authentication token on the request. + /// + /// The request to set the token on. + private void SetAuthenticationToken(HttpWebRequest req) + { + string s = GetAuthenticationToken(req.Method, req.RequestUri.ToString()); + if (!string.IsNullOrEmpty(s)) + req.Headers[AUTHORIZATION_HEADER] = s; + req.UserAgent = USER_AGENT; + } + + /// + /// Gets a Passport authentication token for a verb and url. + /// + /// The http verb. + /// The http request URL. + /// + private string GetAuthenticationToken(string verb, string url) + { + if (string.IsNullOrEmpty(m_noncetemplate)) + return null; + + try + { + //Get a authentification token + HttpWebRequest getToken = (HttpWebRequest)WebRequest.Create(m_loginUrl); + getToken.UserAgent = USER_AGENT; + getToken.Headers[AUTHORIZATION_HEADER] = string.Format(LOGIN_HEADER_TEMPLATE, HttpUtility.UrlEncode(m_username), HttpUtility.UrlEncode(m_password), verb, url, CreateNonce(verb, url)); + + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(getToken)) + { + string token_header = resp.Headers[AUTHENTIFICATION_RESULT_HEADER]; + string redir = resp.Headers[REDIR_LOCATION_HEADER]; + + string status = LOGIN_STATUS_MATCH.Match(token_header).Groups["status"].Value; + string token = LOGIN_TOKEN_MATCH.Match(token_header).Groups["token"].Value; + + if (status != "success") + throw new Exception(string.Format(Strings.SkyDrive.LoginFailedError, status)); + if (string.IsNullOrEmpty(token)) + throw new Exception(Strings.SkyDrive.NoTokenError); + + return string.Format(TOKEN_FORMAT_TEMPLATE, token); + } + } + catch (WebException wex) + { + if (wex.Response is HttpWebResponse && ((HttpWebResponse)wex.Response).StatusCode == HttpStatusCode.Unauthorized) + { + if (IsPasswordTooLong(m_password)) + throw new Exception(Strings.SkyDriveSession.InvalidPasswordLongPasswordFound, wex); + else if (HasInvalidChars(m_password)) + throw new Exception(Strings.SkyDriveSession.InvalidPasswordInvalidCharsFound, wex); + else + throw new Exception(Strings.SkyDriveSession.InvalidUsernameOrPassword, wex); + } + + throw; + } + } + + /// + /// Converts the Passport token to a WLID token. + /// + /// The passport token. + /// The WLID token + private static string ConvertPassportTokenToWLIDToken(string passporttoken) + { + string basetoken = LOGIN_TOKEN_MATCH.Match(passporttoken).Groups["token"].Value; + if (basetoken.StartsWith("'") && basetoken.EndsWith("'")) + basetoken = basetoken.Substring(1, basetoken.Length - 2); + return string.Format(WLID_TOKEN_FORMAT_TEMPLATE, basetoken); + } + + /// + /// Creates a new folder. + /// + /// The name of the folder to create. + /// The parent cid. If this is null, a new root folder will be created. + public void CreateFolder(string name, string parentCid) + { + HttpWebRequest req; + if (string.IsNullOrEmpty(parentCid)) + req = (HttpWebRequest)WebRequest.Create(RootUrl); + else + req = (HttpWebRequest)WebRequest.Create(GetFolderUrl(parentCid)); + req.Method = WebRequestMethods.Http.Post; + req.Accept = "application/atom+xml"; + req.ContentType = req.Accept; + + SetWlidAuthenticationToken(req); + + byte[] data; + if (string.IsNullOrEmpty(parentCid)) + data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_ROOTFOLDER_TEMPLATE, name)); + else + data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_SUBFOLDER_TEMPLATE, name)); + + req.ContentLength = data.Length; + + using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) + s.Write(data, 0, data.Length); + + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) + { + int code = (int)resp.StatusCode; + if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically + throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); + } + + //If we added a root folder, update the webdab list + if (string.IsNullOrEmpty(parentCid)) + SetupConnection(); + } + + private TaggedFileEntry ParseXmlAtomEntry(XmlNode node, XmlNamespaceManager mgr) + { + if (mgr == null) + mgr = new XmlNamespaceManager(node.OwnerDocument.NameTable); + + if (!mgr.HasNamespace("live")) + mgr.AddNamespace("live", "http://api.live.com/schemas"); + if (!mgr.HasNamespace("atom")) + mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); + + XmlNode title = node.SelectSingleNode("atom:title", mgr); + XmlNode id = node.SelectSingleNode("live:resourceId", mgr); + XmlNode url = node.SelectSingleNode("atom:id", mgr); + + if (title == null || id == null || url == null) + return null; + + XmlNode updated = node.SelectSingleNode("atom:updated", mgr); + XmlNode size = node.SelectSingleNode("live:size", mgr); + XmlNode type = node.SelectSingleNode("live:type", mgr); + + string u = url.InnerText; + XmlNode content = node.SelectSingleNode("atom:content", mgr); + if (content != null) content = content.Attributes["src"]; + if (content != null) u = content.Value; + + string editLink = null; + string altLink = null; + + foreach (XmlNode n in node.SelectNodes("atom:link", mgr)) + if (n.Attributes["rel"] != null && n.Attributes["href"] != null) + { + if (n.Attributes["rel"].Value == "edit-media") + editLink = n.Attributes["href"].Value; + else if (n.Attributes["rel"].Value == "alternate") + altLink = n.Attributes["href"].Value; + } + + TaggedFileEntry tf = new TaggedFileEntry(title.InnerText, u, id.InnerText, altLink, editLink); + try { tf.LastAccess = tf.LastModification = DateTime.Parse(updated.InnerText); } + catch { } + + try { tf.Size = long.Parse(size.InnerText); } + catch { } + + try { tf.IsFolder = type.InnerText.Equals("Library", StringComparison.InvariantCultureIgnoreCase) || type.InnerText.Equals("Folder", StringComparison.InvariantCultureIgnoreCase); } + catch { } + + return tf; + } + + private List ParseXmlAtomDocument(XmlDocument doc) + { + List results = new List(); + XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); + mgr.AddNamespace("live", "http://api.live.com/schemas"); + mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom"); + + foreach (XmlNode n in doc.SelectNodes("//atom:entry", mgr)) + { + TaggedFileEntry tf = ParseXmlAtomEntry(n, mgr); + if (tf != null) + results.Add(tf); + } + + return results; + } + + /// + /// Lists the items in a specific folder. + /// + /// The parent folder CID. If this is null, the root folder items are listed + /// A list of elements in the folder + public List ListFolderItems(string parentCid) + { + HttpWebRequest req; + if (string.IsNullOrEmpty(parentCid)) + req = (HttpWebRequest)WebRequest.Create(RootUrl); + else + req = (HttpWebRequest)WebRequest.Create(GetFolderUrl(parentCid)); + + req.Accept = "application/atom+xml"; + req.ContentType = req.Accept; + req.ContentLength = 0; + SetWlidAuthenticationToken(req); + + XmlDocument doc = new XmlDocument(); + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) + using (System.IO.Stream s = resp.GetResponseStream()) + doc.Load(s); + + List results = ParseXmlAtomDocument(doc); + + //Cache the results if we listed the folder that the session is bound to + if (m_folderCID != null && m_folderCID.Equals(parentCid)) + m_filenameCache = new List(results); + + return results; + + } + + /// + /// Returns a WEBDAV url for a given file + /// + /// The name of the file to generate an URL for + /// The WEBDAV URL for the file + private string GetWebDavUrl(string remotename) + { + if (!m_webdav_rootFolderCache.ContainsKey(m_rootfolder)) + throw new FolderMissingException(); + + return m_webdav_rootFolderCache[m_rootfolder] + m_prefix + remotename; + } + + /// + /// Looks for a file and returns the file information, including the CID + /// + /// The name of the file to look for + /// The file information or null + private TaggedFileEntry GetFileInfoFromCache(string name) + { + if (m_filenameCache == null) + return GetFileInfo(name); + + foreach (TaggedFileEntry f in m_filenameCache) + { + if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + return f; + } + + return null; + + } + + /// + /// Looks for a file and returns the file information, including the CID + /// + /// The name of the file to look for + /// The file information or null + private TaggedFileEntry GetFileInfo(string name) + { + //Look in the cache first + if (m_filenameCache != null) + foreach (TaggedFileEntry f in m_filenameCache) + { + if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + return f; + } + + //No match, perform a new list request + foreach (TaggedFileEntry f in ListFolderItems(m_folderCID)) + { + if (f.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + return f; + } + + return null; + } + + /// + /// Deletes a file. + /// + /// The remote file name. + /// The server response + public void DeleteFile(string remotename) + { + try + { + TaggedFileEntry fileinfo = GetFileInfo(remotename); + if (fileinfo == null) + throw new System.IO.FileNotFoundException(); //TODO: Should throw 404? + + //First we need the content url + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetFileUrl(fileinfo.CID)); + req.AllowAutoRedirect = false; + req.Method = "DELETE"; + req.KeepAlive = false; + //Delete uses the default timeout + //req.Timeout = System.Threading.Timeout.Infinite; + SetWlidAuthenticationToken(req); + + using (HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) + { + int code = (int)resp.StatusCode; + if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically + throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); + m_filenameCache.Remove(fileinfo); + } + } + catch + { + m_filenameCache = null; + throw; + } + } + + /// + /// Uploads a file. + /// + /// The remote file name. + /// The data that represents the file. + /// The server response + public void UploadFile(string remotename, System.IO.Stream data) + { + TaggedFileEntry fileinfo = null; + + //This is done slightly odd, because we do not want to + // re-issue the file listing if it can be avoided + if (m_filenameCache == null) + fileinfo = GetFileInfoFromCache(remotename); + else + { + foreach (TaggedFileEntry f in m_filenameCache) + if (f.Name.Equals(remotename, StringComparison.InvariantCultureIgnoreCase)) + { + fileinfo = f; + break; + } + } + + if (fileinfo != null && fileinfo.EditUrl == null) + { + try { this.DeleteFile(remotename); } + catch { } + + fileinfo = GetFileInfo(remotename); + if (fileinfo != null && fileinfo.EditUrl == null) + throw new Exception(string.Format(Strings.SkyDrive.FileIsReadOnlyError, remotename)); + } + + HttpWebRequest req; + if (fileinfo == null) + { + try + { + string boundary = String.Format("---------------------------{0:x}", DateTime.Now.Ticks); + + req = (HttpWebRequest)WebRequest.Create(FolderUrl); + + req.Method = WebRequestMethods.Http.Post; + req.ContentType = "multipart/related; type=\"application/atom+xml\"; boundary=\"" + boundary + "\""; + req.Accept = "application/atom+xml"; + req.AllowAutoRedirect = false; + req.KeepAlive = false; + + //We only depend on the ReadWriteTimeout + req.Timeout = System.Threading.Timeout.Infinite; + SetWlidAuthenticationToken(req); + + byte[] createRequest = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_FILE_TEMPLATE, HttpUtility.HtmlEncode(remotename))); + + byte[] newLine = Encoding.UTF8.GetBytes("\r\n"); + byte[] lastBoundary = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); + + + byte[] xmlHeader = System.Text.Encoding.UTF8.GetBytes( + "--" + boundary + "\r\n" + + "MIME-Version: 1.0" + "\r\n" + + "Content-Type: application/atom+xml" + "\r\n\r\n"); + + byte[] dataHeader = System.Text.Encoding.UTF8.GetBytes( + "--" + boundary + "\r\n" + + "MIME-Version: 1.0" + "\r\n" + + "Content-Type: application/octet-stream" + "\r\n\r\n"); + + + try { req.ContentLength = xmlHeader.Length + createRequest.Length + newLine.Length + dataHeader.Length + data.Length + newLine.Length + lastBoundary.Length; } + catch { } + + using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) + { + s.Write(xmlHeader, 0, xmlHeader.Length); + s.Write(createRequest, 0, createRequest.Length); + s.Write(newLine, 0, newLine.Length); + s.Write(dataHeader, 0, dataHeader.Length); + Utility.Utility.CopyStream(data, s); + s.Write(newLine, 0, newLine.Length); + s.Write(lastBoundary, 0, lastBoundary.Length); + } + + using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) + { + int code = (int)resp.StatusCode; + if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically + throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); + + //If all goes well, we should now get an atom entry describing the new element + System.Xml.XmlDocument xml = new XmlDocument(); + using (System.IO.Stream s = resp.GetResponseStream()) + xml.Load(s); + + TaggedFileEntry tf = ParseXmlAtomEntry(xml["entry"], null); + if (tf == null) + throw new Exception(string.Format(Strings.SkyDrive.UploadFailedError, remotename)); + + if (fileinfo != null) + m_filenameCache.Remove(fileinfo); + m_filenameCache.Add(tf); + } + } + catch + { + m_filenameCache = null; + throw; + } + } + else + { + //Overwrite is simpler + req = (HttpWebRequest)WebRequest.Create(fileinfo.EditUrl); + req.Method = WebRequestMethods.Http.Put; + req.ContentLength = data.Length; + req.ContentType = "application/octet-stream"; + req.AllowAutoRedirect = false; + req.KeepAlive = false; + + //We only depend on the ReadWriteTimeout + req.Timeout = System.Threading.Timeout.Infinite; + SetWlidAuthenticationToken(req); + + using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) + Utility.Utility.CopyStream(data, s); + + using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req)) + { + int code = (int)resp.StatusCode; + if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically + throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); + } + } + } + + /// + /// Downloads a file. + /// + /// The remote file name. + /// The server response + public HttpWebResponse DownloadFile(string remotename) + { + TaggedFileEntry fileinfo = GetFileInfo(remotename); + if (fileinfo == null) + throw new System.IO.FileNotFoundException(); //TODO: Should throw 404? + + //This method is much preferred because it allows us to use non-url-friendly characters + //For some reason it will not accept the WLID token :( + //I also tried creating WLID and passport tokens for the URL and + // the hostnames, but everything redirects. + //The browser redirects to an auth page that sets a WLSRDAuth + // cookie, but I cannot find any information on that + + /*HttpWebRequest req = (HttpWebRequest)WebRequest.Create(fileinfo.Url); + req.Method = WebRequestMethods.Http.Get; + req.AllowAutoRedirect = false; + req.KeepAlive = false; + //We only depend on the ReadWriteTimeout + req.Timeout = System.Threading.Timeout.Infinite; + SetWlidAuthenticationToken(req); + */ + + //This uses a WebDAV hack to download files via WebDAV, + //I have not yet figured out how to encode special characters, + // but it works with -_. and that should be enough for Duplicati + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetWebDavUrl(remotename)); + req.Method = WebRequestMethods.Http.Get; + req.AllowAutoRedirect = false; + req.KeepAlive = false; + //We only depend on the ReadWriteTimeout + req.Timeout = System.Threading.Timeout.Infinite; + req.UserAgent = USER_AGENT; + req.Headers[AUTHORIZATION_HEADER] = m_mainpassporttoken; + + HttpWebResponse resp = (HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req); + int code = (int)resp.StatusCode; + + //For some reason Mono does not throw this automatically + if (code < 200 || code >= 300) + { + //if (resp.StatusCode == HttpStatusCode.Moved || resp.StatusCode == HttpStatusCode.MovedPermanently) + using (resp) + throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp); + } + + return resp; + } + + + //For some reason this always throws 403, perhaps some locking is required? + /*private HttpWebResponse Upload_webdav(string remotename, System.IO.Stream data) + { + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(GetWebDavUrl(remotename)); + req.AllowAutoRedirect = false; + req.Method = "PUT"; + req.ContentType = "application/octet-stream"; + req.ContentLength = data.Length; + //We only depend on the ReadWriteTimeout + req.Timeout = System.Threading.Timeout.Infinite; + SetAuthenticationToken(req); + + using (System.IO.Stream s = Utility.Utility.SafeGetRequestStream(req)) + Utility.Utility.CopyStream(data, s); + + return (System.Net.HttpWebResponse)Library.Utility.Utility.SafeGetResponse(req); + + }*/ + + #region IDisposable Members + + public void Dispose() + { + m_username = null; + m_password = null; + m_rootfolder = null; + m_prefix = null; + m_folderCID = null; + m_mainwlidtoken = null; + m_noncetemplate = null; + m_userCID = null; + m_filenameCache = null; + m_webdav_rootFolderCache = null; + } + + #endregion + + + /// + /// Utility function to test if password is so long that it can cause problems + /// + /// The password to validate + /// True if the password is too long, false otherwise + public static bool IsPasswordTooLong(string password) + { + if (string.IsNullOrEmpty(password)) + return false; + + return password.Length > 16; + } + + /// + /// Utility function to test if the password contains non-standard characters + /// + /// The password to validate + /// True if the password has invalid chars, false otherwise + public static bool HasInvalidChars(string password) + { + bool invalidChars = false; + foreach (char c in password) + invalidChars |= !("!&()[~@".IndexOf(c) >= 0 || char.IsLetterOrDigit(c)); + + return invalidChars; + } + } +} diff --git a/Duplicati/Library/Backend/SkyDrive/SkyDriveUI.cs b/Duplicati/Library/Backend/SkyDrive/SkyDriveUI.cs index a61340ed8..bc385d4c4 100644 --- a/Duplicati/Library/Backend/SkyDrive/SkyDriveUI.cs +++ b/Duplicati/Library/Backend/SkyDrive/SkyDriveUI.cs @@ -16,8 +16,12 @@ namespace Duplicati.Library.Backend private const string HAS_TESTED = "UI: Has tested"; private const string INITIALPASSWORD = "UI: Temp password"; + private const string HAS_WARNED_LONG_PASSWORD = "UI: Warned long password"; + private const string HAS_WARNED_PASSWORD_CHARS = "UI: Warned password chars"; private bool m_hasTested; + private bool m_hasWarnedPasswordChars; + private bool m_hasWarnedLongPassword; private const string DUPLICATI_ACTION_MARKER = "*duplicati-action*"; private string m_uiAction = null; @@ -83,6 +87,12 @@ namespace Duplicati.Library.Backend if (!m_options.ContainsKey(HAS_TESTED) || !bool.TryParse(m_options[HAS_TESTED], out m_hasTested)) m_hasTested = false; + if (!m_options.ContainsKey(HAS_WARNED_LONG_PASSWORD) || !bool.TryParse(m_options[HAS_WARNED_LONG_PASSWORD], out m_hasWarnedLongPassword)) + m_hasWarnedLongPassword = false; + + if (!m_options.ContainsKey(HAS_WARNED_PASSWORD_CHARS) || !bool.TryParse(m_options[HAS_WARNED_PASSWORD_CHARS], out m_hasWarnedPasswordChars)) + m_hasWarnedPasswordChars = false; + m_options.TryGetValue(DUPLICATI_ACTION_MARKER, out m_uiAction); } @@ -116,6 +126,32 @@ namespace Duplicati.Library.Backend return false; } + if (!m_hasWarnedLongPassword && SkyDriveSession.IsPasswordTooLong(Password.Text)) + { + if (MessageBox.Show(this, Strings.SkyDriveUI.LongPasswordWarning, Application.ProductName, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning) != DialogResult.Yes) + { + try { Password.Focus(); } + catch { } + + return false; + } + + m_hasWarnedLongPassword = true; + } + + if (!m_hasWarnedPasswordChars && SkyDriveSession.HasInvalidChars(Password.Text)) + { + if (MessageBox.Show(this, Strings.SkyDriveUI.PasswordCharactersWarning, Application.ProductName, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning) != DialogResult.Yes) + { + try { Password.Focus(); } + catch { } + + return false; + } + + m_hasWarnedPasswordChars = true; + } + return true; } @@ -126,6 +162,8 @@ namespace Duplicati.Library.Backend m_options.Clear(); m_options[HAS_TESTED] = m_hasTested.ToString(); + m_options[HAS_WARNED_LONG_PASSWORD] = m_hasWarnedLongPassword.ToString(); + m_options[HAS_WARNED_PASSWORD_CHARS] = m_hasWarnedPasswordChars.ToString(); m_options[FOLDER] = Path.Text; m_options[USERNAME] = Username.Text; m_options[PASSWORD] = Password.Text; @@ -208,6 +246,8 @@ namespace Duplicati.Library.Backend private void Password_TextChanged(object sender, EventArgs e) { m_hasTested = false; + m_hasWarnedLongPassword = false; + m_hasWarnedPasswordChars = false; } private void Path_TextChanged(object sender, EventArgs e) diff --git a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.Designer.cs b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.Designer.cs new file mode 100644 index 000000000..303dfdf31 --- /dev/null +++ b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.269 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Duplicati.Library.Backend.Strings { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SkyDriveSession { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SkyDriveSession() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Duplicati.Library.Backend.Strings.SkyDriveSession", typeof(SkyDriveSession).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Login failed, check your username and password. Consider using a password with only alphanumeric characters.. + /// + internal static string InvalidPasswordInvalidCharsFound { + get { + return ResourceManager.GetString("InvalidPasswordInvalidCharsFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login failed, check your username and password. Consider using a password with less than 16 characters.. + /// + internal static string InvalidPasswordLongPasswordFound { + get { + return ResourceManager.GetString("InvalidPasswordLongPasswordFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login failed, check your username and password.. + /// + internal static string InvalidUsernameOrPassword { + get { + return ResourceManager.GetString("InvalidUsernameOrPassword", resourceCulture); + } + } + } +} diff --git a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.resx b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.resx new file mode 100644 index 000000000..76138ff0a --- /dev/null +++ b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveSession.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Login failed, check your username and password. Consider using a password with only alphanumeric characters. + + + Login failed, check your username and password. Consider using a password with less than 16 characters. + + + Login failed, check your username and password. + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.Designer.cs b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.Designer.cs index aea3faa3d..3d1183d3b 100644 --- a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.Designer.cs +++ b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.237 +// Runtime Version:4.0.30319.269 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -78,6 +78,17 @@ namespace Duplicati.Library.Backend.Strings { } } + /// + /// Looks up a localized string similar to Your password is more than 16 characters. SkyDrive may have unexpected issues, consider changing the password. + /// + ///Do you want to continue with the current password?. + /// + internal static string LongPasswordWarning { + get { + return ResourceManager.GetString("LongPasswordWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to On this page you can select where to store the backup data.. /// @@ -95,5 +106,16 @@ namespace Duplicati.Library.Backend.Strings { return ResourceManager.GetString("PageTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to Your password contains characters that are not alphanumeric. SkyDrive may have unexpected issues, consider changing the password. + /// + ///Do you want to continue with the current password?. + /// + internal static string PasswordCharactersWarning { + get { + return ResourceManager.GetString("PasswordCharactersWarning", resourceCulture); + } + } } } diff --git a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.resx b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.resx index b86366066..ee751c23e 100644 --- a/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.resx +++ b/Duplicati/Library/Backend/SkyDrive/Strings/SkyDriveUI.resx @@ -123,10 +123,20 @@ You must enter a folder to store the backup in + + Your password is more than 16 characters. SkyDrive may have unexpected issues, consider changing the password. + +Do you want to continue with the current password? + On this page you can select where to store the backup data. Backup storage options + + Your password contains characters that are not alphanumeric. SkyDrive may have unexpected issues, consider changing the password. + +Do you want to continue with the current password? + \ No newline at end of file