Skip to content

Commit b983e1d

Browse files
authored
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.
1 parent f1fa96c commit b983e1d

File tree

8 files changed

+368
-7
lines changed

8 files changed

+368
-7
lines changed

credentials.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ void _go_git_populate_credential_ssh_custom(git_credential_ssh_custom *cred);
1111
import "C"
1212
import (
1313
"crypto/rand"
14+
"errors"
1415
"fmt"
1516
"runtime"
1617
"strings"
@@ -106,6 +107,19 @@ func (o *Credential) Free() {
106107
o.ptr = nil
107108
}
108109

110+
// GetUserpassPlaintext returns the plaintext username/password combination stored in the Cred.
111+
func (o *Credential) GetUserpassPlaintext() (username, password string, err error) {
112+
if o.Type() != CredentialTypeUserpassPlaintext {
113+
err = errors.New("credential is not userpass plaintext")
114+
return
115+
}
116+
117+
plaintextCredPtr := (*C.git_cred_userpass_plaintext)(unsafe.Pointer(o.ptr))
118+
username = C.GoString(plaintextCredPtr.username)
119+
password = C.GoString(plaintextCredPtr.password)
120+
return
121+
}
122+
109123
func NewCredentialUsername(username string) (*Credential, error) {
110124
runtime.LockOSThread()
111125
defer runtime.UnlockOSThread()

git.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,28 @@ func initLibGit2() {
139139
remotePointers = newRemotePointerList()
140140

141141
C.git_libgit2_init()
142+
features := Features()
142143

143144
// Due to the multithreaded nature of Go and its interaction with
144145
// calling C functions, we cannot work with a library that was not built
145146
// with multi-threading support. The most likely outcome is a segfault
146147
// or panic at an incomprehensible time, so let's make it easy by
147148
// panicking right here.
148-
if Features()&FeatureThreads == 0 {
149+
if features&FeatureThreads == 0 {
149150
panic("libgit2 was not built with threading support")
150151
}
151152

152-
// This is not something we should be doing, as we may be
153-
// stomping all over someone else's setup. The user should do
154-
// this themselves or use some binding/wrapper which does it
155-
// in such a way that they can be sure they're the only ones
156-
// setting it up.
157-
C.git_openssl_set_locking()
153+
if features&FeatureHTTPS == 0 {
154+
if err := registerManagedHTTP(); err != nil {
155+
panic(err)
156+
}
157+
} else {
158+
// This is not something we should be doing, as we may be stomping all over
159+
// someone else's setup. The user should do this themselves or use some
160+
// binding/wrapper which does it in such a way that they can be sure
161+
// they're the only ones setting it up.
162+
C.git_openssl_set_locking()
163+
}
158164
}
159165

160166
// Shutdown frees all the resources acquired by libgit2. Make sure no

git_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
)
1212

1313
func TestMain(m *testing.M) {
14+
if err := registerManagedHTTP(); err != nil {
15+
panic(err)
16+
}
17+
1418
ret := m.Run()
1519

1620
if err := unregisterManagedTransports(); err != nil {

http.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package git
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"sync"
10+
)
11+
12+
// RegisterManagedHTTPTransport registers a Go-native implementation of an
13+
// HTTP/S transport that doesn't rely on any system libraries (e.g.
14+
// libopenssl/libmbedtls).
15+
//
16+
// If Shutdown or ReInit are called, make sure that the smart transports are
17+
// freed before it.
18+
func RegisterManagedHTTPTransport(protocol string) (*RegisteredSmartTransport, error) {
19+
return NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory)
20+
}
21+
22+
func registerManagedHTTP() error {
23+
globalRegisteredSmartTransports.Lock()
24+
defer globalRegisteredSmartTransports.Unlock()
25+
26+
for _, protocol := range []string{"http", "https"} {
27+
if _, ok := globalRegisteredSmartTransports.transports[protocol]; ok {
28+
continue
29+
}
30+
managed, err := newRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory, true)
31+
if err != nil {
32+
return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
33+
}
34+
globalRegisteredSmartTransports.transports[protocol] = managed
35+
}
36+
return nil
37+
}
38+
39+
func httpSmartSubtransportFactory(remote *Remote, transport *Transport) (SmartSubtransport, error) {
40+
var proxyFn func(*http.Request) (*url.URL, error)
41+
proxyOpts, err := transport.SmartProxyOptions()
42+
if err != nil {
43+
return nil, err
44+
}
45+
switch proxyOpts.Type {
46+
case ProxyTypeNone:
47+
proxyFn = nil
48+
case ProxyTypeAuto:
49+
proxyFn = http.ProxyFromEnvironment
50+
case ProxyTypeSpecified:
51+
parsedUrl, err := url.Parse(proxyOpts.Url)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
proxyFn = http.ProxyURL(parsedUrl)
57+
}
58+
59+
return &httpSmartSubtransport{
60+
transport: transport,
61+
client: &http.Client{
62+
Transport: &http.Transport{
63+
Proxy: proxyFn,
64+
},
65+
},
66+
}, nil
67+
}
68+
69+
type httpSmartSubtransport struct {
70+
transport *Transport
71+
client *http.Client
72+
}
73+
74+
func (t *httpSmartSubtransport) Action(url string, action SmartServiceAction) (SmartSubtransportStream, error) {
75+
var req *http.Request
76+
var err error
77+
switch action {
78+
case SmartServiceActionUploadpackLs:
79+
req, err = http.NewRequest("GET", url+"/info/refs?service=git-upload-pack", nil)
80+
81+
case SmartServiceActionUploadpack:
82+
req, err = http.NewRequest("POST", url+"/git-upload-pack", nil)
83+
if err != nil {
84+
break
85+
}
86+
req.Header.Set("Content-Type", "application/x-git-upload-pack-request")
87+
88+
case SmartServiceActionReceivepackLs:
89+
req, err = http.NewRequest("GET", url+"/info/refs?service=git-receive-pack", nil)
90+
91+
case SmartServiceActionReceivepack:
92+
req, err = http.NewRequest("POST", url+"/info/refs?service=git-upload-pack", nil)
93+
if err != nil {
94+
break
95+
}
96+
req.Header.Set("Content-Type", "application/x-git-receive-pack-request")
97+
98+
default:
99+
err = errors.New("unknown action")
100+
}
101+
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
req.Header.Set("User-Agent", "git/2.0 (git2go)")
107+
108+
stream := newManagedHttpStream(t, req)
109+
if req.Method == "POST" {
110+
stream.recvReply.Add(1)
111+
stream.sendRequestBackground()
112+
}
113+
114+
return stream, nil
115+
}
116+
117+
func (t *httpSmartSubtransport) Close() error {
118+
return nil
119+
}
120+
121+
func (t *httpSmartSubtransport) Free() {
122+
t.client = nil
123+
}
124+
125+
type httpSmartSubtransportStream struct {
126+
owner *httpSmartSubtransport
127+
req *http.Request
128+
resp *http.Response
129+
reader *io.PipeReader
130+
writer *io.PipeWriter
131+
sentRequest bool
132+
recvReply sync.WaitGroup
133+
httpError error
134+
}
135+
136+
func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request) *httpSmartSubtransportStream {
137+
r, w := io.Pipe()
138+
return &httpSmartSubtransportStream{
139+
owner: owner,
140+
req: req,
141+
reader: r,
142+
writer: w,
143+
}
144+
}
145+
146+
func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) {
147+
if !self.sentRequest {
148+
self.recvReply.Add(1)
149+
if err := self.sendRequest(); err != nil {
150+
return 0, err
151+
}
152+
}
153+
154+
if err := self.writer.Close(); err != nil {
155+
return 0, err
156+
}
157+
158+
self.recvReply.Wait()
159+
160+
if self.httpError != nil {
161+
return 0, self.httpError
162+
}
163+
164+
return self.resp.Body.Read(buf)
165+
}
166+
167+
func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) {
168+
if self.httpError != nil {
169+
return 0, self.httpError
170+
}
171+
return self.writer.Write(buf)
172+
}
173+
174+
func (self *httpSmartSubtransportStream) Free() {
175+
if self.resp != nil {
176+
self.resp.Body.Close()
177+
}
178+
}
179+
180+
func (self *httpSmartSubtransportStream) sendRequestBackground() {
181+
go func() {
182+
self.httpError = self.sendRequest()
183+
}()
184+
self.sentRequest = true
185+
}
186+
187+
func (self *httpSmartSubtransportStream) sendRequest() error {
188+
defer self.recvReply.Done()
189+
self.resp = nil
190+
191+
var resp *http.Response
192+
var err error
193+
var userName string
194+
var password string
195+
for {
196+
req := &http.Request{
197+
Method: self.req.Method,
198+
URL: self.req.URL,
199+
Header: self.req.Header,
200+
}
201+
if req.Method == "POST" {
202+
req.Body = self.reader
203+
req.ContentLength = -1
204+
}
205+
206+
req.SetBasicAuth(userName, password)
207+
resp, err = http.DefaultClient.Do(req)
208+
if err != nil {
209+
return err
210+
}
211+
212+
if resp.StatusCode == http.StatusOK {
213+
break
214+
}
215+
216+
if resp.StatusCode == http.StatusUnauthorized {
217+
resp.Body.Close()
218+
219+
cred, err := self.owner.transport.SmartCredentials("", CredentialTypeUserpassPlaintext)
220+
if err != nil {
221+
return err
222+
}
223+
defer cred.Free()
224+
225+
userName, password, err = cred.GetUserpassPlaintext()
226+
if err != nil {
227+
return err
228+
}
229+
230+
continue
231+
}
232+
233+
// Any other error we treat as a hard error and punt back to the caller
234+
resp.Body.Close()
235+
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
236+
}
237+
238+
self.sentRequest = true
239+
self.resp = resp
240+
return nil
241+
}

remote.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ type ProxyOptions struct {
168168
Url string
169169
}
170170

171+
func proxyOptionsFromC(copts *C.git_proxy_options) *ProxyOptions {
172+
return &ProxyOptions{
173+
Type: ProxyType(copts._type),
174+
Url: C.GoString(copts.url),
175+
}
176+
}
177+
171178
type Remote struct {
172179
doNotCompare
173180
ptr *C.git_remote

remote_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/rand"
66
"crypto/rsa"
7+
"errors"
78
"fmt"
89
"io"
910
"net"
@@ -232,6 +233,31 @@ func TestRemotePrune(t *testing.T) {
232233
}
233234
}
234235

236+
func TestRemoteCredentialsCalled(t *testing.T) {
237+
t.Parallel()
238+
239+
repo := createTestRepo(t)
240+
defer cleanupTestRepo(t, repo)
241+
242+
remote, err := repo.Remotes.CreateAnonymous("https://github.com/libgit2/non-existent")
243+
checkFatal(t, err)
244+
defer remote.Free()
245+
246+
errNonExistent := errors.New("non-existent repository")
247+
fetchOpts := FetchOptions{
248+
RemoteCallbacks: RemoteCallbacks{
249+
CredentialsCallback: func(url, username string, allowedTypes CredentialType) (*Credential, error) {
250+
return nil, errNonExistent
251+
},
252+
},
253+
}
254+
255+
err = remote.Fetch(nil, &fetchOpts, "fetch")
256+
if err != errNonExistent {
257+
t.Fatalf("remote.Fetch() = %v, want %v", err, errNonExistent)
258+
}
259+
}
260+
235261
func newChannelPipe(t *testing.T, w io.Writer, wg *sync.WaitGroup) (*os.File, error) {
236262
pr, pw, err := os.Pipe()
237263
if err != nil {

script/build-libgit2.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ cmake -DTHREADSAFE=ON \
6767
-DBUILD_CLAR=OFF \
6868
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
6969
-DREGEX_BACKEND=builtin \
70+
-DUSE_HTTPS=OFF \
7071
-DCMAKE_C_FLAGS=-fPIC \
7172
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
7273
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \

0 commit comments

Comments
 (0)