Skip to content

Commit c0026d2

Browse files
authored
Add support for managed HTTP/S transports (#810) (#812)
This change uses the newly-exposed Transport interface to use Go's implementation of http.Client instead of httpclient via libgit2. (cherry picked from commit b983e1d)
1 parent feaae57 commit c0026d2

8 files changed

+375
-7
lines changed

credentials.go

+25
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ void _go_git_populate_credential_ssh_custom(git_cred_ssh_custom *cred);
88
import "C"
99
import (
1010
"crypto/rand"
11+
"errors"
12+
"runtime"
1113
"unsafe"
1214

1315
"golang.org/x/crypto/ssh"
@@ -42,6 +44,29 @@ func credFromC(ptr *C.git_cred) *Cred {
4244
return &Cred{ptr: ptr}
4345
}
4446

47+
// GetUserpassPlaintext returns the plaintext username/password combination stored in the Cred.
48+
func (o *Cred) GetUserpassPlaintext() (username, password string, err error) {
49+
if o.Type() != CredTypeUserpassPlaintext {
50+
err = errors.New("credential is not userpass plaintext")
51+
return
52+
}
53+
54+
plaintextCredPtr := (*C.git_cred_userpass_plaintext)(unsafe.Pointer(o.ptr))
55+
username = C.GoString(plaintextCredPtr.username)
56+
password = C.GoString(plaintextCredPtr.password)
57+
return
58+
}
59+
60+
func NewCredUsername(username string) (int, Cred) {
61+
runtime.LockOSThread()
62+
defer runtime.UnlockOSThread()
63+
64+
cred := Cred{}
65+
cusername := C.CString(username)
66+
ret := C.git_cred_username_new(&cred.ptr, cusername)
67+
return int(ret), cred
68+
}
69+
4570
func NewCredUserpassPlaintext(username string, password string) (int, Cred) {
4671
cred := Cred{}
4772
cusername := C.CString(username)

git.go

+13-7
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

+4
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

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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("", CredTypeUserpassPlaintext)
220+
if err != nil {
221+
return err
222+
}
223+
224+
userName, password, err = cred.GetUserpassPlaintext()
225+
if err != nil {
226+
return err
227+
}
228+
229+
continue
230+
}
231+
232+
// Any other error we treat as a hard error and punt back to the caller
233+
resp.Body.Close()
234+
return fmt.Errorf("Unhandled HTTP error %s", resp.Status)
235+
}
236+
237+
self.sentRequest = true
238+
self.resp = resp
239+
return nil
240+
}

remote.go

+7
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ type ProxyOptions struct {
167167
Url string
168168
}
169169

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

remote_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,30 @@ func TestRemotePrune(t *testing.T) {
232232
}
233233
}
234234

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

script/build-libgit2.sh

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ cd "${BUILD_PATH}/build" &&
5858
cmake -DTHREADSAFE=ON \
5959
-DBUILD_CLAR=OFF \
6060
-DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \
61+
-DUSE_HTTPS=OFF \
6162
-DCMAKE_C_FLAGS=-fPIC \
6263
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
6364
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \

0 commit comments

Comments
 (0)