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 + #include #include @@ -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 {