diff --git a/frontend/src/components/setting/SetSystem.vue b/frontend/src/components/setting/SetSystem.vue
index 069a18e..51b54db 100644
--- a/frontend/src/components/setting/SetSystem.vue
+++ b/frontend/src/components/setting/SetSystem.vue
@@ -2,8 +2,12 @@
@@ -13,17 +17,46 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -34,7 +67,7 @@
-
+
@@ -81,9 +119,13 @@ const storeList = [
title: "远程存储",
value: "net",
},
+ {
+ title: "webdav",
+ value: "webdav",
+ },
];
-const items = ["个人存储","备份还原"];
+const items = ["个人存储", "备份还原"];
const activeIndex = ref(0);
@@ -102,6 +144,11 @@ function submitOsInfo() {
//name: "osPath",
type: saveData.storeType,
};
+ if (saveData.storeType === "browser") {
+ setSystemConfig(saveData);
+ RestartApp();
+ return;
+ }
if (saveData.storeType === "local") {
if (saveData.storePath === "") {
setSystemConfig(saveData);
@@ -119,13 +166,7 @@ function submitOsInfo() {
.then((res) => {
if (res.code === 0) {
setSystemConfig(saveData);
- Dialog.showMessageBox({
- message: t("save.success"),
- title: t("language"),
- type: "info",
- }).then(() => {
- RestartApp();
- });
+ RestartApp();
} else {
Dialog.showMessageBox({
message: res.message,
@@ -143,7 +184,7 @@ function submitOsInfo() {
return;
}
const urlRegex = /^(https?:\/\/)[^\/]+$/;
- if(!urlRegex.test(saveData.storenet.url)){
+ if (!urlRegex.test(saveData.storenet.url)) {
Dialog.showMessageBox({
message: "服务器地址格式错误",
type: "error",
@@ -151,15 +192,44 @@ function submitOsInfo() {
return;
}
setSystemConfig(saveData);
- Dialog.showMessageBox({
- message: t("save.success"),
- title: t("language"),
- type: "info",
- }).then(() => {
- RestartApp();
- });
+ RestartApp();
+ }
+ if (saveData.storeType === "webdav") {
+ const urlRegex = /^(https?:\/\/)[^\/]+$/;
+ if (!urlRegex.test(saveData.webdavClient.url)) {
+ Dialog.showMessageBox({
+ message: "服务器地址格式错误",
+ type: "error",
+ });
+ return;
+ }
+ if (saveData.webdavClient.username === "" || saveData.webdavClient.password === "") {
+ Dialog.showMessageBox({
+ message: "用户名或密码不能为空",
+ type: "error",
+ });
+ return;
+ }
+ postData.name = "webdavClient";
+ postData.value = saveData.webdavClient;
+ const postUrl = config.value.apiUrl + "/system/setting";
+ fetch(postUrl, {
+ method: "POST",
+ body: JSON.stringify([postData]),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (res.code === 0) {
+ setSystemConfig(saveData);
+ RestartApp();
+ } else {
+ Dialog.showMessageBox({
+ message: res.message,
+ type: "error",
+ });
+ }
+ });
}
-
}
async function exportBackup() {
diff --git a/frontend/src/system/config.ts b/frontend/src/system/config.ts
index 71ce552..d445ee4 100644
--- a/frontend/src/system/config.ts
+++ b/frontend/src/system/config.ts
@@ -1,4 +1,3 @@
-import { get } from "http";
import { generateRandomString } from "../util/common.ts"
export const configStoreType = localStorage.getItem('GodoOS-storeType') || 'browser';
/**
@@ -98,6 +97,13 @@ export const getSystemConfig = (ifset = false) => {
password: '',
};
}
+ if (!config.webdavClient) {
+ config.webdavClient = {
+ url: '',
+ username: '',
+ password: '',
+ };
+ }
if (!config.dbInfo) {
config.dbInfo = {
url: '',
@@ -132,13 +138,15 @@ export function getApiUrl() {
}
export function getFileUrl() {
const config = getSystemConfig();
- if(config.storeType == 'local'){
- return config.apiUrl
- }
if(config.storeType == 'net'){
- return config.storenet.url
+ return config.storenet.url + '/file'
+ }
+ else if (config.storeType == 'webdav') {
+ return config.apiUrl + '/webdav'
+ }else{
+ return config.apiUrl + '/file'
}
- return config.apiUrl
+
}
export function isWindowsOS() {
diff --git a/frontend/src/system/core/FileOs.ts b/frontend/src/system/core/FileOs.ts
index a135c4e..55426b8 100644
--- a/frontend/src/system/core/FileOs.ts
+++ b/frontend/src/system/core/FileOs.ts
@@ -1,5 +1,5 @@
import { getFileUrl } from "../config.ts";
-const API_BASE_URL = getFileUrl() + "/file"
+const API_BASE_URL = getFileUrl()
import { OsFileMode } from '../core/FileMode';
export async function handleReadDir(path: string): Promise {
const res = await fetch(`${API_BASE_URL}/read?path=${encodeURIComponent(path)}`);
diff --git a/frontend/src/system/index.ts b/frontend/src/system/index.ts
index 4620f5d..b92d108 100644
--- a/frontend/src/system/index.ts
+++ b/frontend/src/system/index.ts
@@ -117,11 +117,11 @@ export class System {
}
const upgradeStore = useUpgradeStore();
upgradeStore.checkUpdate()
- setTimeout(() => {
- if (this._rootState.magnet?.length < 1) {
- this.recover()
- }
- }, 3000);
+ // setTimeout(() => {
+ // if (this._rootState.magnet?.length < 1) {
+ // this.recover()
+ // }
+ // }, 3000);
}, 6000);
}
@@ -247,7 +247,8 @@ export class System {
}
private async initFileSystem() {
- if (getSystemKey("storeType") == 'local'){
+ const storeType = getSystemKey("storeType")
+ if (storeType == 'local' || storeType == 'net'){
//this.fs = this._options.fs;
this.fs = useOsFile();
await this.initOutSystem()
diff --git a/godo/cmd/main.go b/godo/cmd/main.go
index 72583ae..b280ff7 100644
--- a/godo/cmd/main.go
+++ b/godo/cmd/main.go
@@ -7,6 +7,7 @@ import (
"godo/localchat"
"godo/store"
"godo/sys"
+ "godo/webdav"
"log"
"net/http"
"time"
@@ -22,7 +23,8 @@ var staticSrv *http.Server
var staticRouter *http.ServeMux
func OsStart() {
- libs.Initdir()
+ libs.InitServer()
+ webdav.InitWebdav()
router := mux.NewRouter()
router.Use(corsMiddleware())
// 使用带有日志装饰的处理器注册路由
@@ -58,28 +60,47 @@ func OsStart() {
router.HandleFunc("/system/updateInfo", sys.GetUpdateUrlHandler).Methods(http.MethodGet)
router.HandleFunc("/system/update", sys.UpdateAppHandler).Methods(http.MethodGet)
router.HandleFunc("/system/setting", sys.ConfigHandler).Methods(http.MethodPost)
+ fileRouter := router.PathPrefix("/file").Subrouter()
+ fileRouter.HandleFunc("/info", files.HandleSystemInfo).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/read", files.HandleReadDir).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/stat", files.HandleStat).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/chmod", files.HandleChmod).Methods(http.MethodPost)
+ fileRouter.HandleFunc("/exists", files.HandleExists).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/readfile", files.HandleReadFile).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/unlink", files.HandleUnlink).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/clear", files.HandleClear).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/rename", files.HandleRename).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/mkdir", files.HandleMkdir).Methods(http.MethodPost)
+ fileRouter.HandleFunc("/rmdir", files.HandleRmdir).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/copyfile", files.HandleCopyFile).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/writefile", files.HandleWriteFile).Methods(http.MethodPost)
+ fileRouter.HandleFunc("/appendfile", files.HandleAppendFile).Methods(http.MethodPost)
+ fileRouter.HandleFunc("/zip", files.HandleZip).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/unzip", files.HandleUnZip).Methods(http.MethodGet)
+ fileRouter.HandleFunc("/watch", files.WatchHandler).Methods(http.MethodGet)
+
+ localchatRouter := router.PathPrefix("/localchat").Subrouter()
+ localchatRouter.HandleFunc("/sse", localchat.SseHandler).Methods(http.MethodGet)
+ localchatRouter.HandleFunc("/message", localchat.HandleMessage).Methods(http.MethodPost)
+ localchatRouter.HandleFunc("/upload", localchat.MultiUploadHandler).Methods(http.MethodPost)
+ localchatRouter.HandleFunc("/check", localchat.CheckUserHanlder).Methods(http.MethodGet)
+
+ // 注册 WebDAV 路由
+ webdavRouter := router.PathPrefix("/webdav").Subrouter()
+ webdavRouter.HandleFunc("/read", webdav.HandleReadDir).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/stat", webdav.HandleStat).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/chmod", webdav.HandleChmod).Methods(http.MethodPost)
+ webdavRouter.HandleFunc("/exists", webdav.HandleExists).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/readfile", webdav.HandleReadFile).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/unlink", webdav.HandleUnlink).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/clear", webdav.HandleClear).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/rename", webdav.HandleRename).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/mkdir", webdav.HandleMkdir).Methods(http.MethodPost)
+ webdavRouter.HandleFunc("/rmdir", webdav.HandleRmdir).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/copyfile", webdav.HandleCopyFile).Methods(http.MethodGet)
+ webdavRouter.HandleFunc("/writefile", webdav.HandleWriteFile).Methods(http.MethodPost)
+ webdavRouter.HandleFunc("/appendfile", webdav.HandleAppendFile).Methods(http.MethodPost)
- router.HandleFunc("/file/info", files.HandleSystemInfo).Methods(http.MethodGet)
- router.HandleFunc("/file/read", files.HandleReadDir).Methods(http.MethodGet)
- router.HandleFunc("/file/stat", files.HandleStat).Methods(http.MethodGet)
- router.HandleFunc("/file/chmod", files.HandleChmod).Methods(http.MethodPost)
- router.HandleFunc("/file/exists", files.HandleExists).Methods(http.MethodGet)
- router.HandleFunc("/file/readfile", files.HandleReadFile).Methods(http.MethodGet)
- router.HandleFunc("/file/unlink", files.HandleUnlink).Methods(http.MethodGet)
- router.HandleFunc("/file/clear", files.HandleClear).Methods(http.MethodGet)
- router.HandleFunc("/file/rename", files.HandleRename).Methods(http.MethodGet)
- router.HandleFunc("/file/mkdir", files.HandleMkdir).Methods(http.MethodPost)
- router.HandleFunc("/file/rmdir", files.HandleRmdir).Methods(http.MethodGet)
- router.HandleFunc("/file/copyfile", files.HandleCopyFile).Methods(http.MethodGet)
- router.HandleFunc("/file/writefile", files.HandleWriteFile).Methods(http.MethodPost)
- router.HandleFunc("/file/appendfile", files.HandleAppendFile).Methods(http.MethodPost)
- router.HandleFunc("/file/zip", files.HandleZip).Methods(http.MethodGet)
- router.HandleFunc("/file/unzip", files.HandleUnZip).Methods(http.MethodGet)
- router.HandleFunc("/file/watch", files.WatchHandler).Methods(http.MethodGet)
- router.HandleFunc("/localchat/sse", localchat.SseHandler).Methods(http.MethodGet)
- router.HandleFunc("/localchat/message", localchat.HandleMessage).Methods(http.MethodPost)
- router.HandleFunc("/localchat/upload", localchat.MultiUploadHandler).Methods(http.MethodPost)
- router.HandleFunc("/localchat/check", localchat.CheckUserHanlder).Methods(http.MethodGet)
// 将静态文件服务放在最后,作为默认处理程序
router.PathPrefix("/").Handler(http.NotFoundHandler())
if staticRouter != nil {
diff --git a/godo/libs/dir.go b/godo/libs/dir.go
index a07db89..4eb89f7 100644
--- a/godo/libs/dir.go
+++ b/godo/libs/dir.go
@@ -7,7 +7,7 @@ import (
"runtime"
)
-func Initdir() error {
+func InitServer() error {
err := LoadConfig()
if err != nil {
return err
@@ -30,6 +30,7 @@ func Initdir() error {
}
SetConfig(osInfo)
}
+
return nil
}
func InitOsDir() (string, error) {
diff --git a/godo/sys/setting.go b/godo/sys/setting.go
index 52bcbbf..cdf7cd2 100644
--- a/godo/sys/setting.go
+++ b/godo/sys/setting.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"godo/libs"
+ "godo/webdav"
"net"
"net/http"
"os"
@@ -33,10 +34,11 @@ func ConfigHandler(w http.ResponseWriter, r *http.Request) {
libs.SetConfig(req)
}
}
- if req.Name == "ipList" {
- err = SetIplist(req)
+ if req.Name == "webdavClient" {
+ libs.SetConfig(req)
+ err := webdav.InitWebdav()
if err != nil {
- libs.ErrorMsg(w, err.Error())
+ libs.ErrorMsg(w, "The webdav client init is error!")
return
}
}
diff --git a/godo/webdav/auth.go b/godo/webdav/auth.go
new file mode 100644
index 0000000..16815b4
--- /dev/null
+++ b/godo/webdav/auth.go
@@ -0,0 +1,409 @@
+package webdav
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+)
+
+// AuthFactory prototype function to create a new Authenticator
+type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error)
+
+// Authorizer our Authenticator factory which creates an
+// `Authenticator` per action/request.
+type Authorizer interface {
+ // Creates a new Authenticator Shim per request.
+ // It may track request related states and perform payload buffering
+ // for authentication round trips.
+ // The underlying Authenticator will perform the real authentication.
+ NewAuthenticator(body io.Reader) (Authenticator, io.Reader)
+ // Registers a new Authenticator factory to a key.
+ AddAuthenticator(key string, fn AuthFactory)
+}
+
+// An Authenticator implements a specific way to authorize requests.
+// Each request is bound to a separate Authenticator instance.
+//
+// The authentication flow itself is broken down into `Authorize`
+// and `Verify` steps. The former method runs before, and the latter
+// runs after the `Request` is submitted.
+// This makes it easy to encapsulate and control complex
+// authentication challenges.
+//
+// Some authentication flows causing authentication round trips,
+// which can be archived by returning the `redo` of the Verify
+// method. `True` restarts the authentication process for the
+// current action: A new `Request` is spawned, which must be
+// authorized, sent, and re-verified again, until the action
+// is successfully submitted.
+// The preferred way is to handle the authentication ping-pong
+// within `Verify`, and then `redo` with fresh credentials.
+//
+// The result of the `Verify` method can also trigger an
+// `Authenticator` change by returning the `ErrAuthChanged`
+// as an error. Depending on the `Authorizer` this may trigger
+// an `Authenticator` negotiation.
+//
+// Set the `XInhibitRedirect` header to '1' in the `Authorize`
+// method to get control over request redirection.
+// Attention! You must handle the incoming request yourself.
+//
+// To store a shared session state the `Clone` method **must**
+// return a new instance, initialized with the shared state.
+type Authenticator interface {
+ // Authorizes a request. Usually by adding some authorization headers.
+ Authorize(c *http.Client, rq *http.Request, path string) error
+ // Verifies the response if the authorization was successful.
+ // May trigger some round trips to pass the authentication.
+ // May also trigger a new Authenticator negotiation by returning `ErrAuthChenged`
+ Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
+ // Creates a copy of the underlying Authenticator.
+ Clone() Authenticator
+ io.Closer
+}
+
+type authfactory struct {
+ key string
+ create AuthFactory
+}
+
+// authorizer structure holds our Authenticator create functions
+type authorizer struct {
+ factories []authfactory
+ defAuthMux sync.Mutex
+ defAuth Authenticator
+}
+
+// preemptiveAuthorizer structure holds the preemptive Authenticator
+type preemptiveAuthorizer struct {
+ auth Authenticator
+}
+
+// authShim structure that wraps the real Authenticator
+type authShim struct {
+ factory AuthFactory
+ body io.Reader
+ auth Authenticator
+}
+
+// negoAuth structure holds the authenticators that are going to be negotiated
+type negoAuth struct {
+ auths []Authenticator
+ setDefaultAuthenticator func(auth Authenticator)
+}
+
+// nullAuth initializes the whole authentication flow
+type nullAuth struct{}
+
+// noAuth structure to perform no authentication at all
+type noAuth struct{}
+
+// NewAutoAuth creates an auto Authenticator factory.
+// It negotiates the default authentication method
+// based on the order of the registered Authenticators
+// and the remotely offered authentication methods.
+// First In, First Out.
+func NewAutoAuth(login string, secret string) Authorizer {
+ fmap := make([]authfactory, 0)
+ az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}
+
+ az.AddAuthenticator("basic", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
+ return &BasicAuth{user: login, pw: secret}, nil
+ })
+
+ az.AddAuthenticator("digest", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
+ return NewDigestAuth(login, secret, rs)
+ })
+
+ az.AddAuthenticator("passport1.4", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
+ return NewPassportAuth(c, login, secret, rs.Request.URL.String(), &rs.Header)
+ })
+
+ return az
+}
+
+// NewEmptyAuth creates an empty Authenticator factory
+// The order of adding the Authenticator matters.
+// First In, First Out.
+// It offers the `NewAutoAuth` features.
+func NewEmptyAuth() Authorizer {
+ fmap := make([]authfactory, 0)
+ az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}}
+ return az
+}
+
+// NewPreemptiveAuth creates a preemptive Authenticator
+// The preemptive authorizer uses the provided Authenticator
+// for every request regardless of any `Www-Authenticate` header.
+//
+// It may only have one authentication method,
+// so calling `AddAuthenticator` **will panic**!
+//
+// Look out!! This offers the skinniest and slickest implementation
+// without any synchronisation!!
+// Still applicable with `BasicAuth` within go routines.
+func NewPreemptiveAuth(auth Authenticator) Authorizer {
+ return &preemptiveAuthorizer{auth: auth}
+}
+
+// NewAuthenticator creates an Authenticator (Shim) per request
+func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
+ var retryBuf io.Reader = body
+ if body != nil {
+ // If the authorization fails, we will need to restart reading
+ // from the passed body stream.
+ // When body is seekable, use seek to reset the streams
+ // cursor to the start.
+ // Otherwise, copy the stream into a buffer while uploading
+ // and use the buffers content on retry.
+ if _, ok := retryBuf.(io.Seeker); ok {
+ body = io.NopCloser(body)
+ } else {
+ buff := &bytes.Buffer{}
+ retryBuf = buff
+ body = io.TeeReader(body, buff)
+ }
+ }
+ a.defAuthMux.Lock()
+ defAuth := a.defAuth.Clone()
+ a.defAuthMux.Unlock()
+
+ return &authShim{factory: a.factory, body: retryBuf, auth: defAuth}, body
+}
+
+// AddAuthenticator appends the AuthFactory to our factories.
+// It converts the key to lower case and preserves the order.
+func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
+ key = strings.ToLower(key)
+ for _, f := range a.factories {
+ if f.key == key {
+ panic("Authenticator exists: " + key)
+ }
+ }
+ a.factories = append(a.factories, authfactory{key, fn})
+}
+
+// factory picks all valid Authenticators based on Www-Authenticate headers
+func (a *authorizer) factory(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
+ headers := rs.Header.Values("Www-Authenticate")
+ if len(headers) > 0 {
+ auths := make([]Authenticator, 0)
+ for _, f := range a.factories {
+ for _, header := range headers {
+ headerLower := strings.ToLower(header)
+ if strings.Contains(headerLower, f.key) {
+ rs.Header.Set("Www-Authenticate", header)
+ if auth, err = f.create(c, rs, path); err == nil {
+ auths = append(auths, auth)
+ break
+ }
+ }
+ }
+ }
+
+ switch len(auths) {
+ case 0:
+ return nil, NewPathError("NoAuthenticator", path, rs.StatusCode)
+ case 1:
+ auth = auths[0]
+ default:
+ auth = &negoAuth{auths: auths, setDefaultAuthenticator: a.setDefaultAuthenticator}
+ }
+ } else {
+ auth = &noAuth{}
+ }
+
+ a.setDefaultAuthenticator(auth)
+
+ return auth, nil
+}
+
+// setDefaultAuthenticator sets the default Authenticator
+func (a *authorizer) setDefaultAuthenticator(auth Authenticator) {
+ a.defAuthMux.Lock()
+ a.defAuth.Close()
+ a.defAuth = auth
+ a.defAuthMux.Unlock()
+}
+
+// Authorize the current request
+func (s *authShim) Authorize(c *http.Client, rq *http.Request, path string) error {
+ if err := s.auth.Authorize(c, rq, path); err != nil {
+ return err
+ }
+ body := s.body
+ rq.GetBody = func() (io.ReadCloser, error) {
+ if body != nil {
+ if sk, ok := body.(io.Seeker); ok {
+ if _, err := sk.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+ }
+ return io.NopCloser(body), nil
+ }
+ return nil, nil
+ }
+ return nil
+}
+
+// Verify checks for authentication issues and may trigger a re-authentication.
+// Catches AlgoChangedErr to update the current Authenticator
+func (s *authShim) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ redo, err = s.auth.Verify(c, rs, path)
+ if err != nil && errors.Is(err, ErrAuthChanged) {
+ if auth, aerr := s.factory(c, rs, path); aerr == nil {
+ s.auth.Close()
+ s.auth = auth
+ return true, nil
+ } else {
+ return false, aerr
+ }
+ }
+ return
+}
+
+// Close closes all resources
+func (s *authShim) Close() error {
+ s.auth.Close()
+ s.auth, s.factory = nil, nil
+ if s.body != nil {
+ if closer, ok := s.body.(io.Closer); ok {
+ return closer.Close()
+ }
+ }
+ return nil
+}
+
+// It's not intend to Clone the shim
+// therefore it returns a noAuth instance
+func (s *authShim) Clone() Authenticator {
+ return &noAuth{}
+}
+
+// String toString
+func (s *authShim) String() string {
+ return "AuthShim"
+}
+
+// Authorize authorizes the current request with the top most Authorizer
+func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ if len(n.auths) == 0 {
+ return NewPathError("NoAuthenticator", path, 400)
+ }
+ return n.auths[0].Authorize(c, rq, path)
+}
+
+// Verify verifies the authentication and selects the next one based on the result
+func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ if len(n.auths) == 0 {
+ return false, NewPathError("NoAuthenticator", path, 400)
+ }
+ redo, err = n.auths[0].Verify(c, rs, path)
+ if err != nil {
+ if len(n.auths) > 1 {
+ n.auths[0].Close()
+ n.auths = n.auths[1:]
+ return true, nil
+ }
+ } else if redo {
+ return
+ } else {
+ auth := n.auths[0]
+ n.auths = n.auths[1:]
+ n.setDefaultAuthenticator(auth)
+ return
+ }
+
+ return false, NewPathError("NoAuthenticator", path, rs.StatusCode)
+}
+
+// Close will close the underlying authenticators.
+func (n *negoAuth) Close() error {
+ for _, a := range n.auths {
+ a.Close()
+ }
+ n.setDefaultAuthenticator = nil
+ return nil
+}
+
+// Clone clones the underlying authenticators.
+func (n *negoAuth) Clone() Authenticator {
+ auths := make([]Authenticator, len(n.auths))
+ for i, e := range n.auths {
+ auths[i] = e.Clone()
+ }
+ return &negoAuth{auths: auths, setDefaultAuthenticator: n.setDefaultAuthenticator}
+}
+
+func (n *negoAuth) String() string {
+ return "NegoAuth"
+}
+
+// Authorize the current request
+func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ return nil
+}
+
+// Verify checks for authentication issues and may trigger a re-authentication
+func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ if "" != rs.Header.Get("Www-Authenticate") {
+ err = ErrAuthChanged
+ }
+ return
+}
+
+// Close closes all resources
+func (n *noAuth) Close() error {
+ return nil
+}
+
+// Clone creates a copy of itself
+func (n *noAuth) Clone() Authenticator {
+ // no copy due to read only access
+ return n
+}
+
+// String toString
+func (n *noAuth) String() string {
+ return "NoAuth"
+}
+
+// Authorize the current request
+func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ rq.Header.Set(XInhibitRedirect, "1")
+ return nil
+}
+
+// Verify checks for authentication issues and may trigger a re-authentication
+func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ return true, ErrAuthChanged
+}
+
+// Close closes all resources
+func (n *nullAuth) Close() error {
+ return nil
+}
+
+// Clone creates a copy of itself
+func (n *nullAuth) Clone() Authenticator {
+ // no copy due to read only access
+ return n
+}
+
+// String toString
+func (n *nullAuth) String() string {
+ return "NullAuth"
+}
+
+// NewAuthenticator creates an Authenticator (Shim) per request
+func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
+ return b.auth.Clone(), body
+}
+
+// AddAuthenticator Will PANIC because it may only have a single authentication method
+func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
+ panic("You're funny! A preemptive authorizer may only have a single authentication method")
+}
diff --git a/godo/webdav/basicAuth.go b/godo/webdav/basicAuth.go
new file mode 100644
index 0000000..fd8b4ea
--- /dev/null
+++ b/godo/webdav/basicAuth.go
@@ -0,0 +1,42 @@
+package webdav
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// BasicAuth structure holds our credentials
+type BasicAuth struct {
+ user string
+ pw string
+}
+
+// Authorize the current request
+func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ rq.SetBasicAuth(b.user, b.pw)
+ return nil
+}
+
+// Verify verifies if the authentication
+func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ if rs.StatusCode == 401 {
+ err = NewPathError("Authorize", path, rs.StatusCode)
+ }
+ return
+}
+
+// Close cleans up all resources
+func (b *BasicAuth) Close() error {
+ return nil
+}
+
+// Clone creates a Copy of itself
+func (b *BasicAuth) Clone() Authenticator {
+ // no copy due to read only access
+ return b
+}
+
+// String toString
+func (b *BasicAuth) String() string {
+ return fmt.Sprintf("BasicAuth login: %s", b.user)
+}
diff --git a/godo/webdav/client.go b/godo/webdav/client.go
new file mode 100644
index 0000000..1dc87fb
--- /dev/null
+++ b/godo/webdav/client.go
@@ -0,0 +1,438 @@
+package webdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ pathpkg "path"
+ "strings"
+ "time"
+)
+
+const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
+
+// Client defines our structure
+type Client struct {
+ root string
+ headers http.Header
+ interceptor func(method string, rq *http.Request)
+ c *http.Client
+ auth Authorizer
+}
+
+// NewClient creates a new instance of client
+func NewClient(uri, user, pw string) *Client {
+ return NewAuthClient(uri, NewAutoAuth(user, pw))
+}
+
+// NewAuthClient creates a new client instance with a custom Authorizer
+func NewAuthClient(uri string, auth Authorizer) *Client {
+ c := &http.Client{
+ CheckRedirect: func(rq *http.Request, via []*http.Request) error {
+ if len(via) >= 10 {
+ return ErrTooManyRedirects
+ }
+ if via[0].Header.Get(XInhibitRedirect) != "" {
+ return http.ErrUseLastResponse
+ }
+ return nil
+ },
+ }
+ return &Client{root: FixSlash(uri), headers: make(http.Header), interceptor: nil, c: c, auth: auth}
+}
+
+// SetHeader lets us set arbitrary headers for a given client
+func (c *Client) SetHeader(key, value string) {
+ c.headers.Add(key, value)
+}
+
+// SetInterceptor lets us set an arbitrary interceptor for a given client
+func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
+ c.interceptor = interceptor
+}
+
+// SetTimeout exposes the ability to set a time limit for requests
+func (c *Client) SetTimeout(timeout time.Duration) {
+ c.c.Timeout = timeout
+}
+
+// SetTransport exposes the ability to define custom transports
+func (c *Client) SetTransport(transport http.RoundTripper) {
+ c.c.Transport = transport
+}
+
+// SetJar exposes the ability to set a cookie jar to the client.
+func (c *Client) SetJar(jar http.CookieJar) {
+ c.c.Jar = jar
+}
+
+// Connect connects to our dav server
+func (c *Client) Connect() error {
+ rs, err := c.options("/")
+ if err != nil {
+ return err
+ }
+
+ err = rs.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ if rs.StatusCode != 200 {
+ return NewPathError("Connect", c.root, rs.StatusCode)
+ }
+
+ return nil
+}
+
+type props struct {
+ Status string `xml:"DAV: status"`
+ Name string `xml:"DAV: prop>displayname,omitempty"`
+ Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
+ Size string `xml:"DAV: prop>getcontentlength,omitempty"`
+ ContentType string `xml:"DAV: prop>getcontenttype,omitempty"`
+ ETag string `xml:"DAV: prop>getetag,omitempty"`
+ Modified string `xml:"DAV: prop>getlastmodified,omitempty"`
+}
+
+type response struct {
+ Href string `xml:"DAV: href"`
+ Props []props `xml:"DAV: propstat"`
+}
+
+func getProps(r *response, status string) *props {
+ for _, prop := range r.Props {
+ if strings.Contains(prop.Status, status) {
+ return &prop
+ }
+ }
+ return nil
+}
+
+// ReadDir reads the contents of a remote directory
+func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
+ path = FixSlashes(path)
+ files := make([]os.FileInfo, 0)
+ skipSelf := true
+ parse := func(resp interface{}) error {
+ r := resp.(*response)
+
+ if skipSelf {
+ skipSelf = false
+ if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
+ r.Props = nil
+ return nil
+ }
+ return NewPathError("ReadDir", path, 405)
+ }
+
+ if p := getProps(r, "200"); p != nil {
+ f := new(File)
+ if ps, err := url.PathUnescape(r.Href); err == nil {
+ f.name = pathpkg.Base(ps)
+ } else {
+ f.name = p.Name
+ }
+ f.path = path + f.name
+ f.modified = parseModified(&p.Modified)
+ f.etag = p.ETag
+ f.contentType = p.ContentType
+
+ if p.Type.Local == "collection" {
+ f.path += "/"
+ f.size = 0
+ f.isdir = true
+ } else {
+ f.size = parseInt64(&p.Size)
+ f.isdir = false
+ }
+
+ files = append(files, *f)
+ }
+
+ r.Props = nil
+ return nil
+ }
+
+ err := c.propfind(path, false,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &response{},
+ parse)
+
+ if err != nil {
+ if _, ok := err.(*os.PathError); !ok {
+ err = NewPathErrorErr("ReadDir", path, err)
+ }
+ }
+ return files, err
+}
+
+// Stat returns the file stats for a specified path
+func (c *Client) Stat(path string) (os.FileInfo, error) {
+ var f *File
+ parse := func(resp interface{}) error {
+ r := resp.(*response)
+ if p := getProps(r, "200"); p != nil && f == nil {
+ f = new(File)
+ f.name = p.Name
+ f.path = path
+ f.etag = p.ETag
+ f.contentType = p.ContentType
+
+ if p.Type.Local == "collection" {
+ if !strings.HasSuffix(f.path, "/") {
+ f.path += "/"
+ }
+ f.size = 0
+ f.modified = parseModified(&p.Modified)
+ f.isdir = true
+ } else {
+ f.size = parseInt64(&p.Size)
+ f.modified = parseModified(&p.Modified)
+ f.isdir = false
+ }
+ }
+
+ r.Props = nil
+ return nil
+ }
+
+ err := c.propfind(path, true,
+ `
+
+
+
+
+
+
+
+
+ `,
+ &response{},
+ parse)
+
+ if err != nil {
+ if _, ok := err.(*os.PathError); !ok {
+ err = NewPathErrorErr("ReadDir", path, err)
+ }
+ }
+ return f, err
+}
+
+// Remove removes a remote file
+func (c *Client) Remove(path string) error {
+ return c.RemoveAll(path)
+}
+
+// RemoveAll removes remote files
+func (c *Client) RemoveAll(path string) error {
+ rs, err := c.req("DELETE", path, nil, nil)
+ if err != nil {
+ return NewPathError("Remove", path, 400)
+ }
+ err = rs.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
+ return nil
+ }
+
+ return NewPathError("Remove", path, rs.StatusCode)
+}
+
+// Mkdir makes a directory
+func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
+ path = FixSlashes(path)
+ status, err := c.mkcol(path)
+ if err != nil {
+ return
+ }
+ if status == 201 {
+ return nil
+ }
+
+ return NewPathError("Mkdir", path, status)
+}
+
+// MkdirAll like mkdir -p, but for webdav
+func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
+ path = FixSlashes(path)
+ status, err := c.mkcol(path)
+ if err != nil {
+ return
+ }
+ if status == 201 {
+ return nil
+ }
+ if status == 409 {
+ paths := strings.Split(path, "/")
+ sub := "/"
+ for _, e := range paths {
+ if e == "" {
+ continue
+ }
+ sub += e + "/"
+ status, err = c.mkcol(sub)
+ if err != nil {
+ return
+ }
+ if status != 201 {
+ return NewPathError("MkdirAll", sub, status)
+ }
+ }
+ return nil
+ }
+
+ return NewPathError("MkdirAll", path, status)
+}
+
+// Rename moves a file from A to B
+func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
+ return c.copymove("MOVE", oldpath, newpath, overwrite)
+}
+
+// Copy copies a file from A to B
+func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
+ return c.copymove("COPY", oldpath, newpath, overwrite)
+}
+
+// Read reads the contents of a remote file
+func (c *Client) Read(path string) ([]byte, error) {
+ var stream io.ReadCloser
+ var err error
+
+ if stream, err = c.ReadStream(path); err != nil {
+ return nil, err
+ }
+ defer stream.Close()
+
+ buf := new(bytes.Buffer)
+ _, err = buf.ReadFrom(stream)
+ if err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// ReadStream reads the stream for a given path
+func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
+ rs, err := c.req("GET", path, nil, nil)
+ if err != nil {
+ return nil, NewPathErrorErr("ReadStream", path, err)
+ }
+
+ if rs.StatusCode == 200 {
+ return rs.Body, nil
+ }
+
+ rs.Body.Close()
+ return nil, NewPathError("ReadStream", path, rs.StatusCode)
+}
+
+// ReadStreamRange reads the stream representing a subset of bytes for a given path,
+// utilizing HTTP Range Requests if the server supports it.
+// The range is expressed as offset from the start of the file and length, for example
+// offset=10, length=10 will return bytes 10 through 19.
+//
+// If the server does not support partial content requests and returns full content instead,
+// this function will emulate the behavior by skipping `offset` bytes and limiting the result
+// to `length`.
+func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
+ rs, err := c.req("GET", path, nil, func(r *http.Request) {
+ if length > 0 {
+ r.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
+ } else {
+ r.Header.Add("Range", fmt.Sprintf("bytes=%d-", offset))
+ }
+ })
+ if err != nil {
+ return nil, NewPathErrorErr("ReadStreamRange", path, err)
+ }
+
+ if rs.StatusCode == http.StatusPartialContent {
+ // server supported partial content, return as-is.
+ return rs.Body, nil
+ }
+
+ // server returned success, but did not support partial content, so we have the whole
+ // stream in rs.Body
+ if rs.StatusCode == 200 {
+ // discard first 'offset' bytes.
+ if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
+ return nil, NewPathErrorErr("ReadStreamRange", path, err)
+ }
+
+ // return a io.ReadCloser that is limited to `length` bytes.
+ return &limitedReadCloser{rc: rs.Body, remaining: int(length)}, nil
+ }
+
+ rs.Body.Close()
+ return nil, NewPathError("ReadStream", path, rs.StatusCode)
+}
+
+// Write writes data to a given path
+func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
+ s, err := c.put(path, bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+
+ switch s {
+
+ case 200, 201, 204:
+ return nil
+
+ case 404, 409:
+ err = c.createParentCollection(path)
+ if err != nil {
+ return
+ }
+
+ s, err = c.put(path, bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+ if s == 200 || s == 201 || s == 204 {
+ return
+ }
+ }
+
+ return NewPathError("Write", path, s)
+}
+
+// WriteStream writes a stream
+func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) {
+
+ err = c.createParentCollection(path)
+ if err != nil {
+ return err
+ }
+
+ s, err := c.put(path, stream)
+ if err != nil {
+ return err
+ }
+
+ switch s {
+ case 200, 201, 204:
+ return nil
+
+ default:
+ return NewPathError("WriteStream", path, s)
+ }
+}
diff --git a/godo/webdav/digestAuth.go b/godo/webdav/digestAuth.go
new file mode 100644
index 0000000..fbcfc29
--- /dev/null
+++ b/godo/webdav/digestAuth.go
@@ -0,0 +1,183 @@
+package webdav
+
+import (
+ "crypto/md5"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+// DigestAuth structure holds our credentials
+type DigestAuth struct {
+ user string
+ pw string
+ digestParts map[string]string
+}
+
+// NewDigestAuth creates a new instance of our Digest Authenticator
+func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) {
+ return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil
+}
+
+// Authorize the current request
+func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ d.digestParts["uri"] = path
+ d.digestParts["method"] = rq.Method
+ d.digestParts["username"] = d.user
+ d.digestParts["password"] = d.pw
+ rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts))
+ return nil
+}
+
+// Verify checks for authentication issues and may trigger a re-authentication
+func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ if rs.StatusCode == 401 {
+ if isStaled(rs) {
+ redo = true
+ err = ErrAuthChanged
+ } else {
+ err = NewPathError("Authorize", path, rs.StatusCode)
+ }
+ }
+ return
+}
+
+// Close cleans up all resources
+func (d *DigestAuth) Close() error {
+ return nil
+}
+
+// Clone creates a copy of itself
+func (d *DigestAuth) Clone() Authenticator {
+ parts := make(map[string]string, len(d.digestParts))
+ for k, v := range d.digestParts {
+ parts[k] = v
+ }
+ return &DigestAuth{user: d.user, pw: d.pw, digestParts: parts}
+}
+
+// String toString
+func (d *DigestAuth) String() string {
+ return fmt.Sprintf("DigestAuth login: %s", d.user)
+}
+
+func digestParts(resp *http.Response) map[string]string {
+ result := map[string]string{}
+ if len(resp.Header["Www-Authenticate"]) > 0 {
+ wantedHeaders := []string{"nonce", "realm", "qop", "opaque", "algorithm", "entityBody"}
+ responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
+ for _, r := range responseHeaders {
+ for _, w := range wantedHeaders {
+ if strings.Contains(r, w) {
+ result[w] = strings.Trim(
+ strings.SplitN(r, `=`, 2)[1],
+ `"`,
+ )
+ }
+ }
+ }
+ }
+ return result
+}
+
+func getMD5(text string) string {
+ hasher := md5.New()
+ hasher.Write([]byte(text))
+ return hex.EncodeToString(hasher.Sum(nil))
+}
+
+func getCnonce() string {
+ b := make([]byte, 8)
+ io.ReadFull(rand.Reader, b)
+ return fmt.Sprintf("%x", b)[:16]
+}
+
+func getDigestAuthorization(digestParts map[string]string) string {
+ d := digestParts
+ // These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop.
+
+ var (
+ ha1 string
+ ha2 string
+ nonceCount = 00000001
+ cnonce = getCnonce()
+ response string
+ )
+
+ // 'ha1' value depends on value of "algorithm" field
+ switch d["algorithm"] {
+ case "MD5", "":
+ ha1 = getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
+ case "MD5-sess":
+ ha1 = getMD5(
+ fmt.Sprintf("%s:%v:%s",
+ getMD5(d["username"]+":"+d["realm"]+":"+d["password"]),
+ nonceCount,
+ cnonce,
+ ),
+ )
+ }
+
+ // 'ha2' value depends on value of "qop" field
+ switch d["qop"] {
+ case "auth", "":
+ ha2 = getMD5(d["method"] + ":" + d["uri"])
+ case "auth-int":
+ if d["entityBody"] != "" {
+ ha2 = getMD5(d["method"] + ":" + d["uri"] + ":" + getMD5(d["entityBody"]))
+ }
+ }
+
+ // 'response' value depends on value of "qop" field
+ switch d["qop"] {
+ case "":
+ response = getMD5(
+ fmt.Sprintf("%s:%s:%s",
+ ha1,
+ d["nonce"],
+ ha2,
+ ),
+ )
+ case "auth", "auth-int":
+ response = getMD5(
+ fmt.Sprintf("%s:%s:%v:%s:%s:%s",
+ ha1,
+ d["nonce"],
+ nonceCount,
+ cnonce,
+ d["qop"],
+ ha2,
+ ),
+ )
+ }
+
+ authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", nc=%v, cnonce="%s", response="%s"`,
+ d["username"], d["realm"], d["nonce"], d["uri"], nonceCount, cnonce, response)
+
+ if d["qop"] != "" {
+ authorization += fmt.Sprintf(`, qop=%s`, d["qop"])
+ }
+
+ if d["opaque"] != "" {
+ authorization += fmt.Sprintf(`, opaque="%s"`, d["opaque"])
+ }
+
+ return authorization
+}
+
+func isStaled(rs *http.Response) bool {
+ header := rs.Header.Get("Www-Authenticate")
+ if len(header) > 0 {
+ directives := strings.Split(header, ",")
+ for i := range directives {
+ name, value, _ := strings.Cut(strings.Trim(directives[i], " "), "=")
+ if strings.EqualFold(name, "stale") {
+ return strings.EqualFold(value, "true")
+ }
+ }
+ }
+ return false
+}
diff --git a/godo/webdav/errors.go b/godo/webdav/errors.go
new file mode 100644
index 0000000..8a78232
--- /dev/null
+++ b/godo/webdav/errors.go
@@ -0,0 +1,57 @@
+package webdav
+
+import (
+ "errors"
+ "fmt"
+ "os"
+)
+
+// ErrAuthChanged must be returned from the Verify method as an error
+// to trigger a re-authentication / negotiation with a new authenticator.
+var ErrAuthChanged = errors.New("authentication failed, change algorithm")
+
+// ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects.
+var ErrTooManyRedirects = errors.New("stopped after 10 redirects")
+
+// StatusError implements error and wraps
+// an erroneous status code.
+type StatusError struct {
+ Status int
+}
+
+func (se StatusError) Error() string {
+ return fmt.Sprintf("%d", se.Status)
+}
+
+// IsErrCode returns true if the given error
+// is an os.PathError wrapping a StatusError
+// with the given status code.
+func IsErrCode(err error, code int) bool {
+ if pe, ok := err.(*os.PathError); ok {
+ se, ok := pe.Err.(StatusError)
+ return ok && se.Status == code
+ }
+ return false
+}
+
+// IsErrNotFound is shorthand for IsErrCode
+// for status 404.
+func IsErrNotFound(err error) bool {
+ return IsErrCode(err, 404)
+}
+
+func NewPathError(op string, path string, statusCode int) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: StatusError{statusCode},
+ }
+}
+
+func NewPathErrorErr(op string, path string, err error) error {
+ return &os.PathError{
+ Op: op,
+ Path: path,
+ Err: err,
+ }
+}
diff --git a/godo/webdav/file.go b/godo/webdav/file.go
new file mode 100644
index 0000000..77880dc
--- /dev/null
+++ b/godo/webdav/file.go
@@ -0,0 +1,77 @@
+package webdav
+
+import (
+ "fmt"
+ "os"
+ "time"
+)
+
+// File is our structure for a given file
+type File struct {
+ path string
+ name string
+ contentType string
+ size int64
+ modified time.Time
+ etag string
+ isdir bool
+}
+
+// Path returns the full path of a file
+func (f File) Path() string {
+ return f.path
+}
+
+// Name returns the name of a file
+func (f File) Name() string {
+ return f.name
+}
+
+// ContentType returns the content type of a file
+func (f File) ContentType() string {
+ return f.contentType
+}
+
+// Size returns the size of a file
+func (f File) Size() int64 {
+ return f.size
+}
+
+// Mode will return the mode of a given file
+func (f File) Mode() os.FileMode {
+ // TODO check webdav perms
+ if f.isdir {
+ return 0775 | os.ModeDir
+ }
+
+ return 0664
+}
+
+// ModTime returns the modified time of a file
+func (f File) ModTime() time.Time {
+ return f.modified
+}
+
+// ETag returns the ETag of a file
+func (f File) ETag() string {
+ return f.etag
+}
+
+// IsDir let us see if a given file is a directory or not
+func (f File) IsDir() bool {
+ return f.isdir
+}
+
+// Sys ????
+func (f File) Sys() interface{} {
+ return nil
+}
+
+// String lets us see file information
+func (f File) String() string {
+ if f.isdir {
+ return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name)
+ }
+
+ return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType)
+}
diff --git a/godo/webdav/httpclient.go b/godo/webdav/httpclient.go
new file mode 100644
index 0000000..f52909a
--- /dev/null
+++ b/godo/webdav/httpclient.go
@@ -0,0 +1,229 @@
+package webdav
+
+import (
+ "fmt"
+ "godo/libs"
+ "io"
+ "net/http"
+
+ "github.com/gorilla/mux"
+)
+
+var client *Client
+
+func InitWebdav() error {
+ clientInfo, ok := libs.GetConfig("webdavClient")
+ if !ok {
+ return fmt.Errorf("failed to find configuration for webdavClient")
+ }
+
+ infoMap, ok := clientInfo.(map[string]string)
+ if !ok {
+ return fmt.Errorf("configuration for webdavClient is not a map[string]string")
+ }
+
+ url, ok := infoMap["url"]
+ if !ok {
+ return fmt.Errorf("missing 'url' in webdavClient configuration")
+ }
+
+ username, ok := infoMap["username"]
+ if !ok {
+ return fmt.Errorf("missing 'username' in webdavClient configuration")
+ }
+
+ password, ok := infoMap["password"]
+ if !ok {
+ return fmt.Errorf("missing 'password' in webdavClient configuration")
+ }
+
+ client = NewClient(url, username, password)
+
+ if err := client.Connect(); err != nil {
+ return fmt.Errorf("failed to connect to WebDAV server: %v", err)
+ }
+ return nil
+}
+func HandlePing(w http.ResponseWriter, r *http.Request) {
+ err := client.Connect()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+// HandleReadDir: 读取目录内容
+func HandleReadDir(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ files, err := client.ReadDir(path)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ for _, f := range files {
+ w.Write([]byte(f.Name() + "\n"))
+ }
+}
+
+// HandleStat: 获取文件状态
+func HandleStat(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ fi, err := client.Stat(path)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ fileInfo, ok := fi.(*File)
+ if !ok {
+ http.Error(w, "Unexpected file info type", http.StatusInternalServerError)
+ return
+ }
+ // 直接调用 fi.String(),因为 fi 应该是 *File 类型
+ w.Write([]byte(fileInfo.String()))
+}
+
+// HandleChmod: 改变文件权限 (不支持)
+func HandleChmod(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Not supported", http.StatusNotImplemented)
+}
+
+// HandleExists: 检查文件是否存在
+func HandleExists(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ _, err := client.Stat(path)
+ if err != nil {
+ http.Error(w, "File does not exist", http.StatusNotFound)
+ return
+ }
+ w.Write([]byte("File exists"))
+}
+
+// HandleReadFile: 读取文件内容
+func HandleReadFile(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ data, err := client.Read(path)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write(data)
+}
+
+// HandleUnlink: 删除文件
+func HandleUnlink(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ err := client.Remove(path)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("File deleted"))
+}
+
+// HandleClear: 清空目录 (不支持)
+func HandleClear(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "Not supported", http.StatusNotImplemented)
+}
+
+// HandleRename: 重命名文件或目录
+func HandleRename(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ oldPath := vars["oldPath"]
+ newPath := vars["newPath"]
+ err := client.Rename(oldPath, newPath, false)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("File renamed"))
+}
+
+// HandleMkdir: 创建目录
+func HandleMkdir(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ err := client.Mkdir(path, 0)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("Directory created"))
+}
+
+// HandleRmdir: 删除目录
+func HandleRmdir(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ err := client.RemoveAll(path)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("Directory removed"))
+}
+
+// HandleCopyFile: 复制文件
+func HandleCopyFile(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ oldPath := vars["oldPath"]
+ newPath := vars["newPath"]
+ err := client.Copy(oldPath, newPath, false)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("File copied"))
+}
+
+// HandleWriteFile: 写入文件
+func HandleWriteFile(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ err = client.Write(path, body, 0)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("File written"))
+}
+
+// HandleAppendFile: 追加到文件
+func HandleAppendFile(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ path := vars["path"]
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ // 读取现有文件内容
+ existingContent, err := client.Read(path)
+ if err != nil && !IsErrNotFound(err) {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ // 如果文件不存在,则创建新文件
+ if IsErrNotFound(err) {
+ err = client.Write(path, body, 0)
+ } else {
+ // 如果文件存在,则追加内容
+ err = client.Write(path, append(existingContent, body...), 0)
+ }
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write([]byte("File appended"))
+}
diff --git a/godo/webdav/netrc.go b/godo/webdav/netrc.go
new file mode 100644
index 0000000..dc18f56
--- /dev/null
+++ b/godo/webdav/netrc.go
@@ -0,0 +1,54 @@
+package webdav
+
+import (
+ "bufio"
+ "fmt"
+ "net/url"
+ "os"
+ "regexp"
+ "strings"
+)
+
+func parseLine(s string) (login, pass string) {
+ fields := strings.Fields(s)
+ for i, f := range fields {
+ if f == "login" {
+ login = fields[i+1]
+ }
+ if f == "password" {
+ pass = fields[i+1]
+ }
+ }
+ return login, pass
+}
+
+// ReadConfig reads login and password configuration from ~/.netrc
+// machine foo.com login username password 123456
+func ReadConfig(uri, netrc string) (string, string) {
+ u, err := url.Parse(uri)
+ if err != nil {
+ return "", ""
+ }
+
+ file, err := os.Open(netrc)
+ if err != nil {
+ return "", ""
+ }
+ defer file.Close()
+
+ re := fmt.Sprintf(`^.*machine %s.*$`, u.Host)
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ s := scanner.Text()
+
+ matched, err := regexp.MatchString(re, s)
+ if err != nil {
+ return "", ""
+ }
+ if matched {
+ return parseLine(s)
+ }
+ }
+
+ return "", ""
+}
diff --git a/godo/webdav/passportAuth.go b/godo/webdav/passportAuth.go
new file mode 100644
index 0000000..a2d7436
--- /dev/null
+++ b/godo/webdav/passportAuth.go
@@ -0,0 +1,181 @@
+package webdav
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// PassportAuth structure holds our credentials
+type PassportAuth struct {
+ user string
+ pw string
+ cookies []http.Cookie
+ inhibitRedirect bool
+}
+
+// constructor for PassportAuth creates a new PassportAuth object and
+// automatically authenticates against the given partnerURL
+func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) {
+ p := &PassportAuth{
+ user: user,
+ pw: pw,
+ inhibitRedirect: true,
+ }
+ err := p.genCookies(c, partnerURL, header)
+ return p, err
+}
+
+// Authorize the current request
+func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
+ // prevent redirects to detect subsequent authentication requests
+ if p.inhibitRedirect {
+ rq.Header.Set(XInhibitRedirect, "1")
+ } else {
+ p.inhibitRedirect = true
+ }
+ for _, cookie := range p.cookies {
+ rq.AddCookie(&cookie)
+ }
+ return nil
+}
+
+// Verify verifies if the authentication is good
+func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
+ switch rs.StatusCode {
+ case 301, 302, 307, 308:
+ redo = true
+ if rs.Header.Get("Www-Authenticate") != "" {
+ // re-authentication required as we are redirected to the login page
+ err = p.genCookies(c, rs.Request.URL.String(), &rs.Header)
+ } else {
+ // just a redirect, follow it
+ p.inhibitRedirect = false
+ }
+ case 401:
+ err = NewPathError("Authorize", path, rs.StatusCode)
+ }
+ return
+}
+
+// Close cleans up all resources
+func (p *PassportAuth) Close() error {
+ return nil
+}
+
+// Clone creates a Copy of itself
+func (p *PassportAuth) Clone() Authenticator {
+ // create a copy to allow independent cookie updates
+ clonedCookies := make([]http.Cookie, len(p.cookies))
+ copy(clonedCookies, p.cookies)
+
+ return &PassportAuth{
+ user: p.user,
+ pw: p.pw,
+ cookies: clonedCookies,
+ inhibitRedirect: true,
+ }
+}
+
+// String toString
+func (p *PassportAuth) String() string {
+ return fmt.Sprintf("PassportAuth login: %s", p.user)
+}
+
+func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error {
+ // For more details refer to:
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3
+ // Skipping step 1 and 2 as we already have the partner server challenge
+
+ baseAuthenticationServer := header.Get("Location")
+ baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer)
+ if err != nil {
+ return err
+ }
+
+ // Skipping step 3 and 4 as we already know that we need and have the user's credentials
+ // Step 5 (Sign-in request)
+ authenticationServerUrl := url.URL{
+ Scheme: baseAuthenticationServerURL.Scheme,
+ Host: baseAuthenticationServerURL.Host,
+ Path: "/login2.srf",
+ }
+
+ partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1]
+
+ req := http.Request{
+ Method: "GET",
+ URL: &authenticationServerUrl,
+ Header: http.Header{
+ "Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge},
+ },
+ }
+
+ rs, err := c.Do(&req)
+ if err != nil {
+ return err
+ }
+ io.Copy(io.Discard, rs.Body)
+ rs.Body.Close()
+ if rs.StatusCode != 200 {
+ return NewPathError("Authorize", "/", rs.StatusCode)
+ }
+
+ // Step 6 (Token Response from Authentication Server)
+ tokenResponseHeader := rs.Header.Get("Authentication-Info")
+ if tokenResponseHeader == "" {
+ return NewPathError("Authorize", "/", 401)
+ }
+ tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",")
+ token := ""
+ for _, tokenResponseHeader := range tokenResponseHeaderList {
+ if strings.HasPrefix(tokenResponseHeader, "from-PP='") {
+ token = tokenResponseHeader
+ break
+ }
+ }
+ if token == "" {
+ return NewPathError("Authorize", "/", 401)
+ }
+
+ // Step 7 (First Authentication Request to Partner Server)
+ origUrl, err := url.Parse(partnerUrl)
+ if err != nil {
+ return err
+ }
+ req = http.Request{
+ Method: "GET",
+ URL: origUrl,
+ Header: http.Header{
+ "Authorization": []string{"Passport1.4 " + token},
+ },
+ }
+
+ rs, err = c.Do(&req)
+ if err != nil {
+ return err
+ }
+ io.Copy(io.Discard, rs.Body)
+ rs.Body.Close()
+ if rs.StatusCode != 200 && rs.StatusCode != 302 {
+ return NewPathError("Authorize", "/", rs.StatusCode)
+ }
+
+ // Step 8 (Set Token Message from Partner Server)
+ cookies := rs.Header.Values("Set-Cookie")
+ p.cookies = make([]http.Cookie, len(cookies))
+ for i, cookie := range cookies {
+ cookieParts := strings.Split(cookie, ";")
+ cookieName := strings.Split(cookieParts[0], "=")[0]
+ cookieValue := strings.Split(cookieParts[0], "=")[1]
+
+ p.cookies[i] = http.Cookie{
+ Name: cookieName,
+ Value: cookieValue,
+ }
+ }
+
+ return nil
+}
diff --git a/godo/webdav/requests.go b/godo/webdav/requests.go
new file mode 100644
index 0000000..1c59369
--- /dev/null
+++ b/godo/webdav/requests.go
@@ -0,0 +1,181 @@
+package webdav
+
+import (
+ "io"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+)
+
+func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) {
+ var redo bool
+ var r *http.Request
+ var uri = PathEscape(Join(c.root, path))
+ auth, body := c.auth.NewAuthenticator(body)
+ defer auth.Close()
+
+ for { // TODO auth.continue() strategy(true|n times|until)?
+ if r, err = http.NewRequest(method, uri, body); err != nil {
+ return
+ }
+
+ for k, vals := range c.headers {
+ for _, v := range vals {
+ r.Header.Add(k, v)
+ }
+ }
+
+ if err = auth.Authorize(c.c, r, path); err != nil {
+ return
+ }
+
+ if intercept != nil {
+ intercept(r)
+ }
+
+ if c.interceptor != nil {
+ c.interceptor(method, r)
+ }
+
+ if rs, err = c.c.Do(r); err != nil {
+ return
+ }
+
+ if redo, err = auth.Verify(c.c, rs, path); err != nil {
+ rs.Body.Close()
+ return nil, err
+ }
+ if redo {
+ rs.Body.Close()
+ if body, err = r.GetBody(); err != nil {
+ return nil, err
+ }
+ continue
+ }
+ break
+ }
+
+ return rs, err
+}
+
+func (c *Client) mkcol(path string) (status int, err error) {
+ rs, err := c.req("MKCOL", path, nil, nil)
+ if err != nil {
+ return
+ }
+ defer rs.Body.Close()
+
+ status = rs.StatusCode
+ if status == 405 {
+ status = 201
+ }
+
+ return
+}
+
+func (c *Client) options(path string) (*http.Response, error) {
+ return c.req("OPTIONS", path, nil, func(rq *http.Request) {
+ rq.Header.Add("Depth", "0")
+ })
+}
+
+func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error {
+ rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) {
+ if self {
+ rq.Header.Add("Depth", "0")
+ } else {
+ rq.Header.Add("Depth", "1")
+ }
+ rq.Header.Add("Content-Type", "application/xml;charset=UTF-8")
+ rq.Header.Add("Accept", "application/xml,text/xml")
+ rq.Header.Add("Accept-Charset", "utf-8")
+ // TODO add support for 'gzip,deflate;q=0.8,q=0.7'
+ rq.Header.Add("Accept-Encoding", "")
+ })
+ if err != nil {
+ return err
+ }
+ defer rs.Body.Close()
+
+ if rs.StatusCode != 207 {
+ return NewPathError("PROPFIND", path, rs.StatusCode)
+ }
+
+ return parseXML(rs.Body, resp, parse)
+}
+
+func (c *Client) doCopyMove(
+ method string,
+ oldpath string,
+ newpath string,
+ overwrite bool,
+) (
+ status int,
+ r io.ReadCloser,
+ err error,
+) {
+ rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
+ rq.Header.Add("Destination", PathEscape(Join(c.root, newpath)))
+ if overwrite {
+ rq.Header.Add("Overwrite", "T")
+ } else {
+ rq.Header.Add("Overwrite", "F")
+ }
+ })
+ if err != nil {
+ return
+ }
+ status = rs.StatusCode
+ r = rs.Body
+ return
+}
+
+func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) {
+ s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite)
+ if err != nil {
+ return
+ }
+ if data != nil {
+ defer data.Close()
+ }
+
+ switch s {
+ case 201, 204:
+ return nil
+
+ case 207:
+ // TODO handle multistat errors, worst case ...
+ log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data))
+
+ case 409:
+ err := c.createParentCollection(newpath)
+ if err != nil {
+ return err
+ }
+
+ return c.copymove(method, oldpath, newpath, overwrite)
+ }
+
+ return NewPathError(method, oldpath, s)
+}
+
+func (c *Client) put(path string, stream io.Reader) (status int, err error) {
+ rs, err := c.req("PUT", path, stream, nil)
+ if err != nil {
+ return
+ }
+ defer rs.Body.Close()
+
+ status = rs.StatusCode
+ return
+}
+
+func (c *Client) createParentCollection(itemPath string) (err error) {
+ parentPath := path.Dir(itemPath)
+ if parentPath == "." || parentPath == "/" {
+ return nil
+ }
+
+ return c.MkdirAll(parentPath, 0755)
+}
diff --git a/godo/webdav/utils.go b/godo/webdav/utils.go
new file mode 100644
index 0000000..9f97aa6
--- /dev/null
+++ b/godo/webdav/utils.go
@@ -0,0 +1,113 @@
+package webdav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "io"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// PathEscape escapes all segments of a given path
+func PathEscape(path string) string {
+ s := strings.Split(path, "/")
+ for i, e := range s {
+ s[i] = url.PathEscape(e)
+ }
+ return strings.Join(s, "/")
+}
+
+// FixSlash appends a trailing / to our string
+func FixSlash(s string) string {
+ if !strings.HasSuffix(s, "/") {
+ s += "/"
+ }
+ return s
+}
+
+// FixSlashes appends and prepends a / if they are missing
+func FixSlashes(s string) string {
+ if !strings.HasPrefix(s, "/") {
+ s = "/" + s
+ }
+
+ return FixSlash(s)
+}
+
+// Join joins two paths
+func Join(path0 string, path1 string) string {
+ return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/")
+}
+
+// String pulls a string out of our io.Reader
+func String(r io.Reader) string {
+ buf := new(bytes.Buffer)
+ // TODO - make String return an error as well
+ _, _ = buf.ReadFrom(r)
+ return buf.String()
+}
+
+func parseUint(s *string) uint {
+ if n, e := strconv.ParseUint(*s, 10, 32); e == nil {
+ return uint(n)
+ }
+ return 0
+}
+
+func parseInt64(s *string) int64 {
+ if n, e := strconv.ParseInt(*s, 10, 64); e == nil {
+ return n
+ }
+ return 0
+}
+
+func parseModified(s *string) time.Time {
+ if t, e := time.Parse(time.RFC1123, *s); e == nil {
+ return t
+ }
+ return time.Unix(0, 0)
+}
+
+func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) error) error {
+ decoder := xml.NewDecoder(data)
+ for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() {
+ switch se := t.(type) {
+ case xml.StartElement:
+ if se.Name.Local == "response" {
+ if e := decoder.DecodeElement(resp, &se); e == nil {
+ if err := parse(resp); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
+type limitedReadCloser struct {
+ rc io.ReadCloser
+ remaining int
+}
+
+func (l *limitedReadCloser) Read(buf []byte) (int, error) {
+ if l.remaining <= 0 {
+ return 0, io.EOF
+ }
+
+ if len(buf) > l.remaining {
+ buf = buf[0:l.remaining]
+ }
+
+ n, err := l.rc.Read(buf)
+ l.remaining -= n
+
+ return n, err
+}
+
+func (l *limitedReadCloser) Close() error {
+ return l.rc.Close()
+}