Add support for managed HTTP/S transports (#810) #811
|
@ -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
18
git.go
|
@ -139,22 +139,28 @@ 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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}" \
|
||||||
|
|
62
transport.go
62
transport.go
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue