feat: add webdav driver
This commit is contained in:
130
drivers/webdav/driver.go
Normal file
130
drivers/webdav/driver.go
Normal 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
29
drivers/webdav/meta.go
Normal 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)
|
||||
}
|
46
drivers/webdav/odrvcookie/cookie.go
Normal file
46
drivers/webdav/odrvcookie/cookie.go
Normal 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
|
||||
}
|
206
drivers/webdav/odrvcookie/fetch.go
Normal file
206
drivers/webdav/odrvcookie/fetch.go
Normal 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
1
drivers/webdav/types.go
Normal file
@ -0,0 +1 @@
|
||||
package webdav
|
31
drivers/webdav/util.go
Normal file
31
drivers/webdav/util.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user