mirror of https://github.com/Xhofe/alist
				
				
				
			
		
			
				
	
	
		
			364 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			364 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
package onedrive_sharelink
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/tls"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/alist-org/alist/v3/drivers/base"
 | 
						|
	"github.com/alist-org/alist/v3/internal/conf"
 | 
						|
	log "github.com/sirupsen/logrus"
 | 
						|
	"golang.org/x/net/html"
 | 
						|
)
 | 
						|
 | 
						|
// NewNoRedirectClient creates an HTTP client that doesn't follow redirects
 | 
						|
func NewNoRedirectCLient() *http.Client {
 | 
						|
	return &http.Client{
 | 
						|
		Timeout: time.Hour * 48,
 | 
						|
		Transport: &http.Transport{
 | 
						|
			Proxy:           http.ProxyFromEnvironment,
 | 
						|
			TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify},
 | 
						|
		},
 | 
						|
		// Prevent following redirects
 | 
						|
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
 | 
						|
			return http.ErrUseLastResponse
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// getCookiesWithPassword fetches cookies required for authenticated access using the provided password
 | 
						|
func getCookiesWithPassword(link, password string) (string, error) {
 | 
						|
	// Send GET request
 | 
						|
	resp, err := http.Get(link)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
 | 
						|
	// Parse the HTML response
 | 
						|
	doc, err := html.Parse(resp.Body)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	// Initialize variables to store form data
 | 
						|
	var viewstate, eventvalidation, postAction string
 | 
						|
 | 
						|
	// Recursive function to find input fields by their IDs
 | 
						|
	var findInputFields func(*html.Node)
 | 
						|
	findInputFields = func(n *html.Node) {
 | 
						|
		if n.Type == html.ElementNode && n.Data == "input" {
 | 
						|
			for _, attr := range n.Attr {
 | 
						|
				if attr.Key == "id" {
 | 
						|
					switch attr.Val {
 | 
						|
					case "__VIEWSTATE":
 | 
						|
						viewstate = getAttrValue(n, "value")
 | 
						|
					case "__EVENTVALIDATION":
 | 
						|
						eventvalidation = getAttrValue(n, "value")
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if n.Type == html.ElementNode && n.Data == "form" {
 | 
						|
			for _, attr := range n.Attr {
 | 
						|
				if attr.Key == "id" && attr.Val == "inputForm" {
 | 
						|
					postAction = getAttrValue(n, "action")
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		for c := n.FirstChild; c != nil; c = c.NextSibling {
 | 
						|
			findInputFields(c)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	findInputFields(doc)
 | 
						|
 | 
						|
	// Prepare the new URL for the POST request
 | 
						|
	linkParts, err := url.Parse(link)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	newURL := fmt.Sprintf("%s://%s%s", linkParts.Scheme, linkParts.Host, postAction)
 | 
						|
 | 
						|
	// Prepare the request body
 | 
						|
	data := url.Values{
 | 
						|
		"txtPassword":          []string{password},
 | 
						|
		"__EVENTVALIDATION":    []string{eventvalidation},
 | 
						|
		"__VIEWSTATE":          []string{viewstate},
 | 
						|
		"__VIEWSTATEENCRYPTED": []string{""},
 | 
						|
	}
 | 
						|
 | 
						|
	client := &http.Client{
 | 
						|
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
 | 
						|
			return http.ErrUseLastResponse
 | 
						|
		},
 | 
						|
	}
 | 
						|
	// Send the POST request, preventing redirects
 | 
						|
	resp, err = client.PostForm(newURL, data)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	// Extract the desired cookie value
 | 
						|
	cookie := resp.Cookies()
 | 
						|
	var fedAuthCookie string
 | 
						|
	for _, c := range cookie {
 | 
						|
		if c.Name == "FedAuth" {
 | 
						|
			fedAuthCookie = c.Value
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if fedAuthCookie == "" {
 | 
						|
		return "", fmt.Errorf("wrong password")
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("FedAuth=%s;", fedAuthCookie), nil
 | 
						|
}
 | 
						|
 | 
						|
// getAttrValue retrieves the value of the specified attribute from an HTML node
 | 
						|
func getAttrValue(n *html.Node, key string) string {
 | 
						|
	for _, attr := range n.Attr {
 | 
						|
		if attr.Key == key {
 | 
						|
			return attr.Val
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return ""
 | 
						|
}
 | 
						|
 | 
						|
// getHeaders constructs and returns the necessary HTTP headers for accessing the OneDrive share link
 | 
						|
func (d *OnedriveSharelink) getHeaders() (http.Header, error) {
 | 
						|
	header := http.Header{}
 | 
						|
	header.Set("User-Agent", base.UserAgent)
 | 
						|
	header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
 | 
						|
 | 
						|
	// Save current timestamp to d.HeaderTime
 | 
						|
	d.HeaderTime = time.Now().Unix()
 | 
						|
 | 
						|
	if d.ShareLinkPassword == "" {
 | 
						|
		// Create a no-redirect client
 | 
						|
		clientNoDirect := NewNoRedirectCLient()
 | 
						|
		req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		// Set headers for the request
 | 
						|
		req.Header = header
 | 
						|
		answerNoRedirect, err := clientNoDirect.Do(req)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		redirectUrl := answerNoRedirect.Header.Get("Location")
 | 
						|
		log.Debugln("redirectUrl:", redirectUrl)
 | 
						|
		if redirectUrl == "" {
 | 
						|
			return nil, fmt.Errorf("password protected link. Please provide password")
 | 
						|
		}
 | 
						|
		header.Set("Cookie", answerNoRedirect.Header.Get("Set-Cookie"))
 | 
						|
		header.Set("Referer", redirectUrl)
 | 
						|
 | 
						|
		// Extract the host part of the redirect URL and set it as the authority
 | 
						|
		u, err := url.Parse(redirectUrl)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		header.Set("authority", u.Host)
 | 
						|
		return header, nil
 | 
						|
	} else {
 | 
						|
		cookie, err := getCookiesWithPassword(d.ShareLinkURL, d.ShareLinkPassword)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		header.Set("Cookie", cookie)
 | 
						|
		header.Set("Referer", d.ShareLinkURL)
 | 
						|
		header.Set("authority", strings.Split(strings.Split(d.ShareLinkURL, "//")[1], "/")[0])
 | 
						|
		return header, nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// getFiles retrieves the files from the OneDrive share link at the specified path
 | 
						|
func (d *OnedriveSharelink) getFiles(path string) ([]Item, error) {
 | 
						|
	clientNoDirect := NewNoRedirectCLient()
 | 
						|
	req, err := http.NewRequest("GET", d.ShareLinkURL, nil)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	header := req.Header
 | 
						|
	redirectUrl := ""
 | 
						|
	if d.ShareLinkPassword == "" {
 | 
						|
		header.Set("User-Agent", base.UserAgent)
 | 
						|
		header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
 | 
						|
		req.Header = header
 | 
						|
		answerNoRedirect, err := clientNoDirect.Do(req)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		redirectUrl = answerNoRedirect.Header.Get("Location")
 | 
						|
	} else {
 | 
						|
		header = d.Headers
 | 
						|
		req.Header = header
 | 
						|
		answerNoRedirect, err := clientNoDirect.Do(req)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		redirectUrl = answerNoRedirect.Header.Get("Location")
 | 
						|
	}
 | 
						|
	redirectSplitURL := strings.Split(redirectUrl, "/")
 | 
						|
	req.Header = d.Headers
 | 
						|
	downloadLinkPrefix := ""
 | 
						|
	rootFolderPre := ""
 | 
						|
 | 
						|
	// Determine the appropriate URL and root folder based on whether the link is SharePoint
 | 
						|
	if d.IsSharepoint {
 | 
						|
		// update req url
 | 
						|
		req.URL, err = url.Parse(redirectUrl)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		// Get redirectUrl
 | 
						|
		answer, err := clientNoDirect.Do(req)
 | 
						|
		if err != nil {
 | 
						|
			d.Headers, err = d.getHeaders()
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			return d.getFiles(path)
 | 
						|
		}
 | 
						|
		defer answer.Body.Close()
 | 
						|
		re := regexp.MustCompile(`templateUrl":"(.*?)"`)
 | 
						|
		body, err := io.ReadAll(answer.Body)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		template := re.FindString(string(body))
 | 
						|
		template = template[strings.Index(template, "templateUrl\":\"")+len("templateUrl\":\""):]
 | 
						|
		template = template[:strings.Index(template, "?id=")]
 | 
						|
		template = template[:strings.LastIndex(template, "/")]
 | 
						|
		downloadLinkPrefix = template + "/download.aspx?UniqueId="
 | 
						|
		params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		rootFolderPre = params.Get("id")
 | 
						|
	} else {
 | 
						|
		redirectUrlCut := redirectUrl[:strings.LastIndex(redirectUrl, "/")]
 | 
						|
		downloadLinkPrefix = redirectUrlCut + "/download.aspx?UniqueId="
 | 
						|
		params, err := url.ParseQuery(redirectUrl[strings.Index(redirectUrl, "?")+1:])
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		rootFolderPre = params.Get("id")
 | 
						|
	}
 | 
						|
	d.downloadLinkPrefix = downloadLinkPrefix
 | 
						|
	rootFolder, err := url.QueryUnescape(rootFolderPre)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	log.Debugln("rootFolder:", rootFolder)
 | 
						|
	// Extract the relative path up to and including "Documents"
 | 
						|
	relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents"
 | 
						|
 | 
						|
	// URL encode the relative path
 | 
						|
	relativeUrl := url.QueryEscape(relativePath)
 | 
						|
	// Replace underscores and hyphens in the encoded relative path
 | 
						|
	relativeUrl = strings.Replace(relativeUrl, "_", "%5F", -1)
 | 
						|
	relativeUrl = strings.Replace(relativeUrl, "-", "%2D", -1)
 | 
						|
 | 
						|
	// If the path is not the root, append the path to the root folder
 | 
						|
	if path != "/" {
 | 
						|
		rootFolder = rootFolder + path
 | 
						|
	}
 | 
						|
 | 
						|
	// URL encode the full root folder path
 | 
						|
	rootFolderUrl := url.QueryEscape(rootFolder)
 | 
						|
	// Replace underscores and hyphens in the encoded root folder URL
 | 
						|
	rootFolderUrl = strings.Replace(rootFolderUrl, "_", "%5F", -1)
 | 
						|
	rootFolderUrl = strings.Replace(rootFolderUrl, "-", "%2D", -1)
 | 
						|
 | 
						|
	log.Debugln("relativePath:", relativePath, "relativeUrl:", relativeUrl, "rootFolder:", rootFolder, "rootFolderUrl:", rootFolderUrl)
 | 
						|
 | 
						|
	// Construct the GraphQL query with the encoded paths
 | 
						|
	graphqlVar := fmt.Sprintf(`{"query":"query (\n        $listServerRelativeUrl: String!,$renderListDataAsStreamParameters: RenderListDataAsStreamParameters!,$renderListDataAsStreamQueryString: String!\n        )\n      {\n      \n      legacy {\n      \n      renderListDataAsStream(\n      listServerRelativeUrl: $listServerRelativeUrl,\n      parameters: $renderListDataAsStreamParameters,\n      queryString: $renderListDataAsStreamQueryString\n      )\n    }\n      \n      \n  perf {\n    executionTime\n    overheadTime\n    parsingTime\n    queryCount\n    validationTime\n    resolvers {\n      name\n      queryCount\n      resolveTime\n      waitTime\n    }\n  }\n    }","variables":{"listServerRelativeUrl":"%s","renderListDataAsStreamParameters":{"renderOptions":5707527,"allowMultipleValueFilterForTaxonomyFields":true,"addRequiredFields":true,"folderServerRelativeUrl":"%s"},"renderListDataAsStreamQueryString":"@a1=\'%s\'&RootFolder=%s&TryNewExperienceSingle=TRUE"}}`, relativePath, rootFolder, relativeUrl, rootFolderUrl)
 | 
						|
	tempHeader := make(http.Header)
 | 
						|
	for k, v := range d.Headers {
 | 
						|
		tempHeader[k] = v
 | 
						|
	}
 | 
						|
	tempHeader["Content-Type"] = []string{"application/json;odata=verbose"}
 | 
						|
 | 
						|
	client := &http.Client{}
 | 
						|
	postUrl := strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/v2.1/graphql"
 | 
						|
	req, err = http.NewRequest("POST", postUrl, strings.NewReader(graphqlVar))
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	req.Header = tempHeader
 | 
						|
 | 
						|
	resp, err := client.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		d.Headers, err = d.getHeaders()
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		return d.getFiles(path)
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
	var graphqlReq GraphQLRequest
 | 
						|
	json.NewDecoder(resp.Body).Decode(&graphqlReq)
 | 
						|
	log.Debugln("graphqlReq:", graphqlReq)
 | 
						|
	filesData := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row
 | 
						|
	if graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref != "" {
 | 
						|
		nextHref := graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
 | 
						|
		nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
 | 
						|
		log.Debugln("nextHref:", nextHref)
 | 
						|
		filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
 | 
						|
 | 
						|
		listViewXml := graphqlReq.Data.Legacy.RenderListDataAsStream.ViewMetadata.ListViewXml
 | 
						|
		log.Debugln("listViewXml:", listViewXml)
 | 
						|
		renderListDataAsStreamVar := `{"parameters":{"__metadata":{"type":"SP.RenderListDataParameters"},"RenderOptions":1216519,"ViewXml":"REPLACEME","AllowMultipleValueFilterForTaxonomyFields":true,"AddRequiredFields":true}}`
 | 
						|
		listViewXml = strings.Replace(listViewXml, `"`, `\"`, -1)
 | 
						|
		renderListDataAsStreamVar = strings.Replace(renderListDataAsStreamVar, "REPLACEME", listViewXml, -1)
 | 
						|
 | 
						|
		graphqlReqNEW := GraphQLNEWRequest{}
 | 
						|
		postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
 | 
						|
		req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
 | 
						|
		req.Header = tempHeader
 | 
						|
 | 
						|
		resp, err := client.Do(req)
 | 
						|
		if err != nil {
 | 
						|
			d.Headers, err = d.getHeaders()
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			return d.getFiles(path)
 | 
						|
		}
 | 
						|
		defer resp.Body.Close()
 | 
						|
		json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
 | 
						|
		for graphqlReqNEW.ListData.NextHref != "" {
 | 
						|
			graphqlReqNEW = GraphQLNEWRequest{}
 | 
						|
			postUrl = strings.Join(redirectSplitURL[:len(redirectSplitURL)-3], "/") + "/_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream" + nextHref
 | 
						|
			req, _ = http.NewRequest("POST", postUrl, strings.NewReader(renderListDataAsStreamVar))
 | 
						|
			req.Header = tempHeader
 | 
						|
			resp, err := client.Do(req)
 | 
						|
			if err != nil {
 | 
						|
				d.Headers, err = d.getHeaders()
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
				return d.getFiles(path)
 | 
						|
			}
 | 
						|
			defer resp.Body.Close()
 | 
						|
			json.NewDecoder(resp.Body).Decode(&graphqlReqNEW)
 | 
						|
			nextHref = graphqlReqNEW.ListData.NextHref + "&@a1=REPLACEME&TryNewExperienceSingle=TRUE"
 | 
						|
			nextHref = strings.Replace(nextHref, "REPLACEME", "%27"+relativeUrl+"%27", -1)
 | 
						|
			filesData = append(filesData, graphqlReqNEW.ListData.Row...)
 | 
						|
		}
 | 
						|
		filesData = append(filesData, graphqlReqNEW.ListData.Row...)
 | 
						|
	} else {
 | 
						|
		filesData = append(filesData, graphqlReq.Data.Legacy.RenderListDataAsStream.ListData.Row...)
 | 
						|
	}
 | 
						|
	return filesData, nil
 | 
						|
}
 |