Reconstruct KeePassGO repository
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrConflict = errors.New("webdav conflict")
|
||||
|
||||
type Version struct {
|
||||
ETag string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (c Client) Open(path string) ([]byte, Version, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, c.url(path), nil)
|
||||
if err != nil {
|
||||
return nil, Version{}, fmt.Errorf("build GET request: %w", err)
|
||||
}
|
||||
c.applyAuth(req)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return nil, Version{}, fmt.Errorf("GET %s: %w", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, Version{}, fmt.Errorf("GET %s: unexpected status %d", path, resp.StatusCode)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, Version{}, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
return content, Version{ETag: resp.Header.Get("ETag")}, nil
|
||||
}
|
||||
|
||||
func (c Client) Save(path string, content io.Reader, version Version) (Version, error) {
|
||||
req, err := http.NewRequest(http.MethodPut, c.url(path), content)
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("build PUT request: %w", err)
|
||||
}
|
||||
c.applyAuth(req)
|
||||
if version.ETag != "" {
|
||||
req.Header.Set("If-Match", version.ETag)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("PUT %s: %w", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusPreconditionFailed {
|
||||
return Version{}, ErrConflict
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
|
||||
return Version{}, fmt.Errorf("PUT %s: unexpected status %d", path, resp.StatusCode)
|
||||
}
|
||||
|
||||
return Version{ETag: resp.Header.Get("ETag")}, nil
|
||||
}
|
||||
|
||||
func (c Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c Client) applyAuth(req *http.Request) {
|
||||
if c.Username == "" && c.Password == "" {
|
||||
return
|
||||
}
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
}
|
||||
|
||||
func (c Client) url(path string) string {
|
||||
base := strings.TrimRight(c.BaseURL, "/")
|
||||
path = strings.TrimLeft(path, "/")
|
||||
return base + "/" + path
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientOpenDownloadsRemoteVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Fatalf("method = %s, want GET", r.Method)
|
||||
}
|
||||
if user, pass, ok := r.BasicAuth(); !ok || user != "rustyryan" || pass != "secret" {
|
||||
t.Fatalf("basic auth = %q/%q ok=%v, want rustyryan/secret true", user, pass, ok)
|
||||
}
|
||||
w.Header().Set("ETag", `"etag-1"`)
|
||||
_, _ = io.WriteString(w, "vault-bytes")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := Client{
|
||||
HTTPClient: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
Username: "rustyryan",
|
||||
Password: "secret",
|
||||
}
|
||||
|
||||
content, version, err := client.Open("keepass.kdbx")
|
||||
if err != nil {
|
||||
t.Fatalf("Open() error = %v", err)
|
||||
}
|
||||
|
||||
if string(content) != "vault-bytes" {
|
||||
t.Fatalf("Open() content = %q, want %q", string(content), "vault-bytes")
|
||||
}
|
||||
|
||||
if version.ETag != `"etag-1"` {
|
||||
t.Fatalf("Open() ETag = %q, want %q", version.ETag, `"etag-1"`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSaveUploadsVaultWithIfMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Fatalf("method = %s, want PUT", r.Method)
|
||||
}
|
||||
if got := r.Header.Get("If-Match"); got != `"etag-1"` {
|
||||
t.Fatalf("If-Match = %q, want %q", got, `"etag-1"`)
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll() error = %v", err)
|
||||
}
|
||||
if string(body) != "updated-vault" {
|
||||
t.Fatalf("PUT body = %q, want %q", string(body), "updated-vault")
|
||||
}
|
||||
w.Header().Set("ETag", `"etag-2"`)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := Client{
|
||||
HTTPClient: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
Username: "rustyryan",
|
||||
Password: "secret",
|
||||
}
|
||||
|
||||
version, err := client.Save("keepass.kdbx", bytes.NewBufferString("updated-vault"), Version{ETag: `"etag-1"`})
|
||||
if err != nil {
|
||||
t.Fatalf("Save() error = %v", err)
|
||||
}
|
||||
|
||||
if version.ETag != `"etag-2"` {
|
||||
t.Fatalf("Save() ETag = %q, want %q", version.ETag, `"etag-2"`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSaveReturnsConflictOnVersionMismatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := Client{
|
||||
HTTPClient: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
Username: "rustyryan",
|
||||
Password: "secret",
|
||||
}
|
||||
|
||||
_, err := client.Save("keepass.kdbx", bytes.NewBufferString("updated-vault"), Version{ETag: `"etag-1"`})
|
||||
if err != ErrConflict {
|
||||
t.Fatalf("Save() error = %v, want %v", err, ErrConflict)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user