1
1
package main
2
2
3
3
import (
4
- "net/url"
5
- "strconv"
6
- "strings"
7
-
4
+ "github.com/distribution/reference"
8
5
"golang.org/x/text/unicode/norm"
9
6
)
10
7
@@ -17,219 +14,17 @@ func useAuth(imageRef, registryHost string) bool {
17
14
return false
18
15
}
19
16
20
- imageHost := extractRegistryHostname (imageRef )
21
- configHost := normalizeRegistryHostname (registryHost )
17
+ pnn , err := reference .ParseNormalizedNamed (imageRef )
18
+ if err != nil {
19
+ return false
20
+ }
21
+ imageHost := reference .Domain (pnn )
22
22
23
23
// Apply Unicode normalization to prevent homograph attacks
24
24
// Use NFC (Canonical Decomposition followed by Canonical Composition)
25
25
// to ensure consistent Unicode representation
26
- imageHost = norm .NFC .String (imageHost )
27
- configHost = norm .NFC .String (configHost )
28
-
29
- return imageHost == configHost
30
- }
31
-
32
- // extractRegistryHostname extracts the registry hostname from an image reference.
33
- // Examples:
34
- // - "registry.example.com/namespace/image:tag" -> "registry.example.com"
35
- // - "registry.example.com:5000/image" -> "registry.example.com:5000"
36
- // - "localhost:5000/image" -> "localhost:5000"
37
- // - "image" -> "docker.io" (Docker Hub default)
38
- // - "ubuntu:20.04" -> "docker.io" (Docker Hub default)
39
- func extractRegistryHostname (imageRef string ) string {
40
- if imageRef == "" {
41
- return ""
42
- }
43
-
44
- // Split the image reference by '/' to get the potential registry part
45
- parts := strings .Split (imageRef , "/" )
46
- if len (parts ) == 1 {
47
- // Single part means it's a Docker Hub image (e.g., "ubuntu", "ubuntu:20.04")
48
- return "docker.io"
49
- }
50
-
51
- // The first part might be the registry hostname
52
- firstPart := parts [0 ]
53
-
54
- // Check if the first part looks like a registry hostname
55
- // We need to distinguish between:
56
- // - Registry hostnames (registry.example.com, localhost:5000, 192.168.1.1:5000, [::1]:5000)
57
- // - Docker Hub images with tags (ubuntu:20.04, myapp:v1.2.3)
58
- // - Docker Hub usernames (username/image)
59
- if isRegistryHostname (firstPart ) {
60
- return firstPart
61
- }
62
-
63
- // If the first part doesn't look like a hostname, assume it's Docker Hub
64
- // Examples: "library/ubuntu", "username/image", "ubuntu:20.04"
65
- return "docker.io"
66
- }
67
-
68
- // normalizeRegistryHostname normalizes a registry hostname for comparison.
69
- // It handles various formats that might be provided in configuration.
70
- // Examples:
71
- // - "https://registry.example.com" -> "registry.example.com"
72
- // - "http://localhost:5000" -> "localhost:5000"
73
- // - "registry.example.com:443" -> "registry.example.com:443"
74
- // - "registry.example.com" -> "registry.example.com"
75
- func normalizeRegistryHostname (registryHost string ) string {
76
- if registryHost == "" {
77
- return ""
78
- }
79
-
80
- // Handle URL schemes (https:// or http://)
81
- if strings .HasPrefix (registryHost , "https://" ) || strings .HasPrefix (registryHost , "http://" ) {
82
- parsed , err := url .Parse (registryHost )
83
- if err != nil {
84
- // If parsing fails, strip the scheme manually
85
- registryHost = strings .TrimPrefix (registryHost , "https://" )
86
- registryHost = strings .TrimPrefix (registryHost , "http://" )
87
- } else {
88
- registryHost = parsed .Host
89
- }
90
- }
91
-
92
- // Remove any trailing path components
93
- if idx := strings .Index (registryHost , "/" ); idx != - 1 {
94
- registryHost = registryHost [:idx ]
95
- }
96
-
97
- return registryHost
98
- }
99
-
100
- // isRegistryHostname determines if a string represents a registry hostname rather than
101
- // a Docker Hub image name with tag. This function handles various edge cases:
102
- // - IPv6 addresses in brackets: [::1]:5000, [2001:db8::1]:5000
103
- // - IPv4 addresses with ports: 192.168.1.1:5000
104
- // - Hostnames with ports: registry.example.com:5000, localhost:5000
105
- // - Hostnames with dots: registry.example.com, sub.domain.com
106
- // - Known registry patterns: localhost, 127.0.0.1
107
- // - Excludes Docker Hub image:tag patterns: ubuntu:20.04, myapp:v1.2.3.
108
- func isRegistryHostname (part string ) bool {
109
- if part == "" {
110
- return false
111
- }
112
-
113
- // Handle IPv6 addresses in brackets [::1], [2001:db8::1], [::1]:5000, or [2001:db8::1]:5000
114
- if strings .HasPrefix (part , "[" ) && strings .HasSuffix (part , "]" ) {
115
- return true
116
- }
117
- if strings .HasPrefix (part , "[" ) && strings .Contains (part , "]:" ) {
118
- return true
119
- }
120
-
121
- // Check for localhost (with or without port)
122
- if part == "localhost" {
123
- return true
124
- }
125
- if strings .HasPrefix (part , "localhost:" ) {
126
- portStr := part [len ("localhost:" ):]
127
- if portStr == "" {
128
- return false
129
- }
130
- port , err := strconv .Atoi (portStr )
131
- if err != nil || port < 1 || port > 65535 {
132
- return false
133
- }
134
- return true
135
- }
136
-
137
- // Check for IP addresses (IPv4) with optional port
138
- if isIPv4WithOptionalPort (part ) {
139
- return true
140
- }
141
-
142
- // Check if it contains a dot (indicating a domain)
143
- if strings .Contains (part , "." ) {
144
- // Make sure it's not just a single dot or other invalid patterns
145
- if part == "." || part == ".." || strings .HasPrefix (part , "." ) || strings .HasSuffix (part , "." ) {
146
- return false
147
- }
148
-
149
- // Additional check: if it contains a colon, make sure it's likely a port, not a tag
150
- if strings .Contains (part , ":" ) {
151
- return isHostnameWithPort (part )
152
- }
153
-
154
- // Basic validation: should have at least one character before and after dot
155
- dotParts := strings .Split (part , "." )
156
- for _ , dotPart := range dotParts {
157
- if len (dotPart ) == 0 {
158
- return false
159
- }
160
- }
161
-
162
- return true
163
- }
164
-
165
- // If it contains a colon but no dot, it could be:
166
- // 1. A hostname with port (localhost:5000) - already handled above
167
- // 2. A Docker image with tag (ubuntu:20.04) - should return false
168
- // 3. An IPv4 address with port (1.2.3.4:5000) - already handled above
169
- // At this point, assume it's a Docker image with tag
170
- return false
171
- }
172
-
173
- // isIPv4WithOptionalPort checks if the string is an IPv4 address with optional port.
174
- func isIPv4WithOptionalPort (part string ) bool {
175
- // Split by colon to separate potential IP and port
176
- host := part
177
- if colonIndex := strings .LastIndex (part , ":" ); colonIndex != - 1 {
178
- host = part [:colonIndex ]
179
- portStr := part [colonIndex + 1 :]
180
- // Validate port number (1-65535)
181
- if portStr == "" || len (portStr ) > 5 {
182
- return false
183
- }
184
- port , err := strconv .Atoi (portStr )
185
- if err != nil || port < 1 || port > 65535 {
186
- return false
187
- }
188
- }
189
-
190
- // Basic IPv4 validation: check for pattern like x.x.x.x
191
- parts := strings .Split (host , "." )
192
- if len (parts ) != 4 {
193
- return false
194
- }
195
-
196
- for _ , octet := range parts {
197
- if octet == "" || len (octet ) > 3 {
198
- return false
199
- }
200
- // Check if octet contains only digits
201
- for _ , r := range octet {
202
- if r < '0' || r > '9' {
203
- return false
204
- }
205
- }
206
- }
207
-
208
- return true
209
- }
210
-
211
- // isHostnameWithPort checks if a string with both dots and colons represents
212
- // a hostname with port rather than a Docker image with tag.
213
- func isHostnameWithPort (part string ) bool {
214
- // Find the last colon (potential port separator)
215
- colonIndex := strings .LastIndex (part , ":" )
216
- if colonIndex == - 1 {
217
- return true // No colon, just a hostname with dots
218
- }
219
-
220
- portStr := part [colonIndex + 1 :]
221
- hostname := part [:colonIndex ]
222
-
223
- // Port should be numeric and within the valid range (1-65535)
224
- if len (portStr ) == 0 || len (portStr ) > 5 {
225
- return false
226
- }
227
-
228
- port , err := strconv .Atoi (portStr )
229
- if err != nil || port < 1 || port > 65535 {
230
- return false
231
- }
26
+ imageH := norm .NFC .String (imageHost )
27
+ registryH := norm .NFC .String (registryHost )
232
28
233
- // Hostname part should still contain dots for this to be a registry hostname
234
- return strings .Contains (hostname , "." )
29
+ return imageH == registryH
235
30
}
0 commit comments