-
Notifications
You must be signed in to change notification settings - Fork 175
/
client.go
228 lines (208 loc) · 7.38 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package proxy provides a client for interacting with a proxy.
package proxy
import (
"archive/zip"
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"go.opencensus.io/plugin/ochttp"
"golang.org/x/mod/module"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
)
// A Client is used by the fetch service to communicate with a module
// proxy. It handles all methods defined by go help goproxy.
type Client struct {
// URL of the module proxy web server
url string
// client used for HTTP requests. It is mutable for testing purposes.
httpClient *http.Client
}
// A VersionInfo contains metadata about a given version of a module.
type VersionInfo struct {
Version string
Time time.Time
}
// New constructs a *Client using the provided url, which is expected to
// be an absolute URI that can be directly passed to http.Get.
func New(u string) (_ *Client, err error) {
defer derrors.Wrap(&err, "proxy.New(%q)", u)
return &Client{
url: strings.TrimRight(u, "/"),
httpClient: &http.Client{Transport: &ochttp.Transport{}},
}, nil
}
// GetInfo makes a request to $GOPROXY/<module>/@v/<requestedVersion>.info and
// transforms that data into a *VersionInfo.
func (c *Client) GetInfo(ctx context.Context, modulePath, requestedVersion string) (_ *VersionInfo, err error) {
defer derrors.Wrap(&err, "proxy.Client.GetInfo(%q, %q)", modulePath, requestedVersion)
data, err := c.readBody(ctx, modulePath, requestedVersion, "info")
if err != nil {
return nil, err
}
var v VersionInfo
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return &v, nil
}
// GetMod makes a request to $GOPROXY/<module>/@v/<resolvedVersion>.mod and returns the raw data.
func (c *Client) GetMod(ctx context.Context, modulePath, resolvedVersion string) (_ []byte, err error) {
defer derrors.Wrap(&err, "proxy.Client.GetMod(%q, %q)", modulePath, resolvedVersion)
return c.readBody(ctx, modulePath, resolvedVersion, "mod")
}
// GetZip makes a request to $GOPROXY/<modulePath>/@v/<resolvedVersion>.zip and
// transforms that data into a *zip.Reader. <resolvedVersion> must have already
// been resolved by first making a request to
// $GOPROXY/<modulePath>/@v/<requestedVersion>.info to obtained the valid
// semantic version.
func (c *Client) GetZip(ctx context.Context, modulePath, resolvedVersion string) (_ *zip.Reader, err error) {
defer derrors.Wrap(&err, "proxy.Client.GetZip(ctx, %q, %q)", modulePath, resolvedVersion)
bodyBytes, err := c.readBody(ctx, modulePath, resolvedVersion, "zip")
if err != nil {
return nil, err
}
zipReader, err := zip.NewReader(bytes.NewReader(bodyBytes), int64(len(bodyBytes)))
if err != nil {
return nil, fmt.Errorf("zip.NewReader: %v: %w", err, derrors.BadModule)
}
return zipReader, nil
}
// GetZipSize gets the size in bytes of the zip from the proxy, without downloading it.
// The version must be resolved, as by a call to Client.GetInfo.
func (c *Client) GetZipSize(ctx context.Context, modulePath, resolvedVersion string) (_ int64, err error) {
defer derrors.Wrap(&err, "proxy.Client.GetZipSize(ctx, %q, %q)", modulePath, resolvedVersion)
url, err := c.escapedURL(modulePath, resolvedVersion, "zip")
if err != nil {
return 0, err
}
res, err := ctxhttp.Head(ctx, c.httpClient, url)
if err != nil {
return 0, fmt.Errorf("ctxhttp.Head(ctx, client, %q): %v", url, err)
}
defer res.Body.Close()
if err := responseError(res); err != nil {
return 0, err
}
if res.ContentLength < 0 {
return 0, errors.New("unknown content length")
}
return res.ContentLength, nil
}
func (c *Client) escapedURL(modulePath, requestedVersion, suffix string) (_ string, err error) {
defer func() {
derrors.Wrap(&err, "Client.escapedURL(%q, %q, %q)", modulePath, requestedVersion, suffix)
}()
if suffix != "info" && suffix != "mod" && suffix != "zip" {
return "", errors.New(`suffix must be "info", "mod" or "zip"`)
}
escapedPath, err := module.EscapePath(modulePath)
if err != nil {
return "", fmt.Errorf("path: %v: %w", err, derrors.InvalidArgument)
}
if requestedVersion == internal.LatestVersion {
if suffix != "info" {
return "", fmt.Errorf("cannot ask for latest with suffix %q", suffix)
}
return fmt.Sprintf("%s/%s/@latest", c.url, escapedPath), nil
}
escapedVersion, err := module.EscapeVersion(requestedVersion)
if err != nil {
return "", fmt.Errorf("version: %v: %w", err, derrors.InvalidArgument)
}
return fmt.Sprintf("%s/%s/@v/%s.%s", c.url, escapedPath, escapedVersion, suffix), nil
}
func (c *Client) readBody(ctx context.Context, modulePath, requestedVersion, suffix string) (_ []byte, err error) {
defer derrors.Wrap(&err, "Client.readBody(%q, %q, %q)", modulePath, requestedVersion, suffix)
u, err := c.escapedURL(modulePath, requestedVersion, suffix)
if err != nil {
return nil, err
}
var data []byte
err = c.executeRequest(ctx, u, func(body io.Reader) error {
var err error
data, err = ioutil.ReadAll(body)
return err
})
if err != nil {
return nil, err
}
return data, nil
}
// ListVersions makes a request to $GOPROXY/<path>/@v/list and returns the
// resulting version strings.
func (c *Client) ListVersions(ctx context.Context, modulePath string) ([]string, error) {
escapedPath, err := module.EscapePath(modulePath)
if err != nil {
return nil, fmt.Errorf("module.EscapePath(%q): %w", modulePath, derrors.InvalidArgument)
}
u := fmt.Sprintf("%s/%s/@v/list", c.url, escapedPath)
var versions []string
collect := func(body io.Reader) error {
scanner := bufio.NewScanner(body)
for scanner.Scan() {
versions = append(versions, scanner.Text())
}
return scanner.Err()
}
if err := c.executeRequest(ctx, u, collect); err != nil {
return nil, err
}
return versions, nil
}
// executeRequest executes an HTTP GET request for u, then calls the bodyFunc
// on the response body, if no error occurred.
func (c *Client) executeRequest(ctx context.Context, u string, bodyFunc func(body io.Reader) error) (err error) {
defer func() {
if ctx.Err() != nil {
err = fmt.Errorf("%v: %w", err, derrors.ProxyTimedOut)
}
derrors.Wrap(&err, "executeRequest(ctx, %q)", u)
}()
r, err := ctxhttp.Get(ctx, c.httpClient, u)
if err != nil {
return fmt.Errorf("ctxhttp.Get(ctx, client, %q): %v", u, err)
}
defer r.Body.Close()
if err := responseError(r); err != nil {
return err
}
return bodyFunc(r.Body)
}
// responseError translates the response status code to an appropriate error.
func responseError(r *http.Response) error {
switch {
case 200 <= r.StatusCode && r.StatusCode < 300:
return nil
case r.StatusCode == http.StatusNotFound,
r.StatusCode == http.StatusGone:
// Treat both 404 Not Found and 410 Gone responses
// from the proxy as a "not found" error category.
// If the response body contains "fetch timed out", treat this
// as a 504 response so that we retry fetching the module version again
// later.
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("ioutil.readall: %v", err)
}
d := string(data)
if strings.Contains(d, "fetch timed out") {
return fmt.Errorf("%q: %w", d, derrors.ProxyTimedOut)
}
return fmt.Errorf("%q: %w", d, derrors.NotFound)
default:
return fmt.Errorf("unexpected status %d %s", r.StatusCode, r.Status)
}
}