feat: add webdav driver

This commit is contained in:
Noah Hsu
2022-09-04 22:34:54 +08:00
parent 778cee4cdf
commit 3dd4fbd76d
25 changed files with 2675 additions and 1 deletions

130
drivers/webdav/driver.go Normal file
View File

@ -0,0 +1,130 @@
package webdav
import (
"context"
"net/http"
"os"
"path"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/gowebdav"
"github.com/alist-org/alist/v3/pkg/utils"
)
type WebDav struct {
model.Storage
Addition
client *gowebdav.Client
cron *cron.Cron
}
func (d *WebDav) Config() driver.Config {
return config
}
func (d *WebDav) GetAddition() driver.Additional {
return d.Addition
}
func (d *WebDav) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
err = d.setClient()
if err == nil {
d.cron = cron.NewCron(time.Hour * 12)
d.cron.Do(func() {
_ = d.setClient()
})
}
return err
}
func (d *WebDav) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil
}
func (d *WebDav) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.client.ReadDir(dir.GetPath())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src os.FileInfo) (model.Obj, error) {
return &model.Object{
Name: src.Name(),
Size: src.Size(),
Modified: src.ModTime(),
IsFolder: src.IsDir(),
}, nil
})
}
//func (d *WebDav) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *WebDav) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
callback := func(r *http.Request) {
if args.Header.Get("Range") != "" {
r.Header.Set("Range", args.Header.Get("Range"))
}
if args.Header.Get("If-Range") != "" {
r.Header.Set("If-Range", args.Header.Get("If-Range"))
}
}
reader, header, err := d.client.ReadStream(file.GetPath(), callback)
if err != nil {
return nil, err
}
link := &model.Link{Data: reader}
if header.Get("Content-Range") != "" {
link.Status = 206
link.Header = header
}
return link, nil
}
func (d *WebDav) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName), 0644)
}
func (d *WebDav) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()), true)
}
func (d *WebDav) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName), true)
}
func (d *WebDav) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.client.Copy(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()), true)
}
func (d *WebDav) Remove(ctx context.Context, obj model.Obj) error {
return d.client.RemoveAll(obj.GetPath())
}
func (d *WebDav) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
callback := func(r *http.Request) {
r.Header.Set("Content-Type", stream.GetMimetype())
r.ContentLength = stream.GetSize()
}
err := d.client.WriteStream(path.Join(dstDir.GetPath(), stream.GetName()), stream, 0644, callback)
return err
}
func (d *WebDav) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*WebDav)(nil)

29
drivers/webdav/meta.go Normal file
View File

@ -0,0 +1,29 @@
package webdav
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Vendor string `json:"vendor" type:"select" options:"sharepoint,other" default:"other"`
Address string `json:"address" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
driver.RootPath
}
var config = driver.Config{
Name: "WebDav",
LocalSort: true,
OnlyLocal: true,
DefaultRoot: "/",
}
func New() driver.Driver {
return &WebDav{}
}
func init() {
op.RegisterDriver(config, New)
}

View File

@ -0,0 +1,46 @@
package odrvcookie
import (
"net/http"
"github.com/alist-org/alist/v3/pkg/cookie"
)
//type SpCookie struct {
// Cookie string
// expire time.Time
//}
//
//func (sp SpCookie) IsExpire() bool {
// return time.Now().After(sp.expire)
//}
//
//var cookiesMap = struct {
// sync.Mutex
// m map[string]*SpCookie
//}{m: make(map[string]*SpCookie)}
func GetCookie(username, password, siteUrl string) (string, error) {
//cookiesMap.Lock()
//defer cookiesMap.Unlock()
//spCookie, ok := cookiesMap.m[username]
//if ok {
// if !spCookie.IsExpire() {
// log.Debugln("sp use old cookie.")
// return spCookie.Cookie, nil
// }
//}
//log.Debugln("fetch new cookie")
ca := New(username, password, siteUrl)
tokenConf, err := ca.Cookies()
if err != nil {
return "", err
}
return cookie.ToString([]*http.Cookie{&tokenConf.RtFa, &tokenConf.FedAuth}), nil
//spCookie = &SpCookie{
// Cookie: cookie.ToString([]*http.Cookie{&tokenConf.RtFa, &tokenConf.FedAuth}),
// expire: time.Now().Add(time.Hour * 12),
//}
//cookiesMap.m[username] = spCookie
//return spCookie.Cookie, nil
}

View File

@ -0,0 +1,206 @@
// Package odrvcookie can fetch authentication cookies for a sharepoint webdav endpoint
package odrvcookie
import (
"bytes"
"encoding/xml"
"fmt"
"html/template"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"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 = `<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">{{ .LoginUrl }}</a:To>
<o:Security s:mustUnderstand="1"
xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:UsernameToken>
<o:Username>{{ .Username }}</o:Username>
<o:Password>{{ .Password }}</o:Password>
</o:UsernameToken>
</o:Security>
</s:Header>
<s:Body>
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<a:EndpointReference>
<a:Address>{{ .Address }}</a:Address>
</a:EndpointReference>
</wsp:AppliesTo>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>`
// 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) {
spToken, err := ca.getSPToken()
if err != nil {
return CookieResponse{}, err
}
return ca.getSPCookie(spToken)
}
func (ca *CookieAuth) getSPCookie(conf *SuccessResponse) (CookieResponse, error) {
spRoot, err := url.Parse(ca.endpoint)
if err != nil {
return CookieResponse{}, err
}
u, err := url.Parse("https://" + spRoot.Host + "/_forms/default.aspx?wa=wsignin1.0")
if err != nil {
return CookieResponse{}, err
}
// 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 CookieResponse{}, 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 CookieResponse{}, err
}
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, err
}
var loginUrlsMap = map[string]string{
"com": "https://login.microsoftonline.com",
"cn": "https://login.chinacloudapi.cn",
"us": "https://login.microsoftonline.us",
"de": "https://login.microsoftonline.de",
}
func getLoginUrl(endpoint string) (string, error) {
spRoot, err := url.Parse(endpoint)
if err != nil {
return "", err
}
domains := strings.Split(spRoot.Host, ".")
tld := domains[len(domains)-1]
loginUrl, ok := loginUrlsMap[tld]
if !ok {
return "", fmt.Errorf("tld %s is not supported", tld)
}
return loginUrl + "/extSTS.srf", nil
}
func (ca *CookieAuth) getSPToken() (*SuccessResponse, error) {
loginUrl, err := getLoginUrl(ca.endpoint)
if err != nil {
return nil, err
}
reqData := map[string]string{
"Username": ca.user,
"Password": ca.pass,
"Address": ca.endpoint,
"LoginUrl": loginUrl,
}
t := template.Must(template.New("authXML").Parse(reqString))
buf := &bytes.Buffer{}
if err := t.Execute(buf, reqData); err != nil {
return nil, err
}
// Execute the first request which gives us 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", loginUrl, buf)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBuf := bytes.Buffer{}
respBuf.ReadFrom(resp.Body)
s := respBuf.Bytes()
var conf SuccessResponse
err = xml.Unmarshal(s, &conf)
if err != nil {
return nil, err
}
return &conf, err
}

1
drivers/webdav/types.go Normal file
View File

@ -0,0 +1 @@
package webdav

31
drivers/webdav/util.go Normal file
View File

@ -0,0 +1,31 @@
package webdav
import (
"net/http"
"github.com/alist-org/alist/v3/drivers/webdav/odrvcookie"
"github.com/alist-org/alist/v3/pkg/gowebdav"
)
// do others that not defined in Driver interface
func (d *WebDav) isSharepoint() bool {
return d.Vendor == "sharepoint"
}
func (d *WebDav) setClient() error {
c := gowebdav.NewClient(d.Address, d.Username, d.Password)
if d.isSharepoint() {
cookie, err := odrvcookie.GetCookie(d.Username, d.Password, d.Address)
if err == nil {
c.SetInterceptor(func(method string, rq *http.Request) {
rq.Header.Del("Authorization")
rq.Header.Set("Cookie", cookie)
})
} else {
return err
}
}
d.client = c
return nil
}