From 8fe3037301f580b141e3ee32ba00ab443c618d9f Mon Sep 17 00:00:00 2001 From: hensur Date: Mon, 9 Apr 2018 10:05:43 +0200 Subject: [PATCH] webdav: support SharePoint cookie authentication This enables the use of the SharePoint webdav endpoint provided by OneDrive for Business or Office365 Education Accounts. It enables unverified accounts to be accessed with rclone via webdav as it isn't possible through the normal onedrive backend. This integrates the https://github.com/hensur/onedrive-cookie-test package to fetch the required cookies to authorize against the SharePoint webdav endpoint. --- backend/webdav/odrvcookie/fetch.go | 186 +++++++++++++++++++++++++++++ backend/webdav/webdav.go | 22 +++- docs/content/webdav.md | 45 ++++++- 3 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 backend/webdav/odrvcookie/fetch.go diff --git a/backend/webdav/odrvcookie/fetch.go b/backend/webdav/odrvcookie/fetch.go new file mode 100644 index 000000000..2a7a3ee27 --- /dev/null +++ b/backend/webdav/odrvcookie/fetch.go @@ -0,0 +1,186 @@ +// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint +package odrvcookie + +import ( + "bytes" + "encoding/xml" + "html/template" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + "time" + + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/fshttp" + "github.com/pkg/errors" + "golang.org/x/net/publicsuffix" +) + +// CookieAuth hold the authentication information +// These are username and password as well as the authentication endpoint +type CookieAuth struct { + user string + pass string + endpoint string +} + +// CookieResponse contains the requested cookies +type CookieResponse struct { + RtFa http.Cookie + FedAuth http.Cookie +} + +// SuccessResponse hold a response from the sharepoint webdav +type SuccessResponse struct { + XMLName xml.Name `xml:"Envelope"` + Succ SuccessResponseBody `xml:"Body"` +} + +// SuccessResponseBody is the body of a success response, it holds the token +type SuccessResponseBody struct { + XMLName xml.Name + Type string `xml:"RequestSecurityTokenResponse>TokenType"` + Created time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Created"` + Expires time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Expires"` + Token string `xml:"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken"` +} + +// reqString is a template that gets populated with the user data in order to retrieve a "BinarySecurityToken" +const reqString = ` + +http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue + +http://www.w3.org/2005/08/addressing/anonymous + +https://login.microsoftonline.com/extSTS.srf + + + {{ .Username }} + {{ .Password }} + + + + + + + + {{ .Address }} + + +http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +http://schemas.xmlsoap.org/ws/2005/02/trust/Issue +urn:oasis:names:tc:SAML:1.0:assertion + + +` + +// New creates a new CookieAuth struct +func New(pUser, pPass, pEndpoint string) CookieAuth { + retStruct := CookieAuth{ + user: pUser, + pass: pPass, + endpoint: pEndpoint, + } + + return retStruct +} + +// Cookies creates a CookieResponse. It fetches the auth token and then +// retrieves the Cookies +func (ca *CookieAuth) Cookies() (*CookieResponse, error) { + tokenResp, err := ca.getSPToken() + if err != nil { + return nil, err + } + return ca.getSPCookie(tokenResp) +} + +func (ca *CookieAuth) getSPCookie(conf *SuccessResponse) (*CookieResponse, error) { + spRoot, err := url.Parse(ca.endpoint) + if err != nil { + return nil, errors.Wrap(err, "Error while contructing endpoint URL") + } + + u, err := url.Parse("https://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0") + if err != nil { + return nil, errors.Wrap(err, "Error while constructing login URL") + } + + // To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth) + // In order to get them we use the token we got earlier and a cookieJar + jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + if err != nil { + return nil, err + } + + client := &http.Client{ + Jar: jar, + } + + // Send the previously aquired Token as a Post parameter + if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Succ.Token)); err != nil { + return nil, errors.Wrap(err, "Error while grabbing cookies from endpoint: %v") + } + + cookieResponse := CookieResponse{} + for _, cookie := range jar.Cookies(u) { + if (cookie.Name == "rtFa") || (cookie.Name == "FedAuth") { + switch cookie.Name { + case "rtFa": + cookieResponse.RtFa = *cookie + case "FedAuth": + cookieResponse.FedAuth = *cookie + } + } + } + return &cookieResponse, nil +} + +func (ca *CookieAuth) getSPToken() (conf *SuccessResponse, err error) { + reqData := map[string]interface{}{ + "Username": ca.user, + "Password": ca.pass, + "Address": ca.endpoint, + } + + t := template.Must(template.New("authXML").Parse(reqString)) + + buf := &bytes.Buffer{} + if err := t.Execute(buf, reqData); err != nil { + return nil, errors.Wrap(err, "Error while filling auth token template") + } + + // Create and execute the first request which returns an auth token for the sharepoint service + // With this token we can authenticate on the login page and save the returned cookies + req, err := http.NewRequest("POST", "https://login.microsoftonline.com/extSTS.srf", buf) + if err != nil { + return nil, err + } + + client := fshttp.NewClient(fs.Config) + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "Error while logging in to endpoint") + } + defer fs.CheckClose(resp.Body, &err) + + respBuf := bytes.Buffer{} + _, err = respBuf.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + s := respBuf.Bytes() + + conf = &SuccessResponse{} + err = xml.Unmarshal(s, conf) + if err != nil { + // FIXME: Try to parse with FailedResponse struct (check for server error code) + return nil, errors.Wrap(err, "Error while reading endpoint response") + } + + return +} diff --git a/backend/webdav/webdav.go b/backend/webdav/webdav.go index 5ffb9d4b4..9daeb995b 100644 --- a/backend/webdav/webdav.go +++ b/backend/webdav/webdav.go @@ -29,6 +29,7 @@ import ( "time" "github.com/ncw/rclone/backend/webdav/api" + "github.com/ncw/rclone/backend/webdav/odrvcookie" "github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs/config" "github.com/ncw/rclone/fs/config/obscure" @@ -70,6 +71,9 @@ func init() { }, { Value: "owncloud", Help: "Owncloud", + }, { + Value: "sharepoint", + Help: "Sharepoint", }, { Value: "other", Help: "Other site/service or software", @@ -290,7 +294,10 @@ func NewFs(name, root string) (fs.Fs, error) { CanHaveEmptyDirectories: true, }).Fill(f) f.srv.SetErrorHandler(errorHandler) - f.setQuirks(vendor) + err = f.setQuirks(vendor) + if err != nil { + return nil, err + } if root != "" { // Check to see if the root actually an existing file @@ -315,7 +322,7 @@ func NewFs(name, root string) (fs.Fs, error) { } // setQuirks adjusts the Fs for the vendor passed in -func (f *Fs) setQuirks(vendor string) { +func (f *Fs) setQuirks(vendor string) error { if vendor == "" { vendor = "other" } @@ -328,6 +335,16 @@ func (f *Fs) setQuirks(vendor string) { case "nextcloud": f.precision = time.Second f.useOCMtime = true + case "sharepoint": + // To mount sharepoint, two Cookies are required + // They have to be set instead of BasicAuth + f.srv.RemoveHeader("Authorization") // We don't need this Header if using cookies + spCk := odrvcookie.New(f.user, f.pass, f.endpointURL) + spCookies, err := spCk.Cookies() + if err != nil { + return err + } + f.srv.SetCookie(&spCookies.FedAuth, &spCookies.RtFa) case "other": default: fs.Debugf(f, "Unknown vendor %q", vendor) @@ -337,6 +354,7 @@ func (f *Fs) setQuirks(vendor string) { if !f.canStream { f.features.PutStream = nil } + return nil } // Return an Object from a path diff --git a/docs/content/webdav.md b/docs/content/webdav.md index 275e3fc21..7a314d1ae 100644 --- a/docs/content/webdav.md +++ b/docs/content/webdav.md @@ -82,7 +82,9 @@ Choose a number from below, or type in your own value \ "nextcloud" 2 / Owncloud \ "owncloud" - 3 / Other site/service or software + 3 / Sharepoint + \ "sharepoint" + 4 / Other site/service or software \ "other" vendor> 1 User name @@ -171,3 +173,44 @@ If you are using `put.io` with `rclone mount` then use the mount. For more help see [the put.io webdav docs](http://help.put.io/apps-and-integrations/ftp-and-webdav). + +## Sharepoint ## + +Can be used with Sharepoint provided by OneDrive for Business +or Office365 Education Accounts. +This feature is only needed for a few of these Accounts, +mostly Office365 Education ones. These accounts are sometimes not +verified by the domain owner [github#1975](https://github.com/ncw/rclone/issues/1975) + +This means that these accounts can't be added using the official +API (other Accounts should work with the "onedrive" option). However, +it is possible to access them using webdav. + +To use a sharepoint remote with rclone, add it like this: +First, you need to get your remote's URL: + +- Go [here](https://onedrive.live.com/about/en-us/signin/) + to open your OneDrive or to sign in +- Now take a look at your address bar, the URL should look like this: + `https://[YOUR-DOMAIN]-my.sharepoint.com/personal/[YOUR-EMAIL]/_layouts/15/onedrive.aspx` + +You'll only need this URL upto the email address. After that, you'll +most likely want to add "/Documents". That subdirectory contains +the actual data stored on your OneDrive. + +Add the remote to rclone like this: +Configure the `url` as `https://[YOUR-DOMAIN]-my.sharepoint.com/personal/[YOUR-EMAIL]/Documents` +and use your normal account email and password for `user` and `pass`. +If you have 2FA enabled, you have to generate an app password. +Set the `vendor` to `sharepoint`. + +Your config file should look like this: + +``` +[sharepoint] +type = webdav +url = https://[YOUR-DOMAIN]-my.sharepoint.com/personal/[YOUR-EMAIL]/Documents +vendor = other +user = YourEmailAddress +pass = encryptedpassword +``` \ No newline at end of file