summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlhchavez <[email protected]>2021-09-05 16:39:07 -0700
committerGitHub <[email protected]>2021-09-05 16:39:07 -0700
commitb983e1daebf528443e2a3954cd595fa3664ec93f (patch)
tree51ed7f7c359c9548dad7afa1a445749380d59f66
parentf1fa96c7b7f548389c7560d3a1a0bce83be56c9f (diff)
Add support for managed HTTP/S transports (#810)
This change uses the newly-exposed Transport interface to use Go's implementation of http.Client instead of httpclient via libgit2.
-rw-r--r--credentials.go14
-rw-r--r--git.go20
-rw-r--r--git_test.go4
-rw-r--r--http.go241
-rw-r--r--remote.go7
-rw-r--r--remote_test.go26
-rwxr-xr-xscript/build-libgit2.sh1
-rw-r--r--transport.go62
8 files changed, 368 insertions, 7 deletions
diff --git a/credentials.go b/credentials.go
index 843c6b2..273de2f 100644
--- a/credentials.go
+++ b/credentials.go
@@ -11,6 +11,7 @@ void _go_git_populate_credential_ssh_custom(git_credential_ssh_custom *cred);
import "C"
import (
"crypto/rand"
+ "errors"
"fmt"
"runtime"
"strings"
@@ -106,6 +107,19 @@ func (o *Credential) Free() {
o.ptr = nil
}
+// GetUserpassPlaintext returns the plaintext username/password combination stored in the Cred.
+func (o *Credential) GetUserpassPlaintext() (username, password string, err error) {
+ if o.Type() != CredentialTypeUserpassPlaintext {
+ err = errors.New("credential is not userpass plaintext")
+ return
+ }
+
+ plaintextCredPtr := (*C.git_cred_userpass_plaintext)(unsafe.Pointer(o.ptr))
+ username = C.GoString(plaintextCredPtr.username)
+ password = C.GoString(plaintextCredPtr.password)
+ return
+}
+
func NewCredentialUsername(username string) (*Credential, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
diff --git a/git.go b/git.go
index adf07ae..9ad1ffc 100644
--- a/git.go
+++ b/git.go
@@ -139,22 +139,28 @@ func initLibGit2() {
remotePointers = newRemotePointerList()
C.git_libgit2_init()
+ features := Features()
// Due to the multithreaded nature of Go and its interaction with
// calling C functions, we cannot work with a library that was not built
// with multi-threading support. The most likely outcome is a segfault
// or panic at an incomprehensible time, so let's make it easy by
// panicking right here.
- if Features()&FeatureThreads == 0 {
+ if features&FeatureThreads == 0 {
panic("libgit2 was not built with threading support")
}
- // This is not something we should be doing, as we may be
- // stomping all over someone else's setup. The user should do
- // this themselves or use some binding/wrapper which does it
- // in such a way that they can be sure they're the only ones
- // setting it up.
- C.git_openssl_set_locking()
+ if features&FeatureHTTPS == 0 {
+ if err := registerManagedHTTP(); err != nil {
+ panic(err)
+ }
+ } else {
+ // This is not something we should be doing, as we may be stomping all over
+ // someone else's setup. The user should do this themselves or use some
+ // binding/wrapper which does it in such a way that they can be sure
+ // they're the only ones setting it up.
+ C.git_openssl_set_locking()
+ }
}
// Shutdown frees all the resources acquired by libgit2. Make sure no
diff --git a/git_test.go b/git_test.go
index 101350f..592e06f 100644
--- a/git_test.go
+++ b/git_test.go
@@ -11,6 +11,10 @@ import (
)
func TestMain(m *testing.M) {
+ if err := registerManagedHTTP(); err != nil {
+ panic(err)
+ }
+
ret := m.Run()
if err := unregisterManagedTransports(); err != nil {
diff --git a/http.go b/http.go
new file mode 100644
index 0000000..0777c56
--- /dev/null
+++ b/http.go
@@ -0,0 +1,241 @@
+package git
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sync"
+)
+
+// RegisterManagedHTTPTransport registers a Go-native implementation of an
+// HTTP/S transport that doesn't rely on any system libraries (e.g.
+// libopenssl/libmbedtls).
+//
+// If Shutdown or ReInit are called, make sure that the smart transports are
+// freed before it.
+func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) {
+ return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory)
+}
+
+func registerManagedHTTP() error {
+ globalRegisteredSmartTransports.Lock()
+ defer globalRegisteredSmartTransports.Unlock()
+
+ for _, protocol := range []string{"http", "https"} {
+ if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok {
+ continue
+ }
+ managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true)
+ if err != nil {
+ return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
+ }
+ globalRegisteredSmartTransports.transports[protocol] = managed
+ }
+ return nil
+}
+
+func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) {
+ var proxyFn func(*http.Request) (*url.URL, error)
+ proxyOpts, err := transport.SmartProxyOptions()
+ if err != nil {
+ return nil, err
+ }
+ switch proxyOpts.Type {
+ case ProxyTypeNone:
+ proxyFn = nil
+ case ProxyTypeAuto:
+ proxyFn = http.ProxyFromEnvironment
+ case ProxyTypeSpecified:
+ parsedUrl, err := url.Parse(proxyOpts.Url)
+ if err != nil {
+ return nil, err
+ }
+
+ proxyFn = http.ProxyURL(parsedUrl)
+ }
+
+ return &httpSmartSubtransport{
+ transport: transport,
+ client: &http.Client{
+ Transport: &http.Transport{
+ Proxy: proxyFn,
+ },
+ },
+ }, nil
+}
+
+type httpSmartSubtransport struct {
+ transport *Transport
+ client *http.Client
+}
+
+func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (SmartSubtransportStream, error) {
+ var req *http.Request
+ var err error
+ switch action {
+ case SmartServiceActionUploadpackLs:
+ req, err = http.NewRequest("GET", url+"/info/refs?service=git-upload-pack", nil)
+
+ case SmartServiceActionUploadpack:
+ req, err = http.NewRequest("POST", url+"/git-upload-pack", nil)
+ if err != nil {
+ break
+ }
+ req.Header.Set("Content-Type", "application/x-git-upload-pack-request")
+
+ case SmartServiceActionReceivepackLs:
+ req, err = http.NewRequest("GET", url+"/info/refs?service=git-receive-pack", nil)
+
+ case SmartServiceActionReceivepack:
+ req, err = http.NewRequest("POST", url+"/info/refs?service=git-upload-pack", nil)
+ if err != nil {
+ break
+ }
+ req.Header.Set("Content-Type", "application/x-git-receive-pack-request")
+
+ default:
+ err = errors.New("unknown action")
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("User-Agent", "git/2.0 (git2go)")
+
+ stream := newManagedHttpStream(t, req)
+ if req.Method == "POST" {
+ stream.recvReply.Add(1)
+ stream.sendRequestBackground()
+ }
+
+ return stream, nil
+}
+
+func (t *httpSmartSubtransport) Close() error {
+ return nil
+}
+
+func (t *httpSmartSubtransport) Free() {
+ t.client = nil
+}
+
+type httpSmartSubtransportStream struct {
+ owner *httpSmartSubtransport
+ req *http.Request
+ resp *http.Response
+ reader *io.PipeReader
+ writer *io.PipeWriter
+ sentRequest bool
+ recvReply sync.WaitGroup
+ httpError error
+}
+
+func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request) *httpSmartSubtransportStream {
+ r, w := io.Pipe()
+ return &httpSmartSubtransportStream{
+ owner: owner,
+ req: req,
+ reader: r,
+ writer: w,
+ }
+}
+
+func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) {
+ if !self.sentRequest {
+ self.recvReply.Add(1)
+ if err := self.sendRequest(); err != nil {
+ return 0, err
+ }
+ }
+
+ if err := self.writer.Close(); err != nil {
+ return 0, err
+ }
+
+ self.recvReply.Wait()
+
+ if self.httpError != nil {
+ return 0, self.httpError
+ }
+
+ return self.resp.Body.Read(buf)
+}
+
+func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) {
+ if self.httpError != nil {
+ return 0, self.httpError
+ }
+ return self.writer.Write(buf)
+}
+
+func (self *httpSmartSubtransportStream) Free() {
+ if self.resp != nil {
+ self.resp.Body.Close()
+ }
+}
+
+func (self *httpSmartSubtransportStream) sendRequestBackground() {
+ go func() {
+ self.httpError = self.sendRequest()
+ }()
+ self.sentRequest = true
+}
+
+func (self *httpSmartSubtransportStream) sendRequest() error {
+ defer self.recvReply.Done()
+ self.resp = nil
+
+ var resp *http.Response
+ var err error
+ var userName string
+ var password string
+ for {
+ req := &http.Request{
+ Method: self.req.Method,
+ URL: self.req.URL,
+ Header: self.req.Header,
+ }
+ if req.Method == "POST" {
+ req.Body = self.reader
+ req.ContentLength = -1
+ }
+
+ req.SetBasicAuth(userName, password)
+ resp, err = http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode == http.StatusOK {
+ break
+ }
+
+ if resp.StatusCode == http.StatusUnauthorized {
+ resp.Body.Close()
+
+ cred, err := self.owner.transport.SmartCredentials("", CredentialTypeUserpassPlaintext)
+ if err != nil {
+ return err
+ }
+ defer cred.Free()
+
+ userName, password, err = cred.GetUserpassPlaintext()
+ if err != nil {
+ return err
+ }
+
+ continue
+ }
+
+ // Any other error we treat as a hard error and punt back to the caller
+ resp.Body.Close()
+ return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
+ }
+
+ self.sentRequest = true
+ self.resp = resp
+ return nil
+}
diff --git a/remote.go b/remote.go
index fb70f55..3a435d1 100644
--- a/remote.go
+++ b/remote.go
@@ -168,6 +168,13 @@ type ProxyOptions struct {
Url string
}
+func proxyOptionsFromC(copts *C.git_proxy_options) *ProxyOptions {
+ return &ProxyOptions{
+ Type: ProxyType(copts._type),
+ Url: C.GoString(copts.url),
+ }
+}
+
type Remote struct {
doNotCompare
ptr *C.git_remote
diff --git a/remote_test.go b/remote_test.go
index 7e37274..9660a3f 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/rand"
"crypto/rsa"
+ "errors"
"fmt"
"io"
"net"
@@ -232,6 +233,31 @@ func TestRemotePrune(t *testing.T) {
}
}
+func TestRemoteCredentialsCalled(t *testing.T) {
+ t.Parallel()
+
+ repo := createTestRepo(t)
+ defer cleanupTestRepo(t, repo)
+
+ remote, err := repo.Remotes.CreateAnonymous("https://github.com/libgit2/non-existent")
+ checkFatal(t, err)
+ defer remote.Free()
+
+ errNonExistent := errors.New("non-existent repository")
+ fetchOpts := FetchOptions{
+ RemoteCallbacks: RemoteCallbacks{
+ CredentialsCallback: func(url, username string, allowedTypes CredentialType) (*Credential, error) {
+ return nil, errNonExistent
+ },
+ },
+ }
+
+ err = remote.Fetch(nil, &fetchOpts, "fetch")
+ if err != errNonExistent {
+ t.Fatalf("remote.Fetch() = %v, want %v", err, errNonExistent)
+ }
+}
+
func newChannelPipe(t *testing.T, w io.Writer, wg *sync.WaitGroup) (*os.File, error) {
pr, pw, err := os.Pipe()
if err != nil {
diff --git a/script/build-libgit2.sh b/script/build-libgit2.sh
index 271a823..90a225c 100755
--- a/script/build-libgit2.sh
+++ b/script/build-libgit2.sh
@@ -67,6 +67,7 @@ cmake -DTHREADSAFE=ON \
-DBUILD_CLAR=OFF \
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
-DREGEX_BACKEND=builtin \
+ -DUSE_HTTPS=OFF \
-DCMAKE_C_FLAGS=-fPIC \
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \
diff --git a/transport.go b/transport.go
index 94c9ffa..cf43acc 100644
--- a/transport.go
+++ b/transport.go
@@ -1,6 +1,8 @@
package git
/*
+#include <string.h>
+
#include <git2.h>
#include <git2/sys/transport.h>
@@ -83,6 +85,19 @@ type Transport struct {
ptr *C.git_transport
}
+// SmartProxyOptions gets a copy of the proxy options for this transport.
+func (t *Transport) SmartProxyOptions() (*ProxyOptions, error) {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ var cpopts C.git_proxy_options
+ if ret := C.git_transport_smart_proxy_options(&cpopts, t.ptr); ret < 0 {
+ return nil, MakeGitError(ret)
+ }
+
+ return proxyOptionsFromC(&cpopts), nil
+}
+
// SmartCredentials calls the credentials callback for this transport.
func (t *Transport) SmartCredentials(user string, methods CredentialType) (*Credential, error) {
cred := newCredential()
@@ -104,6 +119,53 @@ func (t *Transport) SmartCredentials(user string, methods CredentialType) (*Cred
return cred, nil
}
+// SmartCertificateCheck calls the certificate check for this transport.
+func (t *Transport) SmartCertificateCheck(cert *Certificate, valid bool, hostname string) error {
+ var ccert *C.git_cert
+ switch cert.Kind {
+ case CertificateHostkey:
+ chostkeyCert := C.git_cert_hostkey{
+ parent: C.git_cert{
+ cert_type: C.GIT_CERT_HOSTKEY_LIBSSH2,
+ },
+ _type: C.git_cert_ssh_t(cert.Kind),
+ }
+ C.memcpy(unsafe.Pointer(&chostkeyCert.hash_md5[0]), unsafe.Pointer(&cert.Hostkey.HashMD5[0]), C.size_t(len(cert.Hostkey.HashMD5)))
+ C.memcpy(unsafe.Pointer(&chostkeyCert.hash_sha1[0]), unsafe.Pointer(&cert.Hostkey.HashSHA1[0]), C.size_t(len(cert.Hostkey.HashSHA1)))
+ C.memcpy(unsafe.Pointer(&chostkeyCert.hash_sha256[0]), unsafe.Pointer(&cert.Hostkey.HashSHA256[0]), C.size_t(len(cert.Hostkey.HashSHA256)))
+ ccert = (*C.git_cert)(unsafe.Pointer(&chostkeyCert))
+
+ case CertificateX509:
+ cx509Cert := C.git_cert_x509{
+ parent: C.git_cert{
+ cert_type: C.GIT_CERT_X509,
+ },
+ len: C.size_t(len(cert.X509.Raw)),
+ data: C.CBytes(cert.X509.Raw),
+ }
+ defer C.free(cx509Cert.data)
+ ccert = (*C.git_cert)(unsafe.Pointer(&cx509Cert))
+ }
+
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ chostname := C.CString(hostname)
+ defer C.free(unsafe.Pointer(chostname))
+
+ cvalid := C.int(0)
+ if valid {
+ cvalid = C.int(1)
+ }
+
+ ret := C.git_transport_smart_certificate_check(t.ptr, ccert, cvalid, chostname)
+ if ret != 0 {
+ return MakeGitError(ret)
+ }
+
+ return nil
+}
+
// SmartSubtransport is the interface for custom subtransports which carry data
// for the smart transport.
type SmartSubtransport interface {