Skip to content

Commit bbbbd58

Browse files
Bryan C. Millsromaindoumenc
Bryan C. Mills
authored andcommitted
cmd/go: reroute vcs-test.golang.org HTTPS requests to the test-local server
After this CL, the only test requests that should still reach vcs-test.golang.org are for Subversion repos, which are not yet handled. The interceptor implementation should also allow us to redirect other servers (such as gopkg.in) fairly easily in a followup change if desired. For golang#27494. Change-Id: I8cb85f3a7edbbf0492662ff5cfa779fb9b407136 Reviewed-on: https://go-review.googlesource.com/c/go/+/427254 Auto-Submit: Bryan Mills <[email protected]> Run-TryBot: Bryan Mills <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 12dfc94 commit bbbbd58

23 files changed

+613
-50
lines changed

src/cmd/go/go_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"cmd/go/internal/search"
3636
"cmd/go/internal/vcs"
3737
"cmd/go/internal/vcweb/vcstest"
38+
"cmd/go/internal/web"
3839
"cmd/go/internal/work"
3940
"cmd/internal/sys"
4041

@@ -132,9 +133,21 @@ func TestMain(m *testing.M) {
132133
}
133134
}
134135

135-
if vcsTest := os.Getenv("TESTGO_VCSTEST_URL"); vcsTest != "" {
136-
vcs.VCSTestRepoURL = vcsTest
136+
if vcsTestHost := os.Getenv("TESTGO_VCSTEST_HOST"); vcsTestHost != "" {
137+
vcs.VCSTestRepoURL = "http://" + vcsTestHost
137138
vcs.VCSTestHosts = vcstest.Hosts
139+
vcsTestTLSHost := os.Getenv("TESTGO_VCSTEST_TLS_HOST")
140+
vcsTestClient, err := vcstest.TLSClient(os.Getenv("TESTGO_VCSTEST_CERT"))
141+
if err != nil {
142+
fmt.Fprintf(os.Stderr, "loading certificates from $TESTGO_VCSTEST_CERT: %v", err)
143+
}
144+
var interceptors []web.Interceptor
145+
for _, host := range vcstest.Hosts {
146+
interceptors = append(interceptors,
147+
web.Interceptor{Scheme: "http", FromHost: host, ToHost: vcsTestHost},
148+
web.Interceptor{Scheme: "https", FromHost: host, ToHost: vcsTestTLSHost, Client: vcsTestClient})
149+
}
150+
web.EnableTestHooks(interceptors)
138151
}
139152

140153
cmdgo.Main()

src/cmd/go/internal/auth/auth.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import "net/http"
1010
// AddCredentials fills in the user's credentials for req, if any.
1111
// The return value reports whether any matching credentials were found.
1212
func AddCredentials(req *http.Request) (added bool) {
13-
host := req.URL.Hostname()
13+
host := req.Host
14+
if host == "" {
15+
host = req.URL.Hostname()
16+
}
1417

1518
// TODO(golang.org/issue/26232): Support arbitrary user-provided credentials.
1619
netrcOnce.Do(readNetrc)

src/cmd/go/internal/vcs/vcs.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,9 @@ func interceptVCSTest(repo string, vcs *Cmd, security web.SecurityMode) (repoURL
12081208
return "", false
12091209
}
12101210
if vcs == vcsMod {
1211-
return "", false // Will be implemented in CL 427254.
1211+
// Since the "mod" protocol is implemented internally,
1212+
// requests will be intercepted at a lower level (in cmd/go/internal/web).
1213+
return "", false
12121214
}
12131215
if vcs == vcsSvn {
12141216
return "", false // Will be implemented in CL 427914.

src/cmd/go/internal/vcweb/auth.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2017 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package vcweb
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"io/ioutil"
11+
"log"
12+
"net/http"
13+
"os"
14+
"path"
15+
"strings"
16+
)
17+
18+
// authHandler serves requests only if the Basic Auth data sent with the request
19+
// matches the contents of a ".access" file in the requested directory.
20+
//
21+
// For each request, the handler looks for a file named ".access" and parses it
22+
// as a JSON-serialized accessToken. If the credentials from the request match
23+
// the accessToken, the file is served normally; otherwise, it is rejected with
24+
// the StatusCode and Message provided by the token.
25+
type authHandler struct{}
26+
27+
type accessToken struct {
28+
Username, Password string
29+
StatusCode int // defaults to 401.
30+
Message string
31+
}
32+
33+
func (h *authHandler) Available() bool { return true }
34+
35+
func (h *authHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
36+
fs := http.Dir(dir)
37+
38+
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
39+
urlPath := req.URL.Path
40+
if urlPath != "" && strings.HasPrefix(path.Base(urlPath), ".") {
41+
http.Error(w, "filename contains leading dot", http.StatusBadRequest)
42+
return
43+
}
44+
45+
f, err := fs.Open(urlPath)
46+
if err != nil {
47+
if os.IsNotExist(err) {
48+
http.NotFound(w, req)
49+
} else {
50+
http.Error(w, err.Error(), http.StatusInternalServerError)
51+
}
52+
return
53+
}
54+
55+
accessDir := urlPath
56+
if fi, err := f.Stat(); err == nil && !fi.IsDir() {
57+
accessDir = path.Dir(urlPath)
58+
}
59+
f.Close()
60+
61+
var accessFile http.File
62+
for {
63+
var err error
64+
accessFile, err = fs.Open(path.Join(accessDir, ".access"))
65+
if err == nil {
66+
break
67+
}
68+
69+
if !os.IsNotExist(err) {
70+
http.Error(w, err.Error(), http.StatusInternalServerError)
71+
return
72+
}
73+
if accessDir == "." {
74+
http.Error(w, "failed to locate access file", http.StatusInternalServerError)
75+
return
76+
}
77+
accessDir = path.Dir(accessDir)
78+
}
79+
80+
data, err := ioutil.ReadAll(accessFile)
81+
if err != nil {
82+
http.Error(w, err.Error(), http.StatusInternalServerError)
83+
return
84+
}
85+
86+
var token accessToken
87+
if err := json.Unmarshal(data, &token); err != nil {
88+
logger.Print(err)
89+
http.Error(w, "malformed access file", http.StatusInternalServerError)
90+
return
91+
}
92+
if username, password, ok := req.BasicAuth(); !ok || username != token.Username || password != token.Password {
93+
code := token.StatusCode
94+
if code == 0 {
95+
code = http.StatusUnauthorized
96+
}
97+
if code == http.StatusUnauthorized {
98+
w.Header().Add("WWW-Authenticate", fmt.Sprintf("basic realm=%s", accessDir))
99+
}
100+
http.Error(w, token.Message, code)
101+
return
102+
}
103+
104+
http.FileServer(fs).ServeHTTP(w, req)
105+
})
106+
107+
return handler, nil
108+
}

src/cmd/go/internal/vcweb/insecure.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package vcweb
6+
7+
import (
8+
"log"
9+
"net/http"
10+
)
11+
12+
// insecureHandler redirects requests to the same host and path but using the
13+
// "http" scheme instead of "https".
14+
type insecureHandler struct{}
15+
16+
func (h *insecureHandler) Available() bool { return true }
17+
18+
func (h *insecureHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
19+
// The insecure-redirect handler implementation doesn't depend or dir or env.
20+
//
21+
// The only effect of the directory is to determine which prefix the caller
22+
// will strip from the request before passing it on to this handler.
23+
return h, nil
24+
}
25+
26+
func (h *insecureHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
27+
if req.Host == "" && req.URL.Host == "" {
28+
http.Error(w, "no Host provided in request", http.StatusBadRequest)
29+
return
30+
}
31+
32+
// Note that if the handler is wrapped with http.StripPrefix, the prefix
33+
// will remain stripped in the redirected URL, preventing redirect loops
34+
// if the scheme is already "http".
35+
36+
u := *req.URL
37+
u.Scheme = "http"
38+
u.User = nil
39+
u.Host = req.Host
40+
41+
http.Redirect(w, req, u.String(), http.StatusFound)
42+
}

src/cmd/go/internal/vcweb/script.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"strconv"
2222
"strings"
2323
"time"
24+
25+
"golang.org/x/mod/module"
26+
"golang.org/x/mod/zip"
2427
)
2528

2629
// newScriptEngine returns a script engine augmented with commands for
@@ -38,6 +41,7 @@ func newScriptEngine() *script.Engine {
3841
cmds["git"] = script.Program("git", interrupt, gracePeriod)
3942
cmds["hg"] = script.Program("hg", interrupt, gracePeriod)
4043
cmds["handle"] = scriptHandle()
44+
cmds["modzip"] = scriptModzip()
4145
cmds["svn"] = script.Program("svn", interrupt, gracePeriod)
4246
cmds["unquote"] = scriptUnquote()
4347

@@ -280,6 +284,40 @@ func scriptHandle() script.Cmd {
280284
})
281285
}
282286

287+
func scriptModzip() script.Cmd {
288+
return script.Command(
289+
script.CmdUsage{
290+
Summary: "create a Go module zip file from a directory",
291+
Args: "zipfile path@version dir",
292+
},
293+
func(st *script.State, args ...string) (wait script.WaitFunc, err error) {
294+
if len(args) != 3 {
295+
return nil, script.ErrUsage
296+
}
297+
zipPath := st.Path(args[0])
298+
mPath, version, ok := strings.Cut(args[1], "@")
299+
if !ok {
300+
return nil, script.ErrUsage
301+
}
302+
dir := st.Path(args[2])
303+
304+
if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
305+
return nil, err
306+
}
307+
f, err := os.Create(zipPath)
308+
if err != nil {
309+
return nil, err
310+
}
311+
defer func() {
312+
if closeErr := f.Close(); err == nil {
313+
err = closeErr
314+
}
315+
}()
316+
317+
return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir)
318+
})
319+
}
320+
283321
func scriptUnquote() script.Cmd {
284322
return script.Command(
285323
script.CmdUsage{

src/cmd/go/internal/vcweb/vcstest/vcstest.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ package vcstest
99
import (
1010
"cmd/go/internal/vcs"
1111
"cmd/go/internal/vcweb"
12+
"cmd/go/internal/web"
13+
"crypto/tls"
14+
"crypto/x509"
15+
"encoding/pem"
1216
"fmt"
1317
"internal/testenv"
1418
"io"
1519
"log"
20+
"net/http"
1621
"net/http/httptest"
22+
"net/url"
1723
"os"
1824
"path/filepath"
1925
"testing"
@@ -26,6 +32,7 @@ var Hosts = []string{
2632
type Server struct {
2733
workDir string
2834
HTTP *httptest.Server
35+
HTTPS *httptest.Server
2936
}
3037

3138
// NewServer returns a new test-local vcweb server that serves VCS requests
@@ -58,15 +65,45 @@ func NewServer() (srv *Server, err error) {
5865
}
5966

6067
srvHTTP := httptest.NewServer(handler)
68+
httpURL, err := url.Parse(srvHTTP.URL)
69+
if err != nil {
70+
return nil, err
71+
}
72+
defer func() {
73+
if err != nil {
74+
srvHTTP.Close()
75+
}
76+
}()
77+
78+
srvHTTPS := httptest.NewTLSServer(handler)
79+
httpsURL, err := url.Parse(srvHTTPS.URL)
80+
if err != nil {
81+
return nil, err
82+
}
83+
defer func() {
84+
if err != nil {
85+
srvHTTPS.Close()
86+
}
87+
}()
6188

6289
srv = &Server{
6390
workDir: workDir,
6491
HTTP: srvHTTP,
92+
HTTPS: srvHTTPS,
6593
}
6694
vcs.VCSTestRepoURL = srv.HTTP.URL
6795
vcs.VCSTestHosts = Hosts
6896

97+
var interceptors []web.Interceptor
98+
for _, host := range Hosts {
99+
interceptors = append(interceptors,
100+
web.Interceptor{Scheme: "http", FromHost: host, ToHost: httpURL.Host, Client: srv.HTTP.Client()},
101+
web.Interceptor{Scheme: "https", FromHost: host, ToHost: httpsURL.Host, Client: srv.HTTPS.Client()})
102+
}
103+
web.EnableTestHooks(interceptors)
104+
69105
fmt.Fprintln(os.Stderr, "vcs-test.golang.org rerouted to "+srv.HTTP.URL)
106+
fmt.Fprintln(os.Stderr, "https://vcs-test.golang.org rerouted to "+srv.HTTPS.URL)
70107

71108
return srv, nil
72109
}
@@ -77,7 +114,45 @@ func (srv *Server) Close() error {
77114
}
78115
vcs.VCSTestRepoURL = ""
79116
vcs.VCSTestHosts = nil
117+
web.DisableTestHooks()
80118

81119
srv.HTTP.Close()
120+
srv.HTTPS.Close()
82121
return os.RemoveAll(srv.workDir)
83122
}
123+
124+
func (srv *Server) WriteCertificateFile() (string, error) {
125+
b := pem.EncodeToMemory(&pem.Block{
126+
Type: "CERTIFICATE",
127+
Bytes: srv.HTTPS.Certificate().Raw,
128+
})
129+
130+
filename := filepath.Join(srv.workDir, "cert.pem")
131+
if err := os.WriteFile(filename, b, 0644); err != nil {
132+
return "", err
133+
}
134+
return filename, nil
135+
}
136+
137+
// TLSClient returns an http.Client that can talk to the httptest.Server
138+
// whose certificate is written to the given file path.
139+
func TLSClient(certFile string) (*http.Client, error) {
140+
client := &http.Client{
141+
Transport: http.DefaultTransport.(*http.Transport).Clone(),
142+
}
143+
144+
pemBytes, err := os.ReadFile(certFile)
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
certpool := x509.NewCertPool()
150+
if !certpool.AppendCertsFromPEM(pemBytes) {
151+
return nil, fmt.Errorf("no certificates found in %s", certFile)
152+
}
153+
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
154+
RootCAs: certpool,
155+
}
156+
157+
return client, nil
158+
}

0 commit comments

Comments
 (0)