Files
duplicati/Duplicati/Library/Backend/SkyDrive/SkyDriveSession.cs
T

1020 lines
44 KiB
C#

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
{
/// <summary>
/// Class that encapsulates the http protocol specifics for a SkyDrive request
/// </summary>
public class SkyDriveSession : IDisposable
{
/// <summary>
/// The main url for the SkyDrive service, used to query for root folders
/// </summary>
private static string SKYDRIVE_URL = "https://docs.live.net/SkyDocsService.svc";
/// <summary>
/// The url used to get a the url for authenticating the Passport
/// </summary>
private const string PASSPORT_LIST_URL = "https://nexus.passport.com/rdr/pprdr.asp";
/// <summary>
/// The name of the http header that contains the login url
/// </summary>
private const string PASSPORT_URL_LIST_HEADER = "PassportURLs";
/// <summary>
/// A regular expression for extracting the Passport authentication url from the header value
/// </summary>
private static readonly Regex PASSPORT_LOGIN_MATCH = new Regex("DALogin=(?<url>[^,]+)");
/// <summary>
/// A regular expression for reading the token generation status
/// </summary>
private static readonly Regex LOGIN_STATUS_MATCH = new Regex("da-status=(?<status>[^,]+)");
/// <summary>
/// A regular expression for reading the actual token result
/// </summary>
private static readonly Regex LOGIN_TOKEN_MATCH = new Regex("from-PP=(?<token>[^,]+)");
/// <summary>
/// A regular expressing for detecting a Passport nonce string, which can then be re-used with new nonce values
/// </summary>
private static readonly Regex NONCE_MATCH = new Regex("Passport1.4[^ ]* ct=(?<nonce>[^,]+).*");
/// <summary>
/// A regular expression for identifying a CID as part of a query string
/// </summary>
private static readonly Regex CID_FINDER = new Regex("cid=(?<cid>[^&]+)");
/// <summary>
/// A template for generating Passport authorization requests
/// </summary>
private const string LOGIN_HEADER_TEMPLATE = "Passport1.4 sign-in={0},pwd={1},OrgVerb={2},OrgUrl={3},{4}";
/// <summary>
/// A template for generating Passport authorization responses
/// </summary>
private const string TOKEN_FORMAT_TEMPLATE = "Passport1.4 from-PP={0}";
/// <summary>
/// A template for creating a WLID authorization response
/// </summary>
private const string WLID_TOKEN_FORMAT_TEMPLATE = "WLID1.0 {0}";
/// <summary>
/// The name of the header that contains the authentication nonce
/// </summary>
private const string NONCE_HEADER = "WWW-Authenticate";
/// <summary>
/// The name of the redirect header
/// </summary>
private const string REDIR_LOCATION_HEADER = "Location";
/// <summary>
/// The name of the header that contains authorization information
/// </summary>
private const string AUTHORIZATION_HEADER = "Authorization";
/// <summary>
/// The name of the header that contains the authorization token
/// </summary>
private const string AUTHENTIFICATION_RESULT_HEADER = "Authentication-Info";
/// <summary>
/// An xml SOAP request for getting basic account information
/// </summary>
private readonly string SOAP_GETACCOUNT_INFO_REQUEST = "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
"<s:Body><GetWebAccountInfoRequest xmlns=\"http://schemas.microsoft.com/clouddocuments\">" +
"<BaseRequest xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<ClientAppId>" + USER_AGENT + "</ClientAppId><Market>en-US</Market>" +
"<SkyDocsServiceVersion>v1.0</SkyDocsServiceVersion></BaseRequest>" +
"<GetReadWriteLibrariesOnly>true</GetReadWriteLibrariesOnly></GetWebAccountInfoRequest></s:Body></s:Envelope>";
/// <summary>
/// A template for requesting file creation
/// </summary>
private const string CREATE_FILE_TEMPLATE = "<?xml version=\"1.0\" encoding=\"utf-8\"?><entry xmlns:live=\"http://api.live.com/schemas\" xmlns=\"http://www.w3.org/2005/Atom\"><title>{0}</title><live:type>Document</live:type><live:resolveNameConflict>false</live:resolveNameConflict></entry>";
/// <summary>
/// A template for requesting subfolder creation
/// </summary>
private const string CREATE_SUBFOLDER_TEMPLATE = "<?xml version=\"1.0\" encoding=\"utf-8\"?><entry xmlns:live=\"http://api.live.com/schemas\" xmlns=\"http://www.w3.org/2005/Atom\"><title>{0}</title><live:type>Folder</live:type><live:resolveNameConflict>false</live:resolveNameConflict></entry>";
/// <summary>
/// A template for requesting root folder creation
/// </summary>
private const string CREATE_ROOTFOLDER_TEMPLATE = "<?xml version=\"1.0\" encoding=\"utf-8\"?><entry xmlns:live=\"http://api.live.com/schemas\" xmlns=\"http://www.w3.org/2005/Atom\"><title>{0}</title><live:type>Library</live:type><live:category>Document</live:category><live:sharingLevel>Private</live:sharingLevel></entry>";
/// <summary>
/// The user agent applied to all requests
/// </summary>
private readonly static string USER_AGENT = "Duplicati SkyDrive client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
/// <summary>
/// The Passport username
/// </summary>
private string m_username;
/// <summary>
/// The Passport password
/// </summary>
private string m_password;
/// <summary>
/// The name of the root folder
/// </summary>
private string m_rootfolder;
/// <summary>
/// The prefix (aka subfolder) path
/// </summary>
private string m_prefix;
/// <summary>
/// The url used to authenticate against
/// </summary>
private string m_loginUrl;
/// <summary>
/// The CID of the folder this session is bound to
/// </summary>
private string m_folderCID;
/// <summary>
/// The passport token used to authticate WebDAV requests
/// </summary>
private string m_mainpassporttoken;
/// <summary>
/// The WLID token used to authenticate requests
/// </summary>
private string m_mainwlidtoken;
/// <summary>
/// The template used to generate nonce values
/// </summary>
private string m_noncetemplate;
/// <summary>
/// The CID of the user
/// </summary>
private string m_userCID;
/// <summary>
/// A list of filenames, used to obtain the CID of files
/// </summary>
private List<IFileEntry> m_filenameCache;
/// <summary>
/// A lookup table with all WEBDAV folders, key is the root foldername, value is the WEBDAV url
/// </summary>
private Dictionary<string, string> m_webdav_rootFolderCache = null;
/// <summary>
/// A lock used to prevent loading the loginUrl multiple times
/// </summary>
private static object _lock = new object();
/// <summary>
/// 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
/// </summary>
private static string _loginUrl = null;
/// <summary>
/// Initializes a new instance of the <see cref="SkyDriveSession"/> class.
/// </summary>
/// <param name="username">The Passport username.</param>
/// <param name="password">The Passport password.</param>
/// <param name="rootfolder">The rootfolder.</param>
/// <param name="prefix">The prefix (aka subfolder path).</param>
/// <param name="createFolders">if set to <c>true</c> missing folders will be created.</param>
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);
}
/// <summary>
/// Gets the login URL.
/// </summary>
/// <returns>The login url</returns>
private string GetLoginUrl()
{
HttpWebRequest getUrls = (HttpWebRequest)WebRequest.Create(PASSPORT_LIST_URL);
getUrls.UserAgent = USER_AGENT;
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(getUrls);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
{
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;
}
}
/// <summary>
/// Reads the user CID, generates the noncetemplate and populates the WEBDAV folder list
/// </summary>
public void SetupConnection()
{
m_webdav_rootFolderCache = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
HttpWebRequest listRequest = CreateGetWebAccountInfoRequest();
//We use the first request to extract the nonce template
if (string.IsNullOrEmpty(m_noncetemplate))
{
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(listRequest);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
{
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();
Utility.AsyncHttpRequest areq2 = new Utility.AsyncHttpRequest(listRequest);
using (HttpWebResponse resp = (HttpWebResponse)areq2.GetResponse())
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);
}
/// <summary>
/// Gets the folder CID for the folder this session is bound to.
/// </summary>
/// <param name="rootfolder">The rootfolder.</param>
/// <param name="prefix">The prefix.</param>
/// <param name="createFolder">if set to <c>true</c> missing folders will be created.</param>
/// <returns>The folder CID</returns>
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;
}
/// <summary>
/// Creates a WebAccountInfo request used to read basic account information.
/// </summary>
/// <returns>A configured request</returns>
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);
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(soapRequest);
using (System.IO.Stream s = areq.GetRequestStream())
s.Write(data, 0, data.Length);
return soapRequest;
}
/// <summary>
/// Gets the root URL onto which WLID requests can be made.
/// </summary>
/// <value>The root URL.</value>
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));
}
}
/// <summary>
/// Gets the folder CID for the folder this session is bound to.
/// </summary>
/// <value>The folder CID.</value>
public string FolderCID
{
get { return m_folderCID; }
}
/// <summary>
/// Gets the URL for the folder this session is bound to.
/// </summary>
/// <value>The folder URL.</value>
public string FolderUrl
{
get { return GetFolderUrl(m_folderCID); }
}
/// <summary>
/// Gets the URL for a specific folder.
/// </summary>
/// <param name="folderCID">The folder CID.</param>
/// <returns></returns>
private string GetFolderUrl(string folderCID)
{
return String.Format("{0}/Folders('{1}')", RootUrl, folderCID);
}
/// <summary>
/// Gets the URL for a specific file.
/// </summary>
/// <param name="fileCID">The file CID.</param>
/// <returns></returns>
private string GetFileUrl(string fileCID)
{
return String.Format("{0}/Files('{1}')", RootUrl, fileCID);
}
/// <summary>
/// Creates a nonce using the template.
/// </summary>
/// <param name="verb">The http verb.</param>
/// <param name="url">The http request URL.</param>
/// <returns>The generated nonce</returns>
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));
}
/// <summary>
/// Sets the WLID authentication token on the request.
/// </summary>
/// <param name="req">The req to assign the WLID token to</param>
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;
}
/// <summary>
/// Generates and sets the Passport authentication token on the request.
/// </summary>
/// <param name="req">The request to set the token on.</param>
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;
}
/// <summary>
/// Gets a Passport authentication token for a verb and url.
/// </summary>
/// <param name="verb">The http verb.</param>
/// <param name="url">The http request URL.</param>
/// <returns></returns>
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));
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(getToken);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
{
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;
}
}
/// <summary>
/// Converts the Passport token to a WLID token.
/// </summary>
/// <param name="passporttoken">The passport token.</param>
/// <returns>The WLID token</returns>
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);
}
/// <summary>
/// Creates a new folder.
/// </summary>
/// <param name="name">The name of the folder to create.</param>
/// <param name="parentCid">The parent cid. If this is null, a new root folder will be created.</param>
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;
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.IO.Stream s = areq.GetRequestStream())
s.Write(data, 0, data.Length);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
{
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<IFileEntry> ParseXmlAtomDocument(XmlDocument doc)
{
List<IFileEntry> results = new List<IFileEntry>();
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;
}
/// <summary>
/// Lists the items in a specific folder.
/// </summary>
/// <param name="parentCid">The parent folder CID. If this is null, the root folder items are listed</param>
/// <returns>A list of elements in the folder</returns>
public List<IFileEntry> 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();
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
using (System.IO.Stream s = resp.GetResponseStream())
doc.Load(s);
List<IFileEntry> 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<IFileEntry>(results);
return results;
}
/// <summary>
/// Returns a WEBDAV url for a given file
/// </summary>
/// <param name="remotename">The name of the file to generate an URL for</param>
/// <returns>The WEBDAV URL for the file</returns>
private string GetWebDavUrl(string remotename)
{
if (!m_webdav_rootFolderCache.ContainsKey(m_rootfolder))
throw new FolderMissingException();
return m_webdav_rootFolderCache[m_rootfolder] + m_prefix + remotename;
}
/// <summary>
/// Looks for a file and returns the file information, including the CID
/// </summary>
/// <param name="name">The name of the file to look for</param>
/// <returns>The file information or null</returns>
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;
}
/// <summary>
/// Looks for a file and returns the file information, including the CID
/// </summary>
/// <param name="name">The name of the file to look for</param>
/// <returns>The file information or null</returns>
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;
}
/// <summary>
/// Deletes a file.
/// </summary>
/// <param name="remotename">The remote file name.</param>
/// <returns>The server response</returns>
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);
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
{
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;
}
}
/// <summary>
/// Uploads a file.
/// </summary>
/// <param name="remotename">The remote file name.</param>
/// <param name="data">The data that represents the file.</param>
/// <returns>The server response</returns>
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;
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 { }
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.IO.Stream s = areq.GetRequestStream())
{
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)areq.GetResponse())
{
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;
SetWlidAuthenticationToken(req);
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
using (System.IO.Stream s = areq.GetRequestStream())
Utility.Utility.CopyStream(data, s);
using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse())
{
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);
}
}
}
/// <summary>
/// Downloads a file.
/// </summary>
/// <param name="remotename">The remote file name.</param>
/// <returns>The server response</returns>
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;
req.UserAgent = USER_AGENT;
req.Headers[AUTHORIZATION_HEADER] = m_mainpassporttoken;
Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
HttpWebResponse resp = (HttpWebResponse)areq.GetResponse();
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
/// <summary>
/// Utility function to test if password is so long that it can cause problems
/// </summary>
/// <param name="password">The password to validate</param>
/// <returns>True if the password is too long, false otherwise</returns>
public static bool IsPasswordTooLong(string password)
{
if (string.IsNullOrEmpty(password))
return false;
return password.Length > 16;
}
/// <summary>
/// Utility function to test if the password contains non-standard characters
/// </summary>
/// <param name="password">The password to validate</param>
/// <returns>True if the password has invalid chars, false otherwise</returns>
public static bool HasInvalidChars(string password)
{
bool invalidChars = false;
foreach (char c in password)
invalidChars |= !("!&()[~@".IndexOf(c) >= 0 || char.IsLetterOrDigit(c));
return invalidChars;
}
}
}