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