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
|
package remoteurl
import (
"fmt"
"net/url"
"path"
"strings"
)
// Spec describes the authenticated S3 location used for pushes and the
// optional public HTTP location used for clones.
type Spec struct {
Bucket string
Prefix string
Endpoint string
Profile string
PublicURL string
}
func Parse(raw string) (Spec, error) {
u, err := url.Parse(raw)
if err != nil {
return Spec{}, fmt.Errorf("parse remote URL: %w", err)
}
if u.Scheme != "dumbforge" {
return Spec{}, fmt.Errorf("unsupported remote scheme %q", u.Scheme)
}
if u.Fragment != "" {
return Spec{}, fmt.Errorf("remote URL must not contain a fragment")
}
if u.User != nil {
if _, hasPassword := u.User.Password(); hasPassword {
return Spec{}, fmt.Errorf("credentials must not be embedded in the remote URL")
}
}
spec := Spec{
Bucket: u.Hostname(),
Prefix: strings.Trim(u.Path, "/"),
Endpoint: strings.TrimRight(u.Query().Get("endpoint"), "/"),
PublicURL: strings.TrimRight(u.Query().Get("public"), "/"),
}
if u.User != nil {
spec.Profile = u.User.Username()
}
if err := validate(spec); err != nil {
return Spec{}, err
}
return spec, nil
}
func Build(spec Spec) (string, error) {
spec.Prefix = strings.Trim(spec.Prefix, "/")
if err := validate(spec); err != nil {
return "", err
}
u := &url.URL{
Scheme: "dumbforge",
Host: spec.Bucket,
Path: "/" + strings.Trim(spec.Prefix, "/"),
}
if spec.Profile != "" {
u.User = url.User(spec.Profile)
}
q := url.Values{}
q.Set("endpoint", strings.TrimRight(spec.Endpoint, "/"))
if spec.PublicURL != "" {
q.Set("public", strings.TrimRight(spec.PublicURL, "/"))
}
u.RawQuery = q.Encode()
return u.String(), nil
}
func (s Spec) PublicRepoURL() string {
if s.PublicURL == "" {
return ""
}
base, err := url.Parse(s.PublicURL)
if err != nil {
return ""
}
base.Path = strings.TrimRight(base.Path, "/") + "/" + path.Clean(strings.Trim(s.Prefix, "/"))
base.RawPath = ""
return base.String()
}
func validate(spec Spec) error {
if spec.Bucket == "" {
return fmt.Errorf("bucket is required")
}
if strings.ContainsAny(spec.Bucket, "/\\\x00\r\n\t ") {
return fmt.Errorf("bucket contains invalid characters")
}
if spec.Prefix == "" {
return fmt.Errorf("repository prefix is required")
}
unsafeSegment := false
for _, segment := range strings.Split(spec.Prefix, "/") {
if segment == "." || segment == ".." {
unsafeSegment = true
break
}
}
if strings.ContainsAny(spec.Prefix, "\\\x00\r\n") || path.Clean(spec.Prefix) != spec.Prefix || unsafeSegment {
return fmt.Errorf("repository prefix must be a canonical relative object path")
}
if err := validateHTTPURL("S3 endpoint", spec.Endpoint, true); err != nil {
return err
}
if err := validateHTTPURL("public URL", spec.PublicURL, false); err != nil {
return err
}
return nil
}
func validateHTTPURL(label, value string, required bool) error {
if value == "" {
if required {
return fmt.Errorf("%s is required", label)
}
return nil
}
parsed, err := url.Parse(value)
if err != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") || parsed.Host == "" {
return fmt.Errorf("%s must be an absolute HTTP or HTTPS URL", label)
}
if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
return fmt.Errorf("%s must not contain credentials, a query, or a fragment", label)
}
return nil
}
|