diff --git a/reference/normalize_test.go b/reference/normalize_test.go index 802aaef7..0ee6339e 100644 --- a/reference/normalize_test.go +++ b/reference/normalize_test.go @@ -22,7 +22,14 @@ func TestValidateReferenceName(t *testing.T) { "127.0.0.1:5000/docker/docker", "127.0.0.1:5000/library/debian", "127.0.0.1:5000/debian", + "192.168.0.1", + "192.168.0.1:80", + "192.168.0.1:8/debian", + "192.168.0.2:25000/debian", "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + "[fc00::1]:5000/docker", + "[fc00::1]:5000/docker/docker", + "[fc00:1:2:3:4:5:6:7]:5000/library/debian", // This test case was moved from invalid to valid since it is valid input // when specified with a hostname, it removes the ambiguity from about @@ -40,6 +47,11 @@ func TestValidateReferenceName(t *testing.T) { "docker///docker", "docker.io/docker/Docker", "docker.io/docker///docker", + "[fc00::1]", + "[fc00::1]:5000", + "fc00::1:5000/debian", + "[fe80::1%eth0]:5000/debian", + "[2001:db8:3:4::192.0.2.33]:5000/debian", "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", } diff --git a/reference/reference.go b/reference/reference.go index 8c0c23b2..23490afa 100644 --- a/reference/reference.go +++ b/reference/reference.go @@ -5,7 +5,9 @@ // // reference := name [ ":" tag ] [ "@" digest ] // name := [domain '/'] path-component ['/' path-component]* -// domain := domain-component ['.' domain-component]* [':' port-number] +// domain := host [':' port-number] +// host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A +// domain-name := domain-component ['.' domain-component]* // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ // path-component := alpha-numeric [separator alpha-numeric]* diff --git a/reference/reference_test.go b/reference/reference_test.go index b91d5615..5a82474d 100644 --- a/reference/reference_test.go +++ b/reference/reference_test.go @@ -171,6 +171,101 @@ func TestReferenceParse(t *testing.T) { repository: "foo/foo_bar.com", tag: "8080", }, + { + input: "192.168.1.1", + repository: "192.168.1.1", + }, + { + input: "192.168.1.1:tag", + repository: "192.168.1.1", + tag: "tag", + }, + { + input: "192.168.1.1:5000", + repository: "192.168.1.1", + tag: "5000", + }, + { + input: "192.168.1.1/repo", + domain: "192.168.1.1", + repository: "192.168.1.1/repo", + }, + { + input: "192.168.1.1:5000/repo", + domain: "192.168.1.1:5000", + repository: "192.168.1.1:5000/repo", + }, + { + input: "192.168.1.1:5000/repo:5050", + domain: "192.168.1.1:5000", + repository: "192.168.1.1:5000/repo", + tag: "5050", + }, + { + input: "[2001:db8::1]", + err: ErrReferenceInvalidFormat, + }, + { + input: "[2001:db8::1]:5000", + err: ErrReferenceInvalidFormat, + }, + { + input: "[2001:db8::1]:tag", + err: ErrReferenceInvalidFormat, + }, + { + input: "[2001:db8::1]/repo", + domain: "[2001:db8::1]", + repository: "[2001:db8::1]/repo", + }, + { + input: "[2001:db8:1:2:3:4:5:6]/repo:tag", + domain: "[2001:db8:1:2:3:4:5:6]", + repository: "[2001:db8:1:2:3:4:5:6]/repo", + tag: "tag", + }, + { + input: "[2001:db8::1]:5000/repo", + domain: "[2001:db8::1]:5000", + repository: "[2001:db8::1]:5000/repo", + }, + { + input: "[2001:db8::1]:5000/repo:tag", + domain: "[2001:db8::1]:5000", + repository: "[2001:db8::1]:5000/repo", + tag: "tag", + }, + { + input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + domain: "[2001:db8::1]:5000", + repository: "[2001:db8::1]:5000/repo", + digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + domain: "[2001:db8::1]:5000", + repository: "[2001:db8::1]:5000/repo", + tag: "tag", + digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + input: "[2001:db8::]:5000/repo", + domain: "[2001:db8::]:5000", + repository: "[2001:db8::]:5000/repo", + }, + { + input: "[::1]:5000/repo", + domain: "[::1]:5000", + repository: "[::1]:5000/repo", + }, + { + input: "[fe80::1%eth0]:5000/repo", + err: ErrReferenceInvalidFormat, + }, + { + input: "[fe80::1%@invalidzone]:5000/repo", + err: ErrReferenceInvalidFormat, + }, } for _, testcase := range referenceTestcases { failf := func(format string, v ...interface{}) { diff --git a/reference/regexp.go b/reference/regexp.go index 7677ca15..7ecf4f76 100644 --- a/reference/regexp.go +++ b/reference/regexp.go @@ -23,15 +23,40 @@ var ( alphaNumeric, optional(repeated(separator, alphaNumeric))) - // domainComponent restricts the registry domain component of a - // repository name to start with a component as defined by DomainRegexp - // and followed by an optional port. - domainComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` + // domainNameComponent restricts the registry domain component of a + // repository name to start with a component as defined by DomainRegexp. + domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` + // ipv6address are enclosed between square brackets and may be represented + // in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format + // are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as + // IPv4-Mapped are deliberately excluded. + ipv6address = expression( + literal(`[`), `(?:[a-fA-F0-9:]+)`, literal(`]`), + ) + + // domainName defines the structure of potential domain components + // that may be part of image names. This is purposely a subset of what is + // allowed by DNS to ensure backwards compatibility with Docker image + // names. This includes IPv4 addresses on decimal format. + domainName = expression( + domainNameComponent, + optional(repeated(literal(`.`), domainNameComponent)), + ) + + // host defines the structure of potential domains based on the URI + // Host subcomponent on rfc3986. It may be a subset of DNS domain name, + // or an IPv4 address in decimal format, or an IPv6 address between square + // brackets (excluding zone identifiers as defined by rfc6874 or special + // addresses such as IPv4-Mapped). + host = `(?:` + domainName + `|` + ipv6address + `)` + + // allowed by the URI Host subcomponent on rfc3986 to ensure backwards + // compatibility with Docker image names. domain = expression( - domainComponent, - optional(repeated(literal(`.`), domainComponent)), + host, optional(literal(`:`), `[0-9]+`)) + // DomainRegexp defines the structure of potential domain components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image diff --git a/reference/regexp_test.go b/reference/regexp_test.go index 09bc8192..20dc1166 100644 --- a/reference/regexp_test.go +++ b/reference/regexp_test.go @@ -115,6 +115,46 @@ func TestDomainRegexp(t *testing.T) { input: "Asdf.com", // uppercase character match: true, }, + { + input: "192.168.1.1:75050", // ipv4 + match: true, + }, + { + input: "192.168.1.1:750050", // port with more than 5 digits, it will fail on validation + match: true, + }, + { + input: "[fd00:1:2::3]:75050", // ipv6 compressed + match: true, + }, + { + input: "[fd00:1:2::3]75050", // ipv6 wrong port separator + match: false, + }, + { + input: "[fd00:1:2::3]::75050", // ipv6 wrong port separator + match: false, + }, + { + input: "[fd00:1:2::3%eth0]:75050", // ipv6 with zone + match: false, + }, + { + input: "[fd00123123123]:75050", // ipv6 wrong format, will fail in validation + match: true, + }, + { + input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:75050", // ipv6 long format + match: true, + }, + { + input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:750505", // ipv6 long format and invalid port, it will fail in validation + match: true, + }, + { + input: "fd00:1:2::3:75050", // bad ipv6 without square brackets + match: false, + }, } r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`) for i := range hostcases {