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 (
|
||||
"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()
|
||||
|
|
18
git.go
18
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.
|
||||
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -64,6 +64,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}" \
|
||||
|
|
62
transport.go
62
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 {
|
||||
|
|
Loading…
Reference in New Issue