Merge branch 'master' into lock-test-multi-repo
This commit is contained in:
commit
b92cac52f5
87
api/api.go
87
api/api.go
@ -1,87 +0,0 @@
|
||||
// Package api provides the interface for querying LFS servers (metadata)
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// BatchSingle calls the batch API with just a single object.
|
||||
func BatchSingle(cfg *config.Configuration, inobj *ObjectResource, operation string, transferAdapters []string) (obj *ObjectResource, transferAdapter string, e error) {
|
||||
objs, adapterName, err := Batch(cfg, []*ObjectResource{inobj}, operation, transferAdapters)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(objs) > 0 {
|
||||
return objs[0], adapterName, nil
|
||||
}
|
||||
return nil, "", fmt.Errorf("Object not found")
|
||||
}
|
||||
|
||||
// Batch calls the batch API and returns object results
|
||||
func Batch(cfg *config.Configuration, objects []*ObjectResource, operation string, transferAdapters []string) (objs []*ObjectResource, transferAdapter string, e error) {
|
||||
if len(objects) == 0 {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
// Compatibility; omit transfers list when only basic
|
||||
// older schemas included `additionalproperties=false`
|
||||
if len(transferAdapters) == 1 && transferAdapters[0] == "basic" {
|
||||
transferAdapters = nil
|
||||
}
|
||||
|
||||
o := &batchRequest{Operation: operation, Objects: objects, TransferAdapterNames: transferAdapters}
|
||||
by, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "batch request")
|
||||
}
|
||||
|
||||
req, err := NewBatchRequest(cfg, operation)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "batch request")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", MediaType)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
req.ContentLength = int64(len(by))
|
||||
req.Body = tools.NewReadSeekCloserWrapper(bytes.NewReader(by))
|
||||
|
||||
tracerx.Printf("api: batch %d files", len(objects))
|
||||
|
||||
res, bresp, err := DoBatchRequest(cfg, req)
|
||||
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
return nil, "", errors.NewRetriableError(err)
|
||||
}
|
||||
|
||||
if res.StatusCode == 0 {
|
||||
return nil, "", errors.NewRetriableError(err)
|
||||
}
|
||||
|
||||
if errors.IsAuthError(err) {
|
||||
httputil.SetAuthType(cfg, req, res)
|
||||
return Batch(cfg, objects, operation, transferAdapters)
|
||||
}
|
||||
|
||||
tracerx.Printf("api error: %s", err)
|
||||
return nil, "", errors.Wrap(err, "batch response")
|
||||
}
|
||||
httputil.LogTransfer(cfg, "lfs.batch", res)
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, "", errors.Errorf("Invalid status for %s: %d", httputil.TraceHttpReq(req), res.StatusCode)
|
||||
}
|
||||
|
||||
return bresp.Objects, bresp.TransferAdapterName, nil
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import "github.com/git-lfs/git-lfs/config"
|
||||
|
||||
type Operation string
|
||||
|
||||
const (
|
||||
UploadOperation Operation = "upload"
|
||||
DownloadOperation Operation = "download"
|
||||
)
|
||||
|
||||
// Client exposes the LFS API to callers through a multitude of different
|
||||
// services and transport mechanisms. Callers can make a *RequestSchema using
|
||||
// any service that is attached to the Client, and then execute a request based
|
||||
// on that schema using the `Do()` method.
|
||||
//
|
||||
// A prototypical example follows:
|
||||
// ```
|
||||
// apiResponse, schema := client.Locks.Lock(request)
|
||||
// resp, err := client.Do(schema)
|
||||
// if err != nil {
|
||||
// handleErr(err)
|
||||
// }
|
||||
//
|
||||
// fmt.Println(apiResponse.Lock)
|
||||
// ```
|
||||
type Client struct {
|
||||
// Locks is the LockService used to interact with the Git LFS file-
|
||||
// locking API.
|
||||
Locks LockService
|
||||
|
||||
// lifecycle is the lifecycle used by all requests through this client.
|
||||
lifecycle Lifecycle
|
||||
}
|
||||
|
||||
// NewClient instantiates and returns a new instance of *Client, with the given
|
||||
// lifecycle.
|
||||
//
|
||||
// If no lifecycle is given, a HttpLifecycle is used by default.
|
||||
func NewClient(lifecycle Lifecycle) *Client {
|
||||
if lifecycle == nil {
|
||||
lifecycle = NewHttpLifecycle(config.Config)
|
||||
}
|
||||
|
||||
return &Client{lifecycle: lifecycle}
|
||||
}
|
||||
|
||||
// Do preforms the request assosicated with the given *RequestSchema by
|
||||
// delegating into the Lifecycle in use.
|
||||
//
|
||||
// If any error was encountered while either building, executing or cleaning up
|
||||
// the request, then it will be returned immediately, and the request can be
|
||||
// treated as invalid.
|
||||
//
|
||||
// If no error occured, an api.Response will be returned, along with a `nil`
|
||||
// error. At this point, the body of the response has been serialized into
|
||||
// `schema.Into`, and the body has been closed.
|
||||
func (c *Client) Do(schema *RequestSchema) (Response, error) {
|
||||
req, err := c.lifecycle.Build(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.lifecycle.Execute(req, schema.Into)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = c.lifecycle.Cleanup(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientUsesLifecycleToExecuteSchemas(t *testing.T) {
|
||||
schema := new(api.RequestSchema)
|
||||
req := new(http.Request)
|
||||
resp := new(api.HttpResponse)
|
||||
|
||||
lifecycle := new(MockLifecycle)
|
||||
lifecycle.On("Build", schema).Return(req, nil).Once()
|
||||
lifecycle.On("Execute", req, schema.Into).Return(resp, nil).Once()
|
||||
lifecycle.On("Cleanup", resp).Return(nil).Once()
|
||||
|
||||
client := api.NewClient(lifecycle)
|
||||
r1, err := client.Do(schema)
|
||||
|
||||
assert.Equal(t, resp, r1)
|
||||
assert.Nil(t, err)
|
||||
lifecycle.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestClientHaltsIfSchemaCannotBeBuilt(t *testing.T) {
|
||||
schema := new(api.RequestSchema)
|
||||
|
||||
lifecycle := new(MockLifecycle)
|
||||
lifecycle.On("Build", schema).Return(nil, errors.New("uh-oh!")).Once()
|
||||
|
||||
client := api.NewClient(lifecycle)
|
||||
resp, err := client.Do(schema)
|
||||
|
||||
lifecycle.AssertExpectations(t)
|
||||
assert.Nil(t, resp)
|
||||
assert.Equal(t, "uh-oh!", err.Error())
|
||||
}
|
||||
|
||||
func TestClientHaltsIfSchemaCannotBeExecuted(t *testing.T) {
|
||||
schema := new(api.RequestSchema)
|
||||
req := new(http.Request)
|
||||
|
||||
lifecycle := new(MockLifecycle)
|
||||
lifecycle.On("Build", schema).Return(req, nil).Once()
|
||||
lifecycle.On("Execute", req, schema.Into).Return(nil, errors.New("uh-oh!")).Once()
|
||||
|
||||
client := api.NewClient(lifecycle)
|
||||
resp, err := client.Do(schema)
|
||||
|
||||
lifecycle.AssertExpectations(t)
|
||||
assert.Nil(t, resp)
|
||||
assert.Equal(t, "uh-oh!", err.Error())
|
||||
}
|
||||
|
||||
func TestClientReturnsCleanupErrors(t *testing.T) {
|
||||
schema := new(api.RequestSchema)
|
||||
req := new(http.Request)
|
||||
resp := new(api.HttpResponse)
|
||||
|
||||
lifecycle := new(MockLifecycle)
|
||||
lifecycle.On("Build", schema).Return(req, nil).Once()
|
||||
lifecycle.On("Execute", req, schema.Into).Return(resp, nil).Once()
|
||||
lifecycle.On("Cleanup", resp).Return(errors.New("uh-oh!")).Once()
|
||||
|
||||
client := api.NewClient(lifecycle)
|
||||
r1, err := client.Do(schema)
|
||||
|
||||
lifecycle.AssertExpectations(t)
|
||||
assert.Nil(t, r1)
|
||||
assert.Equal(t, "uh-oh!", err.Error())
|
||||
}
|
||||
|
||||
func newBatchResponse(operation string, objects ...*api.ObjectResource) batchResponse {
|
||||
return batchResponse{
|
||||
Operation: operation,
|
||||
Objects: objects,
|
||||
}
|
||||
}
|
||||
|
||||
type batchResponse struct {
|
||||
Operation string `json:"operation,omitempty"`
|
||||
Objects []*api.ObjectResource `json:"objects"`
|
||||
}
|
@ -1,403 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
)
|
||||
|
||||
func TestSuccessfulDownload(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer func() {
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
tmp := tempdir(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
|
||||
if r.Method != "POST" {
|
||||
t.Error("bad http verb")
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Error("Invalid Accept")
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if len(auth) == 0 {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
|
||||
if auth != expectedAuth(t, server) {
|
||||
t.Errorf("Invalid Authorization: %q", auth)
|
||||
}
|
||||
|
||||
obj := &api.ObjectResource{
|
||||
Oid: "oid",
|
||||
Size: 4,
|
||||
Actions: map[string]*api.LinkRelation{
|
||||
"download": &api.LinkRelation{
|
||||
Href: server.URL + "/download",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
obj, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: "oid"}, "download", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("unexpected size: %d", obj.Size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// nearly identical to TestSuccessfulDownload
|
||||
// called multiple times to return different 3xx status codes
|
||||
func TestSuccessfulDownloadWithRedirects(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer func() {
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
tmp := tempdir(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
mux.HandleFunc("/redirect/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", server.URL+"/redirect2/objects/batch")
|
||||
w.WriteHeader(307)
|
||||
t.Log("redirect with 307")
|
||||
})
|
||||
|
||||
mux.HandleFunc("/redirect2/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", server.URL+"/media/objects/batch")
|
||||
w.WriteHeader(307)
|
||||
t.Log("redirect with 307")
|
||||
})
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Error("Invalid Accept")
|
||||
w.WriteHeader(406)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if len(auth) == 0 {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
|
||||
if auth != expectedAuth(t, server) {
|
||||
t.Errorf("Invalid Authorization: %q", auth)
|
||||
}
|
||||
|
||||
obj := &api.ObjectResource{
|
||||
Oid: "oid",
|
||||
Size: 4,
|
||||
Actions: map[string]*api.LinkRelation{
|
||||
"download": &api.LinkRelation{
|
||||
Href: server.URL + "/download",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/redirect",
|
||||
},
|
||||
})
|
||||
|
||||
obj, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: "oid"}, "download", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected error for 307 status: %s", err)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("unexpected size for 307 status: %d", obj.Size)
|
||||
}
|
||||
}
|
||||
|
||||
// nearly identical to TestSuccessfulDownload
|
||||
// the api request returns a custom Authorization header
|
||||
func TestSuccessfulDownloadWithAuthorization(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer func() {
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
tmp := tempdir(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Error("Invalid Accept")
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if len(auth) == 0 {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
|
||||
if auth != expectedAuth(t, server) {
|
||||
t.Errorf("Invalid Authorization: %q", auth)
|
||||
}
|
||||
|
||||
obj := &api.ObjectResource{
|
||||
Oid: "oid",
|
||||
Size: 4,
|
||||
Actions: map[string]*api.LinkRelation{
|
||||
"download": &api.LinkRelation{
|
||||
Href: server.URL + "/download",
|
||||
Header: map[string]string{
|
||||
"A": "1",
|
||||
"Authorization": "custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", "application/json; charset=utf-8")
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
obj, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: "oid"}, "download", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("unexpected size: %d", obj.Size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDownloadAPIError(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer func() {
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
tmp := tempdir(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.batch": "false",
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
_, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: "oid"}, "download", []string{"basic"})
|
||||
if err == nil {
|
||||
t.Fatal("no error?")
|
||||
}
|
||||
|
||||
if errors.IsFatalError(err) {
|
||||
t.Fatal("should not panic")
|
||||
}
|
||||
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := "batch response: " + fmt.Sprintf(httputil.GetDefaultError(404), server.URL+"/media/objects/batch")
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("Expected: %s\nGot: %s", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// guards against connection errors that only seem to happen on debian docker
|
||||
// images.
|
||||
func isDockerConnectionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if os.Getenv("TRAVIS") == "true" {
|
||||
return false
|
||||
}
|
||||
|
||||
e := err.Error()
|
||||
return strings.Contains(e, "connection reset by peer") ||
|
||||
strings.Contains(e, "connection refused")
|
||||
}
|
||||
|
||||
func tempdir(t *testing.T) string {
|
||||
dir, err := ioutil.TempDir("", "git-lfs-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting temp dir: %s", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func expectedAuth(t *testing.T, server *httptest.Server) string {
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s:%s", u.Host, "monkey")
|
||||
return "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
}
|
||||
|
||||
var (
|
||||
TestCredentialsFunc auth.CredentialFunc
|
||||
origCredentialsFunc auth.CredentialFunc
|
||||
)
|
||||
|
||||
func init() {
|
||||
TestCredentialsFunc = func(cfg *config.Configuration, input auth.Creds, subCommand string) (auth.Creds, error) {
|
||||
output := make(auth.Creds)
|
||||
for key, value := range input {
|
||||
output[key] = value
|
||||
}
|
||||
if _, ok := output["username"]; !ok {
|
||||
output["username"] = input["host"]
|
||||
}
|
||||
output["password"] = "monkey"
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Override the credentials func for testing
|
||||
func SetupTestCredentialsFunc() {
|
||||
origCredentialsFunc = auth.SetCredentialsFunc(TestCredentialsFunc)
|
||||
}
|
||||
|
||||
// Put the original credentials func back
|
||||
func RestoreCredentialsFunc() {
|
||||
auth.SetCredentialsFunc(origCredentialsFunc)
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoOperationGiven is an error which is returned when no operation
|
||||
// is provided in a RequestSchema object.
|
||||
ErrNoOperationGiven = errors.New("lfs/api: no operation provided in schema")
|
||||
)
|
||||
|
||||
// HttpLifecycle serves as the default implementation of the Lifecycle interface
|
||||
// for HTTP requests. Internally, it leverages the *http.Client type to execute
|
||||
// HTTP requests against a root *url.URL, as given in `NewHttpLifecycle`.
|
||||
type HttpLifecycle struct {
|
||||
cfg *config.Configuration
|
||||
}
|
||||
|
||||
var _ Lifecycle = new(HttpLifecycle)
|
||||
|
||||
// NewHttpLifecycle initializes a new instance of the *HttpLifecycle type with a
|
||||
// new *http.Client, and the given root (see above).
|
||||
// Passing a nil Configuration will use the global config
|
||||
func NewHttpLifecycle(cfg *config.Configuration) *HttpLifecycle {
|
||||
if cfg == nil {
|
||||
cfg = config.Config
|
||||
}
|
||||
return &HttpLifecycle{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements the Lifecycle.Build function.
|
||||
//
|
||||
// HttpLifecycle in particular, builds an absolute path by parsing and then
|
||||
// relativizing the `schema.Path` with respsect to the `HttpLifecycle.root`. If
|
||||
// there was an error in determining this URL, then that error will be returned,
|
||||
//
|
||||
// After this is complete, a body is attached to the request if the
|
||||
// schema contained one. If a body was present, and there an error occurred while
|
||||
// serializing it into JSON, then that error will be returned and the
|
||||
// *http.Request will not be generated.
|
||||
//
|
||||
// In all cases, credentials are attached to the HTTP request as described in
|
||||
// the `auth` package (see github.com/git-lfs/git-lfs/auth#GetCreds).
|
||||
//
|
||||
// Finally, all of these components are combined together and the resulting
|
||||
// request is returned.
|
||||
func (l *HttpLifecycle) Build(schema *RequestSchema) (*http.Request, error) {
|
||||
path, err := l.absolutePath(schema.Operation, schema.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := l.body(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(schema.Method, path.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = auth.GetCreds(l.cfg, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.URL.RawQuery = l.queryParameters(schema).Encode()
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Execute implements the Lifecycle.Execute function.
|
||||
//
|
||||
// Internally, the *http.Client is used to execute the underlying *http.Request.
|
||||
// If the client returned an error corresponding to a failure to make the
|
||||
// request, then that error will be returned immediately, and the response is
|
||||
// guaranteed not to be serialized.
|
||||
//
|
||||
// Once the response has been gathered from the server, it is unmarshled into
|
||||
// the given `into interface{}` which is identical to the one provided in the
|
||||
// original RequestSchema. If an error occured while decoding, then that error
|
||||
// is returned.
|
||||
//
|
||||
// Otherwise, the api.Response is returned, along with no error, signaling that
|
||||
// the request completed successfully.
|
||||
func (l *HttpLifecycle) Execute(req *http.Request, into interface{}) (Response, error) {
|
||||
resp, err := httputil.DoHttpRequestWithRedirects(l.cfg, req, []*http.Request{}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(taylor): check status >=500, handle content type, return error,
|
||||
// halt immediately.
|
||||
|
||||
if into != nil {
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
if err = decoder.Decode(into); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return WrapHttpResponse(resp), nil
|
||||
}
|
||||
|
||||
// Cleanup implements the Lifecycle.Cleanup function by closing the Body
|
||||
// attached to the response.
|
||||
func (l *HttpLifecycle) Cleanup(resp Response) error {
|
||||
return resp.Body().Close()
|
||||
}
|
||||
|
||||
// absolutePath returns the absolute path made by combining a given relative
|
||||
// path with the root URL of the endpoint corresponding to the given operation.
|
||||
//
|
||||
// If there was an error in parsing the relative path, then that error will be
|
||||
// returned.
|
||||
func (l *HttpLifecycle) absolutePath(operation Operation, path string) (*url.URL, error) {
|
||||
if len(operation) == 0 {
|
||||
return nil, ErrNoOperationGiven
|
||||
}
|
||||
|
||||
root, err := url.Parse(l.cfg.Endpoint(string(operation)).Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rel, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return root.ResolveReference(rel), nil
|
||||
}
|
||||
|
||||
// body returns an io.Reader which reads out a JSON-encoded copy of the payload
|
||||
// attached to a given *RequestSchema, if it is present. If no body is present
|
||||
// in the request, then nil is returned instead.
|
||||
//
|
||||
// If an error was encountered while attempting to marshal the body, then that
|
||||
// will be returned instead, along with a nil io.Reader.
|
||||
func (l *HttpLifecycle) body(schema *RequestSchema) (io.ReadCloser, error) {
|
||||
if schema.Body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
body, err := json.Marshal(schema.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioutil.NopCloser(bytes.NewReader(body)), nil
|
||||
}
|
||||
|
||||
// queryParameters returns a url.Values containing all of the provided query
|
||||
// parameters as given in the *RequestSchema. If no query parameters were given,
|
||||
// then an empty url.Values is returned instead.
|
||||
func (l *HttpLifecycle) queryParameters(schema *RequestSchema) url.Values {
|
||||
vals := url.Values{}
|
||||
if schema.Query != nil {
|
||||
for k, v := range schema.Query {
|
||||
vals.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return vals
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewTestConfig() *config.Configuration {
|
||||
c := config.NewFrom(config.Values{})
|
||||
c.SetManualEndpoint(config.Endpoint{Url: "https://example.com"})
|
||||
return c
|
||||
}
|
||||
func TestHttpLifecycleMakesRequestsAgainstAbsolutePath(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
req, err := l.Build(&api.RequestSchema{
|
||||
Path: "/foo",
|
||||
Operation: api.DownloadOperation,
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://example.com/foo", req.URL.String())
|
||||
}
|
||||
|
||||
func TestHttpLifecycleAttachesQueryParameters(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
req, err := l.Build(&api.RequestSchema{
|
||||
Path: "/foo",
|
||||
Operation: api.DownloadOperation,
|
||||
Query: map[string]string{
|
||||
"a": "b",
|
||||
},
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://example.com/foo?a=b", req.URL.String())
|
||||
}
|
||||
|
||||
func TestHttpLifecycleAttachesBodyWhenPresent(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
req, err := l.Build(&api.RequestSchema{
|
||||
Operation: api.DownloadOperation,
|
||||
Body: struct {
|
||||
Foo string `json:"foo"`
|
||||
}{"bar"},
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", string(body))
|
||||
}
|
||||
|
||||
func TestHttpLifecycleDoesNotAttachBodyWhenEmpty(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
req, err := l.Build(&api.RequestSchema{
|
||||
Operation: api.DownloadOperation,
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, req.Body)
|
||||
}
|
||||
|
||||
func TestHttpLifecycleErrsWithoutOperation(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
req, err := l.Build(&api.RequestSchema{
|
||||
Path: "/foo",
|
||||
})
|
||||
|
||||
assert.Equal(t, api.ErrNoOperationGiven, err)
|
||||
assert.Nil(t, req)
|
||||
}
|
||||
|
||||
func TestHttpLifecycleExecutesRequestWithoutBody(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
var called bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
|
||||
assert.Equal(t, "/path", r.URL.RequestURI())
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", server.URL+"/path", nil)
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
_, err := l.Execute(req, nil)
|
||||
|
||||
assert.True(t, called)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestHttpLifecycleExecutesRequestWithBody(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
type Response struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
|
||||
var called bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
|
||||
w.Write([]byte("{\"foo\":\"bar\"}"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", server.URL+"/path", nil)
|
||||
|
||||
l := api.NewHttpLifecycle(NewTestConfig())
|
||||
resp := new(Response)
|
||||
_, err := l.Execute(req, resp)
|
||||
|
||||
assert.True(t, called)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "bar", resp.Foo)
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HttpResponse is an implementation of the Response interface capable of
|
||||
// handling HTTP responses. At its core, it works by wrapping an *http.Response.
|
||||
type HttpResponse struct {
|
||||
// r is the underlying *http.Response that is being wrapped.
|
||||
r *http.Response
|
||||
}
|
||||
|
||||
// WrapHttpResponse returns a wrapped *HttpResponse implementing the Repsonse
|
||||
// type by using the given *http.Response.
|
||||
func WrapHttpResponse(r *http.Response) *HttpResponse {
|
||||
return &HttpResponse{
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Response = new(HttpResponse)
|
||||
|
||||
// Status implements the Response.Status function, and returns the status given
|
||||
// by the underlying *http.Response.
|
||||
func (h *HttpResponse) Status() string {
|
||||
return h.r.Status
|
||||
}
|
||||
|
||||
// StatusCode implements the Response.StatusCode function, and returns the
|
||||
// status code given by the underlying *http.Response.
|
||||
func (h *HttpResponse) StatusCode() int {
|
||||
return h.r.StatusCode
|
||||
}
|
||||
|
||||
// Proto implements the Response.Proto function, and returns the proto given by
|
||||
// the underlying *http.Response.
|
||||
func (h *HttpResponse) Proto() string {
|
||||
return h.r.Proto
|
||||
}
|
||||
|
||||
// Body implements the Response.Body function, and returns the body as given by
|
||||
// the underlying *http.Response.
|
||||
func (h *HttpResponse) Body() io.ReadCloser {
|
||||
return h.r.Body
|
||||
}
|
||||
|
||||
// Header returns the underlying *http.Response's header.
|
||||
func (h *HttpResponse) Header() http.Header {
|
||||
return h.r.Header
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWrappedHttpResponsesMatchInternal(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.1",
|
||||
Body: ioutil.NopCloser(new(bytes.Buffer)),
|
||||
}
|
||||
wrapped := api.WrapHttpResponse(resp)
|
||||
|
||||
assert.Equal(t, resp.Status, wrapped.Status())
|
||||
assert.Equal(t, resp.StatusCode, wrapped.StatusCode())
|
||||
assert.Equal(t, resp.Proto, wrapped.Proto())
|
||||
assert.Equal(t, resp.Body, wrapped.Body())
|
||||
assert.Equal(t, resp.Header, wrapped.Header())
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
// TODO: extract interface for *http.Request; update methods. This will be in a
|
||||
// later iteration of the API client.
|
||||
|
||||
// A Lifecycle represents and encapsulates the behavior on an API request from
|
||||
// inception to cleanup.
|
||||
//
|
||||
// At a high level, it turns an *api.RequestSchema into an
|
||||
// api.Response (and optionally an error). Lifecycle does so by providing
|
||||
// several more fine-grained methods that are used by the client to manage the
|
||||
// lifecycle of a request in a platform-agnostic fashion.
|
||||
type Lifecycle interface {
|
||||
// Build creates a sendable request by using the given RequestSchema as
|
||||
// a model.
|
||||
Build(req *RequestSchema) (*http.Request, error)
|
||||
|
||||
// Execute transforms generated request into a wrapped repsonse, (and
|
||||
// optionally an error, if the request failed), and serializes the
|
||||
// response into the `into interface{}`, if one was provided.
|
||||
Execute(req *http.Request, into interface{}) (Response, error)
|
||||
|
||||
// Cleanup is called after the request has been completed and its
|
||||
// response has been processed. It is meant to preform any post-request
|
||||
// actions necessary, like closing or resetting the connection. If an
|
||||
// error was encountered in doing this operation, it should be returned
|
||||
// from this method, otherwise nil.
|
||||
Cleanup(resp Response) error
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockLifecycle struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var _ api.Lifecycle = new(MockLifecycle)
|
||||
|
||||
func (l *MockLifecycle) Build(req *api.RequestSchema) (*http.Request, error) {
|
||||
args := l.Called(req)
|
||||
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return args.Get(0).(*http.Request), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *MockLifecycle) Execute(req *http.Request, into interface{}) (api.Response, error) {
|
||||
args := l.Called(req, into)
|
||||
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return args.Get(0).(api.Response), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *MockLifecycle) Cleanup(resp api.Response) error {
|
||||
args := l.Called(resp)
|
||||
return args.Error(0)
|
||||
}
|
247
api/lock_api.go
247
api/lock_api.go
@ -1,247 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LockService is an API service which encapsulates the Git LFS Locking API.
|
||||
type LockService struct{}
|
||||
|
||||
// Lock generates a *RequestSchema that is used to preform the "attempt lock"
|
||||
// API method.
|
||||
//
|
||||
// If a lock is already present, or if the server was unable to generate the
|
||||
// lock, the Err field of the LockResponse type will be populated with a more
|
||||
// detailed error describing the situation.
|
||||
//
|
||||
// If the caller does not have the minimum commit necessary to obtain the lock
|
||||
// on that file, then the CommitNeeded field will be populated in the
|
||||
// LockResponse, signaling that more commits are needed.
|
||||
//
|
||||
// In the successful case, a new Lock will be returned and granted to the
|
||||
// caller.
|
||||
func (s *LockService) Lock(req *LockRequest) (*RequestSchema, *LockResponse) {
|
||||
var resp LockResponse
|
||||
|
||||
return &RequestSchema{
|
||||
Method: "POST",
|
||||
Path: "/locks",
|
||||
Operation: UploadOperation,
|
||||
Body: req,
|
||||
Into: &resp,
|
||||
}, &resp
|
||||
}
|
||||
|
||||
// Search generates a *RequestSchema that is used to preform the "search for
|
||||
// locks" API method.
|
||||
//
|
||||
// Searches can be scoped to match specific parameters by using the Filters
|
||||
// field in the given LockSearchRequest. If no matching Locks were found, then
|
||||
// the Locks field of the response will be empty.
|
||||
//
|
||||
// If the client expects that the server will return many locks, then the client
|
||||
// can choose to paginate that response. Pagination is preformed by limiting the
|
||||
// amount of results per page, and the server will inform the client of the ID
|
||||
// of the last returned lock. Since the server is guaranteed to return results
|
||||
// in reverse chronological order, the client simply sends the last ID it
|
||||
// processed along with the next request, and the server will continue where it
|
||||
// left off.
|
||||
//
|
||||
// If the server was unable to process the lock search request, then the Error
|
||||
// field will be populated in the response.
|
||||
//
|
||||
// In the successful case, one or more locks will be returned as a part of the
|
||||
// response.
|
||||
func (s *LockService) Search(req *LockSearchRequest) (*RequestSchema, *LockList) {
|
||||
var resp LockList
|
||||
|
||||
query := make(map[string]string)
|
||||
for _, filter := range req.Filters {
|
||||
query[filter.Property] = filter.Value
|
||||
}
|
||||
|
||||
if req.Cursor != "" {
|
||||
query["cursor"] = req.Cursor
|
||||
}
|
||||
|
||||
if req.Limit != 0 {
|
||||
query["limit"] = strconv.Itoa(req.Limit)
|
||||
}
|
||||
|
||||
return &RequestSchema{
|
||||
Method: "GET",
|
||||
Path: "/locks",
|
||||
Operation: UploadOperation,
|
||||
Query: query,
|
||||
Into: &resp,
|
||||
}, &resp
|
||||
}
|
||||
|
||||
// Unlock generates a *RequestSchema that is used to preform the "unlock" API
|
||||
// method, against a particular lock potentially with --force.
|
||||
//
|
||||
// This method's corresponding response type will either contain a reference to
|
||||
// the lock that was unlocked, or an error that was experienced by the server in
|
||||
// unlocking it.
|
||||
func (s *LockService) Unlock(id string, force bool) (*RequestSchema, *UnlockResponse) {
|
||||
var resp UnlockResponse
|
||||
|
||||
return &RequestSchema{
|
||||
Method: "POST",
|
||||
Path: fmt.Sprintf("/locks/%s/unlock", id),
|
||||
Operation: UploadOperation,
|
||||
Body: &UnlockRequest{id, force},
|
||||
Into: &resp,
|
||||
}, &resp
|
||||
}
|
||||
|
||||
// Lock represents a single lock that against a particular path.
|
||||
//
|
||||
// Locks returned from the API may or may not be currently active, according to
|
||||
// the Expired flag.
|
||||
type Lock struct {
|
||||
// Id is the unique identifier corresponding to this particular Lock. It
|
||||
// must be consistent with the local copy, and the server's copy.
|
||||
Id string `json:"id"`
|
||||
// Path is an absolute path to the file that is locked as a part of this
|
||||
// lock.
|
||||
Path string `json:"path"`
|
||||
// Committer is the author who initiated this lock.
|
||||
Committer Committer `json:"committer"`
|
||||
// CommitSHA is the commit that this Lock was created against. It is
|
||||
// strictly equal to the SHA of the minimum commit negotiated in order
|
||||
// to create this lock.
|
||||
CommitSHA string `json:"commit_sha"`
|
||||
// LockedAt is a required parameter that represents the instant in time
|
||||
// that this lock was created. For most server implementations, this
|
||||
// should be set to the instant at which the lock was initially
|
||||
// received.
|
||||
LockedAt time.Time `json:"locked_at"`
|
||||
// ExpiresAt is an optional parameter that represents the instant in
|
||||
// time that the lock stopped being active. If the lock is still active,
|
||||
// the server can either a) not send this field, or b) send the
|
||||
// zero-value of time.Time.
|
||||
UnlockedAt time.Time `json:"unlocked_at,omitempty"`
|
||||
}
|
||||
|
||||
// Active returns whether or not the given lock is still active against the file
|
||||
// that it is protecting.
|
||||
func (l *Lock) Active() bool {
|
||||
return l.UnlockedAt.IsZero()
|
||||
}
|
||||
|
||||
// Committer represents a "First Last <email@domain.com>" pair.
|
||||
type Committer struct {
|
||||
// Name is the name of the individual who would like to obtain the
|
||||
// lock, for instance: "Rick Olson".
|
||||
Name string `json:"name"`
|
||||
// Email is the email assopsicated with the individual who would
|
||||
// like to obtain the lock, for instance: "rick@github.com".
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func NewCommitter(name, email string) Committer {
|
||||
return Committer{Name: name, Email: email}
|
||||
}
|
||||
|
||||
// LockRequest encapsulates the payload sent across the API when a client would
|
||||
// like to obtain a lock against a particular path on a given remote.
|
||||
type LockRequest struct {
|
||||
// Path is the path that the client would like to obtain a lock against.
|
||||
Path string `json:"path"`
|
||||
// LatestRemoteCommit is the SHA of the last known commit from the
|
||||
// remote that we are trying to create the lock against, as found in
|
||||
// `.git/refs/origin/<name>`.
|
||||
LatestRemoteCommit string `json:"latest_remote_commit"`
|
||||
// Committer is the individual that wishes to obtain the lock.
|
||||
Committer Committer `json:"committer"`
|
||||
}
|
||||
|
||||
// LockResponse encapsulates the information sent over the API in response to
|
||||
// a `LockRequest`.
|
||||
type LockResponse struct {
|
||||
// Lock is the Lock that was optionally created in response to the
|
||||
// payload that was sent (see above). If the lock already exists, then
|
||||
// the existing lock is sent in this field instead, and the author of
|
||||
// that lock remains the same, meaning that the client failed to obtain
|
||||
// that lock. An HTTP status of "409 - Conflict" is used here.
|
||||
//
|
||||
// If the lock was unable to be created, this field will hold the
|
||||
// zero-value of Lock and the Err field will provide a more detailed set
|
||||
// of information.
|
||||
//
|
||||
// If an error was experienced in creating this lock, then the
|
||||
// zero-value of Lock should be sent here instead.
|
||||
Lock *Lock `json:"lock"`
|
||||
// CommitNeeded holds the minimum commit SHA that client must have to
|
||||
// obtain the lock.
|
||||
CommitNeeded string `json:"commit_needed,omitempty"`
|
||||
// Err is the optional error that was encountered while trying to create
|
||||
// the above lock.
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UnlockRequest encapsulates the data sent in an API request to remove a lock.
|
||||
type UnlockRequest struct {
|
||||
// Id is the Id of the lock that the user wishes to unlock.
|
||||
Id string `json:"id"`
|
||||
// Force determines whether or not the lock should be "forcibly"
|
||||
// unlocked; that is to say whether or not a given individual should be
|
||||
// able to break a different individual's lock.
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
// UnlockResponse is the result sent back from the API when asked to remove a
|
||||
// lock.
|
||||
type UnlockResponse struct {
|
||||
// Lock is the lock corresponding to the asked-about lock in the
|
||||
// `UnlockPayload` (see above). If no matching lock was found, this
|
||||
// field will take the zero-value of Lock, and Err will be non-nil.
|
||||
Lock *Lock `json:"lock"`
|
||||
// Err is an optional field which holds any error that was experienced
|
||||
// while removing the lock.
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Filter represents a single qualifier to apply against a set of locks.
|
||||
type Filter struct {
|
||||
// Property is the property to search against.
|
||||
// Value is the value that the property must take.
|
||||
Property, Value string
|
||||
}
|
||||
|
||||
// LockSearchRequest encapsulates the request sent to the server when the client
|
||||
// would like a list of locks that match the given criteria.
|
||||
type LockSearchRequest struct {
|
||||
// Filters is the set of filters to query against. If the client wishes
|
||||
// to obtain a list of all locks, an empty array should be passed here.
|
||||
Filters []Filter
|
||||
// Cursor is an optional field used to tell the server which lock was
|
||||
// seen last, if scanning through multiple pages of results.
|
||||
//
|
||||
// Servers must return a list of locks sorted in reverse chronological
|
||||
// order, so the Cursor provides a consistent method of viewing all
|
||||
// locks, even if more were created between two requests.
|
||||
Cursor string
|
||||
// Limit is the maximum number of locks to return in a single page.
|
||||
Limit int
|
||||
}
|
||||
|
||||
// LockList encapsulates a set of Locks.
|
||||
type LockList struct {
|
||||
// Locks is the set of locks returned back, typically matching the query
|
||||
// parameters sent in the LockListRequest call. If no locks were matched
|
||||
// from a given query, then `Locks` will be represented as an empty
|
||||
// array.
|
||||
Locks []Lock `json:"locks"`
|
||||
// NextCursor returns the Id of the Lock the client should update its
|
||||
// cursor to, if there are multiple pages of results for a particular
|
||||
// `LockListRequest`.
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
// Err populates any error that was encountered during the search. If no
|
||||
// error was encountered and the operation was succesful, then a value
|
||||
// of nil will be passed here.
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/git-lfs/git-lfs/api/schema"
|
||||
)
|
||||
|
||||
var LockService api.LockService
|
||||
|
||||
func TestSuccessfullyObtainingALock(t *testing.T) {
|
||||
got, body := LockService.Lock(new(api.LockRequest))
|
||||
|
||||
AssertRequestSchema(t, &api.RequestSchema{
|
||||
Method: "POST",
|
||||
Path: "/locks",
|
||||
Operation: api.UploadOperation,
|
||||
Body: new(api.LockRequest),
|
||||
Into: body,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestLockSearchWithFilters(t *testing.T) {
|
||||
got, body := LockService.Search(&api.LockSearchRequest{
|
||||
Filters: []api.Filter{
|
||||
{"branch", "master"},
|
||||
{"path", "/path/to/file"},
|
||||
},
|
||||
})
|
||||
|
||||
AssertRequestSchema(t, &api.RequestSchema{
|
||||
Method: "GET",
|
||||
Query: map[string]string{
|
||||
"branch": "master",
|
||||
"path": "/path/to/file",
|
||||
},
|
||||
Path: "/locks",
|
||||
Operation: api.UploadOperation,
|
||||
Into: body,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestLockSearchWithNextCursor(t *testing.T) {
|
||||
got, body := LockService.Search(&api.LockSearchRequest{
|
||||
Cursor: "some-lock-id",
|
||||
})
|
||||
|
||||
AssertRequestSchema(t, &api.RequestSchema{
|
||||
Method: "GET",
|
||||
Query: map[string]string{
|
||||
"cursor": "some-lock-id",
|
||||
},
|
||||
Path: "/locks",
|
||||
Operation: api.UploadOperation,
|
||||
Into: body,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestLockSearchWithLimit(t *testing.T) {
|
||||
got, body := LockService.Search(&api.LockSearchRequest{
|
||||
Limit: 20,
|
||||
})
|
||||
|
||||
AssertRequestSchema(t, &api.RequestSchema{
|
||||
Method: "GET",
|
||||
Query: map[string]string{
|
||||
"limit": "20",
|
||||
},
|
||||
Path: "/locks",
|
||||
Operation: api.UploadOperation,
|
||||
Into: body,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestUnlockingALock(t *testing.T) {
|
||||
got, body := LockService.Unlock("some-lock-id", true)
|
||||
|
||||
AssertRequestSchema(t, &api.RequestSchema{
|
||||
Method: "POST",
|
||||
Path: "/locks/some-lock-id/unlock",
|
||||
Operation: api.UploadOperation,
|
||||
Body: &api.UnlockRequest{
|
||||
Id: "some-lock-id",
|
||||
Force: true,
|
||||
},
|
||||
Into: body,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestLockRequest(t *testing.T) {
|
||||
schema.Validate(t, schema.LockRequestSchema, &api.LockRequest{
|
||||
Path: "/path/to/lock",
|
||||
LatestRemoteCommit: "deadbeef",
|
||||
Committer: api.Committer{
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockResponseWithLockedLock(t *testing.T) {
|
||||
schema.Validate(t, schema.LockResponseSchema, &api.LockResponse{
|
||||
Lock: &api.Lock{
|
||||
Id: "some-lock-id",
|
||||
Path: "/lock/path",
|
||||
Committer: api.Committer{
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
},
|
||||
LockedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockResponseWithUnlockedLock(t *testing.T) {
|
||||
schema.Validate(t, schema.LockResponseSchema, &api.LockResponse{
|
||||
Lock: &api.Lock{
|
||||
Id: "some-lock-id",
|
||||
Path: "/lock/path",
|
||||
Committer: api.Committer{
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
},
|
||||
LockedAt: time.Now(),
|
||||
UnlockedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockResponseWithError(t *testing.T) {
|
||||
schema.Validate(t, schema.LockResponseSchema, &api.LockResponse{
|
||||
Err: "some error",
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockResponseWithCommitNeeded(t *testing.T) {
|
||||
schema.Validate(t, schema.LockResponseSchema, &api.LockResponse{
|
||||
CommitNeeded: "deadbeef",
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockResponseInvalidWithCommitAndError(t *testing.T) {
|
||||
schema.Refute(t, schema.LockResponseSchema, &api.LockResponse{
|
||||
Err: "some error",
|
||||
CommitNeeded: "deadbeef",
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnlockRequest(t *testing.T) {
|
||||
schema.Validate(t, schema.UnlockRequestSchema, &api.UnlockRequest{
|
||||
Id: "some-lock-id",
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnlockResponseWithLock(t *testing.T) {
|
||||
schema.Validate(t, schema.UnlockResponseSchema, &api.UnlockResponse{
|
||||
Lock: &api.Lock{
|
||||
Id: "some-lock-id",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnlockResponseWithError(t *testing.T) {
|
||||
schema.Validate(t, schema.UnlockResponseSchema, &api.UnlockResponse{
|
||||
Err: "some-error",
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnlockResponseDoesNotAllowLockAndError(t *testing.T) {
|
||||
schema.Refute(t, schema.UnlockResponseSchema, &api.UnlockResponse{
|
||||
Lock: &api.Lock{
|
||||
Id: "some-lock-id",
|
||||
},
|
||||
Err: "some-error",
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockListWithLocks(t *testing.T) {
|
||||
schema.Validate(t, schema.LockListSchema, &api.LockList{
|
||||
Locks: []api.Lock{
|
||||
api.Lock{Id: "foo"},
|
||||
api.Lock{Id: "bar"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockListWithNoResults(t *testing.T) {
|
||||
schema.Validate(t, schema.LockListSchema, &api.LockList{
|
||||
Locks: []api.Lock{},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockListWithNextCursor(t *testing.T) {
|
||||
schema.Validate(t, schema.LockListSchema, &api.LockList{
|
||||
Locks: []api.Lock{
|
||||
api.Lock{Id: "foo"},
|
||||
api.Lock{Id: "bar"},
|
||||
},
|
||||
NextCursor: "baz",
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockListWithError(t *testing.T) {
|
||||
schema.Validate(t, schema.LockListSchema, &api.LockList{
|
||||
Err: "some error",
|
||||
})
|
||||
}
|
||||
|
||||
func TestLockListWithErrorAndLocks(t *testing.T) {
|
||||
schema.Refute(t, schema.LockListSchema, &api.LockList{
|
||||
Locks: []api.Lock{
|
||||
api.Lock{Id: "foo"},
|
||||
api.Lock{Id: "bar"},
|
||||
},
|
||||
Err: "this isn't possible!",
|
||||
})
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
)
|
||||
|
||||
type ObjectError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *ObjectError) Error() string {
|
||||
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
type ObjectResource struct {
|
||||
Oid string `json:"oid,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
Authenticated bool `json:"authenticated,omitempty"`
|
||||
Actions map[string]*LinkRelation `json:"actions,omitempty"`
|
||||
Links map[string]*LinkRelation `json:"_links,omitempty"`
|
||||
Error *ObjectError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TODO LEGACY API: remove when legacy API removed
|
||||
func (o *ObjectResource) NewRequest(relation, method string) (*http.Request, error) {
|
||||
rel, ok := o.Rel(relation)
|
||||
if !ok {
|
||||
if relation == "download" {
|
||||
return nil, errors.New("Object not found on the server.")
|
||||
}
|
||||
return nil, fmt.Errorf("No %q action for this object.", relation)
|
||||
|
||||
}
|
||||
|
||||
req, err := httputil.NewHttpRequest(method, rel.Href, rel.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (o *ObjectResource) Rel(name string) (*LinkRelation, bool) {
|
||||
var rel *LinkRelation
|
||||
var ok bool
|
||||
|
||||
if o.Actions != nil {
|
||||
rel, ok = o.Actions[name]
|
||||
} else {
|
||||
rel, ok = o.Links[name]
|
||||
}
|
||||
|
||||
return rel, ok
|
||||
}
|
||||
|
||||
// IsExpired returns true, and the time of the expired action, if any of the
|
||||
// actions in this object resource have an ExpiresAt field that is after the
|
||||
// given instant "now".
|
||||
//
|
||||
// If the object contains no actions, or none of the actions it does contain
|
||||
// have non-zero ExpiresAt fields, the object is not expired.
|
||||
func (o *ObjectResource) IsExpired(now time.Time) (time.Time, bool) {
|
||||
for _, a := range o.Actions {
|
||||
if !a.ExpiresAt.IsZero() && a.ExpiresAt.Before(now) {
|
||||
return a.ExpiresAt, true
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func (o *ObjectResource) NeedsAuth() bool {
|
||||
return !o.Authenticated
|
||||
}
|
||||
|
||||
type LinkRelation struct {
|
||||
Href string `json:"href"`
|
||||
Header map[string]string `json:"header,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObjectsWithNoActionsAreNotExpired(t *testing.T) {
|
||||
o := &api.ObjectResource{
|
||||
Oid: "some-oid",
|
||||
Actions: map[string]*api.LinkRelation{},
|
||||
}
|
||||
|
||||
_, expired := o.IsExpired(time.Now())
|
||||
assert.False(t, expired)
|
||||
}
|
||||
|
||||
func TestObjectsWithZeroValueTimesAreNotExpired(t *testing.T) {
|
||||
o := &api.ObjectResource{
|
||||
Oid: "some-oid",
|
||||
Actions: map[string]*api.LinkRelation{
|
||||
"upload": &api.LinkRelation{
|
||||
Href: "http://your-lfs-server.com",
|
||||
ExpiresAt: time.Time{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, expired := o.IsExpired(time.Now())
|
||||
assert.False(t, expired)
|
||||
}
|
||||
|
||||
func TestObjectsWithExpirationDatesAreExpired(t *testing.T) {
|
||||
now := time.Now()
|
||||
expires := time.Now().Add(-60 * 60 * time.Second)
|
||||
|
||||
o := &api.ObjectResource{
|
||||
Oid: "some-oid",
|
||||
Actions: map[string]*api.LinkRelation{
|
||||
"upload": &api.LinkRelation{
|
||||
Href: "http://your-lfs-server.com",
|
||||
ExpiresAt: expires,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expiredAt, expired := o.IsExpired(now)
|
||||
assert.Equal(t, expires, expiredAt)
|
||||
assert.True(t, expired)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
// RequestSchema provides a schema from which to generate sendable requests.
|
||||
type RequestSchema struct {
|
||||
// Method is the method that should be used when making a particular API
|
||||
// call.
|
||||
Method string
|
||||
// Path is the relative path that this API call should be made against.
|
||||
Path string
|
||||
// Operation is the operation used to determine which endpoint to make
|
||||
// the request against (see github.com/git-lfs/git-lfs/config).
|
||||
Operation Operation
|
||||
// Query is the query parameters used in the request URI.
|
||||
Query map[string]string
|
||||
// Body is the body of the request.
|
||||
Body interface{}
|
||||
// Into is an optional field used to represent the data structure into
|
||||
// which a response should be serialized.
|
||||
Into interface{}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// AssertRequestSchema encapsulates a single assertion of equality against two
|
||||
// generated RequestSchema instances.
|
||||
//
|
||||
// This assertion is meant only to test that the request schema generated by an
|
||||
// API service matches what we expect it to be. It does not make use of the
|
||||
// *api.Client, any particular lifecycle, or spin up a test server. All of that
|
||||
// behavior is tested at a higher strata in the client/lifecycle tests.
|
||||
//
|
||||
// - t is the *testing.T used to preform the assertion.
|
||||
// - Expected is the *api.RequestSchema that we expected to be generated.
|
||||
// - Got is the *api.RequestSchema that was generated by a service.
|
||||
func AssertRequestSchema(t *testing.T, expected, got *api.RequestSchema) {
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package api
|
||||
|
||||
import "io"
|
||||
|
||||
// Response is an interface that represents a response returned as a result of
|
||||
// executing an API call. It is designed to represent itself across multiple
|
||||
// response type, be it HTTP, SSH, or something else.
|
||||
//
|
||||
// The Response interface is meant to be small enough such that it can be
|
||||
// sufficiently general, but is easily accessible if a caller needs more
|
||||
// information specific to a particular protocol.
|
||||
type Response interface {
|
||||
// Status is a human-readable string representing the status the
|
||||
// response was returned with.
|
||||
Status() string
|
||||
// StatusCode is the numeric code associated with a particular status.
|
||||
StatusCode() int
|
||||
// Proto is the protocol with which the response was delivered.
|
||||
Proto() string
|
||||
// Body returns an io.ReadCloser containg the contents of the response's
|
||||
// body.
|
||||
Body() io.ReadCloser
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"locks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"committer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email"]
|
||||
},
|
||||
"commit_sha": {
|
||||
"type": "string"
|
||||
},
|
||||
"locked_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"unlocked_at": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "path", "commit_sha", "locked_at"],
|
||||
"additionalItems": false
|
||||
}
|
||||
},
|
||||
"next_cursor": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["locks"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"locks": {
|
||||
"type": "null"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["error"]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"latest_remote_commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"committer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email"]
|
||||
}
|
||||
},
|
||||
"required": ["path", "latest_remote_commit", "committer"]
|
||||
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["error"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"commit_needed": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["commit_needed"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"committer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email"]
|
||||
},
|
||||
"commit_sha": {
|
||||
"type": "string"
|
||||
},
|
||||
"locked_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"unlocked_at": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "path", "commit_sha", "locked_at"]
|
||||
}
|
||||
},
|
||||
"required": ["lock"]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
)
|
||||
|
||||
// SchemaValidator uses the gojsonschema library to validate the JSON encoding
|
||||
// of Go objects against a pre-defined JSON schema.
|
||||
type SchemaValidator struct {
|
||||
// Schema is the JSON schema to validate against.
|
||||
//
|
||||
// Subject is the instance of Go type that will be validated.
|
||||
Schema, Subject gojsonschema.JSONLoader
|
||||
}
|
||||
|
||||
func NewSchemaValidator(t *testing.T, schemaName string, got interface{}) *SchemaValidator {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Platform compatibility: use "/" separators always for file://
|
||||
dir = filepath.ToSlash(dir)
|
||||
|
||||
schema := gojsonschema.NewReferenceLoader(fmt.Sprintf(
|
||||
"file:///%s/schema/%s", dir, schemaName),
|
||||
)
|
||||
|
||||
marshalled, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
subject := gojsonschema.NewStringLoader(string(marshalled))
|
||||
|
||||
return &SchemaValidator{
|
||||
Schema: schema,
|
||||
Subject: subject,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates a Go object against JSON schema in a testing environment.
|
||||
// If the validation fails, then the test will fail after logging all of the
|
||||
// validation errors experienced by the validator.
|
||||
func Validate(t *testing.T, schemaName string, got interface{}) {
|
||||
NewSchemaValidator(t, schemaName, got).Assert(t)
|
||||
}
|
||||
|
||||
// Refute ensures that a particular Go object does not validate the JSON schema
|
||||
// given.
|
||||
//
|
||||
// If validation against the schema is successful, then the test will fail after
|
||||
// logging.
|
||||
func Refute(t *testing.T, schemaName string, got interface{}) {
|
||||
NewSchemaValidator(t, schemaName, got).Refute(t)
|
||||
}
|
||||
|
||||
// Assert preforms the validation assertion against the given *testing.T.
|
||||
func (v *SchemaValidator) Assert(t *testing.T) {
|
||||
if result, err := gojsonschema.Validate(v.Schema, v.Subject); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !result.Valid() {
|
||||
for _, err := range result.Errors() {
|
||||
t.Logf("Validation error: %s", err.Description())
|
||||
}
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// Refute refutes that the given subject will validate against a particular
|
||||
// schema.
|
||||
func (v *SchemaValidator) Refute(t *testing.T) {
|
||||
if result, err := gojsonschema.Validate(v.Schema, v.Subject); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if result.Valid() {
|
||||
t.Fatal("api/schema: expected validation to fail, succeeded")
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// schema provides a testing utility for testing API types against a predefined
|
||||
// JSON schema.
|
||||
//
|
||||
// The core philosophy for this package is as follows: when a new API is
|
||||
// accepted, JSON Schema files should be added to document the types that are
|
||||
// exchanged over this new API. Those files are placed in the `/api/schema`
|
||||
// directory, and are used by the schema.Validate function to test that
|
||||
// particular instances of these types as represented in Go match the predefined
|
||||
// schema that was proposed as a part of the API.
|
||||
//
|
||||
// For ease of use, this file defines several constants, one for each schema
|
||||
// file's name, to easily pass around during tests.
|
||||
//
|
||||
// As briefly described above, to validate that a Go type matches the schema for
|
||||
// a particular API call, one should use the schema.Validate() function.
|
||||
package schema
|
||||
|
||||
const (
|
||||
LockListSchema = "lock_list_schema.json"
|
||||
LockRequestSchema = "lock_request_schema.json"
|
||||
LockResponseSchema = "lock_response_schema.json"
|
||||
UnlockRequestSchema = "unlock_request_schema.json"
|
||||
UnlockResponseSchema = "unlock_response_schema.json"
|
||||
)
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["id", "force"],
|
||||
"additionalItems": false
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"committer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email"]
|
||||
},
|
||||
"commit_sha": {
|
||||
"type": "string"
|
||||
},
|
||||
"locked_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"unlocked_at": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "path", "commit_sha", "locked_at"]
|
||||
}
|
||||
},
|
||||
"required": ["lock"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["error"]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,627 +0,0 @@
|
||||
package api_test // prevent import cycles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/api"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/git-lfs/git-lfs/test"
|
||||
)
|
||||
|
||||
func TestExistingUpload(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
tmp := tempdir(t)
|
||||
defer server.Close()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
postCalled := false
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Errorf("Invalid Accept")
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") != api.MediaType {
|
||||
t.Errorf("Invalid Content-Type")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tee := io.TeeReader(r.Body, buf)
|
||||
reqObj := batchResponse{}
|
||||
err := json.NewDecoder(tee).Decode(&reqObj)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
t.Logf("request body: %s", buf.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var obj *api.ObjectResource
|
||||
if len(reqObj.Objects) != 1 {
|
||||
t.Errorf("Invalid number of objects")
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
} else {
|
||||
obj = reqObj.Objects[0]
|
||||
if obj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" {
|
||||
t.Errorf("invalid oid from request: %s", obj.Oid)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("invalid size from request: %d", obj.Size)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Actions = map[string]*api.LinkRelation{
|
||||
"download": &api.LinkRelation{
|
||||
Href: server.URL + "/download",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
postCalled = true
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11")
|
||||
if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oid := filepath.Base(oidPath)
|
||||
stat, _ := os.Stat(oidPath)
|
||||
o, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if o == nil {
|
||||
t.Fatal("Got no objects back")
|
||||
}
|
||||
|
||||
if _, ok := o.Rel("upload"); ok {
|
||||
t.Errorf("has upload relation")
|
||||
}
|
||||
|
||||
if _, ok := o.Rel("download"); !ok {
|
||||
t.Errorf("has no download relation")
|
||||
}
|
||||
|
||||
if !postCalled {
|
||||
t.Errorf("POST not called")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUploadWithRedirect(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
tmp := tempdir(t)
|
||||
defer server.Close()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
mux.HandleFunc("/redirect/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", server.URL+"/redirect2/objects/batch")
|
||||
w.WriteHeader(307)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/redirect2/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", server.URL+"/media/objects/batch")
|
||||
w.WriteHeader(307)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Errorf("Invalid Accept")
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") != api.MediaType {
|
||||
t.Errorf("Invalid Content-Type")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tee := io.TeeReader(r.Body, buf)
|
||||
reqObj := batchResponse{}
|
||||
err := json.NewDecoder(tee).Decode(&reqObj)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
t.Logf("request body: %s", buf.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var obj *api.ObjectResource
|
||||
if len(reqObj.Objects) != 1 {
|
||||
t.Errorf("Invalid number of objects")
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
} else {
|
||||
obj = reqObj.Objects[0]
|
||||
if obj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" {
|
||||
t.Errorf("invalid oid from request: %s", obj.Oid)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("invalid size from request: %d", obj.Size)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Actions = map[string]*api.LinkRelation{
|
||||
"upload": &api.LinkRelation{
|
||||
Href: server.URL + "/upload",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
"verify": &api.LinkRelation{
|
||||
Href: server.URL + "/verify",
|
||||
Header: map[string]string{"B": "2"},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/redirect",
|
||||
},
|
||||
})
|
||||
|
||||
oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11")
|
||||
if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oid := filepath.Base(oidPath)
|
||||
stat, _ := os.Stat(oidPath)
|
||||
o, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if o == nil {
|
||||
t.Fatal("Got no objects back")
|
||||
}
|
||||
|
||||
if _, ok := o.Rel("download"); ok {
|
||||
t.Errorf("has download relation")
|
||||
}
|
||||
|
||||
if _, ok := o.Rel("upload"); !ok {
|
||||
t.Errorf("has no upload relation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccessfulUploadWithVerify(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
tmp := tempdir(t)
|
||||
defer server.Close()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
postCalled := false
|
||||
verifyCalled := false
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Errorf("Invalid Accept")
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") != api.MediaType {
|
||||
t.Errorf("Invalid Content-Type")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tee := io.TeeReader(r.Body, buf)
|
||||
reqObj := batchResponse{}
|
||||
err := json.NewDecoder(tee).Decode(&reqObj)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
t.Logf("request body: %s", buf.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var obj *api.ObjectResource
|
||||
if len(reqObj.Objects) != 1 {
|
||||
t.Errorf("Invalid number of objects")
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
} else {
|
||||
obj = reqObj.Objects[0]
|
||||
if obj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" {
|
||||
t.Errorf("invalid oid from request: %s", obj.Oid)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("invalid size from request: %d", obj.Size)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Actions = map[string]*api.LinkRelation{
|
||||
"upload": &api.LinkRelation{
|
||||
Href: server.URL + "/upload",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
"verify": &api.LinkRelation{
|
||||
Href: server.URL + "/verify",
|
||||
Header: map[string]string{"B": "2"},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
postCalled = true
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("B") != "2" {
|
||||
t.Error("Invalid B")
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") != api.MediaType {
|
||||
t.Error("Invalid Content-Type")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tee := io.TeeReader(r.Body, buf)
|
||||
reqObj := &api.ObjectResource{}
|
||||
err := json.NewDecoder(tee).Decode(reqObj)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
t.Logf("request body: %s", buf.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if reqObj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" {
|
||||
t.Errorf("invalid oid from request: %s", reqObj.Oid)
|
||||
}
|
||||
|
||||
if reqObj.Size != 4 {
|
||||
t.Errorf("invalid size from request: %d", reqObj.Size)
|
||||
}
|
||||
|
||||
verifyCalled = true
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11")
|
||||
if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oid := filepath.Base(oidPath)
|
||||
stat, _ := os.Stat(oidPath)
|
||||
o, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
api.VerifyUpload(cfg, o)
|
||||
|
||||
if !postCalled {
|
||||
t.Errorf("POST not called")
|
||||
}
|
||||
|
||||
if !verifyCalled {
|
||||
t.Errorf("verify not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadApiError(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
tmp := tempdir(t)
|
||||
defer server.Close()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
postCalled := false
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
postCalled = true
|
||||
w.WriteHeader(404)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11")
|
||||
if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oid := filepath.Base(oidPath)
|
||||
stat, _ := os.Stat(oidPath)
|
||||
_, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"})
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if errors.IsFatalError(err) {
|
||||
t.Fatal("should not panic")
|
||||
}
|
||||
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := "batch response: " + fmt.Sprintf(httputil.GetDefaultError(404), server.URL+"/media/objects/batch")
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("Expected: %s\nGot: %s", expected, err.Error())
|
||||
}
|
||||
|
||||
if !postCalled {
|
||||
t.Errorf("POST not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadVerifyError(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
RestoreCredentialsFunc()
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
tmp := tempdir(t)
|
||||
defer server.Close()
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
postCalled := false
|
||||
verifyCalled := false
|
||||
|
||||
mux.HandleFunc("/media/objects/batch", func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("Server: %s %s", r.Method, r.URL)
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("Accept") != api.MediaType {
|
||||
t.Errorf("Invalid Accept")
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") != api.MediaType {
|
||||
t.Errorf("Invalid Content-Type")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tee := io.TeeReader(r.Body, buf)
|
||||
reqObj := batchResponse{}
|
||||
err := json.NewDecoder(tee).Decode(&reqObj)
|
||||
t.Logf("request header: %v", r.Header)
|
||||
t.Logf("request body: %s", buf.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var obj *api.ObjectResource
|
||||
if len(reqObj.Objects) != 1 {
|
||||
t.Errorf("Invalid number of objects")
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
} else {
|
||||
obj = reqObj.Objects[0]
|
||||
if obj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" {
|
||||
t.Errorf("invalid oid from request: %s", obj.Oid)
|
||||
}
|
||||
|
||||
if obj.Size != 4 {
|
||||
t.Errorf("invalid size from request: %d", obj.Size)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Actions = map[string]*api.LinkRelation{
|
||||
"upload": &api.LinkRelation{
|
||||
Href: server.URL + "/upload",
|
||||
Header: map[string]string{"A": "1"},
|
||||
},
|
||||
"verify": &api.LinkRelation{
|
||||
Href: server.URL + "/verify",
|
||||
Header: map[string]string{"B": "2"},
|
||||
},
|
||||
}
|
||||
|
||||
by, err := json.Marshal(newBatchResponse("", obj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
postCalled = true
|
||||
head := w.Header()
|
||||
head.Set("Content-Type", api.MediaType)
|
||||
head.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
w.WriteHeader(200)
|
||||
w.Write(by)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) {
|
||||
verifyCalled = true
|
||||
w.WriteHeader(404)
|
||||
})
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": server.URL + "/media",
|
||||
},
|
||||
})
|
||||
|
||||
oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11")
|
||||
if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oid := filepath.Base(oidPath)
|
||||
stat, _ := os.Stat(oidPath)
|
||||
o, _, err := api.BatchSingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"})
|
||||
if err != nil {
|
||||
if isDockerConnectionError(err) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = api.VerifyUpload(cfg, o)
|
||||
if err == nil {
|
||||
t.Fatal("verify should fail")
|
||||
}
|
||||
|
||||
if errors.IsFatalError(err) {
|
||||
t.Fatal("should not panic")
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(httputil.GetDefaultError(404), server.URL+"/verify")
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("Expected: %s\nGot: %s", expected, err.Error())
|
||||
}
|
||||
|
||||
if !postCalled {
|
||||
t.Errorf("POST not called")
|
||||
}
|
||||
|
||||
if !verifyCalled {
|
||||
t.Errorf("verify not called")
|
||||
}
|
||||
|
||||
}
|
143
api/v1.go
143
api/v1.go
@ -1,143 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
const (
|
||||
MediaType = "application/vnd.git-lfs+json; charset=utf-8"
|
||||
)
|
||||
|
||||
type batchRequest struct {
|
||||
TransferAdapterNames []string `json:"transfers,omitempty"`
|
||||
Operation string `json:"operation"`
|
||||
Objects []*ObjectResource `json:"objects"`
|
||||
}
|
||||
type batchResponse struct {
|
||||
TransferAdapterName string `json:"transfer"`
|
||||
Objects []*ObjectResource `json:"objects"`
|
||||
}
|
||||
|
||||
// doApiBatchRequest runs the request to the LFS batch API. If the API returns a
|
||||
// 401, the repo will be marked as having private access and the request will be
|
||||
// re-run. When the repo is marked as having private access, credentials will
|
||||
// be retrieved.
|
||||
func DoBatchRequest(cfg *config.Configuration, req *http.Request) (*http.Response, *batchResponse, error) {
|
||||
res, err := DoRequest(req, cfg.PrivateAccess(auth.GetOperationForRequest(req)))
|
||||
|
||||
if err != nil {
|
||||
if res != nil && res.StatusCode == 401 {
|
||||
return res, nil, errors.NewAuthError(err)
|
||||
}
|
||||
return res, nil, err
|
||||
}
|
||||
|
||||
resp := &batchResponse{}
|
||||
err = httputil.DecodeResponse(res, resp)
|
||||
|
||||
if err != nil {
|
||||
httputil.SetErrorResponseContext(cfg, err, res)
|
||||
}
|
||||
|
||||
return res, resp, err
|
||||
}
|
||||
|
||||
// DoRequest runs a request to the LFS API, without parsing the response
|
||||
// body. If the API returns a 401, the repo will be marked as having private
|
||||
// access and the request will be re-run. When the repo is marked as having
|
||||
// private access, credentials will be retrieved.
|
||||
func DoRequest(req *http.Request, useCreds bool) (*http.Response, error) {
|
||||
via := make([]*http.Request, 0, 4)
|
||||
return httputil.DoHttpRequestWithRedirects(config.Config, req, via, useCreds)
|
||||
}
|
||||
|
||||
func NewRequest(cfg *config.Configuration, method, oid string) (*http.Request, error) {
|
||||
objectOid := oid
|
||||
operation := "download"
|
||||
if method == "POST" {
|
||||
if oid != "batch" {
|
||||
objectOid = ""
|
||||
operation = "upload"
|
||||
}
|
||||
}
|
||||
|
||||
res, endpoint, err := auth.SshAuthenticate(cfg, operation, oid)
|
||||
if err != nil {
|
||||
tracerx.Printf("ssh: %s with %s failed, error: %s, message: %s",
|
||||
operation, endpoint.SshUserAndHost, err.Error(), res.Message,
|
||||
)
|
||||
return nil, errors.Wrap(errors.New(res.Message), err.Error())
|
||||
}
|
||||
|
||||
if len(res.Href) > 0 {
|
||||
endpoint.Url = res.Href
|
||||
}
|
||||
|
||||
u, err := ObjectUrl(endpoint, objectOid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := httputil.NewHttpRequest(method, u.String(), res.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", MediaType)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewBatchRequest(cfg *config.Configuration, operation string) (*http.Request, error) {
|
||||
res, endpoint, err := auth.SshAuthenticate(cfg, operation, "")
|
||||
if err != nil {
|
||||
tracerx.Printf("ssh: %s with %s failed, error: %s, message: %s",
|
||||
operation, endpoint.SshUserAndHost, err.Error(), res.Message,
|
||||
)
|
||||
return nil, errors.Wrap(errors.New(res.Message), err.Error())
|
||||
}
|
||||
|
||||
if len(res.Href) > 0 {
|
||||
endpoint.Url = res.Href
|
||||
}
|
||||
|
||||
u, err := ObjectUrl(endpoint, "batch")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := httputil.NewHttpRequest("POST", u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", MediaType)
|
||||
if res.Header != nil {
|
||||
for key, value := range res.Header {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func ObjectUrl(endpoint config.Endpoint, oid string) (*url.URL, error) {
|
||||
u, err := url.Parse(endpoint.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, "objects")
|
||||
if len(oid) > 0 {
|
||||
u.Path = path.Join(u.Path, oid)
|
||||
}
|
||||
return u, nil
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
)
|
||||
|
||||
// VerifyUpload calls the "verify" API link relation on obj if it exists
|
||||
func VerifyUpload(cfg *config.Configuration, obj *ObjectResource) error {
|
||||
// Do we need to do verify?
|
||||
if _, ok := obj.Rel("verify"); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
req, err := obj.NewRequest("verify", "POST")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "verify")
|
||||
}
|
||||
|
||||
by, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "verify")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", MediaType)
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(by)))
|
||||
req.ContentLength = int64(len(by))
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(by))
|
||||
res, err := DoRequest(req, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httputil.LogTransfer(cfg, "lfs.data.verify", res)
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
return err
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
// Package auth provides common authentication tools
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package auth
|
@ -1,290 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// getCreds gets the credentials for a HTTP request and sets the given
|
||||
// request's Authorization header with them using Basic Authentication.
|
||||
// 1. Check the URL for authentication. Ex: http://user:pass@example.com
|
||||
// 2. Check netrc for authentication.
|
||||
// 3. Check the Git remote URL for authentication IF it's the same scheme and
|
||||
// host of the URL.
|
||||
// 4. Ask 'git credential' to fill in the password from one of the above URLs.
|
||||
//
|
||||
// This prefers the Git remote URL for checking credentials so that users only
|
||||
// have to enter their passwords once for Git and Git LFS. It uses the same
|
||||
// URL path that Git does, in case 'useHttpPath' is enabled in the Git config.
|
||||
func GetCreds(cfg *config.Configuration, req *http.Request) (Creds, error) {
|
||||
if skipCredsCheck(cfg, req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
credsUrl, err := getCredURLForAPI(cfg, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if credsUrl == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if setCredURLFromNetrc(cfg, req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return fillCredentials(cfg, req, credsUrl)
|
||||
}
|
||||
|
||||
func getCredURLForAPI(cfg *config.Configuration, req *http.Request) (*url.URL, error) {
|
||||
operation := GetOperationForRequest(req)
|
||||
apiUrl, err := url.Parse(cfg.Endpoint(operation).Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if the LFS request doesn't match the current LFS url, don't bother
|
||||
// attempting to set the Authorization header from the LFS or Git remote URLs.
|
||||
if req.URL.Scheme != apiUrl.Scheme ||
|
||||
req.URL.Host != apiUrl.Host {
|
||||
return req.URL, nil
|
||||
}
|
||||
|
||||
if setRequestAuthFromUrl(cfg, req, apiUrl) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
credsUrl := apiUrl
|
||||
if len(cfg.CurrentRemote) > 0 {
|
||||
if u := cfg.GitRemoteUrl(cfg.CurrentRemote, operation == "upload"); u != "" {
|
||||
gitRemoteUrl, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gitRemoteUrl.Scheme == apiUrl.Scheme &&
|
||||
gitRemoteUrl.Host == apiUrl.Host {
|
||||
|
||||
if setRequestAuthFromUrl(cfg, req, gitRemoteUrl) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
credsUrl = gitRemoteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return credsUrl, nil
|
||||
}
|
||||
|
||||
func setCredURLFromNetrc(cfg *config.Configuration, req *http.Request) bool {
|
||||
hostname := req.URL.Host
|
||||
var host string
|
||||
|
||||
if strings.Contains(hostname, ":") {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
host = hostname
|
||||
}
|
||||
|
||||
machine, err := cfg.FindNetrcHost(host)
|
||||
if err != nil {
|
||||
tracerx.Printf("netrc: error finding match for %q: %s", hostname, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if machine == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
setRequestAuth(cfg, req, machine.Login, machine.Password)
|
||||
return true
|
||||
}
|
||||
|
||||
func skipCredsCheck(cfg *config.Configuration, req *http.Request) bool {
|
||||
if cfg.NtlmAccess(GetOperationForRequest(req)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(req.Header.Get("Authorization")) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
return len(q["token"]) > 0
|
||||
}
|
||||
|
||||
func fillCredentials(cfg *config.Configuration, req *http.Request, u *url.URL) (Creds, error) {
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host, "path": path}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
|
||||
creds, err := execCreds(cfg, input, "fill")
|
||||
if creds == nil || len(creds) < 1 {
|
||||
errmsg := fmt.Sprintf("Git credentials for %s not found", u)
|
||||
if err != nil {
|
||||
errmsg = errmsg + ":\n" + err.Error()
|
||||
} else {
|
||||
errmsg = errmsg + "."
|
||||
}
|
||||
err = errors.New(errmsg)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracerx.Printf("Filled credentials for %s", u)
|
||||
setRequestAuth(cfg, req, creds["username"], creds["password"])
|
||||
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func SaveCredentials(cfg *config.Configuration, creds Creds, res *http.Response) {
|
||||
if creds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 401, 403:
|
||||
execCreds(cfg, creds, "reject")
|
||||
default:
|
||||
if res.StatusCode < 300 {
|
||||
execCreds(cfg, creds, "approve")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Creds map[string]string
|
||||
|
||||
func (c Creds) Buffer() *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// Credentials function which will be called whenever credentials are requested
|
||||
type CredentialFunc func(*config.Configuration, Creds, string) (Creds, error)
|
||||
|
||||
func execCredsCommand(cfg *config.Configuration, input Creds, subCommand string) (Creds, error) {
|
||||
output := new(bytes.Buffer)
|
||||
cmd := exec.Command("git", "credential", subCommand)
|
||||
cmd.Stdin = input.Buffer()
|
||||
cmd.Stdout = output
|
||||
/*
|
||||
There is a reason we don't hook up stderr here:
|
||||
Git's credential cache daemon helper does not close its stderr, so if this
|
||||
process is the process that fires up the daemon, it will wait forever
|
||||
(until the daemon exits, really) trying to read from stderr.
|
||||
|
||||
See https://github.com/git-lfs/git-lfs/issues/117 for more details.
|
||||
*/
|
||||
|
||||
err := cmd.Start()
|
||||
if err == nil {
|
||||
err = cmd.Wait()
|
||||
}
|
||||
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
if !cfg.Os.Bool("GIT_TERMINAL_PROMPT", true) {
|
||||
return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
|
||||
input["protocol"], input["host"])
|
||||
}
|
||||
|
||||
// 'git credential' exits with 128 if the helper doesn't fill the username
|
||||
// and password values.
|
||||
if subCommand == "fill" && err.Error() == "exit status 128" {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'git credential %s' error: %s\n", subCommand, err.Error())
|
||||
}
|
||||
|
||||
creds := make(Creds)
|
||||
for _, line := range strings.Split(output.String(), "\n") {
|
||||
pieces := strings.SplitN(line, "=", 2)
|
||||
if len(pieces) < 2 || len(pieces[1]) < 1 {
|
||||
continue
|
||||
}
|
||||
creds[pieces[0]] = pieces[1]
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func setRequestAuthFromUrl(cfg *config.Configuration, req *http.Request, u *url.URL) bool {
|
||||
if !cfg.NtlmAccess(GetOperationForRequest(req)) && u.User != nil {
|
||||
if pass, ok := u.User.Password(); ok {
|
||||
fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials")
|
||||
setRequestAuth(cfg, req, u.User.Username(), pass)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func setRequestAuth(cfg *config.Configuration, req *http.Request, user, pass string) {
|
||||
if cfg.NtlmAccess(GetOperationForRequest(req)) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(user) == 0 && len(pass) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s:%s", user, pass)
|
||||
auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
|
||||
var execCreds CredentialFunc = execCredsCommand
|
||||
|
||||
// GetCredentialsFunc returns the current credentials function
|
||||
func GetCredentialsFunc() CredentialFunc {
|
||||
return execCreds
|
||||
}
|
||||
|
||||
// SetCredentialsFunc overrides the default credentials function (which is to call git)
|
||||
// Returns the previous credentials func
|
||||
func SetCredentialsFunc(f CredentialFunc) CredentialFunc {
|
||||
oldf := execCreds
|
||||
execCreds = f
|
||||
return oldf
|
||||
}
|
||||
|
||||
// GetOperationForRequest determines the operation type for a http.Request
|
||||
func GetOperationForRequest(req *http.Request) string {
|
||||
operation := "download"
|
||||
if req.Method == "POST" || req.Method == "PUT" {
|
||||
operation = "upload"
|
||||
}
|
||||
return operation
|
||||
}
|
@ -1,326 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bgentry/go-netrc/netrc"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
)
|
||||
|
||||
func TestGetCredentialsForApi(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
checkGetCredentials(t, GetCreds, []*getCredentialCheck{
|
||||
{
|
||||
Desc: "simple",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Protocol: "https",
|
||||
Host: "git-server.com",
|
||||
Username: "git-server.com",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "username in url",
|
||||
Config: map[string]string{"lfs.url": "https://user@git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Protocol: "https",
|
||||
Host: "git-server.com",
|
||||
Username: "user",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "auth header",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Header: map[string]string{"Authorization": "Test monkey"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Authorization: "Test monkey",
|
||||
},
|
||||
{
|
||||
Desc: "scheme mismatch",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "http://git-server.com/foo",
|
||||
Protocol: "http",
|
||||
Host: "git-server.com",
|
||||
Path: "foo",
|
||||
Username: "git-server.com",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "host mismatch",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server2.com/foo",
|
||||
Protocol: "https",
|
||||
Host: "git-server2.com",
|
||||
Path: "foo",
|
||||
Username: "git-server2.com",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "port mismatch",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com:8080/foo",
|
||||
Protocol: "https",
|
||||
Host: "git-server.com:8080",
|
||||
Path: "foo",
|
||||
Username: "git-server.com:8080",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "api url auth",
|
||||
Config: map[string]string{"lfs.url": "https://testuser:testpass@git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Authorization: "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))),
|
||||
},
|
||||
{
|
||||
Desc: "git url auth",
|
||||
CurrentRemote: "origin",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com",
|
||||
"remote.origin.url": "https://gituser:gitpass@git-server.com",
|
||||
},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Authorization: "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte("gituser:gitpass"))),
|
||||
},
|
||||
{
|
||||
Desc: "username in url",
|
||||
Config: map[string]string{"lfs.url": "https://user@git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo",
|
||||
Protocol: "https",
|
||||
Host: "git-server.com",
|
||||
Username: "user",
|
||||
Password: "monkey",
|
||||
},
|
||||
{
|
||||
Desc: "?token query",
|
||||
Config: map[string]string{"lfs.url": "https://git-server.com"},
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/foo?token=abc",
|
||||
SkipAuth: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type fakeNetrc struct{}
|
||||
|
||||
func (n *fakeNetrc) FindMachine(host string) *netrc.Machine {
|
||||
if host == "some-host" {
|
||||
return &netrc.Machine{Login: "abc", Password: "def"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNetrcWithHostAndPort(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
cfg := config.New()
|
||||
cfg.SetNetrc(&fakeNetrc{})
|
||||
u, err := url.Parse("http://some-host:123/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setCredURLFromNetrc(cfg, req) {
|
||||
t.Fatal("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithHost(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
cfg := config.New()
|
||||
cfg.SetNetrc(&fakeNetrc{})
|
||||
u, err := url.Parse("http://some-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setCredURLFromNetrc(cfg, req) {
|
||||
t.Fatalf("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithBadHost(t *testing.T) {
|
||||
SetupTestCredentialsFunc()
|
||||
defer RestoreCredentialsFunc()
|
||||
|
||||
cfg := config.New()
|
||||
cfg.SetNetrc(&fakeNetrc{})
|
||||
u, err := url.Parse("http://other-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if setCredURLFromNetrc(cfg, req) {
|
||||
t.Fatalf("unexpected netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func checkGetCredentials(t *testing.T, getCredsFunc func(*config.Configuration, *http.Request) (Creds, error), checks []*getCredentialCheck) {
|
||||
for _, check := range checks {
|
||||
t.Logf("Checking %q", check.Desc)
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: check.Config,
|
||||
})
|
||||
cfg.CurrentRemote = check.CurrentRemote
|
||||
|
||||
req, err := http.NewRequest(check.Method, check.Href, nil)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] %s", check.Desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for key, value := range check.Header {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
creds, err := getCredsFunc(cfg, req)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] %s", check.Desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if check.ExpectCreds() {
|
||||
if creds == nil {
|
||||
t.Errorf("[%s], no credentials returned", check.Desc)
|
||||
continue
|
||||
}
|
||||
|
||||
if value := creds["protocol"]; len(check.Protocol) > 0 && value != check.Protocol {
|
||||
t.Errorf("[%s] bad protocol: %q, expected: %q", check.Desc, value, check.Protocol)
|
||||
}
|
||||
|
||||
if value := creds["host"]; len(check.Host) > 0 && value != check.Host {
|
||||
t.Errorf("[%s] bad host: %q, expected: %q", check.Desc, value, check.Host)
|
||||
}
|
||||
|
||||
if value := creds["username"]; len(check.Username) > 0 && value != check.Username {
|
||||
t.Errorf("[%s] bad username: %q, expected: %q", check.Desc, value, check.Username)
|
||||
}
|
||||
|
||||
if value := creds["password"]; len(check.Password) > 0 && value != check.Password {
|
||||
t.Errorf("[%s] bad password: %q, expected: %q", check.Desc, value, check.Password)
|
||||
}
|
||||
|
||||
if value := creds["path"]; len(check.Path) > 0 && value != check.Path {
|
||||
t.Errorf("[%s] bad path: %q, expected: %q", check.Desc, value, check.Path)
|
||||
}
|
||||
} else {
|
||||
if creds != nil {
|
||||
t.Errorf("[%s], unexpected credentials: %v // %v", check.Desc, creds, check)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
reqAuth := req.Header.Get("Authorization")
|
||||
if check.SkipAuth {
|
||||
} else if len(check.Authorization) > 0 {
|
||||
if reqAuth != check.Authorization {
|
||||
t.Errorf("[%s] Unexpected Authorization header: %s", check.Desc, reqAuth)
|
||||
}
|
||||
} else {
|
||||
rawtoken := fmt.Sprintf("%s:%s", check.Username, check.Password)
|
||||
expected := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(rawtoken)))
|
||||
if reqAuth != expected {
|
||||
t.Errorf("[%s] Bad Authorization. Expected '%s', got '%s'", check.Desc, expected, reqAuth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type getCredentialCheck struct {
|
||||
Desc string
|
||||
Config map[string]string
|
||||
Header map[string]string
|
||||
Method string
|
||||
Href string
|
||||
Protocol string
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
Path string
|
||||
Authorization string
|
||||
CurrentRemote string
|
||||
SkipAuth bool
|
||||
}
|
||||
|
||||
func (c *getCredentialCheck) ExpectCreds() bool {
|
||||
return len(c.Protocol) > 0 || len(c.Host) > 0 || len(c.Username) > 0 ||
|
||||
len(c.Password) > 0 || len(c.Path) > 0
|
||||
}
|
||||
|
||||
var (
|
||||
TestCredentialsFunc CredentialFunc
|
||||
origCredentialsFunc CredentialFunc
|
||||
)
|
||||
|
||||
func init() {
|
||||
TestCredentialsFunc = func(cfg *config.Configuration, input Creds, subCommand string) (Creds, error) {
|
||||
output := make(Creds)
|
||||
for key, value := range input {
|
||||
output[key] = value
|
||||
}
|
||||
if _, ok := output["username"]; !ok {
|
||||
output["username"] = input["host"]
|
||||
}
|
||||
output["password"] = "monkey"
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Override the credentials func for testing
|
||||
func SetupTestCredentialsFunc() {
|
||||
origCredentialsFunc = SetCredentialsFunc(TestCredentialsFunc)
|
||||
}
|
||||
|
||||
// Put the original credentials func back
|
||||
func RestoreCredentialsFunc() {
|
||||
SetCredentialsFunc(origCredentialsFunc)
|
||||
}
|
@ -33,6 +33,7 @@ dependencies:
|
||||
|
||||
# needed for git-lfs-test-server-api
|
||||
- go get -d -v github.com/spf13/cobra
|
||||
- go get -d -v github.com/ThomsonReutersEikon/go-ntlm/ntlm
|
||||
|
||||
test:
|
||||
override:
|
||||
|
@ -62,19 +62,21 @@ func cloneCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Now just call pull with default args
|
||||
// Support --origin option to clone
|
||||
var remote string
|
||||
if len(cloneFlags.Origin) > 0 {
|
||||
cfg.CurrentRemote = cloneFlags.Origin
|
||||
remote = cloneFlags.Origin
|
||||
} else {
|
||||
cfg.CurrentRemote = "origin"
|
||||
remote = "origin"
|
||||
}
|
||||
|
||||
includeArg, excludeArg := getIncludeExcludeArgs(cmd)
|
||||
filter := buildFilepathFilter(cfg, includeArg, excludeArg)
|
||||
if cloneFlags.NoCheckout || cloneFlags.Bare {
|
||||
// If --no-checkout or --bare then we shouldn't check out, just fetch instead
|
||||
cfg.CurrentRemote = remote
|
||||
fetchRef("HEAD", filter)
|
||||
} else {
|
||||
pull(filter)
|
||||
pull(remote, filter)
|
||||
err := postCloneSubmodules(args)
|
||||
if err != nil {
|
||||
Exit("Error performing 'git lfs pull' for submodules: %v", err)
|
||||
|
@ -35,7 +35,7 @@ func envCommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, env := range lfs.Environ(cfg, TransferManifest()) {
|
||||
for _, env := range lfs.Environ(cfg, getTransferManifest()) {
|
||||
Print(env)
|
||||
}
|
||||
|
||||
|
@ -279,7 +279,7 @@ func fetchAndReportToChan(allpointers []*lfs.WrappedPointer, filter *filepathfil
|
||||
}
|
||||
|
||||
ready, pointers, meter := readyAndMissingPointers(allpointers, filter)
|
||||
q := newDownloadQueue(tq.WithProgress(meter))
|
||||
q := newDownloadQueue(getTransferManifest(), cfg.CurrentRemote, tq.WithProgress(meter))
|
||||
|
||||
if out != nil {
|
||||
// If we already have it, or it won't be fetched
|
||||
|
@ -1,13 +1,13 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,7 +17,6 @@ var (
|
||||
)
|
||||
|
||||
func lockCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(args) == 0 {
|
||||
Print("Usage: git lfs lock <path>")
|
||||
return
|
||||
@ -28,20 +27,21 @@ func lockCommand(cmd *cobra.Command, args []string) {
|
||||
Exit(err.Error())
|
||||
}
|
||||
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.CurrentRemote = lockRemote
|
||||
}
|
||||
|
||||
lockClient, err := locking.NewClient(cfg)
|
||||
if err != nil {
|
||||
Exit("Unable to create lock system: %v", err.Error())
|
||||
}
|
||||
lockClient := newLockClient(lockRemote)
|
||||
defer lockClient.Close()
|
||||
|
||||
lock, err := lockClient.LockFile(path)
|
||||
if err != nil {
|
||||
Exit("Lock failed: %v", err)
|
||||
}
|
||||
|
||||
if locksCmdFlags.JSON {
|
||||
if err := json.NewEncoder(os.Stdout).Encode(lock); err != nil {
|
||||
Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Print("\n'%s' was locked (%s)", args[0], lock.Id)
|
||||
}
|
||||
|
||||
@ -91,5 +91,6 @@ func init() {
|
||||
|
||||
RegisterCommand("lock", lockCommand, func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", cfg.CurrentRemote, lockRemoteHelp)
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -10,23 +12,25 @@ var (
|
||||
)
|
||||
|
||||
func locksCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
filters, err := locksCmdFlags.Filters()
|
||||
if err != nil {
|
||||
Exit("Error building filters: %v", err)
|
||||
}
|
||||
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.CurrentRemote = lockRemote
|
||||
}
|
||||
lockClient, err := locking.NewClient(cfg)
|
||||
if err != nil {
|
||||
Exit("Unable to create lock system: %v", err.Error())
|
||||
}
|
||||
lockClient := newLockClient(lockRemote)
|
||||
defer lockClient.Close()
|
||||
|
||||
var lockCount int
|
||||
locks, err := lockClient.SearchLocks(filters, locksCmdFlags.Limit, locksCmdFlags.Local)
|
||||
// Print any we got before exiting
|
||||
|
||||
if locksCmdFlags.JSON {
|
||||
if err := json.NewEncoder(os.Stdout).Encode(locks); err != nil {
|
||||
Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, lock := range locks {
|
||||
Print("%s\t%s <%s>", lock.Path, lock.Name, lock.Email)
|
||||
lockCount++
|
||||
@ -35,7 +39,6 @@ func locksCommand(cmd *cobra.Command, args []string) {
|
||||
if err != nil {
|
||||
Exit("Error while retrieving locks: %v", err)
|
||||
}
|
||||
|
||||
Print("\n%d lock(s) matched query.", lockCount)
|
||||
}
|
||||
|
||||
@ -54,6 +57,8 @@ type locksFlags struct {
|
||||
// local limits the scope of lock reporting to the locally cached record
|
||||
// of locks for the current user & doesn't query the server
|
||||
Local bool
|
||||
// JSON is an optional parameter to output data in json format.
|
||||
JSON bool
|
||||
}
|
||||
|
||||
// Filters produces a filter based on locksFlags instance.
|
||||
@ -86,5 +91,6 @@ func init() {
|
||||
cmd.Flags().StringVarP(&locksCmdFlags.Id, "id", "i", "", "filter locks results matching a particular ID")
|
||||
cmd.Flags().IntVarP(&locksCmdFlags.Limit, "limit", "l", 0, "optional limit for number of results to return")
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.Local, "local", "", false, "only list cached local record of own locks")
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
|
||||
})
|
||||
}
|
||||
|
@ -2,13 +2,11 @@ package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/rubyist/tracerx"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -53,11 +51,10 @@ func prePushCommand(cmd *cobra.Command, args []string) {
|
||||
Exit("Invalid remote name %q", args[0])
|
||||
}
|
||||
|
||||
cfg.CurrentRemote = args[0]
|
||||
ctx := newUploadContext(prePushDryRun)
|
||||
ctx := newUploadContext(args[0], prePushDryRun)
|
||||
|
||||
gitscanner := lfs.NewGitScanner(nil)
|
||||
if err := gitscanner.RemoteForPush(cfg.CurrentRemote); err != nil {
|
||||
if err := gitscanner.RemoteForPush(ctx.Remote); err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
|
||||
@ -66,21 +63,6 @@ func prePushCommand(cmd *cobra.Command, args []string) {
|
||||
// We can be passed multiple lines of refs
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
name, email := cfg.CurrentCommitter()
|
||||
lc, err := locking.NewClient(cfg)
|
||||
if err != nil {
|
||||
Exit("Unable to create lock system: %v", err.Error())
|
||||
}
|
||||
defer lc.Close()
|
||||
|
||||
lockSet, err := findLocks(lc, nil, 0, false)
|
||||
if err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
|
||||
lockConflicts := make([]string, 0, len(lockSet))
|
||||
myLocks := make([]string, 0, len(lockSet))
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
@ -95,62 +77,13 @@ func prePushCommand(cmd *cobra.Command, args []string) {
|
||||
continue
|
||||
}
|
||||
|
||||
pointers, err := scanLeftOrAll(gitscanner, left)
|
||||
if err != nil {
|
||||
if err := uploadLeftOrAll(gitscanner, ctx, left); err != nil {
|
||||
Print("Error scanning for Git LFS files in %q", left)
|
||||
ExitWithError(err)
|
||||
}
|
||||
|
||||
for _, p := range pointers {
|
||||
if l, ok := lockSet[p.Name]; ok {
|
||||
if l.Name == name && l.Email == email {
|
||||
myLocks = append(myLocks, l.Path)
|
||||
} else {
|
||||
lockConflicts = append(lockConflicts, p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(lockConflicts) > 0 {
|
||||
Error("Some files are locked in %s...%s", left, cfg.CurrentRemote)
|
||||
for _, file := range lockConflicts {
|
||||
Error("* %s", file)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
uploadPointers(ctx, pointers)
|
||||
}
|
||||
|
||||
if len(myLocks) > 0 {
|
||||
Print("Pushing your locked files:")
|
||||
for _, file := range myLocks {
|
||||
Print("* %s", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scanLeft(g *lfs.GitScanner, ref string) ([]*lfs.WrappedPointer, error) {
|
||||
var pointers []*lfs.WrappedPointer
|
||||
var multiErr error
|
||||
cb := func(p *lfs.WrappedPointer, err error) {
|
||||
if err != nil {
|
||||
if multiErr != nil {
|
||||
multiErr = fmt.Errorf("%v\n%v", multiErr, err)
|
||||
} else {
|
||||
multiErr = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pointers = append(pointers, p)
|
||||
}
|
||||
|
||||
if err := g.ScanLeftToRemote(ref, cb); err != nil {
|
||||
return pointers, err
|
||||
}
|
||||
|
||||
return pointers, multiErr
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
// decodeRefs pulls the sha1s out of the line read from the pre-push
|
||||
|
@ -120,9 +120,7 @@ func prune(fetchPruneConfig config.FetchPruneConfig, verifyRemote, dryRun, verbo
|
||||
var verifywait sync.WaitGroup
|
||||
|
||||
if verifyRemote {
|
||||
cfg.CurrentRemote = fetchPruneConfig.PruneRemoteName
|
||||
// build queue now, no estimates or progress output
|
||||
verifyQueue = newDownloadCheckQueue()
|
||||
verifyQueue = newDownloadCheckQueue(getTransferManifest(), fetchPruneConfig.PruneRemoteName)
|
||||
verifiedObjects = tools.NewStringSetWithCapacity(len(localObjects) / 2)
|
||||
|
||||
// this channel is filled with oids for which Check() succeeded & Transfer() was called
|
||||
|
@ -18,27 +18,29 @@ func pullCommand(cmd *cobra.Command, args []string) {
|
||||
requireGitVersion()
|
||||
requireInRepo()
|
||||
|
||||
var remote string
|
||||
if len(args) > 0 {
|
||||
// Remote is first arg
|
||||
if err := git.ValidateRemote(args[0]); err != nil {
|
||||
Panic(err, fmt.Sprintf("Invalid remote name '%v'", args[0]))
|
||||
}
|
||||
cfg.CurrentRemote = args[0]
|
||||
remote = args[0]
|
||||
} else {
|
||||
// Actively find the default remote, don't just assume origin
|
||||
defaultRemote, err := git.DefaultRemote()
|
||||
if err != nil {
|
||||
Panic(err, "No default remote")
|
||||
}
|
||||
cfg.CurrentRemote = defaultRemote
|
||||
remote = defaultRemote
|
||||
}
|
||||
|
||||
includeArg, excludeArg := getIncludeExcludeArgs(cmd)
|
||||
filter := buildFilepathFilter(cfg, includeArg, excludeArg)
|
||||
pull(filter)
|
||||
pull(remote, filter)
|
||||
}
|
||||
|
||||
func pull(filter *filepathfilter.Filter) {
|
||||
func pull(remote string, filter *filepathfilter.Filter) {
|
||||
cfg.CurrentRemote = remote
|
||||
ref, err := git.CurrentRef()
|
||||
if err != nil {
|
||||
Panic(err, "Could not pull")
|
||||
@ -47,7 +49,7 @@ func pull(filter *filepathfilter.Filter) {
|
||||
pointers := newPointerMap()
|
||||
meter := progress.NewMeter(progress.WithOSEnv(cfg.Os))
|
||||
singleCheckout := newSingleCheckout()
|
||||
q := newDownloadQueue(tq.WithProgress(meter))
|
||||
q := newDownloadQueue(singleCheckout.manifest, remote, tq.WithProgress(meter))
|
||||
gitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
|
||||
if err != nil {
|
||||
LoggedError(err, "Scanner error")
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/rubyist/tracerx"
|
||||
@ -20,10 +21,10 @@ var (
|
||||
)
|
||||
|
||||
func uploadsBetweenRefAndRemote(ctx *uploadContext, refnames []string) {
|
||||
tracerx.Printf("Upload refs %v to remote %v", refnames, cfg.CurrentRemote)
|
||||
tracerx.Printf("Upload refs %v to remote %v", refnames, ctx.Remote)
|
||||
|
||||
gitscanner := lfs.NewGitScanner(nil)
|
||||
if err := gitscanner.RemoteForPush(cfg.CurrentRemote); err != nil {
|
||||
if err := gitscanner.RemoteForPush(ctx.Remote); err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
defer gitscanner.Close()
|
||||
@ -35,17 +36,16 @@ func uploadsBetweenRefAndRemote(ctx *uploadContext, refnames []string) {
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
pointers, err := scanLeftOrAll(gitscanner, ref.Name)
|
||||
if err != nil {
|
||||
if err = uploadLeftOrAll(gitscanner, ctx, ref.Name); err != nil {
|
||||
Print("Error scanning for Git LFS files in the %q ref", ref.Name)
|
||||
ExitWithError(err)
|
||||
}
|
||||
uploadPointers(ctx, pointers)
|
||||
}
|
||||
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
func scanLeftOrAll(g *lfs.GitScanner, ref string) ([]*lfs.WrappedPointer, error) {
|
||||
var pointers []*lfs.WrappedPointer
|
||||
func uploadLeftOrAll(g *lfs.GitScanner, ctx *uploadContext, ref string) error {
|
||||
var multiErr error
|
||||
cb := func(p *lfs.WrappedPointer, err error) {
|
||||
if err != nil {
|
||||
@ -57,26 +57,44 @@ func scanLeftOrAll(g *lfs.GitScanner, ref string) ([]*lfs.WrappedPointer, error)
|
||||
return
|
||||
}
|
||||
|
||||
pointers = append(pointers, p)
|
||||
uploadPointers(ctx, p)
|
||||
}
|
||||
|
||||
if pushAll {
|
||||
if err := g.ScanRefWithDeleted(ref, cb); err != nil {
|
||||
return pointers, err
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := g.ScanLeftToRemote(ref, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := g.ScanLeftToRemote(ref, cb); err != nil {
|
||||
return pointers, err
|
||||
}
|
||||
return pointers, multiErr
|
||||
|
||||
return multiErr
|
||||
}
|
||||
|
||||
func uploadsWithObjectIDs(ctx *uploadContext, oids []string) {
|
||||
pointers := make([]*lfs.WrappedPointer, len(oids))
|
||||
for idx, oid := range oids {
|
||||
pointers[idx] = &lfs.WrappedPointer{Pointer: &lfs.Pointer{Oid: oid}}
|
||||
for _, oid := range oids {
|
||||
mp, err := lfs.LocalMediaPath(oid)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to find local media path:"))
|
||||
}
|
||||
|
||||
stat, err := os.Stat(mp)
|
||||
if err != nil {
|
||||
ExitWithError(errors.Wrap(err, "Unable to stat local media path"))
|
||||
}
|
||||
|
||||
uploadPointers(ctx, &lfs.WrappedPointer{
|
||||
Name: mp,
|
||||
Pointer: &lfs.Pointer{
|
||||
Oid: oid,
|
||||
Size: stat.Size(),
|
||||
},
|
||||
})
|
||||
}
|
||||
uploadPointers(ctx, pointers)
|
||||
|
||||
ctx.Await()
|
||||
}
|
||||
|
||||
func refsByNames(refnames []string) ([]*git.Ref, error) {
|
||||
@ -128,8 +146,7 @@ func pushCommand(cmd *cobra.Command, args []string) {
|
||||
Exit("Invalid remote name %q", args[0])
|
||||
}
|
||||
|
||||
cfg.CurrentRemote = args[0]
|
||||
ctx := newUploadContext(pushDryRun)
|
||||
ctx := newUploadContext(args[0], pushDryRun)
|
||||
|
||||
if pushObjectIDs {
|
||||
if len(args) < 2 {
|
||||
|
@ -50,7 +50,7 @@ func smudge(to io.Writer, from io.Reader, filename string, skip bool, filter *fi
|
||||
download = filter.Allows(filename)
|
||||
}
|
||||
|
||||
err = ptr.Smudge(to, filename, download, TransferManifest(), cb)
|
||||
err = ptr.Smudge(to, filename, download, getTransferManifest(), cb)
|
||||
if file != nil {
|
||||
file.Close()
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -21,16 +22,9 @@ type unlockFlags struct {
|
||||
}
|
||||
|
||||
func unlockCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(lockRemote) > 0 {
|
||||
cfg.CurrentRemote = lockRemote
|
||||
}
|
||||
|
||||
lockClient, err := locking.NewClient(cfg)
|
||||
if err != nil {
|
||||
Exit("Unable to create lock system: %v", err.Error())
|
||||
}
|
||||
lockClient := newLockClient(lockRemote)
|
||||
defer lockClient.Close()
|
||||
|
||||
if len(args) != 0 {
|
||||
path, err := lockPath(args[0])
|
||||
if err != nil {
|
||||
@ -50,6 +44,14 @@ func unlockCommand(cmd *cobra.Command, args []string) {
|
||||
Error("Usage: git lfs unlock (--id my-lock-id | <path>)")
|
||||
}
|
||||
|
||||
if locksCmdFlags.JSON {
|
||||
if err := json.NewEncoder(os.Stdout).Encode(struct {
|
||||
Unlocked bool `json:"unlocked"`
|
||||
}{true}); err != nil {
|
||||
Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
Print("'%s' was unlocked", args[0])
|
||||
}
|
||||
|
||||
@ -62,5 +64,6 @@ func init() {
|
||||
cmd.Flags().StringVarP(&lockRemote, "remote", "r", cfg.CurrentRemote, lockRemoteHelp)
|
||||
cmd.Flags().StringVarP(&unlockCmdFlags.Id, "id", "i", "", "unlock a lock by its ID")
|
||||
cmd.Flags().BoolVarP(&unlockCmdFlags.Force, "force", "f", false, "forcibly break another user's lock(s)")
|
||||
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -10,7 +10,7 @@ var (
|
||||
)
|
||||
|
||||
func versionCommand(cmd *cobra.Command, args []string) {
|
||||
Print(httputil.UserAgent)
|
||||
Print(lfsapi.UserAgent)
|
||||
|
||||
if lovesComics {
|
||||
Print("Nothing may see Gah Lak Tus and survive!")
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/git-lfs/git-lfs/filepathfilter"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/git-lfs/git-lfs/progress"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
@ -33,29 +35,72 @@ var (
|
||||
ManPages = make(map[string]string, 20)
|
||||
cfg = config.Config
|
||||
|
||||
tqManifest *tq.Manifest
|
||||
apiClient *lfsapi.Client
|
||||
global sync.Mutex
|
||||
|
||||
includeArg string
|
||||
excludeArg string
|
||||
)
|
||||
|
||||
// TransferManifest builds a tq.Manifest from the commands package global
|
||||
// cfg var.
|
||||
func TransferManifest() *tq.Manifest {
|
||||
return lfs.TransferManifest(cfg)
|
||||
// getTransferManifest builds a tq.Manifest from the global os and git
|
||||
// environments.
|
||||
func getTransferManifest() *tq.Manifest {
|
||||
c := getAPIClient()
|
||||
|
||||
global.Lock()
|
||||
defer global.Unlock()
|
||||
|
||||
if tqManifest == nil {
|
||||
tqManifest = tq.NewManifestWithClient(c)
|
||||
}
|
||||
|
||||
return tqManifest
|
||||
}
|
||||
|
||||
func getAPIClient() *lfsapi.Client {
|
||||
global.Lock()
|
||||
defer global.Unlock()
|
||||
|
||||
if apiClient == nil {
|
||||
c, err := lfsapi.NewClient(cfg.Os, cfg.Git)
|
||||
if err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
apiClient = c
|
||||
}
|
||||
return apiClient
|
||||
}
|
||||
|
||||
func newLockClient(remote string) *locking.Client {
|
||||
lockClient, err := locking.NewClient(remote, getAPIClient())
|
||||
if err == nil {
|
||||
err = lockClient.SetupFileCache(filepath.Join(config.LocalGitStorageDir, "lfs"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Exit("Unable to create lock system: %v", err.Error())
|
||||
}
|
||||
|
||||
return lockClient
|
||||
}
|
||||
|
||||
// newDownloadCheckQueue builds a checking queue, checks that objects are there but doesn't download
|
||||
func newDownloadCheckQueue(options ...tq.Option) *tq.TransferQueue {
|
||||
return lfs.NewDownloadCheckQueue(cfg, options...)
|
||||
func newDownloadCheckQueue(manifest *tq.Manifest, remote string, options ...tq.Option) *tq.TransferQueue {
|
||||
allOptions := make([]tq.Option, 0, len(options)+1)
|
||||
allOptions = append(allOptions, options...)
|
||||
allOptions = append(allOptions, tq.DryRun(true))
|
||||
return newDownloadQueue(manifest, remote, allOptions...)
|
||||
}
|
||||
|
||||
// newDownloadQueue builds a DownloadQueue, allowing concurrent downloads.
|
||||
func newDownloadQueue(options ...tq.Option) *tq.TransferQueue {
|
||||
return lfs.NewDownloadQueue(cfg, options...)
|
||||
func newDownloadQueue(manifest *tq.Manifest, remote string, options ...tq.Option) *tq.TransferQueue {
|
||||
return tq.NewTransferQueue(tq.Download, manifest, remote, options...)
|
||||
}
|
||||
|
||||
// newUploadQueue builds an UploadQueue, allowing `workers` concurrent uploads.
|
||||
func newUploadQueue(options ...tq.Option) *tq.TransferQueue {
|
||||
return lfs.NewUploadQueue(cfg, options...)
|
||||
func newUploadQueue(manifest *tq.Manifest, remote string, options ...tq.Option) *tq.TransferQueue {
|
||||
return tq.NewTransferQueue(tq.Upload, manifest, remote, options...)
|
||||
}
|
||||
|
||||
func buildFilepathFilter(config *config.Configuration, includeArg, excludeArg *string) *filepathfilter.Filter {
|
||||
@ -69,28 +114,26 @@ func downloadTransfer(p *lfs.WrappedPointer) (name, path, oid string, size int64
|
||||
return p.Name, path, p.Oid, p.Size
|
||||
}
|
||||
|
||||
func uploadTransfer(oid, filename string) (*tq.Transfer, error) {
|
||||
func uploadTransfer(p *lfs.WrappedPointer) (*tq.Transfer, error) {
|
||||
filename := p.Name
|
||||
oid := p.Oid
|
||||
|
||||
localMediaPath, err := lfs.LocalMediaPath(oid)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
|
||||
}
|
||||
|
||||
if len(filename) > 0 {
|
||||
if err = ensureFile(filename, localMediaPath); err != nil {
|
||||
if err = ensureFile(filename, localMediaPath); err != nil && !errors.IsCleanPointerError(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := os.Stat(localMediaPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
|
||||
}
|
||||
|
||||
return &tq.Transfer{
|
||||
Name: filename,
|
||||
Path: localMediaPath,
|
||||
Oid: oid,
|
||||
Size: fi.Size(),
|
||||
Size: p.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -101,7 +144,6 @@ func ensureFile(smudgePath, cleanPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
expectedOid := filepath.Base(cleanPath)
|
||||
localPath := filepath.Join(config.LocalWorkingDir, smudgePath)
|
||||
file, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
@ -123,11 +165,6 @@ func ensureFile(smudgePath, cleanPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expectedOid != cleaned.Oid {
|
||||
return fmt.Errorf("Trying to push %q with OID %s.\nNot found in %s.", smudgePath, expectedOid, filepath.Dir(cleanPath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -321,7 +358,7 @@ func logPanicToWriter(w io.Writer, loggedError error) {
|
||||
fmt.Fprintln(w, "\nENV:")
|
||||
|
||||
// log the environment
|
||||
for _, env := range lfs.Environ(cfg, TransferManifest()) {
|
||||
for _, env := range lfs.Environ(cfg, getTransferManifest()) {
|
||||
fmt.Fprintln(w, env)
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func newSingleCheckout() *singleCheckout {
|
||||
return &singleCheckout{
|
||||
gitIndexer: &gitIndexer{},
|
||||
pathConverter: pathConverter,
|
||||
manifest: TransferManifest(),
|
||||
manifest: getTransferManifest(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,13 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/httputil"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/localstorage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -64,7 +66,7 @@ func Run() {
|
||||
}
|
||||
|
||||
root.Execute()
|
||||
httputil.LogHttpStats(cfg)
|
||||
logHTTPStats(getAPIClient())
|
||||
}
|
||||
|
||||
func gitlfsCommand(cmd *cobra.Command, args []string) {
|
||||
@ -103,3 +105,28 @@ func printHelp(commandName string) {
|
||||
fmt.Fprintf(os.Stderr, "Sorry, no usage text found for %q\n", commandName)
|
||||
}
|
||||
}
|
||||
|
||||
func logHTTPStats(c *lfsapi.Client) {
|
||||
if !c.LoggingStats {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := statsLogFile()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error logging http stats: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
c.LogStats(file)
|
||||
}
|
||||
|
||||
func statsLogFile() (*os.File, error) {
|
||||
logBase := filepath.Join(config.LocalLogDir, "http")
|
||||
if err := os.MkdirAll(logBase, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logFile := fmt.Sprintf("http-%d.log", time.Now().Unix())
|
||||
return os.Create(filepath.Join(logBase, logFile))
|
||||
}
|
||||
|
@ -2,25 +2,46 @@ package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/lfs"
|
||||
"github.com/git-lfs/git-lfs/locking"
|
||||
"github.com/git-lfs/git-lfs/progress"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
"github.com/git-lfs/git-lfs/tq"
|
||||
)
|
||||
|
||||
var uploadMissingErr = "%s does not exist in .git/lfs/objects. Tried %s, which matches %s."
|
||||
|
||||
type uploadContext struct {
|
||||
Remote string
|
||||
DryRun bool
|
||||
Manifest *tq.Manifest
|
||||
uploadedOids tools.StringSet
|
||||
|
||||
meter progress.Meter
|
||||
tq *tq.TransferQueue
|
||||
|
||||
lockClient *locking.Client
|
||||
committerName string
|
||||
committerEmail string
|
||||
unownedLocks uint64
|
||||
}
|
||||
|
||||
func newUploadContext(dryRun bool) *uploadContext {
|
||||
return &uploadContext{
|
||||
func newUploadContext(remote string, dryRun bool) *uploadContext {
|
||||
cfg.CurrentRemote = remote
|
||||
|
||||
ctx := &uploadContext{
|
||||
Remote: remote,
|
||||
Manifest: getTransferManifest(),
|
||||
DryRun: dryRun,
|
||||
uploadedOids: tools.NewStringSet(),
|
||||
lockClient: newLockClient(remote),
|
||||
}
|
||||
|
||||
ctx.meter = buildProgressMeter(ctx.DryRun)
|
||||
ctx.tq = newUploadQueue(ctx.Manifest, ctx.Remote, tq.WithProgress(ctx.meter), tq.DryRun(ctx.DryRun))
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// AddUpload adds the given oid to the set of oids that have been uploaded in
|
||||
@ -35,12 +56,9 @@ func (c *uploadContext) HasUploaded(oid string) bool {
|
||||
return c.uploadedOids.Contains(oid)
|
||||
}
|
||||
|
||||
func (c *uploadContext) prepareUpload(unfiltered []*lfs.WrappedPointer) (*tq.TransferQueue, []*lfs.WrappedPointer) {
|
||||
func (c *uploadContext) prepareUpload(unfiltered ...*lfs.WrappedPointer) (*tq.TransferQueue, []*lfs.WrappedPointer) {
|
||||
numUnfiltered := len(unfiltered)
|
||||
uploadables := make([]*lfs.WrappedPointer, 0, numUnfiltered)
|
||||
missingLocalObjects := make([]*lfs.WrappedPointer, 0, numUnfiltered)
|
||||
missingSize := int64(0)
|
||||
meter := buildProgressMeter(c.DryRun)
|
||||
|
||||
// XXX(taylor): temporary measure to fix duplicate (broken) results from
|
||||
// scanner
|
||||
@ -56,75 +74,48 @@ func (c *uploadContext) prepareUpload(unfiltered []*lfs.WrappedPointer) (*tq.Tra
|
||||
}
|
||||
uniqOids.Add(p.Oid)
|
||||
|
||||
// estimate in meter early (even if it's not going into uploadables), since
|
||||
// we will call Skip() based on the results of the download check queue.
|
||||
meter.Add(p.Size)
|
||||
// canUpload determines whether the current pointer "p" can be
|
||||
// uploaded through the TransferQueue below. It is set to false
|
||||
// only when the file is locked by someone other than the
|
||||
// current committer.
|
||||
var canUpload bool = true
|
||||
|
||||
if lfs.ObjectExistsOfSize(p.Oid, p.Size) {
|
||||
uploadables = append(uploadables, p)
|
||||
} else {
|
||||
// We think we need to push this but we don't have it
|
||||
// Store for server checking later
|
||||
missingLocalObjects = append(missingLocalObjects, p)
|
||||
missingSize += p.Size
|
||||
lockQuery := map[string]string{"path": p.Name}
|
||||
locks, err := c.lockClient.SearchLocks(lockQuery, 1, true)
|
||||
if err != nil {
|
||||
ExitWithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if the server has the missing objects.
|
||||
c.checkMissing(missingLocalObjects, missingSize)
|
||||
if len(locks) > 0 {
|
||||
lock := locks[0]
|
||||
|
||||
owned := lock.Name == c.committerName &&
|
||||
lock.Email == c.committerEmail
|
||||
|
||||
if owned {
|
||||
Print("Consider unlocking your locked file: %s", lock.Path)
|
||||
} else {
|
||||
Print("Unable to push file %s locked by: %s", lock.Path, lock.Name)
|
||||
|
||||
atomic.AddUint64(&c.unownedLocks, 1)
|
||||
canUpload = false
|
||||
}
|
||||
}
|
||||
|
||||
if canUpload {
|
||||
// estimate in meter early (even if it's not going into
|
||||
// uploadables), since we will call Skip() based on the
|
||||
// results of the download check queue.
|
||||
c.meter.Add(p.Size)
|
||||
|
||||
// build the TransferQueue, automatically skipping any missing objects that
|
||||
// the server already has.
|
||||
uploadQueue := newUploadQueue(tq.WithProgress(meter), tq.DryRun(c.DryRun))
|
||||
for _, p := range missingLocalObjects {
|
||||
if c.HasUploaded(p.Oid) {
|
||||
// if the server already has this object, call Skip() on
|
||||
// the progressmeter to decrement the number of files by
|
||||
// 1 and the number of bytes by `p.Size`.
|
||||
uploadQueue.Skip(p.Size)
|
||||
} else {
|
||||
uploadables = append(uploadables, p)
|
||||
}
|
||||
}
|
||||
|
||||
return uploadQueue, uploadables
|
||||
return c.tq, uploadables
|
||||
}
|
||||
|
||||
// This checks the given slice of pointers that don't exist in .git/lfs/objects
|
||||
// against the server. Anything the server already has does not need to be
|
||||
// uploaded again.
|
||||
func (c *uploadContext) checkMissing(missing []*lfs.WrappedPointer, missingSize int64) {
|
||||
numMissing := len(missing)
|
||||
if numMissing == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkQueue := newDownloadCheckQueue()
|
||||
transferCh := checkQueue.Watch()
|
||||
|
||||
done := make(chan int)
|
||||
go func() {
|
||||
// this channel is filled with oids for which Check() succeeded
|
||||
// and Transfer() was called
|
||||
for oid := range transferCh {
|
||||
c.SetUploaded(oid)
|
||||
}
|
||||
done <- 1
|
||||
}()
|
||||
|
||||
for _, p := range missing {
|
||||
checkQueue.Add(downloadTransfer(p))
|
||||
}
|
||||
|
||||
// Currently this is needed to flush the batch but is not enough to sync
|
||||
// transferc completely. By the time that checkQueue.Wait() returns, the
|
||||
// transferCh will have been closed, allowing the goroutine above to
|
||||
// send "1" into the `done` channel.
|
||||
checkQueue.Wait()
|
||||
<-done
|
||||
}
|
||||
|
||||
func uploadPointers(c *uploadContext, unfiltered []*lfs.WrappedPointer) {
|
||||
func uploadPointers(c *uploadContext, unfiltered ...*lfs.WrappedPointer) {
|
||||
if c.DryRun {
|
||||
for _, p := range unfiltered {
|
||||
if c.HasUploaded(p.Oid) {
|
||||
@ -138,28 +129,30 @@ func uploadPointers(c *uploadContext, unfiltered []*lfs.WrappedPointer) {
|
||||
return
|
||||
}
|
||||
|
||||
q, pointers := c.prepareUpload(unfiltered)
|
||||
q, pointers := c.prepareUpload(unfiltered...)
|
||||
for _, p := range pointers {
|
||||
t, err := uploadTransfer(p.Oid, p.Name)
|
||||
if err != nil {
|
||||
if errors.IsCleanPointerError(err) {
|
||||
Exit(uploadMissingErr, p.Oid, p.Name, errors.GetContext(err, "pointer").(*lfs.Pointer).Oid)
|
||||
} else {
|
||||
ExitWithError(err)
|
||||
}
|
||||
t, err := uploadTransfer(p)
|
||||
if err != nil && !errors.IsCleanPointerError(err) {
|
||||
ExitWithError(err)
|
||||
}
|
||||
|
||||
q.Add(t.Name, t.Path, t.Oid, t.Size)
|
||||
c.SetUploaded(p.Oid)
|
||||
}
|
||||
}
|
||||
|
||||
q.Wait()
|
||||
func (c *uploadContext) Await() {
|
||||
c.tq.Wait()
|
||||
|
||||
for _, err := range q.Errors() {
|
||||
for _, err := range c.tq.Errors() {
|
||||
FullError(err)
|
||||
}
|
||||
|
||||
if len(q.Errors()) > 0 {
|
||||
if len(c.tq.Errors()) > 0 {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if nl := atomic.LoadUint64(&c.unownedLocks); nl > 0 {
|
||||
ExitWithError(errors.Errorf("lfs: refusing to push %d un-owned lock(s)", nl))
|
||||
}
|
||||
}
|
||||
|
233
config/config.go
233
config/config.go
@ -5,17 +5,12 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
"github.com/bgentry/go-netrc/netrc"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -57,34 +52,20 @@ type Configuration struct {
|
||||
// configuration.
|
||||
Git Environment
|
||||
|
||||
CurrentRemote string
|
||||
NtlmSession ntlm.ClientSession
|
||||
envVars map[string]string
|
||||
envVarsMutex sync.Mutex
|
||||
IsTracingHttp bool
|
||||
IsDebuggingHttp bool
|
||||
IsLoggingStats bool
|
||||
CurrentRemote string
|
||||
|
||||
loading sync.Mutex // guards initialization of gitConfig and remotes
|
||||
remotes []string
|
||||
extensions map[string]Extension
|
||||
manualEndpoint *Endpoint
|
||||
parsedNetrc netrcfinder
|
||||
urlAliasesMap map[string]string
|
||||
urlAliasMu sync.Mutex
|
||||
manualEndpoint *lfsapi.Endpoint
|
||||
endpointFinder lfsapi.EndpointFinder
|
||||
endpointMu sync.Mutex
|
||||
}
|
||||
|
||||
func New() *Configuration {
|
||||
c := &Configuration{
|
||||
Os: EnvironmentOf(NewOsFetcher()),
|
||||
CurrentRemote: defaultRemote,
|
||||
envVars: make(map[string]string),
|
||||
}
|
||||
|
||||
c := &Configuration{Os: EnvironmentOf(NewOsFetcher())}
|
||||
c.Git = &gitEnvironment{config: c}
|
||||
c.IsTracingHttp = c.Os.Bool("GIT_CURL_VERBOSE", false)
|
||||
c.IsDebuggingHttp = c.Os.Bool("LFS_DEBUG_HTTP", false)
|
||||
c.IsLoggingStats = c.Os.Bool("GIT_LOG_STATS", false)
|
||||
initConfig(c)
|
||||
return c
|
||||
}
|
||||
|
||||
@ -103,12 +84,16 @@ type Values struct {
|
||||
//
|
||||
// This method should only be used during testing.
|
||||
func NewFrom(v Values) *Configuration {
|
||||
return &Configuration{
|
||||
c := &Configuration{
|
||||
Os: EnvironmentOf(mapFetcher(v.Os)),
|
||||
Git: EnvironmentOf(mapFetcher(v.Git)),
|
||||
|
||||
envVars: make(map[string]string, 0),
|
||||
}
|
||||
initConfig(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func initConfig(c *Configuration) {
|
||||
c.CurrentRemote = defaultRemote
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the *Configuration in context into all of `v`'s fields,
|
||||
@ -202,51 +187,19 @@ func (c *Configuration) parseTag(tag reflect.StructTag) (key string, env Environ
|
||||
// GitRemoteUrl returns the git clone/push url for a given remote (blank if not found)
|
||||
// the forpush argument is to cater for separate remote.name.pushurl settings
|
||||
func (c *Configuration) GitRemoteUrl(remote string, forpush bool) string {
|
||||
if forpush {
|
||||
if u, ok := c.Git.Get("remote." + remote + ".pushurl"); ok {
|
||||
return u
|
||||
}
|
||||
}
|
||||
|
||||
if u, ok := c.Git.Get("remote." + remote + ".url"); ok {
|
||||
return u
|
||||
}
|
||||
|
||||
if err := git.ValidateRemote(remote); err == nil {
|
||||
return remote
|
||||
}
|
||||
|
||||
return ""
|
||||
|
||||
return c.endpointConfig().GitRemoteURL(remote, forpush)
|
||||
}
|
||||
|
||||
// Manually set an Endpoint to use instead of deriving from Git config
|
||||
func (c *Configuration) SetManualEndpoint(e Endpoint) {
|
||||
func (c *Configuration) SetManualEndpoint(e lfsapi.Endpoint) {
|
||||
c.manualEndpoint = &e
|
||||
}
|
||||
|
||||
func (c *Configuration) Endpoint(operation string) Endpoint {
|
||||
func (c *Configuration) Endpoint(operation string) lfsapi.Endpoint {
|
||||
if c.manualEndpoint != nil {
|
||||
return *c.manualEndpoint
|
||||
}
|
||||
|
||||
if operation == "upload" {
|
||||
if url, ok := c.Git.Get("lfs.pushurl"); ok {
|
||||
return NewEndpointWithConfig(url, c)
|
||||
}
|
||||
}
|
||||
|
||||
if url, ok := c.Git.Get("lfs.url"); ok {
|
||||
return NewEndpointWithConfig(url, c)
|
||||
}
|
||||
|
||||
if len(c.CurrentRemote) > 0 && c.CurrentRemote != defaultRemote {
|
||||
if endpoint := c.RemoteEndpoint(c.CurrentRemote, operation); len(endpoint.Url) > 0 {
|
||||
return endpoint
|
||||
}
|
||||
}
|
||||
|
||||
return c.RemoteEndpoint(defaultRemote, operation)
|
||||
return c.endpointConfig().Endpoint(operation, c.CurrentRemote)
|
||||
}
|
||||
|
||||
func (c *Configuration) ConcurrentTransfers() int {
|
||||
@ -283,74 +236,16 @@ func (c *Configuration) BatchTransfer() bool {
|
||||
}
|
||||
|
||||
func (c *Configuration) NtlmAccess(operation string) bool {
|
||||
return c.Access(operation) == "ntlm"
|
||||
}
|
||||
|
||||
// PrivateAccess will retrieve the access value and return true if
|
||||
// the value is set to private. When a repo is marked as having private
|
||||
// access, the http requests for the batch api will fetch the credentials
|
||||
// before running, otherwise the request will run without credentials.
|
||||
func (c *Configuration) PrivateAccess(operation string) bool {
|
||||
return c.Access(operation) != "none"
|
||||
return c.Access(operation) == lfsapi.NTLMAccess
|
||||
}
|
||||
|
||||
// Access returns the access auth type.
|
||||
func (c *Configuration) Access(operation string) string {
|
||||
func (c *Configuration) Access(operation string) lfsapi.Access {
|
||||
return c.EndpointAccess(c.Endpoint(operation))
|
||||
}
|
||||
|
||||
// SetAccess will set the private access flag in .git/config.
|
||||
func (c *Configuration) SetAccess(operation string, authType string) {
|
||||
c.SetEndpointAccess(c.Endpoint(operation), authType)
|
||||
}
|
||||
|
||||
func (c *Configuration) FindNetrcHost(host string) (*netrc.Machine, error) {
|
||||
c.loading.Lock()
|
||||
defer c.loading.Unlock()
|
||||
if c.parsedNetrc == nil {
|
||||
n, err := c.parseNetrc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.parsedNetrc = n
|
||||
}
|
||||
|
||||
return c.parsedNetrc.FindMachine(host), nil
|
||||
}
|
||||
|
||||
// Manually override the netrc config
|
||||
func (c *Configuration) SetNetrc(n netrcfinder) {
|
||||
c.parsedNetrc = n
|
||||
}
|
||||
|
||||
func (c *Configuration) EndpointAccess(e Endpoint) string {
|
||||
key := fmt.Sprintf("lfs.%s.access", e.Url)
|
||||
if v, ok := c.Git.Get(key); ok && len(v) > 0 {
|
||||
lower := strings.ToLower(v)
|
||||
if lower == "private" {
|
||||
return "basic"
|
||||
}
|
||||
return lower
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (c *Configuration) SetEndpointAccess(e Endpoint, authType string) {
|
||||
c.loadGitConfig()
|
||||
|
||||
tracerx.Printf("setting repository access to %s", authType)
|
||||
key := fmt.Sprintf("lfs.%s.access", e.Url)
|
||||
|
||||
// Modify the config cache because it's checked again in this process
|
||||
// without being reloaded.
|
||||
switch authType {
|
||||
case "", "none":
|
||||
git.Config.UnsetLocalKey("", key)
|
||||
c.Git.del(key)
|
||||
default:
|
||||
git.Config.SetLocal("", key, authType)
|
||||
c.Git.set(key, authType)
|
||||
}
|
||||
func (c *Configuration) EndpointAccess(e lfsapi.Endpoint) lfsapi.Access {
|
||||
return c.endpointConfig().AccessFor(e.Url)
|
||||
}
|
||||
|
||||
func (c *Configuration) FetchIncludePaths() []string {
|
||||
@ -363,27 +258,8 @@ func (c *Configuration) FetchExcludePaths() []string {
|
||||
return tools.CleanPaths(patterns, ",")
|
||||
}
|
||||
|
||||
func (c *Configuration) RemoteEndpoint(remote, operation string) Endpoint {
|
||||
if len(remote) == 0 {
|
||||
remote = defaultRemote
|
||||
}
|
||||
|
||||
// Support separate push URL if specified and pushing
|
||||
if operation == "upload" {
|
||||
if url, ok := c.Git.Get("remote." + remote + ".lfspushurl"); ok {
|
||||
return NewEndpointWithConfig(url, c)
|
||||
}
|
||||
}
|
||||
if url, ok := c.Git.Get("remote." + remote + ".lfsurl"); ok {
|
||||
return NewEndpointWithConfig(url, c)
|
||||
}
|
||||
|
||||
// finally fall back on git remote url (also supports pushurl)
|
||||
if url := c.GitRemoteUrl(remote, operation == "upload"); url != "" {
|
||||
return NewEndpointFromCloneURLWithConfig(url, c)
|
||||
}
|
||||
|
||||
return Endpoint{}
|
||||
func (c *Configuration) RemoteEndpoint(remote, operation string) lfsapi.Endpoint {
|
||||
return c.endpointConfig().RemoteEndpoint(operation, remote)
|
||||
}
|
||||
|
||||
func (c *Configuration) Remotes() []string {
|
||||
@ -392,18 +268,23 @@ func (c *Configuration) Remotes() []string {
|
||||
return c.remotes
|
||||
}
|
||||
|
||||
// GitProtocol returns the protocol for the LFS API when converting from a
|
||||
// git:// remote url.
|
||||
func (c *Configuration) GitProtocol() string {
|
||||
if value, ok := c.Git.Get("lfs.gitprotocol"); ok {
|
||||
return value
|
||||
return c.endpointConfig().GitProtocol()
|
||||
}
|
||||
|
||||
func (c *Configuration) endpointConfig() lfsapi.EndpointFinder {
|
||||
c.endpointMu.Lock()
|
||||
defer c.endpointMu.Unlock()
|
||||
|
||||
if c.endpointFinder == nil {
|
||||
c.endpointFinder = lfsapi.NewEndpointFinder(c.Git)
|
||||
}
|
||||
return "https"
|
||||
|
||||
return c.endpointFinder
|
||||
}
|
||||
|
||||
func (c *Configuration) Extensions() map[string]Extension {
|
||||
c.loadGitConfig()
|
||||
|
||||
return c.extensions
|
||||
}
|
||||
|
||||
@ -412,50 +293,6 @@ func (c *Configuration) SortedExtensions() ([]Extension, error) {
|
||||
return SortExtensions(c.Extensions())
|
||||
}
|
||||
|
||||
func (c *Configuration) urlAliases() map[string]string {
|
||||
c.urlAliasMu.Lock()
|
||||
defer c.urlAliasMu.Unlock()
|
||||
|
||||
if c.urlAliasesMap == nil {
|
||||
c.urlAliasesMap = make(map[string]string)
|
||||
prefix := "url."
|
||||
suffix := ".insteadof"
|
||||
for gitkey, gitval := range c.Git.All() {
|
||||
if strings.HasPrefix(gitkey, prefix) && strings.HasSuffix(gitkey, suffix) {
|
||||
if _, ok := c.urlAliasesMap[gitval]; ok {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Multiple 'url.*.insteadof' keys with the same alias: %q\n", gitval)
|
||||
}
|
||||
c.urlAliasesMap[gitval] = gitkey[len(prefix) : len(gitkey)-len(suffix)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.urlAliasesMap
|
||||
}
|
||||
|
||||
// ReplaceUrlAlias returns a url with a prefix from a `url.*.insteadof` git
|
||||
// config setting. If multiple aliases match, use the longest one.
|
||||
// See https://git-scm.com/docs/git-config for Git's docs.
|
||||
func (c *Configuration) ReplaceUrlAlias(rawurl string) string {
|
||||
var longestalias string
|
||||
aliases := c.urlAliases()
|
||||
for alias, _ := range aliases {
|
||||
if !strings.HasPrefix(rawurl, alias) {
|
||||
continue
|
||||
}
|
||||
|
||||
if longestalias < alias {
|
||||
longestalias = alias
|
||||
}
|
||||
}
|
||||
|
||||
if len(longestalias) > 0 {
|
||||
return aliases[longestalias] + rawurl[len(longestalias):]
|
||||
}
|
||||
|
||||
return rawurl
|
||||
}
|
||||
|
||||
func (c *Configuration) FetchPruneConfig() FetchPruneConfig {
|
||||
f := &FetchPruneConfig{
|
||||
FetchRecentRefsDays: 7,
|
||||
|
@ -7,266 +7,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEndpointDefaultsToOrigin(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.lfsurl": "abc"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "abc", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointOverridesOrigin(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": "abc",
|
||||
"remote.origin.lfsurl": "def",
|
||||
},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "abc", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointNoOverrideDefaultRemote(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.lfsurl": "abc",
|
||||
"remote.other.lfsurl": "def",
|
||||
},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "abc", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointUseAlternateRemote(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.lfsurl": "abc",
|
||||
"remote.other.lfsurl": "def",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.CurrentRemote = "other"
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "def", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "https://example.com/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestBareEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "https://example.com/foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointSeparateClonePushUrl(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar.git",
|
||||
"remote.origin.pushurl": "https://readwrite.com/foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
|
||||
endpoint = cfg.Endpoint("upload")
|
||||
assert.Equal(t, "https://readwrite.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointOverriddenSeparateClonePushLfsUrl(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar.git",
|
||||
"remote.origin.pushurl": "https://readwrite.com/foo/bar.git",
|
||||
"remote.origin.lfsurl": "https://examplelfs.com/foo/bar",
|
||||
"remote.origin.lfspushurl": "https://readwritelfs.com/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://examplelfs.com/foo/bar", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
|
||||
endpoint = cfg.Endpoint("upload")
|
||||
assert.Equal(t, "https://readwritelfs.com/foo/bar", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointGlobalSeparateLfsPush(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": "https://readonly.com/foo/bar",
|
||||
"lfs.pushurl": "https://write.com/foo/bar",
|
||||
},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://readonly.com/foo/bar", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
|
||||
endpoint = cfg.Endpoint("upload")
|
||||
assert.Equal(t, "https://write.com/foo/bar", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
}
|
||||
|
||||
func TestSSHEndpointOverridden(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.url": "git@example.com:foo/bar",
|
||||
"remote.origin.lfsurl": "lfs",
|
||||
},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "ssh://git@example.com/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "git@example.com", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHCustomPortEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "ssh://git@example.com:9000/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "git@example.com", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar", endpoint.SshPath)
|
||||
assert.Equal(t, "9000", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestBareSSHEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "git@example.com:foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "git@example.com", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar.git", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHEndpointFromGlobalLfsUrl(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"lfs.url": "git@example.com:foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git", endpoint.Url)
|
||||
assert.Equal(t, "git@example.com", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar.git", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestHTTPEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "http://example.com/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestBareHTTPEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "http://example.com/foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestGitEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "git://example.com/foo/bar"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestGitEndpointAddsLfsSuffixWithCustomProtocol(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"remote.origin.url": "git://example.com/foo/bar",
|
||||
"lfs.gitprotocol": "http",
|
||||
},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestBareGitEndpointAddsLfsSuffix(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{"remote.origin.url": "git://example.com/foo/bar.git"},
|
||||
})
|
||||
|
||||
endpoint := cfg.Endpoint("download")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", endpoint.Url)
|
||||
assert.Equal(t, "", endpoint.SshUserAndHost)
|
||||
assert.Equal(t, "", endpoint.SshPath)
|
||||
assert.Equal(t, "", endpoint.SshPort)
|
||||
}
|
||||
|
||||
func TestConcurrentTransfersSetValue(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
@ -405,82 +145,6 @@ func TestBatchAbsentIsTrue(t *testing.T) {
|
||||
assert.True(t, v)
|
||||
}
|
||||
|
||||
func TestAccessConfig(t *testing.T) {
|
||||
type accessTest struct {
|
||||
Access string
|
||||
PrivateAccess bool
|
||||
}
|
||||
|
||||
tests := map[string]accessTest{
|
||||
"": {"none", false},
|
||||
"basic": {"basic", true},
|
||||
"BASIC": {"basic", true},
|
||||
"private": {"basic", true},
|
||||
"PRIVATE": {"basic", true},
|
||||
"invalidauth": {"invalidauth", true},
|
||||
}
|
||||
|
||||
for value, expected := range tests {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": "http://example.com",
|
||||
"lfs.http://example.com.access": value,
|
||||
"lfs.https://example.com.access": "bad",
|
||||
},
|
||||
})
|
||||
|
||||
if access := cfg.Access("download"); access != expected.Access {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
if access := cfg.Access("upload"); access != expected.Access {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
|
||||
if priv := cfg.PrivateAccess("download"); priv != expected.PrivateAccess {
|
||||
t.Errorf("Expected PrivateAccess() with value %q to be %v, got %v", value, expected.PrivateAccess, priv)
|
||||
}
|
||||
if priv := cfg.PrivateAccess("upload"); priv != expected.PrivateAccess {
|
||||
t.Errorf("Expected PrivateAccess() with value %q to be %v, got %v", value, expected.PrivateAccess, priv)
|
||||
}
|
||||
}
|
||||
|
||||
// Test again but with separate push url
|
||||
for value, expected := range tests {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": "http://example.com",
|
||||
"lfs.pushurl": "http://examplepush.com",
|
||||
"lfs.http://example.com.access": value,
|
||||
"lfs.http://examplepush.com.access": value,
|
||||
"lfs.https://example.com.access": "bad",
|
||||
},
|
||||
})
|
||||
|
||||
if access := cfg.Access("download"); access != expected.Access {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
if access := cfg.Access("upload"); access != expected.Access {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
|
||||
if priv := cfg.PrivateAccess("download"); priv != expected.PrivateAccess {
|
||||
t.Errorf("Expected PrivateAccess() with value %q to be %v, got %v", value, expected.PrivateAccess, priv)
|
||||
}
|
||||
if priv := cfg.PrivateAccess("upload"); priv != expected.PrivateAccess {
|
||||
t.Errorf("Expected PrivateAccess() with value %q to be %v, got %v", value, expected.PrivateAccess, priv)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAccessAbsentConfig(t *testing.T) {
|
||||
cfg := NewFrom(Values{})
|
||||
assert.Equal(t, "none", cfg.Access("download"))
|
||||
assert.Equal(t, "none", cfg.Access("upload"))
|
||||
assert.False(t, cfg.PrivateAccess("download"))
|
||||
assert.False(t, cfg.PrivateAccess("upload"))
|
||||
}
|
||||
|
||||
func TestLoadValidExtension(t *testing.T) {
|
||||
cfg := NewFrom(Values{
|
||||
Git: map[string]string{},
|
||||
|
@ -40,10 +40,6 @@ type Environment interface {
|
||||
|
||||
// All returns a copy of all the key/value pairs for the current environment.
|
||||
All() map[string]string
|
||||
|
||||
// deprecated, don't use
|
||||
set(key, value string)
|
||||
del(key string)
|
||||
}
|
||||
|
||||
type environment struct {
|
||||
@ -94,11 +90,3 @@ func (e *environment) Int(key string, def int) (val int) {
|
||||
func (e *environment) All() map[string]string {
|
||||
return e.Fetcher.All()
|
||||
}
|
||||
|
||||
func (e *environment) set(key, value string) {
|
||||
e.Fetcher.set(key, value)
|
||||
}
|
||||
|
||||
func (e *environment) del(key string) {
|
||||
e.Fetcher.del(key)
|
||||
}
|
||||
|
@ -10,8 +10,4 @@ type Fetcher interface {
|
||||
|
||||
// All returns a copy of all the key/value pairs for the current environment.
|
||||
All() map[string]string
|
||||
|
||||
// deprecated, don't use
|
||||
set(key, value string)
|
||||
del(key string)
|
||||
}
|
||||
|
@ -44,18 +44,6 @@ func (g *gitEnvironment) All() map[string]string {
|
||||
return g.git.All()
|
||||
}
|
||||
|
||||
func (g *gitEnvironment) set(key, value string) {
|
||||
g.loadGitConfig()
|
||||
|
||||
g.git.set(key, value)
|
||||
}
|
||||
|
||||
func (g *gitEnvironment) del(key string) {
|
||||
g.loadGitConfig()
|
||||
|
||||
g.git.del(key)
|
||||
}
|
||||
|
||||
// loadGitConfig reads and parses the .gitconfig by calling ReadGitConfig. It
|
||||
// also sets values on the configuration instance `g.config`.
|
||||
//
|
||||
|
@ -139,18 +139,6 @@ func (g *GitFetcher) All() map[string]string {
|
||||
return newmap
|
||||
}
|
||||
|
||||
func (g *GitFetcher) set(key, value string) {
|
||||
g.vmu.Lock()
|
||||
defer g.vmu.Unlock()
|
||||
g.vals[strings.ToLower(key)] = value
|
||||
}
|
||||
|
||||
func (g *GitFetcher) del(key string) {
|
||||
g.vmu.Lock()
|
||||
defer g.vmu.Unlock()
|
||||
delete(g.vals, strings.ToLower(key))
|
||||
}
|
||||
|
||||
func getGitConfigs() (sources []*GitConfig) {
|
||||
if lfsconfig := getFileGitConfig(".lfsconfig"); lfsconfig != nil {
|
||||
sources = append(sources, lfsconfig)
|
||||
|
@ -21,11 +21,3 @@ func (m mapFetcher) All() map[string]string {
|
||||
}
|
||||
return newmap
|
||||
}
|
||||
|
||||
func (m mapFetcher) set(key, value string) {
|
||||
m[key] = value
|
||||
}
|
||||
|
||||
func (m mapFetcher) del(key string) {
|
||||
delete(m, key)
|
||||
}
|
||||
|
@ -57,11 +57,3 @@ func (o *OsFetcher) Get(key string) (val string, ok bool) {
|
||||
func (o *OsFetcher) All() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OsFetcher) set(key, value string) {
|
||||
panic("cannot modify OS ENV keys")
|
||||
}
|
||||
|
||||
func (o *OsFetcher) del(key string) {
|
||||
panic("cannot modify OS ENV keys")
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -28,4 +30,5 @@ func init() {
|
||||
gitCommit,
|
||||
)
|
||||
|
||||
lfsapi.UserAgent = VersionDesc
|
||||
}
|
||||
|
@ -129,8 +129,10 @@ Or if there was an error:
|
||||
|
||||
#### Stage 2: 0..N Transfers
|
||||
|
||||
After the initiation exchange, git-lfs will send any number of transfer
|
||||
requests to the stdin of the transfer process.
|
||||
After the initiation exchange, git-lfs will send any number of transfer
|
||||
requests to the stdin of the transfer process, in a serial sequence. Once a
|
||||
transfer request is sent to the process, it awaits a completion response before
|
||||
sending the next request.
|
||||
|
||||
##### Uploads
|
||||
|
||||
|
30
git/git.go
30
git/git.go
@ -573,6 +573,27 @@ func GetCommitSummary(commit string) (*CommitSummary, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func isCygwin() bool {
|
||||
cmd := subprocess.ExecCommand("uname")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Contains(out, []byte("CYGWIN"))
|
||||
}
|
||||
|
||||
func translateCygwinPath(path string) (string, error) {
|
||||
cmd := subprocess.ExecCommand("cygpath", "-w", path)
|
||||
buf := &bytes.Buffer{}
|
||||
cmd.Stderr = buf
|
||||
out, err := cmd.Output()
|
||||
output := strings.TrimSpace(string(out))
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("Failed to translate path from cygwin to windows: %s", buf.String())
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func GitAndRootDirs() (string, string, error) {
|
||||
cmd := subprocess.ExecCommand("git", "rev-parse", "--git-dir", "--show-toplevel")
|
||||
buf := &bytes.Buffer{}
|
||||
@ -587,6 +608,12 @@ func GitAndRootDirs() (string, string, error) {
|
||||
paths := strings.Split(output, "\n")
|
||||
pathLen := len(paths)
|
||||
|
||||
if isCygwin() {
|
||||
for i := 0; i < pathLen; i++ {
|
||||
paths[i], err = translateCygwinPath(paths[i])
|
||||
}
|
||||
}
|
||||
|
||||
if pathLen == 0 {
|
||||
return "", "", fmt.Errorf("Bad git rev-parse output: %q", output)
|
||||
}
|
||||
@ -612,6 +639,9 @@ func RootDir() (string, error) {
|
||||
}
|
||||
|
||||
path := strings.TrimSpace(string(out))
|
||||
if isCygwin() {
|
||||
path, err = translateCygwinPath(path)
|
||||
}
|
||||
if len(path) > 0 {
|
||||
return filepath.Abs(path)
|
||||
}
|
||||
|
351
httputil/http.go
351
httputil/http.go
@ -1,351 +0,0 @@
|
||||
// Package httputil provides additional helper functions for http services
|
||||
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
type httpTransferStats struct {
|
||||
HeaderSize int
|
||||
BodySize int
|
||||
Start time.Time
|
||||
Stop time.Time
|
||||
}
|
||||
|
||||
type httpTransfer struct {
|
||||
requestStats *httpTransferStats
|
||||
responseStats *httpTransferStats
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO should use some locks
|
||||
httpTransfers = make(map[*http.Response]*httpTransfer)
|
||||
httpTransferBuckets = make(map[string][]*http.Response)
|
||||
httpTransfersLock sync.Mutex
|
||||
httpTransferBucketsLock sync.Mutex
|
||||
httpClients map[string]*HttpClient
|
||||
httpClientsMutex sync.Mutex
|
||||
UserAgent string
|
||||
)
|
||||
|
||||
func LogTransfer(cfg *config.Configuration, key string, res *http.Response) {
|
||||
if cfg.IsLoggingStats {
|
||||
httpTransferBucketsLock.Lock()
|
||||
httpTransferBuckets[key] = append(httpTransferBuckets[key], res)
|
||||
httpTransferBucketsLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type HttpClient struct {
|
||||
Config *config.Configuration
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func (c *HttpClient) Do(req *http.Request) (*http.Response, error) {
|
||||
traceHttpRequest(c.Config, req)
|
||||
|
||||
crc := countingRequest(c.Config, req)
|
||||
if req.Body != nil {
|
||||
// Only set the body if we have a body, but create the countingRequest
|
||||
// anyway to make using zeroed stats easier.
|
||||
req.Body = crc
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
res, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
traceHttpResponse(c.Config, res)
|
||||
|
||||
cresp := countingResponse(c.Config, res)
|
||||
res.Body = cresp
|
||||
|
||||
if c.Config.IsLoggingStats {
|
||||
reqHeaderSize := 0
|
||||
resHeaderSize := 0
|
||||
|
||||
if dump, err := httputil.DumpRequest(req, false); err == nil {
|
||||
reqHeaderSize = len(dump)
|
||||
}
|
||||
|
||||
if dump, err := httputil.DumpResponse(res, false); err == nil {
|
||||
resHeaderSize = len(dump)
|
||||
}
|
||||
|
||||
reqstats := &httpTransferStats{HeaderSize: reqHeaderSize, BodySize: crc.Count}
|
||||
|
||||
// Response body size cannot be figured until it is read. Do not rely on a Content-Length
|
||||
// header because it may not exist or be -1 in the case of chunked responses.
|
||||
resstats := &httpTransferStats{HeaderSize: resHeaderSize, Start: start}
|
||||
t := &httpTransfer{requestStats: reqstats, responseStats: resstats}
|
||||
httpTransfersLock.Lock()
|
||||
httpTransfers[res] = t
|
||||
httpTransfersLock.Unlock()
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// NewHttpClient returns a new HttpClient for the given host (which may be "host:port")
|
||||
func NewHttpClient(c *config.Configuration, host string) *HttpClient {
|
||||
httpClientsMutex.Lock()
|
||||
defer httpClientsMutex.Unlock()
|
||||
|
||||
if httpClients == nil {
|
||||
httpClients = make(map[string]*HttpClient)
|
||||
}
|
||||
if client, ok := httpClients[host]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
dialtime := c.Git.Int("lfs.dialtimeout", 30)
|
||||
keepalivetime := c.Git.Int("lfs.keepalive", 1800) // 30 minutes
|
||||
tlstime := c.Git.Int("lfs.tlstimeout", 30)
|
||||
|
||||
tr := &http.Transport{
|
||||
Proxy: ProxyFromGitConfigOrEnvironment(c),
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: time.Duration(dialtime) * time.Second,
|
||||
KeepAlive: time.Duration(keepalivetime) * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: time.Duration(tlstime) * time.Second,
|
||||
MaxIdleConnsPerHost: c.ConcurrentTransfers(),
|
||||
}
|
||||
|
||||
tr.TLSClientConfig = &tls.Config{}
|
||||
if isCertVerificationDisabledForHost(c, host) {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
} else {
|
||||
tr.TLSClientConfig.RootCAs = getRootCAsForHost(c, host)
|
||||
}
|
||||
|
||||
client := &HttpClient{
|
||||
Config: c,
|
||||
Client: &http.Client{Transport: tr, CheckRedirect: CheckRedirect},
|
||||
}
|
||||
httpClients[host] = client
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func CheckRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return errors.New("stopped after 3 redirects")
|
||||
}
|
||||
|
||||
oldest := via[0]
|
||||
for key, _ := range oldest.Header {
|
||||
if key == "Authorization" {
|
||||
if req.URL.Scheme != oldest.URL.Scheme || req.URL.Host != oldest.URL.Host {
|
||||
continue
|
||||
}
|
||||
}
|
||||
req.Header.Set(key, oldest.Header.Get(key))
|
||||
}
|
||||
|
||||
oldestUrl := strings.SplitN(oldest.URL.String(), "?", 2)[0]
|
||||
newUrl := strings.SplitN(req.URL.String(), "?", 2)[0]
|
||||
tracerx.Printf("api: redirect %s %s to %s", oldest.Method, oldestUrl, newUrl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var tracedTypes = []string{"json", "text", "xml", "html"}
|
||||
|
||||
func traceHttpRequest(cfg *config.Configuration, req *http.Request) {
|
||||
tracerx.Printf("HTTP: %s", TraceHttpReq(req))
|
||||
|
||||
if cfg.IsTracingHttp == false {
|
||||
return
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
traceHttpDump(cfg, ">", dump)
|
||||
}
|
||||
|
||||
func traceHttpResponse(cfg *config.Configuration, res *http.Response) {
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tracerx.Printf("HTTP: %d", res.StatusCode)
|
||||
|
||||
if cfg.IsTracingHttp == false {
|
||||
return
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpResponse(res, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isTraceableContent(res.Header) {
|
||||
fmt.Fprintf(os.Stderr, "\n\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
traceHttpDump(cfg, "<", dump)
|
||||
}
|
||||
|
||||
func traceHttpDump(cfg *config.Configuration, direction string, dump []byte) {
|
||||
scanner := bufio.NewScanner(bytes.NewBuffer(dump))
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !cfg.IsDebuggingHttp && strings.HasPrefix(strings.ToLower(line), "authorization: basic") {
|
||||
fmt.Fprintf(os.Stderr, "%s Authorization: Basic * * * * *\n", direction)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", direction, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTraceableContent(h http.Header) bool {
|
||||
ctype := strings.ToLower(strings.SplitN(h.Get("Content-Type"), ";", 2)[0])
|
||||
for _, tracedType := range tracedTypes {
|
||||
if strings.Contains(ctype, tracedType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func countingRequest(cfg *config.Configuration, req *http.Request) *CountingReadCloser {
|
||||
return &CountingReadCloser{
|
||||
request: req,
|
||||
cfg: cfg,
|
||||
ReadCloser: req.Body,
|
||||
isTraceableType: isTraceableContent(req.Header),
|
||||
useGitTrace: false,
|
||||
}
|
||||
}
|
||||
|
||||
func countingResponse(cfg *config.Configuration, res *http.Response) *CountingReadCloser {
|
||||
return &CountingReadCloser{
|
||||
response: res,
|
||||
cfg: cfg,
|
||||
ReadCloser: res.Body,
|
||||
isTraceableType: isTraceableContent(res.Header),
|
||||
useGitTrace: true,
|
||||
}
|
||||
}
|
||||
|
||||
type CountingReadCloser struct {
|
||||
Count int
|
||||
request *http.Request
|
||||
response *http.Response
|
||||
cfg *config.Configuration
|
||||
isTraceableType bool
|
||||
useGitTrace bool
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (c *CountingReadCloser) Read(b []byte) (int, error) {
|
||||
n, err := c.ReadCloser.Read(b)
|
||||
if err != nil && err != io.EOF {
|
||||
return n, err
|
||||
}
|
||||
|
||||
c.Count += n
|
||||
|
||||
if n > 0 && c.isTraceableType {
|
||||
chunk := string(b[0:n])
|
||||
if c.useGitTrace {
|
||||
tracerx.Printf("HTTP: %s", chunk)
|
||||
}
|
||||
|
||||
if c.cfg.IsTracingHttp {
|
||||
fmt.Fprint(os.Stderr, chunk)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF && c.cfg.IsLoggingStats {
|
||||
// This httpTransfer is done, we're checking it this way so we can also
|
||||
// catch httpTransfers where the caller forgets to Close() the Body.
|
||||
if c.response != nil {
|
||||
httpTransfersLock.Lock()
|
||||
if httpTransfer, ok := httpTransfers[c.response]; ok {
|
||||
httpTransfer.responseStats.BodySize = c.Count
|
||||
httpTransfer.responseStats.Stop = time.Now()
|
||||
}
|
||||
httpTransfersLock.Unlock()
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// LogHttpStats is intended to be called after all HTTP operations for the
|
||||
// commmand have finished. It dumps k/v logs, one line per httpTransfer into
|
||||
// a log file with the current timestamp.
|
||||
func LogHttpStats(cfg *config.Configuration) {
|
||||
if !cfg.IsLoggingStats {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := statsLogFile()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error logging http stats: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(file, "concurrent=%d batch=%v time=%d version=%s\n", cfg.ConcurrentTransfers(), cfg.BatchTransfer(), time.Now().Unix(), config.Version)
|
||||
|
||||
for key, responses := range httpTransferBuckets {
|
||||
for _, response := range responses {
|
||||
stats := httpTransfers[response]
|
||||
fmt.Fprintf(file, "key=%s reqheader=%d reqbody=%d resheader=%d resbody=%d restime=%d status=%d url=%s\n",
|
||||
key,
|
||||
stats.requestStats.HeaderSize,
|
||||
stats.requestStats.BodySize,
|
||||
stats.responseStats.HeaderSize,
|
||||
stats.responseStats.BodySize,
|
||||
stats.responseStats.Stop.Sub(stats.responseStats.Start).Nanoseconds(),
|
||||
response.StatusCode,
|
||||
response.Request.URL)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "HTTP Stats logged to file %s\n", file.Name())
|
||||
}
|
||||
|
||||
func statsLogFile() (*os.File, error) {
|
||||
logBase := filepath.Join(config.LocalLogDir, "http")
|
||||
if err := os.MkdirAll(logBase, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logFile := fmt.Sprintf("http-%d.log", time.Now().Unix())
|
||||
return os.Create(filepath.Join(logBase, logFile))
|
||||
}
|
||||
|
||||
func TraceHttpReq(req *http.Request) string {
|
||||
return fmt.Sprintf("%s %s", req.Method, strings.SplitN(req.URL.String(), "?", 2)[0])
|
||||
}
|
||||
|
||||
func init() {
|
||||
UserAgent = config.VersionDesc
|
||||
}
|
317
httputil/ntlm.go
317
httputil/ntlm.go
@ -1,317 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
)
|
||||
|
||||
func ntlmClientSession(c *config.Configuration, creds auth.Creds) (ntlm.ClientSession, error) {
|
||||
if c.NtlmSession != nil {
|
||||
return c.NtlmSession, nil
|
||||
}
|
||||
splits := strings.Split(creds["username"], "\\")
|
||||
|
||||
if len(splits) != 2 {
|
||||
errorMessage := fmt.Sprintf("Your user name must be of the form DOMAIN\\user. It is currently %s", creds["username"])
|
||||
return nil, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.SetUserInfo(splits[1], creds["password"], strings.ToUpper(splits[0]))
|
||||
c.NtlmSession = session
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func doNTLMRequest(cfg *config.Configuration, request *http.Request, retry bool) (*http.Response, error) {
|
||||
handReq, err := cloneRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := NewHttpClient(cfg, handReq.Host).Do(handReq)
|
||||
if err != nil && res == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//If the status is 401 then we need to re-authenticate, otherwise it was successful
|
||||
if res.StatusCode == 401 {
|
||||
creds, err := auth.GetCreds(cfg, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
negotiateReq, err := cloneRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challengeMessage, err := negotiate(cfg, negotiateReq, ntlmNegotiateMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challengeReq, err := cloneRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := challenge(cfg, challengeReq, challengeMessage, creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//If the status is 401 then we need to re-authenticate
|
||||
if res.StatusCode == 401 && retry == true {
|
||||
return doNTLMRequest(cfg, challengeReq, false)
|
||||
}
|
||||
|
||||
auth.SaveCredentials(cfg, creds, res)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func negotiate(cfg *config.Configuration, request *http.Request, message string) ([]byte, error) {
|
||||
request.Header.Add("Authorization", message)
|
||||
res, err := NewHttpClient(cfg, request.Host).Do(request)
|
||||
|
||||
if res == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
ret, err := parseChallengeResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func challenge(cfg *config.Configuration, request *http.Request, challengeBytes []byte, creds auth.Creds) (*http.Response, error) {
|
||||
challenge, err := ntlm.ParseChallengeMessage(challengeBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := ntlmClientSession(cfg, creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.ProcessChallengeMessage(challenge)
|
||||
authenticate, err := session.GenerateAuthenticateMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authMsg := base64.StdEncoding.EncodeToString(authenticate.Bytes())
|
||||
request.Header.Add("Authorization", "NTLM "+authMsg)
|
||||
return NewHttpClient(cfg, request.Host).Do(request)
|
||||
}
|
||||
|
||||
func parseChallengeResponse(response *http.Response) ([]byte, error) {
|
||||
header := response.Header.Get("Www-Authenticate")
|
||||
if len(header) < 6 {
|
||||
return nil, fmt.Errorf("Invalid NTLM challenge response: %q", header)
|
||||
}
|
||||
|
||||
//parse out the "NTLM " at the beginning of the response
|
||||
challenge := header[5:]
|
||||
val, err := base64.StdEncoding.DecodeString(challenge)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(val), nil
|
||||
}
|
||||
|
||||
func cloneRequest(request *http.Request) (*http.Request, error) {
|
||||
cloneReqBody, err := cloneRequestBody(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clonedReq, err := http.NewRequest(request.Method, request.URL.String(), cloneReqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, _ := range request.Header {
|
||||
clonedReq.Header.Add(k, request.Header.Get(k))
|
||||
}
|
||||
|
||||
clonedReq.TransferEncoding = request.TransferEncoding
|
||||
clonedReq.ContentLength = request.ContentLength
|
||||
|
||||
return clonedReq, nil
|
||||
}
|
||||
|
||||
func cloneRequestBody(req *http.Request) (io.ReadCloser, error) {
|
||||
if req.Body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var cb *cloneableBody
|
||||
var err error
|
||||
isCloneableBody := true
|
||||
|
||||
// check to see if the request body is already a cloneableBody
|
||||
body := req.Body
|
||||
if existingCb, ok := body.(*cloneableBody); ok {
|
||||
isCloneableBody = false
|
||||
cb, err = existingCb.CloneBody()
|
||||
} else {
|
||||
cb, err = newCloneableBody(req.Body, 0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isCloneableBody {
|
||||
cb2, err := cb.CloneBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Body = cb2
|
||||
}
|
||||
|
||||
return cb, nil
|
||||
}
|
||||
|
||||
type cloneableBody struct {
|
||||
bytes []byte // in-memory buffer of body
|
||||
file *os.File // file buffer of in-memory overflow
|
||||
reader io.Reader // internal reader for Read()
|
||||
closed bool // tracks whether body is closed
|
||||
dup *dupTracker
|
||||
}
|
||||
|
||||
func newCloneableBody(r io.Reader, limit int64) (*cloneableBody, error) {
|
||||
if limit < 1 {
|
||||
limit = 1048576 // default
|
||||
}
|
||||
|
||||
b := &cloneableBody{}
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := io.CopyN(buf, r, limit)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.bytes = buf.Bytes()
|
||||
byReader := bytes.NewBuffer(b.bytes)
|
||||
|
||||
if w >= limit {
|
||||
tmp, err := ioutil.TempFile("", "git-lfs-clone-reader")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(tmp, r)
|
||||
tmp.Close()
|
||||
if err != nil {
|
||||
os.RemoveAll(tmp.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Open(tmp.Name())
|
||||
if err != nil {
|
||||
os.RemoveAll(tmp.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dups := int32(0)
|
||||
b.dup = &dupTracker{name: f.Name(), dups: &dups}
|
||||
b.file = f
|
||||
b.reader = io.MultiReader(byReader, b.file)
|
||||
} else {
|
||||
// no file, so set the reader to just the in-memory buffer
|
||||
b.reader = byReader
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *cloneableBody) Read(p []byte) (int, error) {
|
||||
if b.closed {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return b.reader.Read(p)
|
||||
}
|
||||
|
||||
func (b *cloneableBody) Close() error {
|
||||
if !b.closed {
|
||||
b.closed = true
|
||||
if b.file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.file.Close()
|
||||
b.dup.Rm()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *cloneableBody) CloneBody() (*cloneableBody, error) {
|
||||
if b.closed {
|
||||
return &cloneableBody{closed: true}, nil
|
||||
}
|
||||
|
||||
b2 := &cloneableBody{bytes: b.bytes}
|
||||
|
||||
if b.file == nil {
|
||||
b2.reader = bytes.NewBuffer(b.bytes)
|
||||
} else {
|
||||
f, err := os.Open(b.file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2.file = f
|
||||
b2.reader = io.MultiReader(bytes.NewBuffer(b.bytes), b2.file)
|
||||
b2.dup = b.dup
|
||||
b.dup.Add()
|
||||
}
|
||||
|
||||
return b2, nil
|
||||
}
|
||||
|
||||
type dupTracker struct {
|
||||
name string
|
||||
dups *int32
|
||||
}
|
||||
|
||||
func (t *dupTracker) Add() {
|
||||
atomic.AddInt32(t.dups, 1)
|
||||
}
|
||||
|
||||
func (t *dupTracker) Rm() {
|
||||
newval := atomic.AddInt32(t.dups, -1)
|
||||
if newval < 0 {
|
||||
os.RemoveAll(t.name)
|
||||
}
|
||||
}
|
||||
|
||||
const ntlmNegotiateMessage = "NTLM TlRMTVNTUAABAAAAB7IIogwADAAzAAAACwALACgAAAAKAAAoAAAAD1dJTExISS1NQUlOTk9SVEhBTUVSSUNB"
|
@ -1,147 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNtlmClientSession(t *testing.T) {
|
||||
cfg := config.New()
|
||||
creds := auth.Creds{"username": "MOOSEDOMAIN\\canadian", "password": "MooseAntlersYeah"}
|
||||
_, err := ntlmClientSession(cfg, creds)
|
||||
assert.Nil(t, err)
|
||||
|
||||
//The second call should ignore creds and give the session we just created.
|
||||
badCreds := auth.Creds{"username": "badusername", "password": "MooseAntlersYeah"}
|
||||
_, err = ntlmClientSession(cfg, badCreds)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestNtlmClientSessionBadCreds(t *testing.T) {
|
||||
cfg := config.New()
|
||||
creds := auth.Creds{"username": "badusername", "password": "MooseAntlersYeah"}
|
||||
_, err := ntlmClientSession(cfg, creds)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestNtlmCloneRequest(t *testing.T) {
|
||||
req1, _ := http.NewRequest("Method", "url", nil)
|
||||
cloneOfReq1, err := cloneRequest(req1)
|
||||
assert.Nil(t, err)
|
||||
assertRequestsEqual(t, req1, cloneOfReq1)
|
||||
|
||||
req2, _ := http.NewRequest("Method", "url", bytes.NewReader([]byte("Moose can be request bodies")))
|
||||
cloneOfReq2, err := cloneRequest(req2)
|
||||
assert.Nil(t, err)
|
||||
assertRequestsEqual(t, req2, cloneOfReq2)
|
||||
}
|
||||
|
||||
func assertRequestsEqual(t *testing.T, req1 *http.Request, req2 *http.Request) {
|
||||
assert.Equal(t, req1.Method, req2.Method)
|
||||
|
||||
for k, v := range req1.Header {
|
||||
assert.Equal(t, v, req2.Header[k])
|
||||
}
|
||||
|
||||
if req1.Body == nil {
|
||||
assert.Nil(t, req2.Body)
|
||||
} else {
|
||||
bytes1, _ := ioutil.ReadAll(req1.Body)
|
||||
bytes2, _ := ioutil.ReadAll(req2.Body)
|
||||
assert.Equal(t, bytes1, bytes2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseValid(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", "NTLM "+base64.StdEncoding.EncodeToString([]byte("I am a moose")))
|
||||
bytes, err := parseChallengeResponse(&res)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, strings.HasPrefix(string(bytes), "NTLM"))
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseInvalidLength(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", "NTL")
|
||||
ret, err := parseChallengeResponse(&res)
|
||||
if ret != nil {
|
||||
t.Errorf("Unexpected challenge response: %v", ret)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got none!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseInvalid(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", base64.StdEncoding.EncodeToString([]byte("NTLM I am a moose")))
|
||||
_, err := parseChallengeResponse(&res)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestCloneSmallBody(t *testing.T) {
|
||||
cloneable1, err := newCloneableBody(strings.NewReader("abc"), 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cloneable2, err := cloneable1.CloneBody()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assertCloneableBody(t, cloneable2, "abc", "abc")
|
||||
assertCloneableBody(t, cloneable1, "abc", "abc")
|
||||
}
|
||||
|
||||
func TestCloneBigBody(t *testing.T) {
|
||||
cloneable1, err := newCloneableBody(strings.NewReader("abc"), 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cloneable2, err := cloneable1.CloneBody()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assertCloneableBody(t, cloneable2, "abc", "ab")
|
||||
assertCloneableBody(t, cloneable1, "abc", "ab")
|
||||
}
|
||||
|
||||
func assertCloneableBody(t *testing.T, cloneable *cloneableBody, expectedBody, expectedBuffer string) {
|
||||
buffer := string(cloneable.bytes)
|
||||
if buffer != expectedBuffer {
|
||||
t.Errorf("Expected buffer %q, got %q", expectedBody, buffer)
|
||||
}
|
||||
|
||||
if cloneable.closed {
|
||||
t.Errorf("already closed?")
|
||||
}
|
||||
|
||||
by, err := ioutil.ReadAll(cloneable)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cloneable.Close(); err != nil {
|
||||
t.Errorf("Error closing: %v", err)
|
||||
}
|
||||
|
||||
actual := string(by)
|
||||
if actual != expectedBody {
|
||||
t.Errorf("Expected to read %q, got %q", expectedBody, actual)
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProxyFromGitConfig(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"http.proxy": "https://proxy-from-git-config:8080",
|
||||
},
|
||||
Os: map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proxyURL, err := ProxyFromGitConfigOrEnvironment(cfg)(req)
|
||||
|
||||
assert.Equal(t, "proxy-from-git-config:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestHttpProxyFromGitConfig(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"http.proxy": "http://proxy-from-git-config:8080",
|
||||
},
|
||||
Os: map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proxyURL, err := ProxyFromGitConfigOrEnvironment(cfg)(req)
|
||||
|
||||
assert.Equal(t, "proxy-from-env:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyFromEnvironment(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Os: map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proxyURL, err := ProxyFromGitConfigOrEnvironment(cfg)(req)
|
||||
|
||||
assert.Equal(t, "proxy-from-env:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyIsNil(t *testing.T) {
|
||||
cfg := config.New()
|
||||
|
||||
req, err := http.NewRequest("GET", "http://some-host.com:123/foo/bar", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proxyURL, err := ProxyFromGitConfigOrEnvironment(cfg)(req)
|
||||
|
||||
assert.Nil(t, proxyURL)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyNoProxy(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"http.proxy": "https://proxy-from-git-config:8080",
|
||||
},
|
||||
Os: map[string]string{
|
||||
"NO_PROXY": "some-host",
|
||||
},
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host:8080", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proxyUrl, err := ProxyFromGitConfigOrEnvironment(cfg)(req)
|
||||
|
||||
assert.Nil(t, proxyUrl)
|
||||
assert.Nil(t, err)
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
type ClientError struct {
|
||||
Message string `json:"message"`
|
||||
DocumentationUrl string `json:"documentation_url,omitempty"`
|
||||
RequestId string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
basicAuthType = "basic"
|
||||
ntlmAuthType = "ntlm"
|
||||
negotiateAuthType = "negotiate"
|
||||
)
|
||||
|
||||
var (
|
||||
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
|
||||
)
|
||||
|
||||
func (e *ClientError) Error() string {
|
||||
msg := e.Message
|
||||
if len(e.DocumentationUrl) > 0 {
|
||||
msg += "\nDocs: " + e.DocumentationUrl
|
||||
}
|
||||
if len(e.RequestId) > 0 {
|
||||
msg += "\nRequest ID: " + e.RequestId
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Internal http request management
|
||||
func doHttpRequest(cfg *config.Configuration, req *http.Request, creds auth.Creds) (*http.Response, error) {
|
||||
var (
|
||||
res *http.Response
|
||||
cause string
|
||||
err error
|
||||
)
|
||||
|
||||
if cfg.NtlmAccess(auth.GetOperationForRequest(req)) {
|
||||
cause = "ntlm"
|
||||
res, err = doNTLMRequest(cfg, req, true)
|
||||
} else {
|
||||
cause = "http"
|
||||
res, err = NewHttpClient(cfg, req.Host).Do(req)
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
res = &http.Response{
|
||||
StatusCode: 0,
|
||||
Header: make(http.Header),
|
||||
Request: req,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("")),
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.IsAuthError(err) {
|
||||
SetAuthType(cfg, req, res)
|
||||
doHttpRequest(cfg, req, creds)
|
||||
} else {
|
||||
err = errors.Wrap(err, cause)
|
||||
}
|
||||
} else {
|
||||
err = handleResponse(cfg, res, creds)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
SetErrorResponseContext(cfg, err, res)
|
||||
} else {
|
||||
setErrorRequestContext(cfg, err, req)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// DoHttpRequest performs a single HTTP request
|
||||
func DoHttpRequest(cfg *config.Configuration, req *http.Request, useCreds bool) (*http.Response, error) {
|
||||
var creds auth.Creds
|
||||
if useCreds {
|
||||
c, err := auth.GetCreds(cfg, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds = c
|
||||
}
|
||||
|
||||
return doHttpRequest(cfg, req, creds)
|
||||
}
|
||||
|
||||
// DoHttpRequestWithRedirects runs a HTTP request and responds to redirects
|
||||
func DoHttpRequestWithRedirects(cfg *config.Configuration, req *http.Request, via []*http.Request, useCreds bool) (*http.Response, error) {
|
||||
var creds auth.Creds
|
||||
if useCreds {
|
||||
c, err := auth.GetCreds(cfg, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds = c
|
||||
}
|
||||
|
||||
res, err := doHttpRequest(cfg, req, creds)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res.StatusCode == 307 {
|
||||
redirectTo := res.Header.Get("Location")
|
||||
locurl, err := url.Parse(redirectTo)
|
||||
if err == nil && !locurl.IsAbs() {
|
||||
locurl = req.URL.ResolveReference(locurl)
|
||||
redirectTo = locurl.String()
|
||||
}
|
||||
|
||||
redirectedReq, err := NewHttpRequest(req.Method, redirectTo, nil)
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, err.Error())
|
||||
}
|
||||
|
||||
via = append(via, req)
|
||||
|
||||
// Avoid seeking and re-wrapping the CountingReadCloser, just get the "real" body
|
||||
realBody := req.Body
|
||||
if wrappedBody, ok := req.Body.(*CountingReadCloser); ok {
|
||||
realBody = wrappedBody.ReadCloser
|
||||
}
|
||||
|
||||
seeker, ok := realBody.(io.Seeker)
|
||||
if !ok {
|
||||
return res, errors.Wrapf(nil, "Request body needs to be an io.Seeker to handle redirects.")
|
||||
}
|
||||
|
||||
if _, err := seeker.Seek(0, 0); err != nil {
|
||||
return res, errors.Wrap(err, "request retry")
|
||||
}
|
||||
redirectedReq.Body = realBody
|
||||
redirectedReq.ContentLength = req.ContentLength
|
||||
|
||||
if err = CheckRedirect(redirectedReq, via); err != nil {
|
||||
return res, errors.Wrapf(err, err.Error())
|
||||
}
|
||||
|
||||
return DoHttpRequestWithRedirects(cfg, redirectedReq, via, useCreds)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// NewHttpRequest creates a template request, with the given headers & UserAgent supplied
|
||||
func NewHttpRequest(method, rawurl string, header map[string]string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, rawurl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range header {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func SetAuthType(cfg *config.Configuration, req *http.Request, res *http.Response) {
|
||||
authType := GetAuthType(res)
|
||||
operation := auth.GetOperationForRequest(req)
|
||||
cfg.SetAccess(operation, authType)
|
||||
tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", authType)
|
||||
}
|
||||
|
||||
func GetAuthType(res *http.Response) string {
|
||||
for _, headerName := range authenticateHeaders {
|
||||
for _, auth := range res.Header[headerName] {
|
||||
|
||||
authLower := strings.ToLower(auth)
|
||||
// When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM.
|
||||
// Since git-lfs current does not support Kerberos, we will return NTLM in this case.
|
||||
if strings.HasPrefix(authLower, ntlmAuthType) || strings.HasPrefix(authLower, negotiateAuthType) {
|
||||
return ntlmAuthType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return basicAuthType
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
)
|
||||
|
||||
func TestSuccessStatus(t *testing.T) {
|
||||
cfg := config.New()
|
||||
for _, status := range []int{200, 201, 202} {
|
||||
res := &http.Response{StatusCode: status}
|
||||
if err := handleResponse(cfg, res, nil); err != nil {
|
||||
t.Errorf("Unexpected error for HTTP %d: %s", status, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorStatusWithCustomMessage(t *testing.T) {
|
||||
cfg := config.New()
|
||||
u, err := url.Parse("https://lfs-server.com/objects/oid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
statuses := map[int]string{
|
||||
400: "not panic",
|
||||
401: "not panic",
|
||||
403: "not panic",
|
||||
404: "not panic",
|
||||
405: "not panic",
|
||||
406: "not panic",
|
||||
429: "not panic",
|
||||
500: "panic",
|
||||
501: "not panic",
|
||||
503: "panic",
|
||||
504: "panic",
|
||||
507: "not panic",
|
||||
509: "not panic",
|
||||
}
|
||||
|
||||
for status, panicMsg := range statuses {
|
||||
cliErr := &ClientError{
|
||||
Message: fmt.Sprintf("custom error for %d", status),
|
||||
}
|
||||
|
||||
by, err := json.Marshal(cliErr)
|
||||
if err != nil {
|
||||
t.Errorf("Error building json for status %d: %s", status, err)
|
||||
continue
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: status,
|
||||
Header: make(http.Header),
|
||||
Body: ioutil.NopCloser(bytes.NewReader(by)),
|
||||
Request: &http.Request{URL: u},
|
||||
}
|
||||
res.Header.Set("Content-Type", "application/vnd.git-lfs+json; charset=utf-8")
|
||||
|
||||
err = handleResponse(cfg, res, nil)
|
||||
if err == nil {
|
||||
t.Errorf("No error from HTTP %d", status)
|
||||
continue
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf("custom error for %d", status)
|
||||
if actual := err.Error(); !strings.HasSuffix(actual, expected) {
|
||||
t.Errorf("Expected for HTTP %d:\n%s\nACTUAL:\n%s", status, expected, actual)
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.IsFatalError(err) == (panicMsg != "panic") {
|
||||
t.Errorf("Error for HTTP %d should %s", status, panicMsg)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorStatusWithDefaultMessage(t *testing.T) {
|
||||
cfg := config.New()
|
||||
rawurl := "https://lfs-server.com/objects/oid"
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
statuses := map[int][]string{
|
||||
400: {defaultErrors[400], "not panic"},
|
||||
401: {defaultErrors[401], "not panic"},
|
||||
403: {defaultErrors[401], "not panic"},
|
||||
404: {defaultErrors[404], "not panic"},
|
||||
405: {defaultErrors[400] + " from HTTP 405", "not panic"},
|
||||
406: {defaultErrors[400] + " from HTTP 406", "not panic"},
|
||||
429: {defaultErrors[429], "not panic"},
|
||||
500: {defaultErrors[500], "panic"},
|
||||
501: {defaultErrors[500] + " from HTTP 501", "not panic"},
|
||||
503: {defaultErrors[500] + " from HTTP 503", "panic"},
|
||||
504: {defaultErrors[500] + " from HTTP 504", "panic"},
|
||||
507: {defaultErrors[507], "not panic"},
|
||||
509: {defaultErrors[509], "not panic"},
|
||||
}
|
||||
|
||||
for status, results := range statuses {
|
||||
cliErr := &ClientError{
|
||||
Message: fmt.Sprintf("custom error for %d", status),
|
||||
}
|
||||
|
||||
by, err := json.Marshal(cliErr)
|
||||
if err != nil {
|
||||
t.Errorf("Error building json for status %d: %s", status, err)
|
||||
continue
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: status,
|
||||
Header: make(http.Header),
|
||||
Body: ioutil.NopCloser(bytes.NewReader(by)),
|
||||
Request: &http.Request{URL: u},
|
||||
}
|
||||
|
||||
// purposely wrong content type so it falls back to default
|
||||
res.Header.Set("Content-Type", "application/vnd.git-lfs+json2")
|
||||
|
||||
err = handleResponse(cfg, res, nil)
|
||||
if err == nil {
|
||||
t.Errorf("No error from HTTP %d", status)
|
||||
continue
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(results[0], rawurl)
|
||||
if actual := err.Error(); !strings.HasSuffix(actual, expected) {
|
||||
t.Errorf("Expected for HTTP %d:\n%s\nACTUAL:\n%s", status, expected, actual)
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.IsFatalError(err) == (results[1] != "panic") {
|
||||
t.Errorf("Error for HTTP %d should %s", status, results[1])
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type AuthenticateHeaderTestCase struct {
|
||||
ExpectedAuthType string
|
||||
Headers map[string][]string
|
||||
}
|
||||
|
||||
func (c *AuthenticateHeaderTestCase) Assert(t *testing.T) {
|
||||
t.Logf("lfs/httputil: asserting auth type: %q for: %v", c.ExpectedAuthType, c.Headers)
|
||||
|
||||
assert.Equal(t, c.ExpectedAuthType, GetAuthType(c.HttpResponse()))
|
||||
}
|
||||
|
||||
func (c *AuthenticateHeaderTestCase) HttpResponse() *http.Response {
|
||||
res := &http.Response{Header: make(http.Header)}
|
||||
|
||||
for k, vv := range c.Headers {
|
||||
for _, v := range vv {
|
||||
res.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func TestGetAuthType(t *testing.T) {
|
||||
for _, c := range []AuthenticateHeaderTestCase{
|
||||
{basicAuthType, map[string][]string{}},
|
||||
{ntlmAuthType, map[string][]string{"WWW-Authenticate": {"Basic", "NTLM", "Bearer"}}},
|
||||
{ntlmAuthType, map[string][]string{"LFS-Authenticate": {"Basic", "NTLM", "Bearer"}}},
|
||||
{ntlmAuthType, map[string][]string{"LFS-Authenticate": {"Basic", "Ntlm"}}},
|
||||
{ntlmAuthType, map[string][]string{"Www-Authenticate": {"Basic", "Ntlm"}}},
|
||||
{ntlmAuthType, map[string][]string{"WWW-Authenticate": {"Basic"},
|
||||
"LFS-Authenticate": {"Ntlm"}}},
|
||||
} {
|
||||
c.Assert(t)
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/git-lfs/git-lfs/auth"
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
lfsMediaTypeRE = regexp.MustCompile(`\Aapplication/vnd\.git\-lfs\+json(;|\z)`)
|
||||
jsonMediaTypeRE = regexp.MustCompile(`\Aapplication/json(;|\z)`)
|
||||
hiddenHeaders = map[string]bool{
|
||||
"Authorization": true,
|
||||
}
|
||||
|
||||
defaultErrors = map[int]string{
|
||||
400: "Client error: %s",
|
||||
401: "Authorization error: %s\nCheck that you have proper access to the repository",
|
||||
403: "Authorization error: %s\nCheck that you have proper access to the repository",
|
||||
404: "Repository or object not found: %s\nCheck that it exists and that you have proper access to it",
|
||||
429: "Rate limit exceeded: %s",
|
||||
500: "Server error: %s",
|
||||
507: "Insufficient server storage: %s",
|
||||
509: "Bandwidth limit exceeded: %s",
|
||||
}
|
||||
)
|
||||
|
||||
// DecodeResponse attempts to decode the contents of the response as a JSON object
|
||||
func DecodeResponse(res *http.Response, obj interface{}) error {
|
||||
ctype := res.Header.Get("Content-Type")
|
||||
if !(lfsMediaTypeRE.MatchString(ctype) || jsonMediaTypeRE.MatchString(ctype)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.NewDecoder(res.Body).Decode(obj)
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to parse HTTP response for %s", TraceHttpReq(res.Request))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultError returns the default text for standard error codes (blank if none)
|
||||
func GetDefaultError(code int) string {
|
||||
if s, ok := defaultErrors[code]; ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check the response from a HTTP request for problems
|
||||
func handleResponse(cfg *config.Configuration, res *http.Response, creds auth.Creds) error {
|
||||
auth.SaveCredentials(cfg, creds, res)
|
||||
|
||||
if res.StatusCode < 400 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
}()
|
||||
|
||||
cliErr := &ClientError{}
|
||||
err := DecodeResponse(res, cliErr)
|
||||
if err == nil {
|
||||
if len(cliErr.Message) == 0 {
|
||||
err = defaultError(res)
|
||||
} else {
|
||||
err = errors.Wrap(cliErr, "http")
|
||||
}
|
||||
}
|
||||
|
||||
if res.StatusCode == 401 {
|
||||
if err == nil {
|
||||
err = errors.New("api: received status 401")
|
||||
}
|
||||
return errors.NewAuthError(err)
|
||||
}
|
||||
|
||||
if res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 507 && res.StatusCode != 509 {
|
||||
if err == nil {
|
||||
err = errors.Errorf("api: received status %d", res.StatusCode)
|
||||
}
|
||||
return errors.NewFatalError(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func defaultError(res *http.Response) error {
|
||||
var msgFmt string
|
||||
|
||||
if f, ok := defaultErrors[res.StatusCode]; ok {
|
||||
msgFmt = f
|
||||
} else if res.StatusCode < 500 {
|
||||
msgFmt = defaultErrors[400] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
|
||||
} else {
|
||||
msgFmt = defaultErrors[500] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return errors.Errorf(msgFmt, res.Request.URL)
|
||||
}
|
||||
|
||||
func SetErrorResponseContext(cfg *config.Configuration, err error, res *http.Response) {
|
||||
errors.SetContext(err, "Status", res.Status)
|
||||
setErrorHeaderContext(err, "Request", res.Header)
|
||||
setErrorRequestContext(cfg, err, res.Request)
|
||||
}
|
||||
|
||||
func setErrorRequestContext(cfg *config.Configuration, err error, req *http.Request) {
|
||||
errors.SetContext(err, "Endpoint", cfg.Endpoint(auth.GetOperationForRequest(req)).Url)
|
||||
errors.SetContext(err, "URL", TraceHttpReq(req))
|
||||
setErrorHeaderContext(err, "Response", req.Header)
|
||||
}
|
||||
|
||||
func setErrorHeaderContext(err error, prefix string, head http.Header) {
|
||||
for key, _ := range head {
|
||||
contextKey := fmt.Sprintf("%s:%s", prefix, key)
|
||||
if _, skip := hiddenHeaders[key]; skip {
|
||||
errors.SetContext(err, contextKey, "--")
|
||||
} else {
|
||||
errors.SetContext(err, contextKey, head.Get(key))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/tq"
|
||||
)
|
||||
|
||||
// NewDownloadCheckQueue builds a checking queue, checks that objects are there but doesn't download
|
||||
func NewDownloadCheckQueue(cfg *config.Configuration, options ...tq.Option) *tq.TransferQueue {
|
||||
allOptions := make([]tq.Option, len(options), len(options)+1)
|
||||
allOptions = append(allOptions, options...)
|
||||
allOptions = append(allOptions, tq.DryRun(true))
|
||||
return NewDownloadQueue(cfg, allOptions...)
|
||||
}
|
||||
|
||||
// NewDownloadQueue builds a DownloadQueue, allowing concurrent downloads.
|
||||
func NewDownloadQueue(cfg *config.Configuration, options ...tq.Option) *tq.TransferQueue {
|
||||
return tq.NewTransferQueue(tq.Download, TransferManifest(cfg), options...)
|
||||
}
|
@ -109,7 +109,7 @@ func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string {
|
||||
}
|
||||
|
||||
for _, e := range osEnviron {
|
||||
if !strings.Contains(e, "GIT_") {
|
||||
if !strings.Contains(strings.SplitN(e, "=", 2)[0], "GIT_") {
|
||||
continue
|
||||
}
|
||||
env = append(env, e)
|
||||
@ -118,11 +118,6 @@ func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string {
|
||||
return env
|
||||
}
|
||||
|
||||
// TransferManifest builds a tq.Manifest using the given cfg.
|
||||
func TransferManifest(cfg *config.Configuration) *tq.Manifest {
|
||||
return tq.NewManifestWithGitEnv(cfg.Access("download"), cfg.Git)
|
||||
}
|
||||
|
||||
func InRepo() bool {
|
||||
return config.LocalGitDir != ""
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManifestIsConfigurable(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.transfer.maxretries": "3",
|
||||
},
|
||||
})
|
||||
m := TransferManifest(cfg)
|
||||
assert.Equal(t, 3, m.MaxRetries())
|
||||
}
|
||||
|
||||
func TestManifestChecksNTLM(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.url": "http://foo",
|
||||
"lfs.http://foo.access": "ntlm",
|
||||
"lfs.concurrenttransfers": "3",
|
||||
},
|
||||
})
|
||||
m := TransferManifest(cfg)
|
||||
assert.Equal(t, 1, m.MaxRetries())
|
||||
}
|
||||
|
||||
func TestManifestClampsValidValues(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.transfer.maxretries": "-1",
|
||||
},
|
||||
})
|
||||
m := TransferManifest(cfg)
|
||||
assert.Equal(t, 1, m.MaxRetries())
|
||||
}
|
||||
|
||||
func TestManifestIgnoresNonInts(t *testing.T) {
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"lfs.transfer.maxretries": "not_an_int",
|
||||
},
|
||||
})
|
||||
m := TransferManifest(cfg)
|
||||
assert.Equal(t, 1, m.MaxRetries())
|
||||
}
|
@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
@ -78,7 +77,11 @@ func copyToTemp(reader io.Reader, fileSize int64, cb progress.CopyCallback) (oid
|
||||
}
|
||||
|
||||
ptr, buf, err := DecodeFrom(reader)
|
||||
by, rerr := ioutil.ReadAll(buf)
|
||||
|
||||
by := make([]byte, blobSizeCutoff)
|
||||
n, rerr := buf.Read(by)
|
||||
by = by[:n]
|
||||
|
||||
if rerr != nil || (err == nil && len(by) < 512) {
|
||||
err = errors.NewCleanPointerError(ptr, by)
|
||||
return
|
||||
|
@ -74,7 +74,7 @@ func PointerSmudge(writer io.Writer, ptr *Pointer, workingfile string, download
|
||||
func downloadFile(writer io.Writer, ptr *Pointer, workingfile, mediafile string, manifest *tq.Manifest, cb progress.CopyCallback) error {
|
||||
fmt.Fprintf(os.Stderr, "Downloading %s (%s)\n", workingfile, pb.FormatBytes(ptr.Size))
|
||||
|
||||
q := tq.NewTransferQueue(tq.Download, manifest)
|
||||
q := tq.NewTransferQueue(tq.Download, manifest, "")
|
||||
q.Add(filepath.Base(workingfile), mediafile, ptr.Oid, ptr.Size)
|
||||
q.Wait()
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/git-lfs/tq"
|
||||
)
|
||||
|
||||
// NewUploadQueue builds an UploadQueue, allowing `workers` concurrent uploads.
|
||||
func NewUploadQueue(cfg *config.Configuration, options ...tq.Option) *tq.TransferQueue {
|
||||
return tq.NewTransferQueue(tq.Upload, TransferManifest(cfg), options...)
|
||||
}
|
@ -8,7 +8,34 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriterWithCallback(t *testing.T) {
|
||||
func TestBodyWithCallback(t *testing.T) {
|
||||
called := 0
|
||||
calledRead := make([]int64, 0, 2)
|
||||
|
||||
cb := func(total int64, read int64, current int) error {
|
||||
called += 1
|
||||
calledRead = append(calledRead, read)
|
||||
assert.Equal(t, 5, int(total))
|
||||
return nil
|
||||
}
|
||||
reader := progress.NewByteBodyWithCallback([]byte("BOOYA"), 5, cb)
|
||||
|
||||
readBuf := make([]byte, 3)
|
||||
n, err := reader.Read(readBuf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "BOO", string(readBuf[0:n]))
|
||||
|
||||
n, err = reader.Read(readBuf)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "YA", string(readBuf[0:n]))
|
||||
|
||||
assert.Equal(t, 2, called)
|
||||
assert.Len(t, calledRead, 2)
|
||||
assert.Equal(t, 3, int(calledRead[0]))
|
||||
assert.Equal(t, 5, int(calledRead[1]))
|
||||
}
|
||||
|
||||
func TestReadWithCallback(t *testing.T) {
|
||||
called := 0
|
||||
calledRead := make([]int64, 0, 2)
|
||||
|
||||
|
312
lfsapi/auth.go
Normal file
312
lfsapi/auth.go
Normal file
@ -0,0 +1,312 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bgentry/go-netrc/netrc"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultCredentialHelper = &commandCredentialHelper{}
|
||||
defaultNetrcFinder = &noFinder{}
|
||||
defaultEndpointFinder = NewEndpointFinder(nil)
|
||||
)
|
||||
|
||||
func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
|
||||
credHelper := c.Credentials
|
||||
if credHelper == nil {
|
||||
credHelper = defaultCredentialHelper
|
||||
}
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
ef := c.Endpoints
|
||||
if ef == nil {
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
apiEndpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, remote, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.doWithCreds(req, credHelper, creds, credsURL, access)
|
||||
if err != nil {
|
||||
if errors.IsAuthError(err) {
|
||||
newAccess := getAuthAccess(res)
|
||||
if newAccess != access {
|
||||
c.Endpoints.SetAccess(apiEndpoint.Url, newAccess)
|
||||
}
|
||||
|
||||
if access == NoneAccess || creds != nil {
|
||||
tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess)
|
||||
req.Header.Del("Authorization")
|
||||
if creds != nil {
|
||||
credHelper.Reject(creds)
|
||||
}
|
||||
return c.DoWithAuth(remote, req)
|
||||
}
|
||||
}
|
||||
|
||||
err = errors.Wrap(err, "http")
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 401, 403:
|
||||
credHelper.Reject(creds)
|
||||
default:
|
||||
if res.StatusCode < 300 && res.StatusCode > 199 {
|
||||
credHelper.Approve(creds)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access) (*http.Response, error) {
|
||||
if access == NTLMAccess {
|
||||
return c.doWithNTLM(req, credHelper, creds, credsURL)
|
||||
}
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// getCreds fills the authorization header for the given request if possible,
|
||||
// from the following sources:
|
||||
//
|
||||
// 1. NTLM access is handled elsewhere.
|
||||
// 2. Existing Authorization or ?token query tells LFS that the request is ready.
|
||||
// 3. Netrc based on the hostname.
|
||||
// 4. URL authentication on the Endpoint URL or the Git Remote URL.
|
||||
// 5. Git Credential Helper, potentially prompting the user.
|
||||
//
|
||||
// There are three URLs in play, that make this a little confusing.
|
||||
//
|
||||
// 1. The request URL, which should be something like "https://git.com/repo.git/info/lfs/objects/batch"
|
||||
// 2. The LFS API URL, which should be something like "https://git.com/repo.git/info/lfs"
|
||||
// This URL used for the "lfs.URL.access" git config key, which determines
|
||||
// what kind of auth the LFS server expects. Could be BasicAccess, NTLMAccess,
|
||||
// or NoneAccess, in which the Git Credential Helper step is skipped. We do
|
||||
// not want to prompt the user for a password to fetch public repository data.
|
||||
// 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
|
||||
// This URL is used for the Git Credential Helper. This way existing https
|
||||
// Git remote credentials can be re-used for LFS.
|
||||
func getCreds(credHelper CredentialHelper, netrcFinder NetrcFinder, ef EndpointFinder, remote string, req *http.Request) (Endpoint, Access, Creds, *url.URL, error) {
|
||||
operation := getReqOperation(req)
|
||||
apiEndpoint := ef.Endpoint(operation, remote)
|
||||
access := ef.AccessFor(apiEndpoint.Url)
|
||||
|
||||
if access != NTLMAccess {
|
||||
if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
}
|
||||
|
||||
credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if credsURL == nil {
|
||||
return apiEndpoint, access, nil, nil, nil
|
||||
}
|
||||
|
||||
creds, err := fillGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
}
|
||||
|
||||
credsURL, err := url.Parse(apiEndpoint.Url)
|
||||
if err != nil {
|
||||
return apiEndpoint, access, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
|
||||
creds := Creds{
|
||||
"protocol": credsURL.Scheme,
|
||||
"host": credsURL.Host,
|
||||
"username": netrcMachine.Login,
|
||||
"password": netrcMachine.Password,
|
||||
"source": "netrc",
|
||||
}
|
||||
|
||||
return apiEndpoint, access, creds, credsURL, nil
|
||||
}
|
||||
|
||||
creds, err := getGitCreds(credHelper, ef, req, credsURL)
|
||||
return apiEndpoint, access, creds, credsURL, err
|
||||
}
|
||||
|
||||
func getGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
input := Creds{"protocol": u.Scheme, "host": u.Host, "path": path}
|
||||
if u.User != nil && u.User.Username() != "" {
|
||||
input["username"] = u.User.Username()
|
||||
}
|
||||
|
||||
creds, err := credHelper.Fill(input)
|
||||
if creds == nil || len(creds) < 1 {
|
||||
errmsg := fmt.Sprintf("Git credentials for %s not found", u)
|
||||
if err != nil {
|
||||
errmsg = errmsg + ":\n" + err.Error()
|
||||
} else {
|
||||
errmsg = errmsg + "."
|
||||
}
|
||||
err = errors.New(errmsg)
|
||||
}
|
||||
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func fillGitCreds(credHelper CredentialHelper, ef EndpointFinder, req *http.Request, u *url.URL) (Creds, error) {
|
||||
creds, err := getGitCreds(credHelper, ef, req, u)
|
||||
if err == nil {
|
||||
tracerx.Printf("Filled credentials for %s", u)
|
||||
setRequestAuth(req, creds["username"], creds["password"])
|
||||
}
|
||||
|
||||
return creds, err
|
||||
}
|
||||
|
||||
func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
|
||||
hostname := req.URL.Host
|
||||
var host string
|
||||
|
||||
if strings.Contains(hostname, ":") {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
host = hostname
|
||||
}
|
||||
|
||||
return netrcFinder.FindMachine(host)
|
||||
}
|
||||
|
||||
func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool {
|
||||
if machine := getAuthFromNetrc(netrcFinder, req); machine != nil {
|
||||
setRequestAuth(req, machine.Login, machine.Password)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) {
|
||||
apiURL, err := url.Parse(apiEndpoint.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if the LFS request doesn't match the current LFS url, don't bother
|
||||
// attempting to set the Authorization header from the LFS or Git remote URLs.
|
||||
if req.URL.Scheme != apiURL.Scheme ||
|
||||
req.URL.Host != apiURL.Host {
|
||||
return req.URL, nil
|
||||
}
|
||||
|
||||
if setRequestAuthFromURL(req, apiURL) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(remote) > 0 {
|
||||
if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
|
||||
gitRemoteURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gitRemoteURL.Scheme == apiURL.Scheme &&
|
||||
gitRemoteURL.Host == apiURL.Host {
|
||||
|
||||
if setRequestAuthFromURL(req, gitRemoteURL) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return gitRemoteURL, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return apiURL, nil
|
||||
}
|
||||
|
||||
func requestHasAuth(req *http.Request) bool {
|
||||
if len(req.Header.Get("Authorization")) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(req.URL.Query().Get("token")) > 0
|
||||
}
|
||||
|
||||
func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
|
||||
if u.User == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if pass, ok := u.User.Password(); ok {
|
||||
fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials")
|
||||
setRequestAuth(req, u.User.Username(), pass)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func setRequestAuth(req *http.Request, user, pass string) {
|
||||
// better not be NTLM!
|
||||
if len(user) == 0 && len(pass) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s:%s", user, pass)
|
||||
auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
|
||||
func getReqOperation(req *http.Request) string {
|
||||
operation := "download"
|
||||
if req.Method == "POST" || req.Method == "PUT" {
|
||||
operation = "upload"
|
||||
}
|
||||
return operation
|
||||
}
|
||||
|
||||
var (
|
||||
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
|
||||
)
|
||||
|
||||
func getAuthAccess(res *http.Response) Access {
|
||||
for _, headerName := range authenticateHeaders {
|
||||
for _, auth := range res.Header[headerName] {
|
||||
pieces := strings.SplitN(strings.ToLower(auth), " ", 2)
|
||||
if len(pieces) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch Access(pieces[0]) {
|
||||
case NegotiateAccess, NTLMAccess:
|
||||
// When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM.
|
||||
// Since git-lfs current does not support Kerberos, we will return NTLM in this case.
|
||||
return NTLMAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BasicAccess
|
||||
}
|
556
lfsapi/auth_test.go
Normal file
556
lfsapi/auth_test.go
Normal file
@ -0,0 +1,556 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type authRequest struct {
|
||||
Test string
|
||||
}
|
||||
|
||||
func TestAuthenticateHeaderAccess(t *testing.T) {
|
||||
tests := map[string]Access{
|
||||
"": BasicAccess,
|
||||
"basic 123": BasicAccess,
|
||||
"basic": BasicAccess,
|
||||
"unknown": BasicAccess,
|
||||
"NTLM": NTLMAccess,
|
||||
"ntlm": NTLMAccess,
|
||||
"NTLM 1 2 3": NTLMAccess,
|
||||
"ntlm 1 2 3": NTLMAccess,
|
||||
"NEGOTIATE": NTLMAccess,
|
||||
"negotiate": NTLMAccess,
|
||||
"NEGOTIATE 1 2 3": NTLMAccess,
|
||||
"negotiate 1 2 3": NTLMAccess,
|
||||
}
|
||||
|
||||
for _, key := range authenticateHeaders {
|
||||
for value, expected := range tests {
|
||||
res := &http.Response{Header: make(http.Header)}
|
||||
res.Header.Set(key, value)
|
||||
t.Logf("%s: %s", key, value)
|
||||
assert.Equal(t, expected, getAuthAccess(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoWithAuthApprove(t *testing.T) {
|
||||
var called uint32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
atomic.AddUint32(&called, 1)
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
|
||||
body := &authRequest{}
|
||||
err := json.NewDecoder(req.Body).Decode(body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "Approve", body.Test)
|
||||
|
||||
w.Header().Set("Lfs-Authenticate", "Basic")
|
||||
actual := req.Header.Get("Authorization")
|
||||
if len(actual) == 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
expected := "Basic " + strings.TrimSpace(
|
||||
base64.StdEncoding.EncodeToString([]byte("user:pass")),
|
||||
)
|
||||
assert.Equal(t, expected, actual)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
creds := newMockCredentialHelper()
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"lfs.url": srv.URL + "/repo/lfs",
|
||||
}))
|
||||
require.Nil(t, err)
|
||||
c.Credentials = creds
|
||||
|
||||
assert.Equal(t, NoneAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
|
||||
|
||||
req, err := http.NewRequest("POST", srv.URL+"/repo/lfs/foo", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
err = MarshalToRequest(req, &authRequest{Test: "Approve"})
|
||||
require.Nil(t, err)
|
||||
|
||||
res, err := c.DoWithAuth("", req)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.True(t, creds.IsApproved(Creds(map[string]string{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"path": "repo/lfs",
|
||||
"protocol": "http",
|
||||
"host": srv.Listener.Addr().String(),
|
||||
})))
|
||||
assert.Equal(t, BasicAccess, c.Endpoints.AccessFor(srv.URL+"/repo/lfs"))
|
||||
assert.EqualValues(t, 2, called)
|
||||
}
|
||||
|
||||
func TestDoWithAuthReject(t *testing.T) {
|
||||
var called uint32
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
atomic.AddUint32(&called, 1)
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
|
||||
body := &authRequest{}
|
||||
err := json.NewDecoder(req.Body).Decode(body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "Reject", body.Test)
|
||||
|
||||
actual := req.Header.Get("Authorization")
|
||||
expected := "Basic " + strings.TrimSpace(
|
||||
base64.StdEncoding.EncodeToString([]byte("user:pass")),
|
||||
)
|
||||
|
||||
w.Header().Set("Lfs-Authenticate", "Basic")
|
||||
if actual != expected {
|
||||
// Write http.StatusUnauthorized to force the credential
|
||||
// helper to reject the credentials
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
invalidCreds := Creds(map[string]string{
|
||||
"username": "user",
|
||||
"password": "wrong_pass",
|
||||
"path": "",
|
||||
"protocol": "http",
|
||||
"host": srv.Listener.Addr().String(),
|
||||
})
|
||||
|
||||
creds := newMockCredentialHelper()
|
||||
creds.Approve(invalidCreds)
|
||||
assert.True(t, creds.IsApproved(invalidCreds))
|
||||
|
||||
c := &Client{
|
||||
Credentials: creds,
|
||||
Endpoints: NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": srv.URL,
|
||||
})),
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", srv.URL, nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
err = MarshalToRequest(req, &authRequest{Test: "Reject"})
|
||||
require.Nil(t, err)
|
||||
|
||||
res, err := c.DoWithAuth("", req)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.False(t, creds.IsApproved(invalidCreds))
|
||||
assert.True(t, creds.IsApproved(Creds(map[string]string{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"path": "",
|
||||
"protocol": "http",
|
||||
"host": srv.Listener.Addr().String(),
|
||||
})))
|
||||
assert.EqualValues(t, 3, called)
|
||||
}
|
||||
|
||||
type mockCredentialHelper struct {
|
||||
Approved map[string]Creds
|
||||
}
|
||||
|
||||
func newMockCredentialHelper() *mockCredentialHelper {
|
||||
return &mockCredentialHelper{
|
||||
Approved: make(map[string]Creds),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockCredentialHelper) Fill(input Creds) (Creds, error) {
|
||||
if found, ok := m.Approved[credsToKey(input)]; ok {
|
||||
return found, nil
|
||||
}
|
||||
|
||||
output := make(Creds)
|
||||
for key, value := range input {
|
||||
output[key] = value
|
||||
}
|
||||
if _, ok := output["username"]; !ok {
|
||||
output["username"] = "user"
|
||||
}
|
||||
output["password"] = "pass"
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *mockCredentialHelper) Approve(creds Creds) error {
|
||||
m.Approved[credsToKey(creds)] = creds
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCredentialHelper) Reject(creds Creds) error {
|
||||
delete(m.Approved, credsToKey(creds))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCredentialHelper) IsApproved(creds Creds) bool {
|
||||
if found, ok := m.Approved[credsToKey(creds)]; ok {
|
||||
return found["password"] == creds["password"]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func credsToKey(creds Creds) string {
|
||||
var kvs []string
|
||||
for _, k := range []string{"protocol", "host", "path"} {
|
||||
kvs = append(kvs, fmt.Sprintf("%s:%s", k, creds[k]))
|
||||
}
|
||||
|
||||
return strings.Join(kvs, " ")
|
||||
}
|
||||
|
||||
func basicAuth(user, pass string) string {
|
||||
value := fmt.Sprintf("%s:%s", user, pass)
|
||||
return fmt.Sprintf("Basic %s", strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(value))))
|
||||
}
|
||||
|
||||
type getCredsExpected struct {
|
||||
Endpoint string
|
||||
Access Access
|
||||
Creds Creds
|
||||
CredsURL string
|
||||
Authorization string
|
||||
}
|
||||
|
||||
type getCredsTest struct {
|
||||
Remote string
|
||||
Method string
|
||||
Href string
|
||||
Header map[string]string
|
||||
Config map[string]string
|
||||
Expected getCredsExpected
|
||||
}
|
||||
|
||||
func TestGetCreds(t *testing.T) {
|
||||
tests := map[string]getCredsTest{
|
||||
"no access": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: NoneAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
},
|
||||
},
|
||||
"basic access": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ntlm": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "ntlm",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: NTLMAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
CredsURL: "https://git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ntlm with netrc": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://netrc-host.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://netrc-host.com/repo/lfs",
|
||||
"lfs.https://netrc-host.com/repo/lfs.access": "ntlm",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: NTLMAccess,
|
||||
Endpoint: "https://netrc-host.com/repo/lfs",
|
||||
CredsURL: "https://netrc-host.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "netrc-host.com",
|
||||
"username": "abc",
|
||||
"password": "def",
|
||||
"source": "netrc",
|
||||
},
|
||||
},
|
||||
},
|
||||
"custom auth": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Header: map[string]string{
|
||||
"Authorization": "custom",
|
||||
},
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: "custom",
|
||||
},
|
||||
},
|
||||
"netrc": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://netrc-host.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://netrc-host.com/repo/lfs",
|
||||
"lfs.https://netrc-host.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://netrc-host.com/repo/lfs",
|
||||
Authorization: basicAuth("abc", "def"),
|
||||
},
|
||||
},
|
||||
"username in url": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://user@git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://user@git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("user", "monkey"),
|
||||
CredsURL: "https://user@git-server.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "user",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"different remote url, basic access": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
"remote.origin.url": "https://git-server.com/repo",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "https://git-server.com/repo",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
"api url auth": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://user:pass@git-server.com/repo",
|
||||
"lfs.https://git-server.com/repo.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://user:pass@git-server.com/repo",
|
||||
Authorization: basicAuth("user", "pass"),
|
||||
},
|
||||
},
|
||||
"git url auth": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com/repo/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo",
|
||||
"lfs.https://git-server.com/repo.access": "basic",
|
||||
"remote.origin.url": "https://user:pass@git-server.com/repo",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo",
|
||||
Authorization: basicAuth("user", "pass"),
|
||||
},
|
||||
},
|
||||
"scheme mismatch": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "http://git-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com", "monkey"),
|
||||
CredsURL: "http://git-server.com/repo/lfs/locks",
|
||||
Creds: map[string]string{
|
||||
"protocol": "http",
|
||||
"host": "git-server.com",
|
||||
"username": "git-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
"host mismatch": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://lfs-server.com/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("lfs-server.com", "monkey"),
|
||||
CredsURL: "https://lfs-server.com/repo/lfs/locks",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "lfs-server.com",
|
||||
"username": "lfs-server.com",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
"port mismatch": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://git-server.com:8080/repo/lfs/locks",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://git-server.com/repo/lfs",
|
||||
"lfs.https://git-server.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Endpoint: "https://git-server.com/repo/lfs",
|
||||
Authorization: basicAuth("git-server.com:8080", "monkey"),
|
||||
CredsURL: "https://git-server.com:8080/repo/lfs/locks",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "git-server.com:8080",
|
||||
"username": "git-server.com:8080",
|
||||
"password": "monkey",
|
||||
"path": "repo/lfs/locks",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
credHelper := &fakeCredentialFiller{}
|
||||
netrcFinder := &fakeNetrc{}
|
||||
for desc, test := range tests {
|
||||
t.Log(desc)
|
||||
req, err := http.NewRequest(test.Method, test.Href, nil)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] %s", desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for key, value := range test.Header {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
ef := NewEndpointFinder(TestEnv(test.Config))
|
||||
endpoint, access, creds, credsURL, err := getCreds(credHelper, netrcFinder, ef, test.Remote, req)
|
||||
if !assert.Nil(t, err) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, test.Expected.Endpoint, endpoint.Url, "endpoint")
|
||||
assert.Equal(t, test.Expected.Access, access, "access")
|
||||
assert.Equal(t, test.Expected.Authorization, req.Header.Get("Authorization"), "authorization")
|
||||
|
||||
if test.Expected.Creds != nil {
|
||||
assert.EqualValues(t, test.Expected.Creds, creds)
|
||||
} else {
|
||||
assert.Nil(t, creds, "creds")
|
||||
}
|
||||
|
||||
if len(test.Expected.CredsURL) > 0 {
|
||||
if assert.NotNil(t, credsURL, "credURL") {
|
||||
assert.Equal(t, test.Expected.CredsURL, credsURL.String(), "credURL")
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, credsURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakeCredentialFiller struct{}
|
||||
|
||||
func (f *fakeCredentialFiller) Fill(input Creds) (Creds, error) {
|
||||
output := make(Creds)
|
||||
for key, value := range input {
|
||||
output[key] = value
|
||||
}
|
||||
if _, ok := output["username"]; !ok {
|
||||
output["username"] = input["host"]
|
||||
}
|
||||
output["password"] = "monkey"
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (f *fakeCredentialFiller) Approve(creds Creds) error {
|
||||
return errors.New("Not implemented")
|
||||
}
|
||||
|
||||
func (f *fakeCredentialFiller) Reject(creds Creds) error {
|
||||
return errors.New("Not implemented")
|
||||
}
|
36
lfsapi/body.go
Normal file
36
lfsapi/body.go
Normal file
@ -0,0 +1,36 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ReadSeekCloser interface {
|
||||
io.Seeker
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func MarshalToRequest(req *http.Request, obj interface{}) error {
|
||||
by, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.ContentLength = int64(len(by))
|
||||
req.Body = NewByteBody(by)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewByteBody(by []byte) ReadSeekCloser {
|
||||
return &closingByteReader{Reader: bytes.NewReader(by)}
|
||||
}
|
||||
|
||||
type closingByteReader struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
func (r *closingByteReader) Close() error {
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
@ -6,25 +6,18 @@ import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// isCertVerificationDisabledForHost returns whether SSL certificate verification
|
||||
// has been disabled for the given host, or globally
|
||||
func isCertVerificationDisabledForHost(cfg *config.Configuration, host string) bool {
|
||||
hostSslVerify, _ := cfg.Git.Get(fmt.Sprintf("http.https://%v/.sslverify", host))
|
||||
func isCertVerificationDisabledForHost(c *Client, host string) bool {
|
||||
hostSslVerify, _ := c.gitEnv.Get(fmt.Sprintf("http.https://%v/.sslverify", host))
|
||||
if hostSslVerify == "false" {
|
||||
return true
|
||||
}
|
||||
|
||||
globalSslVerify, _ := cfg.Git.Get("http.sslverify")
|
||||
if globalSslVerify == "false" || cfg.Os.Bool("GIT_SSL_NO_VERIFY", false) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
return c.SkipSSLVerify
|
||||
}
|
||||
|
||||
// getRootCAsForHost returns a certificate pool for that specific host (which may
|
||||
@ -32,50 +25,47 @@ func isCertVerificationDisabledForHost(cfg *config.Configuration, host string) b
|
||||
// source which is not included by default in the golang certificate search)
|
||||
// May return nil if it doesn't have anything to add, in which case the default
|
||||
// RootCAs will be used if passed to TLSClientConfig.RootCAs
|
||||
func getRootCAsForHost(cfg *config.Configuration, host string) *x509.CertPool {
|
||||
|
||||
func getRootCAsForHost(c *Client, host string) *x509.CertPool {
|
||||
// don't init pool, want to return nil not empty if none found; init only on successful add cert
|
||||
var pool *x509.CertPool
|
||||
|
||||
// gitconfig first
|
||||
pool = appendRootCAsForHostFromGitconfig(cfg, pool, host)
|
||||
pool = appendRootCAsForHostFromGitconfig(c.osEnv, c.gitEnv, pool, host)
|
||||
// Platform specific
|
||||
return appendRootCAsForHostFromPlatform(pool, host)
|
||||
|
||||
}
|
||||
|
||||
func appendRootCAsForHostFromGitconfig(cfg *config.Configuration, pool *x509.CertPool, host string) *x509.CertPool {
|
||||
func appendRootCAsForHostFromGitconfig(osEnv Env, gitEnv Env, pool *x509.CertPool, host string) *x509.CertPool {
|
||||
// Accumulate certs from all these locations:
|
||||
|
||||
// GIT_SSL_CAINFO first
|
||||
if cafile, _ := cfg.Os.Get("GIT_SSL_CAINFO"); len(cafile) > 0 {
|
||||
if cafile, _ := osEnv.Get("GIT_SSL_CAINFO"); len(cafile) > 0 {
|
||||
return appendCertsFromFile(pool, cafile)
|
||||
}
|
||||
// http.<url>/.sslcainfo or http.<url>.sslcainfo
|
||||
// we know we have simply "host" or "host:port"
|
||||
hostKeyWithSlash := fmt.Sprintf("http.https://%v/.sslcainfo", host)
|
||||
if cafile, ok := cfg.Git.Get(hostKeyWithSlash); ok {
|
||||
if cafile, ok := gitEnv.Get(hostKeyWithSlash); ok {
|
||||
return appendCertsFromFile(pool, cafile)
|
||||
}
|
||||
hostKeyWithoutSlash := fmt.Sprintf("http.https://%v.sslcainfo", host)
|
||||
if cafile, ok := cfg.Git.Get(hostKeyWithoutSlash); ok {
|
||||
if cafile, ok := gitEnv.Get(hostKeyWithoutSlash); ok {
|
||||
return appendCertsFromFile(pool, cafile)
|
||||
}
|
||||
// http.sslcainfo
|
||||
if cafile, ok := cfg.Git.Get("http.sslcainfo"); ok {
|
||||
if cafile, ok := gitEnv.Get("http.sslcainfo"); ok {
|
||||
return appendCertsFromFile(pool, cafile)
|
||||
}
|
||||
// GIT_SSL_CAPATH
|
||||
if cadir, _ := cfg.Os.Get("GIT_SSL_CAPATH"); len(cadir) > 0 {
|
||||
if cadir, _ := osEnv.Get("GIT_SSL_CAPATH"); len(cadir) > 0 {
|
||||
return appendCertsFromFilesInDir(pool, cadir)
|
||||
}
|
||||
// http.sslcapath
|
||||
if cadir, ok := cfg.Git.Get("http.sslcapath"); ok {
|
||||
if cadir, ok := gitEnv.Get("http.sslcapath"); ok {
|
||||
return appendCertsFromFilesInDir(pool, cadir)
|
||||
}
|
||||
|
||||
return pool
|
||||
|
||||
}
|
||||
|
||||
func appendCertsFromFilesInDir(pool *x509.CertPool, dir string) *x509.CertPool {
|
||||
@ -120,6 +110,7 @@ func appendCerts(pool *x509.CertPool, certs []*x509.Certificate) *x509.CertPool
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
func appendCertsFromPEMData(pool *x509.CertPool, data []byte) *x509.CertPool {
|
||||
if len(data) == 0 {
|
||||
return pool
|
||||
@ -138,5 +129,4 @@ func appendCertsFromPEMData(pool *x509.CertPool, data []byte) *x509.CertPool {
|
||||
return pool
|
||||
}
|
||||
return ret
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import "crypto/x509"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import "crypto/x509"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import "crypto/x509"
|
||||
|
@ -1,13 +1,13 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -60,12 +60,13 @@ func TestCertFromSSLCAInfoConfig(t *testing.T) {
|
||||
// Test http.<url>.sslcainfo
|
||||
for _, hostName := range sslCAInfoConfigHostNames {
|
||||
hostKey := fmt.Sprintf("http.https://%v.sslcainfo", hostName)
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{hostKey: tempfile.Name()},
|
||||
})
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
hostKey: tempfile.Name(),
|
||||
}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, matchedHostTest := range sslCAInfoMatchedHostTests {
|
||||
pool := getRootCAsForHost(cfg, matchedHostTest.hostName)
|
||||
pool := getRootCAsForHost(c, matchedHostTest.hostName)
|
||||
|
||||
var shouldOrShouldnt string
|
||||
if matchedHostTest.shouldMatch {
|
||||
@ -81,16 +82,16 @@ func TestCertFromSSLCAInfoConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test http.sslcainfo
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{"http.sslcainfo": tempfile.Name()},
|
||||
})
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"http.sslcainfo": tempfile.Name(),
|
||||
}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Should match any host at all
|
||||
for _, matchedHostTest := range sslCAInfoMatchedHostTests {
|
||||
pool := getRootCAsForHost(cfg, matchedHostTest.hostName)
|
||||
pool := getRootCAsForHost(c, matchedHostTest.hostName)
|
||||
assert.NotNil(t, pool)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCertFromSSLCAInfoEnv(t *testing.T) {
|
||||
@ -102,18 +103,16 @@ func TestCertFromSSLCAInfoEnv(t *testing.T) {
|
||||
assert.Nil(t, err, "Error writing temp cert file")
|
||||
tempfile.Close()
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Os: map[string]string{
|
||||
"GIT_SSL_CAINFO": tempfile.Name(),
|
||||
},
|
||||
})
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"GIT_SSL_CAINFO": tempfile.Name(),
|
||||
}), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Should match any host at all
|
||||
for _, matchedHostTest := range sslCAInfoMatchedHostTests {
|
||||
pool := getRootCAsForHost(cfg, matchedHostTest.hostName)
|
||||
pool := getRootCAsForHost(c, matchedHostTest.hostName)
|
||||
assert.NotNil(t, pool)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCertFromSSLCAPathConfig(t *testing.T) {
|
||||
@ -124,16 +123,17 @@ func TestCertFromSSLCAPathConfig(t *testing.T) {
|
||||
err = ioutil.WriteFile(filepath.Join(tempdir, "cert1.pem"), []byte(testCert), 0644)
|
||||
assert.Nil(t, err, "Error creating cert file")
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{"http.sslcapath": tempdir},
|
||||
})
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"http.sslcapath": tempdir,
|
||||
}))
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Should match any host at all
|
||||
for _, matchedHostTest := range sslCAInfoMatchedHostTests {
|
||||
pool := getRootCAsForHost(cfg, matchedHostTest.hostName)
|
||||
pool := getRootCAsForHost(c, matchedHostTest.hostName)
|
||||
assert.NotNil(t, pool)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCertFromSSLCAPathEnv(t *testing.T) {
|
||||
@ -144,52 +144,87 @@ func TestCertFromSSLCAPathEnv(t *testing.T) {
|
||||
err = ioutil.WriteFile(filepath.Join(tempdir, "cert1.pem"), []byte(testCert), 0644)
|
||||
assert.Nil(t, err, "Error creating cert file")
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Os: map[string]string{
|
||||
"GIT_SSL_CAPATH": tempdir,
|
||||
},
|
||||
})
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"GIT_SSL_CAPATH": tempdir,
|
||||
}), nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Should match any host at all
|
||||
for _, matchedHostTest := range sslCAInfoMatchedHostTests {
|
||||
pool := getRootCAsForHost(cfg, matchedHostTest.hostName)
|
||||
pool := getRootCAsForHost(c, matchedHostTest.hostName)
|
||||
assert.NotNil(t, pool)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCertVerifyDisabledGlobalEnv(t *testing.T) {
|
||||
empty := config.NewFrom(config.Values{})
|
||||
assert.False(t, isCertVerificationDisabledForHost(empty, "anyhost.com"))
|
||||
empty := &Client{}
|
||||
httpClient := empty.httpClient("anyhost.com")
|
||||
tr, ok := httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.False(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Os: map[string]string{
|
||||
"GIT_SSL_NO_VERIFY": "1",
|
||||
},
|
||||
})
|
||||
assert.True(t, isCertVerificationDisabledForHost(cfg, "anyhost.com"))
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"GIT_SSL_NO_VERIFY": "1",
|
||||
}), nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
httpClient = c.httpClient("anyhost.com")
|
||||
tr, ok = httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.True(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertVerifyDisabledGlobalConfig(t *testing.T) {
|
||||
def := config.New()
|
||||
assert.False(t, isCertVerificationDisabledForHost(def, "anyhost.com"))
|
||||
def := &Client{}
|
||||
httpClient := def.httpClient("anyhost.com")
|
||||
tr, ok := httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.False(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{"http.sslverify": "false"},
|
||||
})
|
||||
assert.True(t, isCertVerificationDisabledForHost(cfg, "anyhost.com"))
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"http.sslverify": "false",
|
||||
}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
httpClient = c.httpClient("anyhost.com")
|
||||
tr, ok = httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.True(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertVerifyDisabledHostConfig(t *testing.T) {
|
||||
def := config.New()
|
||||
assert.False(t, isCertVerificationDisabledForHost(def, "specifichost.com"))
|
||||
assert.False(t, isCertVerificationDisabledForHost(def, "otherhost.com"))
|
||||
def := &Client{}
|
||||
httpClient := def.httpClient("specifichost.com")
|
||||
tr, ok := httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.False(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
cfg := config.NewFrom(config.Values{
|
||||
Git: map[string]string{
|
||||
"http.https://specifichost.com/.sslverify": "false",
|
||||
},
|
||||
})
|
||||
assert.True(t, isCertVerificationDisabledForHost(cfg, "specifichost.com"))
|
||||
assert.False(t, isCertVerificationDisabledForHost(cfg, "otherhost.com"))
|
||||
httpClient = def.httpClient("otherhost.com")
|
||||
tr, ok = httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.False(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"http.https://specifichost.com/.sslverify": "false",
|
||||
}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
httpClient = c.httpClient("specifichost.com")
|
||||
tr, ok = httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.True(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
httpClient = c.httpClient("otherhost.com")
|
||||
tr, ok = httpClient.Transport.(*http.Transport)
|
||||
if assert.True(t, ok) {
|
||||
assert.False(t, tr.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import "crypto/x509"
|
||||
|
217
lfsapi/client.go
Normal file
217
lfsapi/client.go
Normal file
@ -0,0 +1,217 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
var UserAgent = "git-lfs"
|
||||
|
||||
const MediaType = "application/vnd.git-lfs+json; charset=utf-8"
|
||||
|
||||
func (c *Client) NewRequest(method string, e Endpoint, suffix string, body interface{}) (*http.Request, error) {
|
||||
sshRes, err := c.resolveSSHEndpoint(e, method)
|
||||
if err != nil {
|
||||
tracerx.Printf("ssh: %s failed, error: %s, message: %s",
|
||||
e.SshUserAndHost, err.Error(), sshRes.Message,
|
||||
)
|
||||
|
||||
if len(sshRes.Message) > 0 {
|
||||
return nil, errors.Wrap(err, sshRes.Message)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := e.Url
|
||||
if len(sshRes.Href) > 0 {
|
||||
prefix = sshRes.Href
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, joinURL(prefix, suffix), nil)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
for key, value := range sshRes.Header {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req.Header.Set("Accept", MediaType)
|
||||
|
||||
if body != nil {
|
||||
if merr := MarshalToRequest(req, body); merr != nil {
|
||||
return req, merr
|
||||
}
|
||||
req.Header.Set("Content-Type", MediaType)
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
const slash = "/"
|
||||
|
||||
func joinURL(prefix, suffix string) string {
|
||||
if strings.HasSuffix(prefix, slash) {
|
||||
return prefix + suffix
|
||||
}
|
||||
return prefix + slash + suffix
|
||||
}
|
||||
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
res, err := c.doWithRedirects(c.httpClient(req.Host), req, nil)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
return res, c.handleResponse(res)
|
||||
}
|
||||
|
||||
func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, via []*http.Request) (*http.Response, error) {
|
||||
c.traceRequest(req)
|
||||
if err := c.prepareRequestBody(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
res, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
c.traceResponse(res)
|
||||
c.startResponseStats(res, start)
|
||||
|
||||
if res.StatusCode != 307 {
|
||||
return res, err
|
||||
}
|
||||
|
||||
redirectTo := res.Header.Get("Location")
|
||||
locurl, err := url.Parse(redirectTo)
|
||||
if err == nil && !locurl.IsAbs() {
|
||||
locurl = req.URL.ResolveReference(locurl)
|
||||
redirectTo = locurl.String()
|
||||
}
|
||||
|
||||
via = append(via, req)
|
||||
if len(via) >= 3 {
|
||||
return res, errors.New("too many redirects")
|
||||
}
|
||||
|
||||
redirectedReq, err := newRequestForRetry(req, redirectTo)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
return c.doWithRedirects(cli, redirectedReq, via)
|
||||
}
|
||||
|
||||
func (c *Client) httpClient(host string) *http.Client {
|
||||
c.clientMu.Lock()
|
||||
defer c.clientMu.Unlock()
|
||||
|
||||
if c.gitEnv == nil {
|
||||
c.gitEnv = make(TestEnv)
|
||||
}
|
||||
|
||||
if c.osEnv == nil {
|
||||
c.osEnv = make(TestEnv)
|
||||
}
|
||||
|
||||
if c.hostClients == nil {
|
||||
c.hostClients = make(map[string]*http.Client)
|
||||
}
|
||||
|
||||
if client, ok := c.hostClients[host]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
concurrentTransfers := c.ConcurrentTransfers
|
||||
if concurrentTransfers < 1 {
|
||||
concurrentTransfers = 3
|
||||
}
|
||||
|
||||
dialtime := c.DialTimeout
|
||||
if dialtime < 1 {
|
||||
dialtime = 30
|
||||
}
|
||||
|
||||
keepalivetime := c.KeepaliveTimeout
|
||||
if keepalivetime < 1 {
|
||||
keepalivetime = 1800
|
||||
}
|
||||
|
||||
tlstime := c.TLSTimeout
|
||||
if tlstime < 1 {
|
||||
tlstime = 30
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
Proxy: proxyFromClient(c),
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: time.Duration(dialtime) * time.Second,
|
||||
KeepAlive: time.Duration(keepalivetime) * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: time.Duration(tlstime) * time.Second,
|
||||
MaxIdleConnsPerHost: concurrentTransfers,
|
||||
}
|
||||
|
||||
tr.TLSClientConfig = &tls.Config{}
|
||||
if isCertVerificationDisabledForHost(c, host) {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
} else {
|
||||
tr.TLSClientConfig.RootCAs = getRootCAsForHost(c, host)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
c.hostClients[host] = httpClient
|
||||
if c.VerboseOut == nil {
|
||||
c.VerboseOut = os.Stderr
|
||||
}
|
||||
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *Client) CurrentUser() (string, string) {
|
||||
userName, _ := c.gitEnv.Get("user.name")
|
||||
userEmail, _ := c.gitEnv.Get("user.email")
|
||||
return userName, userEmail
|
||||
}
|
||||
|
||||
func newRequestForRetry(req *http.Request, location string) (*http.Request, error) {
|
||||
newReq, err := http.NewRequest(req.Method, location, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key := range req.Header {
|
||||
if key == "Authorization" {
|
||||
if req.URL.Scheme != newReq.URL.Scheme || req.URL.Host != newReq.URL.Host {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newReq.Header.Set(key, req.Header.Get(key))
|
||||
}
|
||||
|
||||
oldestURL := strings.SplitN(req.URL.String(), "?", 2)[0]
|
||||
newURL := strings.SplitN(newReq.URL.String(), "?", 2)[0]
|
||||
tracerx.Printf("api: redirect %s %s to %s", req.Method, oldestURL, newURL)
|
||||
|
||||
newReq.Body = req.Body
|
||||
newReq.ContentLength = req.ContentLength
|
||||
return newReq, nil
|
||||
}
|
183
lfsapi/client_test.go
Normal file
183
lfsapi/client_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type redirectTest struct {
|
||||
Test string
|
||||
}
|
||||
|
||||
func TestClientRedirect(t *testing.T) {
|
||||
var called1 uint32
|
||||
var called2 uint32
|
||||
srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint32(&called2, 1)
|
||||
t.Logf("srv2 req %s %s", r.Method, r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/ok":
|
||||
assert.Equal(t, "", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "1", r.Header.Get("A"))
|
||||
body := &redirectTest{}
|
||||
err := json.NewDecoder(r.Body).Decode(body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "External", body.Test)
|
||||
|
||||
w.WriteHeader(200)
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
|
||||
srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint32(&called1, 1)
|
||||
t.Logf("srv1 req %s %s", r.Method, r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/local":
|
||||
w.Header().Set("Location", "/ok")
|
||||
w.WriteHeader(307)
|
||||
case "/external":
|
||||
w.Header().Set("Location", srv2.URL+"/ok")
|
||||
w.WriteHeader(307)
|
||||
case "/ok":
|
||||
assert.Equal(t, "auth", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "1", r.Header.Get("A"))
|
||||
body := &redirectTest{}
|
||||
err := json.NewDecoder(r.Body).Decode(body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "Local", body.Test)
|
||||
|
||||
w.WriteHeader(200)
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer srv1.Close()
|
||||
defer srv2.Close()
|
||||
|
||||
c := &Client{}
|
||||
|
||||
// local redirect
|
||||
req, err := http.NewRequest("POST", srv1.URL+"/local", nil)
|
||||
require.Nil(t, err)
|
||||
req.Header.Set("Authorization", "auth")
|
||||
req.Header.Set("A", "1")
|
||||
|
||||
require.Nil(t, MarshalToRequest(req, &redirectTest{Test: "Local"}))
|
||||
|
||||
res, err := c.Do(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
assert.EqualValues(t, 2, called1)
|
||||
assert.EqualValues(t, 0, called2)
|
||||
|
||||
// external redirect
|
||||
req, err = http.NewRequest("POST", srv1.URL+"/external", nil)
|
||||
require.Nil(t, err)
|
||||
req.Header.Set("Authorization", "auth")
|
||||
req.Header.Set("A", "1")
|
||||
|
||||
require.Nil(t, MarshalToRequest(req, &redirectTest{Test: "External"}))
|
||||
|
||||
res, err = c.Do(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
assert.EqualValues(t, 3, called1)
|
||||
assert.EqualValues(t, 1, called2)
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
c, err := NewClient(TestEnv(map[string]string{}), TestEnv(map[string]string{
|
||||
"lfs.dialtimeout": "151",
|
||||
"lfs.keepalive": "152",
|
||||
"lfs.tlstimeout": "153",
|
||||
"lfs.concurrenttransfers": "154",
|
||||
}))
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 151, c.DialTimeout)
|
||||
assert.Equal(t, 152, c.KeepaliveTimeout)
|
||||
assert.Equal(t, 153, c.TLSTimeout)
|
||||
assert.Equal(t, 154, c.ConcurrentTransfers)
|
||||
}
|
||||
|
||||
func TestNewClientWithGitSSLVerify(t *testing.T) {
|
||||
c, err := NewClient(nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, c.SkipSSLVerify)
|
||||
|
||||
for _, value := range []string{"true", "1", "t"} {
|
||||
c, err = NewClient(TestEnv(map[string]string{}), TestEnv(map[string]string{
|
||||
"http.sslverify": value,
|
||||
}))
|
||||
t.Logf("http.sslverify: %q", value)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, c.SkipSSLVerify)
|
||||
}
|
||||
|
||||
for _, value := range []string{"false", "0", "f"} {
|
||||
c, err = NewClient(TestEnv(map[string]string{}), TestEnv(map[string]string{
|
||||
"http.sslverify": value,
|
||||
}))
|
||||
t.Logf("http.sslverify: %q", value)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, c.SkipSSLVerify)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientWithOSSSLVerify(t *testing.T) {
|
||||
c, err := NewClient(nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, c.SkipSSLVerify)
|
||||
|
||||
for _, value := range []string{"false", "0", "f"} {
|
||||
c, err = NewClient(TestEnv(map[string]string{
|
||||
"GIT_SSL_NO_VERIFY": value,
|
||||
}), TestEnv(map[string]string{}))
|
||||
t.Logf("GIT_SSL_NO_VERIFY: %q", value)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, c.SkipSSLVerify)
|
||||
}
|
||||
|
||||
for _, value := range []string{"true", "1", "t"} {
|
||||
c, err = NewClient(TestEnv(map[string]string{
|
||||
"GIT_SSL_NO_VERIFY": value,
|
||||
}), TestEnv(map[string]string{}))
|
||||
t.Logf("GIT_SSL_NO_VERIFY: %q", value)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, c.SkipSSLVerify)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequest(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"https://example.com", "a", "https://example.com/a"},
|
||||
{"https://example.com/", "a", "https://example.com/a"},
|
||||
{"https://example.com/a", "b", "https://example.com/a/b"},
|
||||
{"https://example.com/a/", "b", "https://example.com/a/b"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"lfs.url": test[0],
|
||||
}))
|
||||
require.Nil(t, err)
|
||||
|
||||
req, err := c.NewRequest("POST", c.Endpoints.Endpoint("", ""), test[1], nil)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, test[2], req.URL.String(), fmt.Sprintf("endpoint: %s, suffix: %s, expected: %s", test[0], test[1], test[2]))
|
||||
}
|
||||
}
|
95
lfsapi/creds.go
Normal file
95
lfsapi/creds.go
Normal file
@ -0,0 +1,95 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CredentialHelper interface {
|
||||
Fill(Creds) (Creds, error)
|
||||
Reject(Creds) error
|
||||
Approve(Creds) error
|
||||
}
|
||||
|
||||
type Creds map[string]string
|
||||
|
||||
func bufferCreds(c Creds) *bytes.Buffer {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for k, v := range c {
|
||||
buf.Write([]byte(k))
|
||||
buf.Write([]byte("="))
|
||||
buf.Write([]byte(v))
|
||||
buf.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
type commandCredentialHelper struct {
|
||||
SkipPrompt bool
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) Fill(creds Creds) (Creds, error) {
|
||||
return h.exec("fill", creds)
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) Reject(creds Creds) error {
|
||||
_, err := h.exec("reject", creds)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) Approve(creds Creds) error {
|
||||
_, err := h.exec("approve", creds)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *commandCredentialHelper) exec(subcommand string, input Creds) (Creds, error) {
|
||||
output := new(bytes.Buffer)
|
||||
cmd := exec.Command("git", "credential", subcommand)
|
||||
cmd.Stdin = bufferCreds(input)
|
||||
cmd.Stdout = output
|
||||
/*
|
||||
There is a reason we don't hook up stderr here:
|
||||
Git's credential cache daemon helper does not close its stderr, so if this
|
||||
process is the process that fires up the daemon, it will wait forever
|
||||
(until the daemon exits, really) trying to read from stderr.
|
||||
|
||||
See https://github.com/git-lfs/git-lfs/issues/117 for more details.
|
||||
*/
|
||||
|
||||
err := cmd.Start()
|
||||
if err == nil {
|
||||
err = cmd.Wait()
|
||||
}
|
||||
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
if h.SkipPrompt {
|
||||
return nil, fmt.Errorf("Change the GIT_TERMINAL_PROMPT env var to be prompted to enter your credentials for %s://%s.",
|
||||
input["protocol"], input["host"])
|
||||
}
|
||||
|
||||
// 'git credential' exits with 128 if the helper doesn't fill the username
|
||||
// and password values.
|
||||
if subcommand == "fill" && err.Error() == "exit status 128" {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'git credential %s' error: %s\n", subcommand, err.Error())
|
||||
}
|
||||
|
||||
creds := make(Creds)
|
||||
for _, line := range strings.Split(output.String(), "\n") {
|
||||
pieces := strings.SplitN(line, "=", 2)
|
||||
if len(pieces) < 2 || len(pieces[1]) < 1 {
|
||||
continue
|
||||
}
|
||||
creds[pieces[0]] = pieces[1]
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
package config
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const EndpointUrlUnknown = "<unknown>"
|
||||
const UrlUnknown = "<unknown>"
|
||||
|
||||
// An Endpoint describes how to access a Git LFS server.
|
||||
type Endpoint struct {
|
||||
@ -18,62 +17,6 @@ type Endpoint struct {
|
||||
SshPort string
|
||||
}
|
||||
|
||||
// NewEndpointFromCloneURL creates an Endpoint from a git clone URL by appending
|
||||
// "[.git]/info/lfs".
|
||||
func NewEndpointFromCloneURL(url string) Endpoint {
|
||||
return NewEndpointFromCloneURLWithConfig(url, New())
|
||||
}
|
||||
|
||||
// NewEndpoint initializes a new Endpoint for a given URL.
|
||||
func NewEndpoint(rawurl string) Endpoint {
|
||||
return NewEndpointWithConfig(rawurl, New())
|
||||
}
|
||||
|
||||
// NewEndpointFromCloneURLWithConfig creates an Endpoint from a git clone URL by appending
|
||||
// "[.git]/info/lfs".
|
||||
func NewEndpointFromCloneURLWithConfig(url string, c *Configuration) Endpoint {
|
||||
e := NewEndpointWithConfig(url, c)
|
||||
if e.Url == EndpointUrlUnknown {
|
||||
return e
|
||||
}
|
||||
|
||||
if strings.HasSuffix(url, "/") {
|
||||
e.Url = url[0 : len(url)-1]
|
||||
}
|
||||
|
||||
// When using main remote URL for HTTP, append info/lfs
|
||||
if path.Ext(e.Url) == ".git" {
|
||||
e.Url += "/info/lfs"
|
||||
} else {
|
||||
e.Url += ".git/info/lfs"
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// NewEndpointWithConfig initializes a new Endpoint for a given URL.
|
||||
func NewEndpointWithConfig(rawurl string, c *Configuration) Endpoint {
|
||||
rawurl = c.ReplaceUrlAlias(rawurl)
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return endpointFromBareSshUrl(rawurl)
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ssh":
|
||||
return endpointFromSshUrl(u)
|
||||
case "http", "https":
|
||||
return endpointFromHttpUrl(u)
|
||||
case "git":
|
||||
return endpointFromGitUrl(u, c)
|
||||
case "":
|
||||
return endpointFromBareSshUrl(u.String())
|
||||
default:
|
||||
// Just passthrough to preserve
|
||||
return Endpoint{Url: rawurl}
|
||||
}
|
||||
}
|
||||
|
||||
// endpointFromBareSshUrl constructs a new endpoint from a bare SSH URL:
|
||||
//
|
||||
// user@host.com:path/to/repo.git
|
||||
@ -95,7 +38,7 @@ func endpointFromBareSshUrl(rawurl string) Endpoint {
|
||||
newrawurl := fmt.Sprintf("ssh://%v", newPath)
|
||||
newu, err := url.Parse(newrawurl)
|
||||
if err != nil {
|
||||
return Endpoint{Url: EndpointUrlUnknown}
|
||||
return Endpoint{Url: UrlUnknown}
|
||||
}
|
||||
|
||||
return endpointFromSshUrl(newu)
|
||||
@ -108,7 +51,7 @@ func endpointFromSshUrl(u *url.URL) Endpoint {
|
||||
regex := regexp.MustCompile(`^([^\:]+)(?:\:(\d+))?$`)
|
||||
match := regex.FindStringSubmatch(u.Host)
|
||||
if match == nil || len(match) < 2 {
|
||||
endpoint.Url = EndpointUrlUnknown
|
||||
endpoint.Url = UrlUnknown
|
||||
return endpoint
|
||||
}
|
||||
|
||||
@ -145,7 +88,7 @@ func endpointFromHttpUrl(u *url.URL) Endpoint {
|
||||
return Endpoint{Url: u.String()}
|
||||
}
|
||||
|
||||
func endpointFromGitUrl(u *url.URL, c *Configuration) Endpoint {
|
||||
u.Scheme = c.GitProtocol()
|
||||
func endpointFromGitUrl(u *url.URL, e *endpointGitFinder) Endpoint {
|
||||
u.Scheme = e.gitProtocol
|
||||
return Endpoint{Url: u.String()}
|
||||
}
|
284
lfsapi/endpoint_finder.go
Normal file
284
lfsapi/endpoint_finder.go
Normal file
@ -0,0 +1,284 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
type Access string
|
||||
|
||||
const (
|
||||
NoneAccess Access = "none"
|
||||
BasicAccess Access = "basic"
|
||||
PrivateAccess Access = "private"
|
||||
NegotiateAccess Access = "negotiate"
|
||||
NTLMAccess Access = "ntlm"
|
||||
emptyAccess Access = ""
|
||||
defaultRemote = "origin"
|
||||
)
|
||||
|
||||
type EndpointFinder interface {
|
||||
NewEndpointFromCloneURL(rawurl string) Endpoint
|
||||
NewEndpoint(rawurl string) Endpoint
|
||||
Endpoint(operation, remote string) Endpoint
|
||||
RemoteEndpoint(operation, remote string) Endpoint
|
||||
GitRemoteURL(remote string, forpush bool) string
|
||||
AccessFor(rawurl string) Access
|
||||
SetAccess(rawurl string, access Access)
|
||||
GitProtocol() string
|
||||
}
|
||||
|
||||
type endpointGitFinder struct {
|
||||
git Env
|
||||
gitProtocol string
|
||||
|
||||
aliasMu sync.Mutex
|
||||
aliases map[string]string
|
||||
|
||||
accessMu sync.Mutex
|
||||
urlAccess map[string]Access
|
||||
}
|
||||
|
||||
func NewEndpointFinder(git Env) EndpointFinder {
|
||||
e := &endpointGitFinder{
|
||||
gitProtocol: "https",
|
||||
aliases: make(map[string]string),
|
||||
urlAccess: make(map[string]Access),
|
||||
}
|
||||
|
||||
if git != nil {
|
||||
e.git = git
|
||||
if v, ok := git.Get("lfs.gitprotocol"); ok {
|
||||
e.gitProtocol = v
|
||||
}
|
||||
initAliases(e, git)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) Endpoint(operation, remote string) Endpoint {
|
||||
if e.git == nil {
|
||||
return Endpoint{}
|
||||
}
|
||||
|
||||
if operation == "upload" {
|
||||
if url, ok := e.git.Get("lfs.pushurl"); ok {
|
||||
return e.NewEndpoint(url)
|
||||
}
|
||||
}
|
||||
|
||||
if url, ok := e.git.Get("lfs.url"); ok {
|
||||
return e.NewEndpoint(url)
|
||||
}
|
||||
|
||||
if len(remote) > 0 && remote != defaultRemote {
|
||||
if e := e.RemoteEndpoint(operation, remote); len(e.Url) > 0 {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return e.RemoteEndpoint(operation, defaultRemote)
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) RemoteEndpoint(operation, remote string) Endpoint {
|
||||
if e.git == nil {
|
||||
return Endpoint{}
|
||||
}
|
||||
|
||||
if len(remote) == 0 {
|
||||
remote = defaultRemote
|
||||
}
|
||||
|
||||
// Support separate push URL if specified and pushing
|
||||
if operation == "upload" {
|
||||
if url, ok := e.git.Get("remote." + remote + ".lfspushurl"); ok {
|
||||
return e.NewEndpoint(url)
|
||||
}
|
||||
}
|
||||
if url, ok := e.git.Get("remote." + remote + ".lfsurl"); ok {
|
||||
return e.NewEndpoint(url)
|
||||
}
|
||||
|
||||
// finally fall back on git remote url (also supports pushurl)
|
||||
if url := e.GitRemoteURL(remote, operation == "upload"); url != "" {
|
||||
return e.NewEndpointFromCloneURL(url)
|
||||
}
|
||||
|
||||
return Endpoint{}
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) GitRemoteURL(remote string, forpush bool) string {
|
||||
if e.git != nil {
|
||||
if forpush {
|
||||
if u, ok := e.git.Get("remote." + remote + ".pushurl"); ok {
|
||||
return u
|
||||
}
|
||||
}
|
||||
|
||||
if u, ok := e.git.Get("remote." + remote + ".url"); ok {
|
||||
return u
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.ValidateRemote(remote); err == nil {
|
||||
return remote
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) NewEndpointFromCloneURL(rawurl string) Endpoint {
|
||||
ep := e.NewEndpoint(rawurl)
|
||||
if ep.Url == UrlUnknown {
|
||||
return ep
|
||||
}
|
||||
|
||||
if strings.HasSuffix(rawurl, "/") {
|
||||
ep.Url = rawurl[0 : len(rawurl)-1]
|
||||
}
|
||||
|
||||
// When using main remote URL for HTTP, append info/lfs
|
||||
if path.Ext(ep.Url) == ".git" {
|
||||
ep.Url += "/info/lfs"
|
||||
} else {
|
||||
ep.Url += ".git/info/lfs"
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) NewEndpoint(rawurl string) Endpoint {
|
||||
rawurl = e.ReplaceUrlAlias(rawurl)
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return endpointFromBareSshUrl(rawurl)
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ssh":
|
||||
return endpointFromSshUrl(u)
|
||||
case "http", "https":
|
||||
return endpointFromHttpUrl(u)
|
||||
case "git":
|
||||
return endpointFromGitUrl(u, e)
|
||||
case "":
|
||||
return endpointFromBareSshUrl(u.String())
|
||||
default:
|
||||
// Just passthrough to preserve
|
||||
return Endpoint{Url: rawurl}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) AccessFor(rawurl string) Access {
|
||||
if e.git == nil {
|
||||
return NoneAccess
|
||||
}
|
||||
|
||||
accessurl := urlWithoutAuth(rawurl)
|
||||
|
||||
e.accessMu.Lock()
|
||||
defer e.accessMu.Unlock()
|
||||
|
||||
if cached, ok := e.urlAccess[accessurl]; ok {
|
||||
return cached
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("lfs.%s.access", accessurl)
|
||||
e.urlAccess[accessurl] = fetchGitAccess(e.git, key)
|
||||
return e.urlAccess[accessurl]
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) SetAccess(rawurl string, access Access) {
|
||||
accessurl := urlWithoutAuth(rawurl)
|
||||
key := fmt.Sprintf("lfs.%s.access", accessurl)
|
||||
tracerx.Printf("setting repository access to %s", access)
|
||||
|
||||
e.accessMu.Lock()
|
||||
defer e.accessMu.Unlock()
|
||||
|
||||
switch access {
|
||||
case emptyAccess, NoneAccess:
|
||||
git.Config.UnsetLocalKey("", key)
|
||||
e.urlAccess[accessurl] = NoneAccess
|
||||
default:
|
||||
git.Config.SetLocal("", key, string(access))
|
||||
e.urlAccess[accessurl] = access
|
||||
}
|
||||
}
|
||||
|
||||
func urlWithoutAuth(rawurl string) string {
|
||||
if !strings.Contains(rawurl, "@") {
|
||||
return rawurl
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing URL %q: %s", rawurl, err)
|
||||
return rawurl
|
||||
}
|
||||
|
||||
u.User = nil
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func fetchGitAccess(git Env, key string) Access {
|
||||
if v, _ := git.Get(key); len(v) > 0 {
|
||||
access := Access(strings.ToLower(v))
|
||||
if access == PrivateAccess {
|
||||
return BasicAccess
|
||||
}
|
||||
return access
|
||||
}
|
||||
return NoneAccess
|
||||
}
|
||||
|
||||
func (e *endpointGitFinder) GitProtocol() string {
|
||||
return e.gitProtocol
|
||||
}
|
||||
|
||||
// ReplaceUrlAlias returns a url with a prefix from a `url.*.insteadof` git
|
||||
// config setting. If multiple aliases match, use the longest one.
|
||||
// See https://git-scm.com/docs/git-config for Git's docs.
|
||||
func (e *endpointGitFinder) ReplaceUrlAlias(rawurl string) string {
|
||||
e.aliasMu.Lock()
|
||||
defer e.aliasMu.Unlock()
|
||||
|
||||
var longestalias string
|
||||
for alias, _ := range e.aliases {
|
||||
if !strings.HasPrefix(rawurl, alias) {
|
||||
continue
|
||||
}
|
||||
|
||||
if longestalias < alias {
|
||||
longestalias = alias
|
||||
}
|
||||
}
|
||||
|
||||
if len(longestalias) > 0 {
|
||||
return e.aliases[longestalias] + rawurl[len(longestalias):]
|
||||
}
|
||||
|
||||
return rawurl
|
||||
}
|
||||
|
||||
func initAliases(e *endpointGitFinder, git Env) {
|
||||
prefix := "url."
|
||||
suffix := ".insteadof"
|
||||
for gitkey, gitval := range git.All() {
|
||||
if !(strings.HasPrefix(gitkey, prefix) && strings.HasSuffix(gitkey, suffix)) {
|
||||
continue
|
||||
}
|
||||
if _, ok := e.aliases[gitval]; ok {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Multiple 'url.*.insteadof' keys with the same alias: %q\n", gitval)
|
||||
}
|
||||
e.aliases[gitval] = gitkey[len(prefix) : len(gitkey)-len(suffix)]
|
||||
}
|
||||
}
|
350
lfsapi/endpoint_finder_test.go
Normal file
350
lfsapi/endpoint_finder_test.go
Normal file
@ -0,0 +1,350 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEndpointDefaultsToOrigin(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.lfsurl": "abc",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "abc", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointOverridesOrigin(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": "abc",
|
||||
"remote.origin.lfsurl": "def",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "abc", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointNoOverrideDefaultRemote(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.lfsurl": "abc",
|
||||
"remote.other.lfsurl": "def",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "abc", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointUseAlternateRemote(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.lfsurl": "abc",
|
||||
"remote.other.lfsurl": "def",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "other")
|
||||
assert.Equal(t, "def", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestBareEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointSeparateClonePushUrl(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar.git",
|
||||
"remote.origin.pushurl": "https://readwrite.com/foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
|
||||
e = finder.Endpoint("upload", "")
|
||||
assert.Equal(t, "https://readwrite.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointOverriddenSeparateClonePushLfsUrl(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "https://example.com/foo/bar.git",
|
||||
"remote.origin.pushurl": "https://readwrite.com/foo/bar.git",
|
||||
"remote.origin.lfsurl": "https://examplelfs.com/foo/bar",
|
||||
"remote.origin.lfspushurl": "https://readwritelfs.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://examplelfs.com/foo/bar", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
|
||||
e = finder.Endpoint("upload", "")
|
||||
assert.Equal(t, "https://readwritelfs.com/foo/bar", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestEndpointGlobalSeparateLfsPush(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": "https://readonly.com/foo/bar",
|
||||
"lfs.pushurl": "https://write.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://readonly.com/foo/bar", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
|
||||
e = finder.Endpoint("upload", "")
|
||||
assert.Equal(t, "https://write.com/foo/bar", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
}
|
||||
|
||||
func TestSSHEndpointOverridden(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "git@example.com:foo/bar",
|
||||
"remote.origin.lfsurl": "lfs",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "ssh://git@example.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "git@example.com", e.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHCustomPortEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "ssh://git@example.com:9000/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "git@example.com", e.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar", e.SshPath)
|
||||
assert.Equal(t, "9000", e.SshPort)
|
||||
}
|
||||
|
||||
func TestBareSSHEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "git@example.com:foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "git@example.com", e.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar.git", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestSSHEndpointFromGlobalLfsUrl(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": "git@example.com:foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git", e.Url)
|
||||
assert.Equal(t, "git@example.com", e.SshUserAndHost)
|
||||
assert.Equal(t, "foo/bar.git", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestHTTPEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "http://example.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestBareHTTPEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "http://example.com/foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestGitEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "git://example.com/foo/bar",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestGitEndpointAddsLfsSuffixWithCustomProtocol(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "git://example.com/foo/bar",
|
||||
"lfs.gitprotocol": "http",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "http://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestBareGitEndpointAddsLfsSuffix(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"remote.origin.url": "git://example.com/foo/bar.git",
|
||||
}))
|
||||
|
||||
e := finder.Endpoint("download", "")
|
||||
assert.Equal(t, "https://example.com/foo/bar.git/info/lfs", e.Url)
|
||||
assert.Equal(t, "", e.SshUserAndHost)
|
||||
assert.Equal(t, "", e.SshPath)
|
||||
assert.Equal(t, "", e.SshPort)
|
||||
}
|
||||
|
||||
func TestAccessConfig(t *testing.T) {
|
||||
type accessTest struct {
|
||||
Access string
|
||||
PrivateAccess bool
|
||||
}
|
||||
|
||||
tests := map[string]accessTest{
|
||||
"": {"none", false},
|
||||
"basic": {"basic", true},
|
||||
"BASIC": {"basic", true},
|
||||
"private": {"basic", true},
|
||||
"PRIVATE": {"basic", true},
|
||||
"invalidauth": {"invalidauth", true},
|
||||
}
|
||||
|
||||
for value, expected := range tests {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": "http://example.com",
|
||||
"lfs.http://example.com.access": value,
|
||||
"lfs.https://example.com.access": "bad",
|
||||
}))
|
||||
|
||||
dl := finder.Endpoint("upload", "")
|
||||
ul := finder.Endpoint("download", "")
|
||||
|
||||
if access := finder.AccessFor(dl.Url); access != Access(expected.Access) {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
if access := finder.AccessFor(ul.Url); access != Access(expected.Access) {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
}
|
||||
|
||||
// Test again but with separate push url
|
||||
for value, expected := range tests {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.url": "http://example.com",
|
||||
"lfs.pushurl": "http://examplepush.com",
|
||||
"lfs.http://example.com.access": value,
|
||||
"lfs.http://examplepush.com.access": value,
|
||||
"lfs.https://example.com.access": "bad",
|
||||
}))
|
||||
|
||||
dl := finder.Endpoint("upload", "")
|
||||
ul := finder.Endpoint("download", "")
|
||||
|
||||
if access := finder.AccessFor(dl.Url); access != Access(expected.Access) {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
if access := finder.AccessFor(ul.Url); access != Access(expected.Access) {
|
||||
t.Errorf("Expected Access() with value %q to be %v, got %v", value, expected.Access, access)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessAbsentConfig(t *testing.T) {
|
||||
finder := NewEndpointFinder(nil)
|
||||
assert.Equal(t, NoneAccess, finder.AccessFor(finder.Endpoint("download", "").Url))
|
||||
assert.Equal(t, NoneAccess, finder.AccessFor(finder.Endpoint("upload", "").Url))
|
||||
}
|
||||
|
||||
func TestSetAccess(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{}))
|
||||
|
||||
assert.Equal(t, NoneAccess, finder.AccessFor("http://example.com"))
|
||||
finder.SetAccess("http://example.com", NTLMAccess)
|
||||
assert.Equal(t, NTLMAccess, finder.AccessFor("http://example.com"))
|
||||
}
|
||||
|
||||
func TestChangeAccess(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.http://example.com.access": "basic",
|
||||
}))
|
||||
|
||||
assert.Equal(t, BasicAccess, finder.AccessFor("http://example.com"))
|
||||
finder.SetAccess("http://example.com", NTLMAccess)
|
||||
assert.Equal(t, NTLMAccess, finder.AccessFor("http://example.com"))
|
||||
}
|
||||
|
||||
func TestDeleteAccessWithNone(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.http://example.com.access": "basic",
|
||||
}))
|
||||
|
||||
assert.Equal(t, BasicAccess, finder.AccessFor("http://example.com"))
|
||||
finder.SetAccess("http://example.com", NoneAccess)
|
||||
assert.Equal(t, NoneAccess, finder.AccessFor("http://example.com"))
|
||||
}
|
||||
|
||||
func TestDeleteAccessWithEmptyString(t *testing.T) {
|
||||
finder := NewEndpointFinder(TestEnv(map[string]string{
|
||||
"lfs.http://example.com.access": "basic",
|
||||
}))
|
||||
|
||||
assert.Equal(t, BasicAccess, finder.AccessFor("http://example.com"))
|
||||
finder.SetAccess("http://example.com", Access(""))
|
||||
assert.Equal(t, NoneAccess, finder.AccessFor("http://example.com"))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package config
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -13,9 +13,9 @@ func TestNewEndpointFromCloneURLWithConfig(t *testing.T) {
|
||||
"https://foo/bar.git/",
|
||||
}
|
||||
|
||||
cfg := New()
|
||||
finder := NewEndpointFinder(nil)
|
||||
for _, actual := range tests {
|
||||
e := NewEndpointFromCloneURLWithConfig(actual, cfg)
|
||||
e := finder.NewEndpointFromCloneURL(actual)
|
||||
if e.Url != expected {
|
||||
t.Errorf("%s returned bad endpoint url %s", actual, e.Url)
|
||||
}
|
122
lfsapi/errors.go
Normal file
122
lfsapi/errors.go
Normal file
@ -0,0 +1,122 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
)
|
||||
|
||||
type httpError interface {
|
||||
Error() string
|
||||
HTTPResponse() *http.Response
|
||||
}
|
||||
|
||||
func IsHTTP(err error) (*http.Response, bool) {
|
||||
if httpErr, ok := err.(httpError); ok {
|
||||
return httpErr.HTTPResponse(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type ClientError struct {
|
||||
Message string `json:"message"`
|
||||
DocumentationUrl string `json:"documentation_url,omitempty"`
|
||||
RequestId string `json:"request_id,omitempty"`
|
||||
response *http.Response
|
||||
}
|
||||
|
||||
func (e *ClientError) HTTPResponse() *http.Response {
|
||||
return e.response
|
||||
}
|
||||
|
||||
func (e *ClientError) Error() string {
|
||||
msg := e.Message
|
||||
if len(e.DocumentationUrl) > 0 {
|
||||
msg += "\nDocs: " + e.DocumentationUrl
|
||||
}
|
||||
if len(e.RequestId) > 0 {
|
||||
msg += "\nRequest ID: " + e.RequestId
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (c *Client) handleResponse(res *http.Response) error {
|
||||
if res.StatusCode < 400 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cliErr := &ClientError{response: res}
|
||||
err := DecodeJSON(res, cliErr)
|
||||
if IsDecodeTypeError(err) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if len(cliErr.Message) == 0 {
|
||||
err = defaultError(res)
|
||||
} else {
|
||||
err = errors.Wrap(cliErr, "http")
|
||||
}
|
||||
}
|
||||
|
||||
if res.StatusCode == 401 {
|
||||
return errors.NewAuthError(err)
|
||||
}
|
||||
|
||||
if res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 507 && res.StatusCode != 509 {
|
||||
return errors.NewFatalError(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type statusCodeError struct {
|
||||
response *http.Response
|
||||
}
|
||||
|
||||
func NewStatusCodeError(res *http.Response) error {
|
||||
return &statusCodeError{response: res}
|
||||
}
|
||||
|
||||
func (e *statusCodeError) Error() string {
|
||||
req := e.response.Request
|
||||
return fmt.Sprintf("Invalid HTTP status for %s %s: %d",
|
||||
req.Method,
|
||||
strings.SplitN(req.URL.String(), "?", 2)[0],
|
||||
e.response.StatusCode,
|
||||
)
|
||||
}
|
||||
|
||||
func (e *statusCodeError) HTTPResponse() *http.Response {
|
||||
return e.response
|
||||
}
|
||||
|
||||
var (
|
||||
defaultErrors = map[int]string{
|
||||
400: "Client error: %s",
|
||||
401: "Authorization error: %s\nCheck that you have proper access to the repository",
|
||||
403: "Authorization error: %s\nCheck that you have proper access to the repository",
|
||||
404: "Repository or object not found: %s\nCheck that it exists and that you have proper access to it",
|
||||
429: "Rate limit exceeded: %s",
|
||||
500: "Server error: %s",
|
||||
501: "Not Implemented: %s",
|
||||
507: "Insufficient server storage: %s",
|
||||
509: "Bandwidth limit exceeded: %s",
|
||||
}
|
||||
)
|
||||
|
||||
func defaultError(res *http.Response) error {
|
||||
var msgFmt string
|
||||
|
||||
if f, ok := defaultErrors[res.StatusCode]; ok {
|
||||
msgFmt = f
|
||||
} else if res.StatusCode < 500 {
|
||||
msgFmt = defaultErrors[400] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
|
||||
} else {
|
||||
msgFmt = defaultErrors[500] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return errors.Errorf(msgFmt, res.Request.URL)
|
||||
}
|
186
lfsapi/lfsapi.go
Normal file
186
lfsapi/lfsapi.go
Normal file
@ -0,0 +1,186 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
lfsMediaTypeRE = regexp.MustCompile(`\Aapplication/vnd\.git\-lfs\+json(;|\z)`)
|
||||
jsonMediaTypeRE = regexp.MustCompile(`\Aapplication/json(;|\z)`)
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Endpoints EndpointFinder
|
||||
Credentials CredentialHelper
|
||||
Netrc NetrcFinder
|
||||
|
||||
DialTimeout int
|
||||
KeepaliveTimeout int
|
||||
TLSTimeout int
|
||||
ConcurrentTransfers int
|
||||
HTTPSProxy string
|
||||
HTTPProxy string
|
||||
NoProxy string
|
||||
SkipSSLVerify bool
|
||||
|
||||
Verbose bool
|
||||
DebuggingVerbose bool
|
||||
LoggingStats bool
|
||||
VerboseOut io.Writer
|
||||
|
||||
hostClients map[string]*http.Client
|
||||
clientMu sync.Mutex
|
||||
|
||||
ntlmSessions map[string]ntlm.ClientSession
|
||||
ntlmMu sync.Mutex
|
||||
|
||||
transferBuckets map[string][]*http.Response
|
||||
transferBucketMu sync.Mutex
|
||||
transfers map[*http.Response]*httpTransfer
|
||||
transferMu sync.Mutex
|
||||
|
||||
// only used for per-host ssl certs
|
||||
gitEnv Env
|
||||
osEnv Env
|
||||
}
|
||||
|
||||
func NewClient(osEnv Env, gitEnv Env) (*Client, error) {
|
||||
if osEnv == nil {
|
||||
osEnv = make(TestEnv)
|
||||
}
|
||||
|
||||
if gitEnv == nil {
|
||||
gitEnv = make(TestEnv)
|
||||
}
|
||||
|
||||
netrc, err := ParseNetrc(osEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpsProxy, httpProxy, noProxy := getProxyServers(osEnv, gitEnv)
|
||||
|
||||
c := &Client{
|
||||
Endpoints: NewEndpointFinder(gitEnv),
|
||||
Credentials: &commandCredentialHelper{
|
||||
SkipPrompt: !osEnv.Bool("GIT_TERMINAL_PROMPT", true),
|
||||
},
|
||||
Netrc: netrc,
|
||||
DialTimeout: gitEnv.Int("lfs.dialtimeout", 0),
|
||||
KeepaliveTimeout: gitEnv.Int("lfs.keepalive", 0),
|
||||
TLSTimeout: gitEnv.Int("lfs.tlstimeout", 0),
|
||||
ConcurrentTransfers: gitEnv.Int("lfs.concurrenttransfers", 0),
|
||||
SkipSSLVerify: !gitEnv.Bool("http.sslverify", true) || osEnv.Bool("GIT_SSL_NO_VERIFY", false),
|
||||
Verbose: osEnv.Bool("GIT_CURL_VERBOSE", false),
|
||||
DebuggingVerbose: osEnv.Bool("LFS_DEBUG_HTTP", false),
|
||||
LoggingStats: osEnv.Bool("GIT_LOG_STATS", false),
|
||||
HTTPSProxy: httpsProxy,
|
||||
HTTPProxy: httpProxy,
|
||||
NoProxy: noProxy,
|
||||
gitEnv: gitEnv,
|
||||
osEnv: osEnv,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) GitEnv() Env {
|
||||
return c.gitEnv
|
||||
}
|
||||
|
||||
func (c *Client) OSEnv() Env {
|
||||
return c.osEnv
|
||||
}
|
||||
|
||||
func IsDecodeTypeError(err error) bool {
|
||||
_, ok := err.(*decodeTypeError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type decodeTypeError struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (e *decodeTypeError) TypeError() {}
|
||||
|
||||
func (e *decodeTypeError) Error() string {
|
||||
return fmt.Sprintf("Expected json type, got: %q", e.Type)
|
||||
}
|
||||
|
||||
func DecodeJSON(res *http.Response, obj interface{}) error {
|
||||
ctype := res.Header.Get("Content-Type")
|
||||
if !(lfsMediaTypeRE.MatchString(ctype) || jsonMediaTypeRE.MatchString(ctype)) {
|
||||
return &decodeTypeError{Type: ctype}
|
||||
}
|
||||
|
||||
err := json.NewDecoder(res.Body).Decode(obj)
|
||||
res.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to parse HTTP response for %s %s", res.Request.Method, res.Request.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Env is an interface for the config.Environment methods that this package
|
||||
// relies on.
|
||||
type Env interface {
|
||||
Get(string) (string, bool)
|
||||
Int(string, int) int
|
||||
Bool(string, bool) bool
|
||||
All() map[string]string
|
||||
}
|
||||
|
||||
// TestEnv is a basic config.Environment implementation. Only used in tests, or
|
||||
// as a zero value to NewClient().
|
||||
type TestEnv map[string]string
|
||||
|
||||
func (e TestEnv) Get(key string) (string, bool) {
|
||||
v, ok := e[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (e TestEnv) Int(key string, def int) (val int) {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (e TestEnv) Bool(key string, def bool) (val bool) {
|
||||
s, _ := e.Get(key)
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
switch strings.ToLower(s) {
|
||||
case "true", "1", "on", "yes", "t":
|
||||
return true
|
||||
case "false", "0", "off", "no", "f":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (e TestEnv) All() map[string]string {
|
||||
return e
|
||||
}
|
5
lfsapi/lfsapi_nix.go
Normal file
5
lfsapi/lfsapi_nix.go
Normal file
@ -0,0 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package lfsapi
|
||||
|
||||
var netrcBasename = ".netrc"
|
5
lfsapi/lfsapi_windows.go
Normal file
5
lfsapi/lfsapi_windows.go
Normal file
@ -0,0 +1,5 @@
|
||||
// +build windows
|
||||
|
||||
package lfsapi
|
||||
|
||||
var netrcBasename = "_netrc"
|
32
lfsapi/netrc.go
Normal file
32
lfsapi/netrc.go
Normal file
@ -0,0 +1,32 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bgentry/go-netrc/netrc"
|
||||
)
|
||||
|
||||
type NetrcFinder interface {
|
||||
FindMachine(string) *netrc.Machine
|
||||
}
|
||||
|
||||
func ParseNetrc(osEnv Env) (NetrcFinder, error) {
|
||||
home, _ := osEnv.Get("HOME")
|
||||
if len(home) == 0 {
|
||||
return &noFinder{}, nil
|
||||
}
|
||||
|
||||
nrcfilename := filepath.Join(home, netrcBasename)
|
||||
if _, err := os.Stat(nrcfilename); err != nil {
|
||||
return &noFinder{}, nil
|
||||
}
|
||||
|
||||
return netrc.ParseFile(nrcfilename)
|
||||
}
|
||||
|
||||
type noFinder struct{}
|
||||
|
||||
func (f *noFinder) FindMachine(host string) *netrc.Machine {
|
||||
return nil
|
||||
}
|
85
lfsapi/netrc_test.go
Normal file
85
lfsapi/netrc_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bgentry/go-netrc/netrc"
|
||||
)
|
||||
|
||||
func TestNetrcWithHostAndPort(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://netrc-host:123/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatal("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithHost(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://netrc-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatalf("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithBadHost(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://other-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatalf("unexpected netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeNetrc struct{}
|
||||
|
||||
func (n *fakeNetrc) FindMachine(host string) *netrc.Machine {
|
||||
if strings.Contains(host, "netrc") {
|
||||
return &netrc.Machine{Login: "abc", Password: "def"}
|
||||
}
|
||||
return nil
|
||||
}
|
166
lfsapi/ntlm.go
Normal file
166
lfsapi/ntlm.go
Normal file
@ -0,0 +1,166 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
)
|
||||
|
||||
func (c *Client) doWithNTLM(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL) (*http.Response, error) {
|
||||
res, err := c.Do(req)
|
||||
if err != nil && !errors.IsAuthError(err) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res.StatusCode != 401 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return c.ntlmReAuth(req, credHelper, creds, true)
|
||||
}
|
||||
|
||||
// If the status is 401 then we need to re-authenticate
|
||||
func (c *Client) ntlmReAuth(req *http.Request, credHelper CredentialHelper, creds Creds, retry bool) (*http.Response, error) {
|
||||
body, err := rewoundRequestBody(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = body
|
||||
|
||||
chRes, challengeMsg, err := c.ntlmNegotiate(req, ntlmNegotiateMessage)
|
||||
if err != nil {
|
||||
return chRes, err
|
||||
}
|
||||
|
||||
body, err = rewoundRequestBody(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = body
|
||||
|
||||
res, err := c.ntlmChallenge(req, challengeMsg, creds)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 401:
|
||||
credHelper.Reject(creds)
|
||||
if retry {
|
||||
return c.ntlmReAuth(req, credHelper, creds, false)
|
||||
}
|
||||
case 403:
|
||||
credHelper.Reject(creds)
|
||||
default:
|
||||
if res.StatusCode < 300 && res.StatusCode > 199 {
|
||||
credHelper.Approve(creds)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) ntlmNegotiate(req *http.Request, message string) (*http.Response, []byte, error) {
|
||||
req.Header.Add("Authorization", message)
|
||||
res, err := c.Do(req)
|
||||
if err != nil && !errors.IsAuthError(err) {
|
||||
return res, nil, err
|
||||
}
|
||||
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
|
||||
by, err := parseChallengeResponse(res)
|
||||
return res, by, err
|
||||
}
|
||||
|
||||
func (c *Client) ntlmChallenge(req *http.Request, challengeBytes []byte, creds Creds) (*http.Response, error) {
|
||||
challenge, err := ntlm.ParseChallengeMessage(challengeBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := c.ntlmClientSession(creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.ProcessChallengeMessage(challenge)
|
||||
authenticate, err := session.GenerateAuthenticateMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authMsg := base64.StdEncoding.EncodeToString(authenticate.Bytes())
|
||||
req.Header.Set("Authorization", "NTLM "+authMsg)
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) ntlmClientSession(creds Creds) (ntlm.ClientSession, error) {
|
||||
c.ntlmMu.Lock()
|
||||
defer c.ntlmMu.Unlock()
|
||||
|
||||
splits := strings.Split(creds["username"], "\\")
|
||||
if len(splits) != 2 {
|
||||
return nil, fmt.Errorf("Your user name must be of the form DOMAIN\\user. It is currently %s", creds["username"])
|
||||
}
|
||||
|
||||
domain := strings.ToUpper(splits[0])
|
||||
username := splits[1]
|
||||
|
||||
if c.ntlmSessions == nil {
|
||||
c.ntlmSessions = make(map[string]ntlm.ClientSession)
|
||||
}
|
||||
|
||||
if ses, ok := c.ntlmSessions[domain]; ok {
|
||||
return ses, nil
|
||||
}
|
||||
|
||||
session, err := ntlm.CreateClientSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.SetUserInfo(username, creds["password"], strings.ToUpper(splits[0]))
|
||||
c.ntlmSessions[domain] = session
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func parseChallengeResponse(res *http.Response) ([]byte, error) {
|
||||
header := res.Header.Get("Www-Authenticate")
|
||||
if len(header) < 6 {
|
||||
return nil, fmt.Errorf("Invalid NTLM challenge response: %q", header)
|
||||
}
|
||||
|
||||
//parse out the "NTLM " at the beginning of the response
|
||||
challenge := header[5:]
|
||||
val, err := base64.StdEncoding.DecodeString(challenge)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(val), nil
|
||||
}
|
||||
|
||||
func rewoundRequestBody(req *http.Request) (io.ReadCloser, error) {
|
||||
if req.Body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
body, ok := req.Body.(ReadSeekCloser)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Request body must implement io.ReadCloser and io.Seeker. Got: %T", body)
|
||||
}
|
||||
|
||||
_, err := body.Seek(0, io.SeekStart)
|
||||
return body, err
|
||||
}
|
||||
|
||||
const ntlmNegotiateMessage = "NTLM TlRMTVNTUAABAAAAB7IIogwADAAzAAAACwALACgAAAAKAAAoAAAAD1dJTExISS1NQUlOTk9SVEhBTUVSSUNB"
|
170
lfsapi/ntlm_test.go
Normal file
170
lfsapi/ntlm_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/ThomsonReutersEikon/go-ntlm/ntlm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNTLMAuth(t *testing.T) {
|
||||
session, err := ntlm.CreateServerSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
|
||||
require.Nil(t, err)
|
||||
session.SetUserInfo("ntlmuser", "ntlmpass", "NTLMDOMAIN")
|
||||
|
||||
var called uint32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
reqIndex := atomic.LoadUint32(&called)
|
||||
atomic.AddUint32(&called, 1)
|
||||
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
t.Logf("REQUEST %d: %s %s", reqIndex, req.Method, req.URL)
|
||||
t.Logf("AUTH: %q", authHeader)
|
||||
|
||||
// assert full body is sent each time
|
||||
by, err := ioutil.ReadAll(req.Body)
|
||||
req.Body.Close()
|
||||
if assert.Nil(t, err) {
|
||||
assert.Equal(t, "ntlm", string(by))
|
||||
}
|
||||
|
||||
switch authHeader {
|
||||
case "":
|
||||
w.Header().Set("Www-Authenticate", "ntlm")
|
||||
w.WriteHeader(401)
|
||||
case ntlmNegotiateMessage:
|
||||
assert.True(t, strings.HasPrefix(req.Header.Get("Authorization"), "NTLM "))
|
||||
ch, err := session.GenerateChallengeMessage()
|
||||
if !assert.Nil(t, err) {
|
||||
t.Logf("challenge gen error: %+v", err)
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
chMsg := base64.StdEncoding.EncodeToString(ch.Bytes())
|
||||
w.Header().Set("Www-Authenticate", "ntlm "+chMsg)
|
||||
w.WriteHeader(401)
|
||||
default: // should be an auth msg
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
assert.True(t, strings.HasPrefix(strings.ToUpper(authHeader), "NTLM "))
|
||||
auth := authHeader[5:] // strip "ntlm " prefix
|
||||
val, err := base64.StdEncoding.DecodeString(auth)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Logf("auth base64 error: %+v", err)
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ntlm.ParseAuthenticateMessage(val, 2)
|
||||
if !assert.Nil(t, err) {
|
||||
t.Logf("auth parse error: %+v", err)
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", srv.URL+"/ntlm", NewByteBody([]byte("ntlm")))
|
||||
require.Nil(t, err)
|
||||
|
||||
credHelper := newMockCredentialHelper()
|
||||
cli, err := NewClient(nil, TestEnv(map[string]string{
|
||||
"lfs.url": srv.URL + "/ntlm",
|
||||
"lfs." + srv.URL + "/ntlm.access": "ntlm",
|
||||
}))
|
||||
cli.Credentials = credHelper
|
||||
require.Nil(t, err)
|
||||
|
||||
// ntlm support pulls domain and login info from git credentials
|
||||
srvURL, err := url.Parse(srv.URL)
|
||||
require.Nil(t, err)
|
||||
creds := Creds{
|
||||
"protocol": srvURL.Scheme,
|
||||
"host": srvURL.Host,
|
||||
"path": "ntlm",
|
||||
"username": "ntlmdomain\\ntlmuser",
|
||||
"password": "ntlmpass",
|
||||
}
|
||||
credHelper.Approve(creds)
|
||||
|
||||
res, err := cli.DoWithAuth("remote", req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, res.StatusCode)
|
||||
assert.True(t, credHelper.IsApproved(creds))
|
||||
}
|
||||
|
||||
func TestNtlmClientSession(t *testing.T) {
|
||||
cli, err := NewClient(nil, nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
creds := Creds{"username": "MOOSEDOMAIN\\canadian", "password": "MooseAntlersYeah"}
|
||||
session1, err := cli.ntlmClientSession(creds)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, session1)
|
||||
|
||||
// The second call should ignore creds and give the session we just created.
|
||||
badCreds := Creds{"username": "MOOSEDOMAIN\\badusername", "password": "MooseAntlersYeah"}
|
||||
session2, err := cli.ntlmClientSession(badCreds)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, session2)
|
||||
assert.EqualValues(t, session1, session2)
|
||||
}
|
||||
|
||||
func TestNtlmClientSessionBadCreds(t *testing.T) {
|
||||
cli, err := NewClient(nil, nil)
|
||||
require.Nil(t, err)
|
||||
creds := Creds{"username": "badusername", "password": "MooseAntlersYeah"}
|
||||
_, err = cli.ntlmClientSession(creds)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseValid(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", "NTLM "+base64.StdEncoding.EncodeToString([]byte("I am a moose")))
|
||||
bytes, err := parseChallengeResponse(&res)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, strings.HasPrefix(string(bytes), "NTLM"))
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseInvalidLength(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", "NTL")
|
||||
ret, err := parseChallengeResponse(&res)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, ret)
|
||||
}
|
||||
|
||||
func TestNtlmHeaderParseInvalid(t *testing.T) {
|
||||
res := http.Response{}
|
||||
res.Header = make(map[string][]string)
|
||||
res.Header.Add("Www-Authenticate", base64.StdEncoding.EncodeToString([]byte("NTLM I am a moose")))
|
||||
ret, err := parseChallengeResponse(&res)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, ret)
|
||||
}
|
||||
|
||||
func assertRequestsEqual(t *testing.T, req1 *http.Request, req2 *http.Request, req1Body []byte) {
|
||||
assert.Equal(t, req1.Method, req2.Method)
|
||||
|
||||
for k, v := range req1.Header {
|
||||
assert.Equal(t, v, req2.Header[k])
|
||||
}
|
||||
|
||||
if req1.Body == nil {
|
||||
assert.Nil(t, req2.Body)
|
||||
} else {
|
||||
bytes2, _ := ioutil.ReadAll(req2.Body)
|
||||
assert.Equal(t, req1Body, bytes2)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package httputil
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@ -6,54 +6,29 @@ import (
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
)
|
||||
|
||||
// Logic is copied, with small changes, from "net/http".ProxyFromEnvironment in the go std lib.
|
||||
func ProxyFromGitConfigOrEnvironment(c *config.Configuration) func(req *http.Request) (*url.URL, error) {
|
||||
var https_proxy string
|
||||
http_proxy, _ := c.Git.Get("http.proxy")
|
||||
if strings.HasPrefix(http_proxy, "https://") {
|
||||
https_proxy = http_proxy
|
||||
}
|
||||
|
||||
if len(https_proxy) == 0 {
|
||||
https_proxy, _ = c.Os.Get("HTTPS_PROXY")
|
||||
}
|
||||
|
||||
if len(https_proxy) == 0 {
|
||||
https_proxy, _ = c.Os.Get("https_proxy")
|
||||
}
|
||||
|
||||
if len(http_proxy) == 0 {
|
||||
http_proxy, _ = c.Os.Get("HTTP_PROXY")
|
||||
}
|
||||
|
||||
if len(http_proxy) == 0 {
|
||||
http_proxy, _ = c.Os.Get("http_proxy")
|
||||
}
|
||||
|
||||
no_proxy, _ := c.Os.Get("NO_PROXY")
|
||||
if len(no_proxy) == 0 {
|
||||
no_proxy, _ = c.Os.Get("no_proxy")
|
||||
}
|
||||
func proxyFromClient(c *Client) func(req *http.Request) (*url.URL, error) {
|
||||
httpProxy := c.HTTPProxy
|
||||
httpsProxy := c.HTTPSProxy
|
||||
noProxy := c.NoProxy
|
||||
|
||||
return func(req *http.Request) (*url.URL, error) {
|
||||
var proxy string
|
||||
if req.URL.Scheme == "https" {
|
||||
proxy = https_proxy
|
||||
proxy = httpsProxy
|
||||
}
|
||||
|
||||
if len(proxy) == 0 {
|
||||
proxy = http_proxy
|
||||
proxy = httpProxy
|
||||
}
|
||||
|
||||
if len(proxy) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !useProxy(no_proxy, canonicalAddr(req.URL)) {
|
||||
if !useProxy(noProxy, canonicalAddr(req.URL)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -62,8 +37,8 @@ func ProxyFromGitConfigOrEnvironment(c *config.Configuration) func(req *http.Req
|
||||
// proxy was bogus. Try prepending "http://" to it and
|
||||
// see if that parses correctly. If not, we fall
|
||||
// through and complain about the original one.
|
||||
if proxyURL, err := url.Parse("http://" + proxy); err == nil {
|
||||
return proxyURL, nil
|
||||
if httpProxyURL, httpErr := url.Parse("http://" + proxy); httpErr == nil {
|
||||
return httpProxyURL, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@ -73,6 +48,37 @@ func ProxyFromGitConfigOrEnvironment(c *config.Configuration) func(req *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
func getProxyServers(osEnv Env, gitEnv Env) (string, string, string) {
|
||||
var httpsProxy string
|
||||
httpProxy, _ := gitEnv.Get("http.proxy")
|
||||
if strings.HasPrefix(httpProxy, "https://") {
|
||||
httpsProxy = httpProxy
|
||||
}
|
||||
|
||||
if len(httpsProxy) == 0 {
|
||||
httpsProxy, _ = osEnv.Get("HTTPS_PROXY")
|
||||
}
|
||||
|
||||
if len(httpsProxy) == 0 {
|
||||
httpsProxy, _ = osEnv.Get("https_proxy")
|
||||
}
|
||||
|
||||
if len(httpProxy) == 0 {
|
||||
httpProxy, _ = osEnv.Get("HTTP_PROXY")
|
||||
}
|
||||
|
||||
if len(httpProxy) == 0 {
|
||||
httpProxy, _ = osEnv.Get("http_proxy")
|
||||
}
|
||||
|
||||
noProxy, _ := osEnv.Get("NO_PROXY")
|
||||
if len(noProxy) == 0 {
|
||||
noProxy, _ = osEnv.Get("no_proxy")
|
||||
}
|
||||
|
||||
return httpsProxy, httpProxy, noProxy
|
||||
}
|
||||
|
||||
// canonicalAddr returns url.Host but always with a ":port" suffix
|
||||
// Copied from "net/http".ProxyFromEnvironment in the go std lib.
|
||||
func canonicalAddr(url *url.URL) string {
|
||||
@ -84,16 +90,16 @@ func canonicalAddr(url *url.URL) string {
|
||||
}
|
||||
|
||||
// useProxy reports whether requests to addr should use a proxy,
|
||||
// according to the NO_PROXY or no_proxy environment variable.
|
||||
// according to the noProxy or noProxy environment variable.
|
||||
// addr is always a canonicalAddr with a host and port.
|
||||
// Copied from "net/http".ProxyFromEnvironment in the go std lib
|
||||
// and adapted to allow proxy usage even for localhost.
|
||||
func useProxy(no_proxy, addr string) bool {
|
||||
func useProxy(noProxy, addr string) bool {
|
||||
if len(addr) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if no_proxy == "*" {
|
||||
if noProxy == "*" {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -102,7 +108,7 @@ func useProxy(no_proxy, addr string) bool {
|
||||
addr = addr[:strings.LastIndex(addr, ":")]
|
||||
}
|
||||
|
||||
for _, p := range strings.Split(no_proxy, ",") {
|
||||
for _, p := range strings.Split(noProxy, ",") {
|
||||
p = strings.ToLower(strings.TrimSpace(p))
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
@ -114,11 +120,11 @@ func useProxy(no_proxy, addr string) bool {
|
||||
return false
|
||||
}
|
||||
if p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:]) {
|
||||
// no_proxy ".foo.com" matches "bar.foo.com" or "foo.com"
|
||||
// noProxy ".foo.com" matches "bar.foo.com" or "foo.com"
|
||||
return false
|
||||
}
|
||||
if p[0] != '.' && strings.HasSuffix(addr, p) && addr[len(addr)-len(p)-1] == '.' {
|
||||
// no_proxy "foo.com" matches "bar.foo.com"
|
||||
// noProxy "foo.com" matches "bar.foo.com"
|
||||
return false
|
||||
}
|
||||
}
|
82
lfsapi/proxy_test.go
Normal file
82
lfsapi/proxy_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProxyFromGitConfig(t *testing.T) {
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
}), TestEnv(map[string]string{
|
||||
"http.proxy": "https://proxy-from-git-config:8080",
|
||||
}))
|
||||
require.Nil(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
proxyURL, err := proxyFromClient(c)(req)
|
||||
assert.Equal(t, "proxy-from-git-config:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestHttpProxyFromGitConfig(t *testing.T) {
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
}), TestEnv(map[string]string{
|
||||
"http.proxy": "http://proxy-from-git-config:8080",
|
||||
}))
|
||||
require.Nil(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
proxyURL, err := proxyFromClient(c)(req)
|
||||
assert.Equal(t, "proxy-from-env:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyFromEnvironment(t *testing.T) {
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"HTTPS_PROXY": "https://proxy-from-env:8080",
|
||||
}), nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host.com:123/foo/bar", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
proxyURL, err := proxyFromClient(c)(req)
|
||||
assert.Equal(t, "proxy-from-env:8080", proxyURL.Host)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyIsNil(t *testing.T) {
|
||||
c := &Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://some-host.com:123/foo/bar", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
proxyURL, err := proxyFromClient(c)(req)
|
||||
assert.Nil(t, proxyURL)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestProxyNoProxy(t *testing.T) {
|
||||
c, err := NewClient(TestEnv(map[string]string{
|
||||
"NO_PROXY": "some-host",
|
||||
}), TestEnv(map[string]string{
|
||||
"http.proxy": "https://proxy-from-git-config:8080",
|
||||
}))
|
||||
require.Nil(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", "https://some-host:8080", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
proxyURL, err := proxyFromClient(c)(req)
|
||||
assert.Nil(t, proxyURL)
|
||||
assert.Nil(t, err)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user