github.com/artpar/rclone@v1.67.3/backend/webdav/odrvcookie/fetch.go (about)

     1  // Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint
     2  package odrvcookie
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"encoding/xml"
     8  	"fmt"
     9  	"html/template"
    10  	"net/http"
    11  	"net/http/cookiejar"
    12  	"net/url"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/artpar/rclone/fs"
    17  	"github.com/artpar/rclone/fs/fshttp"
    18  	"golang.org/x/net/publicsuffix"
    19  )
    20  
    21  // CookieAuth hold the authentication information
    22  // These are username and password as well as the authentication endpoint
    23  type CookieAuth struct {
    24  	user     string
    25  	pass     string
    26  	endpoint string
    27  }
    28  
    29  // CookieResponse contains the requested cookies
    30  type CookieResponse struct {
    31  	RtFa    http.Cookie
    32  	FedAuth http.Cookie
    33  }
    34  
    35  // SharepointSuccessResponse holds a response from a successful microsoft login
    36  type SharepointSuccessResponse struct {
    37  	XMLName xml.Name            `xml:"Envelope"`
    38  	Body    SuccessResponseBody `xml:"Body"`
    39  }
    40  
    41  // SuccessResponseBody is the body of a successful response, it holds the token
    42  type SuccessResponseBody struct {
    43  	XMLName xml.Name
    44  	Type    string    `xml:"RequestSecurityTokenResponse>TokenType"`
    45  	Created time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Created"`
    46  	Expires time.Time `xml:"RequestSecurityTokenResponse>Lifetime>Expires"`
    47  	Token   string    `xml:"RequestSecurityTokenResponse>RequestedSecurityToken>BinarySecurityToken"`
    48  }
    49  
    50  // SharepointError holds an error response microsoft login
    51  type SharepointError struct {
    52  	XMLName xml.Name          `xml:"Envelope"`
    53  	Body    ErrorResponseBody `xml:"Body"`
    54  }
    55  
    56  func (e *SharepointError) Error() string {
    57  	return fmt.Sprintf("%s: %s (%s)", e.Body.FaultCode, e.Body.Reason, e.Body.Detail)
    58  }
    59  
    60  // ErrorResponseBody contains the body of an erroneous response
    61  type ErrorResponseBody struct {
    62  	XMLName   xml.Name
    63  	FaultCode string `xml:"Fault>Code>Subcode>Value"`
    64  	Reason    string `xml:"Fault>Reason>Text"`
    65  	Detail    string `xml:"Fault>Detail>error>internalerror>text"`
    66  }
    67  
    68  // reqString is a template that gets populated with the user data in order to retrieve a "BinarySecurityToken"
    69  const reqString = `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
    70  xmlns:a="http://www.w3.org/2005/08/addressing"
    71  xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    72  <s:Header>
    73  <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
    74  <a:ReplyTo>
    75  <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    76  </a:ReplyTo>
    77  <a:To s:mustUnderstand="1">{{ .SPTokenURL }}</a:To>
    78  <o:Security s:mustUnderstand="1"
    79   xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    80  <o:UsernameToken>
    81    <o:Username>{{ .Username }}</o:Username>
    82    <o:Password>{{ .Password }}</o:Password>
    83  </o:UsernameToken>
    84  </o:Security>
    85  </s:Header>
    86  <s:Body>
    87  <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
    88  <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
    89    <a:EndpointReference>
    90      <a:Address>{{ .Address }}</a:Address>
    91    </a:EndpointReference>
    92  </wsp:AppliesTo>
    93  <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
    94  <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
    95  <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
    96  </t:RequestSecurityToken>
    97  </s:Body>
    98  </s:Envelope>`
    99  
   100  // New creates a new CookieAuth struct
   101  func New(pUser, pPass, pEndpoint string) CookieAuth {
   102  	retStruct := CookieAuth{
   103  		user:     pUser,
   104  		pass:     pPass,
   105  		endpoint: pEndpoint,
   106  	}
   107  
   108  	return retStruct
   109  }
   110  
   111  // Cookies creates a CookieResponse. It fetches the auth token and then
   112  // retrieves the Cookies
   113  func (ca *CookieAuth) Cookies(ctx context.Context) (*CookieResponse, error) {
   114  	tokenResp, err := ca.getSPToken(ctx)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	return ca.getSPCookie(tokenResp)
   119  }
   120  
   121  func (ca *CookieAuth) getSPCookie(conf *SharepointSuccessResponse) (*CookieResponse, error) {
   122  	spRoot, err := url.Parse(ca.endpoint)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("error while constructing endpoint URL: %w", err)
   125  	}
   126  
   127  	u, err := url.Parse(spRoot.Scheme + "://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
   128  	if err != nil {
   129  		return nil, fmt.Errorf("error while constructing login URL: %w", err)
   130  	}
   131  
   132  	// To authenticate with davfs or anything else we need two cookies (rtFa and FedAuth)
   133  	// In order to get them we use the token we got earlier and a cookieJar
   134  	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	client := &http.Client{
   140  		Jar: jar,
   141  	}
   142  
   143  	// Send the previously acquired Token as a Post parameter
   144  	if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Body.Token)); err != nil {
   145  		return nil, fmt.Errorf("error while grabbing cookies from endpoint: %w", err)
   146  	}
   147  
   148  	cookieResponse := CookieResponse{}
   149  	for _, cookie := range jar.Cookies(u) {
   150  		if (cookie.Name == "rtFa") || (cookie.Name == "FedAuth") {
   151  			switch cookie.Name {
   152  			case "rtFa":
   153  				cookieResponse.RtFa = *cookie
   154  			case "FedAuth":
   155  				cookieResponse.FedAuth = *cookie
   156  			}
   157  		}
   158  	}
   159  	return &cookieResponse, nil
   160  }
   161  
   162  var spTokenURLMap = map[string]string{
   163  	"com": "https://login.microsoftonline.com",
   164  	"cn":  "https://login.chinacloudapi.cn",
   165  	"us":  "https://login.microsoftonline.us",
   166  	"de":  "https://login.microsoftonline.de",
   167  }
   168  
   169  func getSPTokenURL(endpoint string) (string, error) {
   170  	spRoot, err := url.Parse(endpoint)
   171  	if err != nil {
   172  		return "", fmt.Errorf("error while parse endpoint: %w", err)
   173  	}
   174  	domains := strings.Split(spRoot.Host, ".")
   175  	tld := domains[len(domains)-1]
   176  	spTokenURL, ok := spTokenURLMap[tld]
   177  	if !ok {
   178  		return "", fmt.Errorf("error while get SPToken url, unsupported tld: %s", tld)
   179  	}
   180  	return spTokenURL + "/extSTS.srf", nil
   181  }
   182  
   183  func (ca *CookieAuth) getSPToken(ctx context.Context) (conf *SharepointSuccessResponse, err error) {
   184  	spTokenURL, err := getSPTokenURL(ca.endpoint)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	reqData := map[string]interface{}{
   189  		"Username":   ca.user,
   190  		"Password":   ca.pass,
   191  		"Address":    ca.endpoint,
   192  		"SPTokenURL": spTokenURL,
   193  	}
   194  
   195  	t := template.Must(template.New("authXML").Parse(reqString))
   196  
   197  	buf := &bytes.Buffer{}
   198  	if err := t.Execute(buf, reqData); err != nil {
   199  		return nil, fmt.Errorf("error while filling auth token template: %w", err)
   200  	}
   201  
   202  	// Create and execute the first request which returns an auth token for the sharepoint service
   203  	// With this token we can authenticate on the login page and save the returned cookies
   204  	req, err := http.NewRequestWithContext(ctx, "POST", spTokenURL, buf)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	client := fshttp.NewClient(ctx)
   210  	resp, err := client.Do(req)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("error while logging in to endpoint: %w", err)
   213  	}
   214  	defer fs.CheckClose(resp.Body, &err)
   215  
   216  	respBuf := bytes.Buffer{}
   217  	_, err = respBuf.ReadFrom(resp.Body)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	s := respBuf.Bytes()
   222  
   223  	conf = &SharepointSuccessResponse{}
   224  	err = xml.Unmarshal(s, conf)
   225  	if conf.Body.Token == "" {
   226  		// xml Unmarshal won't fail if the response doesn't contain a token
   227  		// However, the token will be empty
   228  		sErr := &SharepointError{}
   229  
   230  		errSErr := xml.Unmarshal(s, sErr)
   231  		if errSErr == nil {
   232  			return nil, sErr
   233  		}
   234  	}
   235  
   236  	if err != nil {
   237  		return nil, fmt.Errorf("error while reading endpoint response: %w", err)
   238  	}
   239  	return
   240  }