Add support for managed HTTP/S transports (#810) #811

Merged
github-actions[bot] merged 1 commits from cherry-pick-1204114198-release-1.0 into release-1.0 2021-09-05 18:41:53 -05:00
8 changed files with 368 additions and 7 deletions

View File

@ -11,6 +11,7 @@ void _go_git_populate_credential_ssh_custom(git_credential_ssh_custom *cred);
import "C" import "C"
import ( import (
"crypto/rand" "crypto/rand"
"errors"
"fmt" "fmt"
"runtime" "runtime"
"strings" "strings"
@ -106,6 +107,19 @@ func (o *Credential) Free() {
o.ptr = nil 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) { func NewCredentialUsername(username string) (*Credential, error) {
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()

18
git.go
View File

@ -139,23 +139,29 @@ func initLibGit2() {
remotePointers = newRemotePointerList() remotePointers = newRemotePointerList()
C.git_libgit2_init() C.git_libgit2_init()
features := Features()
// Due to the multithreaded nature of Go and its interaction with // Due to the multithreaded nature of Go and its interaction with
// calling C functions, we cannot work with a library that was not built // calling C functions, we cannot work with a library that was not built
// with multi-threading support. The most likely outcome is a segfault // with multi-threading support. The most likely outcome is a segfault
// or panic at an incomprehensible time, so let's make it easy by // or panic at an incomprehensible time, so let's make it easy by
// panicking right here. // panicking right here.
if Features()&FeatureThreads == 0 { if features&FeatureThreads == 0 {
panic("libgit2 was not built with threading support") panic("libgit2 was not built with threading support")
} }
// This is not something we should be doing, as we may be if features&FeatureHTTPS == 0 {
// stomping all over someone else's setup. The user should do if err := registerManagedHTTP(); err != nil {
// this themselves or use some binding/wrapper which does it panic(err)
// in such a way that they can be sure they're the only ones }
// setting it up. } 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() C.git_openssl_set_locking()
} }
}
// Shutdown frees all the resources acquired by libgit2. Make sure no // Shutdown frees all the resources acquired by libgit2. Make sure no
// references to any git2go objects are live before calling this. // references to any git2go objects are live before calling this.

View File

@ -11,6 +11,10 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if err := registerManagedHTTP(); err != nil {
panic(err)
}
ret := m.Run() ret := m.Run()
if err := unregisterManagedTransports(); err != nil { if err := unregisterManagedTransports(); err != nil {

241
http.go Normal file
View File

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

View File

@ -168,6 +168,13 @@ type ProxyOptions struct {
Url string Url string
} }
func proxyOptionsFromC(copts *C.git_proxy_options) *ProxyOptions {
return &ProxyOptions{
Type: ProxyType(copts._type),
Url: C.GoString(copts.url),
}
}
type Remote struct { type Remote struct {
doNotCompare doNotCompare
ptr *C.git_remote ptr *C.git_remote

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"errors"
"fmt" "fmt"
"io" "io"
"net" "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) { func newChannelPipe(t *testing.T, w io.Writer, wg *sync.WaitGroup) (*os.File, error) {
pr, pw, err := os.Pipe() pr, pw, err := os.Pipe()
if err != nil { if err != nil {

View File

@ -64,6 +64,7 @@ cmake -DTHREADSAFE=ON \
-DBUILD_CLAR=OFF \ -DBUILD_CLAR=OFF \
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \ -DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
-DREGEX_BACKEND=builtin \ -DREGEX_BACKEND=builtin \
-DUSE_HTTPS=OFF \
-DCMAKE_C_FLAGS=-fPIC \ -DCMAKE_C_FLAGS=-fPIC \
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \ -DCMAKE_BUILD_TYPE="RelWithDebInfo" \
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \ -DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \

View File

@ -1,6 +1,8 @@
package git package git
/* /*
#include <string.h>
#include <git2.h> #include <git2.h>
#include <git2/sys/transport.h> #include <git2/sys/transport.h>
@ -83,6 +85,19 @@ type Transport struct {
ptr *C.git_transport 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. // SmartCredentials calls the credentials callback for this transport.
func (t *Transport) SmartCredentials(user string, methods CredentialType) (*Credential, error) { func (t *Transport) SmartCredentials(user string, methods CredentialType) (*Credential, error) {
cred := newCredential() cred := newCredential()
@ -104,6 +119,53 @@ func (t *Transport) SmartCredentials(user string, methods CredentialType) (*Cred
return cred, nil 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 // SmartSubtransport is the interface for custom subtransports which carry data
// for the smart transport. // for the smart transport.
type SmartSubtransport interface { type SmartSubtransport interface {