Browse Source

add webdav client

master
skyboy 10 months ago
parent
commit
8f1f96ec81
  1. 118
      frontend/src/components/setting/SetSystem.vue
  2. 20
      frontend/src/system/config.ts
  3. 2
      frontend/src/system/core/FileOs.ts
  4. 13
      frontend/src/system/index.ts
  5. 65
      godo/cmd/main.go
  6. 3
      godo/libs/dir.go
  7. 8
      godo/sys/setting.go
  8. 409
      godo/webdav/auth.go
  9. 42
      godo/webdav/basicAuth.go
  10. 438
      godo/webdav/client.go
  11. 183
      godo/webdav/digestAuth.go
  12. 57
      godo/webdav/errors.go
  13. 77
      godo/webdav/file.go
  14. 229
      godo/webdav/httpclient.go
  15. 54
      godo/webdav/netrc.go
  16. 181
      godo/webdav/passportAuth.go
  17. 181
      godo/webdav/requests.go
  18. 113
      godo/webdav/utils.go

118
frontend/src/components/setting/SetSystem.vue

@ -2,8 +2,12 @@
<div class="container">
<div class="nav">
<ul>
<li v-for="(item, index) in items" :key="index" @click="selectItem(index)"
:class="{ active: index === activeIndex }">
<li
v-for="(item, index) in items"
:key="index"
@click="selectItem(index)"
:class="{ active: index === activeIndex }"
>
{{ item }}
</li>
</ul>
@ -13,17 +17,46 @@
<div class="setting-item" style="margin-top: 60px">
<label>存储方式</label>
<el-select v-model="config.storeType">
<el-option v-for="(item, key) in storeList" :key="key" :label="item.title" :value="item.value" />
<el-option
v-for="(item, key) in storeList"
:key="key"
:label="item.title"
:value="item.value"
/>
</el-select>
</div>
<div class="setting-item" v-if="config.storeType === 'local'">
<label>存储地址</label>
<el-input v-model="config.storePath" @click="selectFile()" placeholder="可为空,为空则取系统默认存储地址"/>
<el-input
v-model="config.storePath"
@click="selectFile()"
placeholder="可为空,为空则取系统默认存储地址"
/>
</div>
<template v-if="config.storeType === 'net'">
<div class="setting-item">
<label>服务器地址</label>
<el-input v-model="config.storenet.url" placeholder="http://192.168.1.16 不要加斜杠"/>
<el-input
v-model="config.storenet.url"
placeholder="http://192.168.1.16 不要加斜杠"
/>
</div>
</template>
<template v-if="config.storeType === 'webdav'">
<div class="setting-item">
<label>服务器地址</label>
<el-input
v-model="config.webdavClient.url"
placeholder="https://godoos.com/webdav/"
/>
</div>
<div class="setting-item">
<label>登陆用户名</label>
<el-input v-model="config.webdavClient.username" />
</div>
<div class="setting-item">
<label>登陆密码</label>
<el-input v-model="config.webdavClient.password" type="password" />
</div>
</template>
@ -34,7 +67,7 @@
</el-button>
</div>
</div>
<div v-if="1 === activeIndex">
<div class="setting-item">
<h1 class="setting-title">备份</h1>
@ -49,7 +82,12 @@
<div class="setting-item">
<label></label>
<el-button @click="selectZipfile" type="primary"> 导入 </el-button>
<input type="file" accept=".zip" style="display: none" ref="zipFileInput" />
<input
type="file"
accept=".zip"
style="display: none"
ref="zipFileInput"
/>
</div>
</div>
</div>
@ -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() {

20
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() {

2
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<any> {
const res = await fetch(`${API_BASE_URL}/read?path=${encodeURIComponent(path)}`);

13
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()

65
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 {

3
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) {

8
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
}
}

409
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")
}

42
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)
}

438
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,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&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,
`<d:propfind xmlns:d='DAV:'>
<d:prop>
<d:displayname/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getcontenttype/>
<d:getetag/>
<d:getlastmodified/>
</d:prop>
</d:propfind>`,
&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)
}
}

183
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
}

57
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,
}
}

77
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)
}

229
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"))
}

54
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 "", ""
}

181
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
}

181
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)
}

113
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()
}
Loading…
Cancel
Save