Skip to content
This repository was archived by the owner on May 27, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ef10942
add HTTP header well known type and C++ UUID
asraa Dec 16, 2019
3f0ef08
update validate.pb.go
asraa Dec 16, 2019
0ada956
simplify conditions
asraa Dec 17, 2019
b538b97
fix java build
asraa Dec 17, 2019
183e106
bad test
asraa Dec 17, 2019
cfaec72
java
asraa Dec 17, 2019
c9f0f24
check for slashes in hdr name
asraa Dec 17, 2019
a8bdbfb
add more testcases
asraa Dec 17, 2019
084d9ab
empty
asraa Dec 17, 2019
61dad58
define regex for http header name/value
asraa Dec 18, 2019
52baf9e
make patterns in to well known regex
asraa Dec 23, 2019
cb4812e
fix bazel
asraa Dec 23, 2019
9bc951e
fix bazel maven_jar defn
asraa Dec 23, 2019
70b715f
Merge remote-tracking branch 'upstream/master' into header-valid
asraa Dec 26, 2019
d311924
Merge remote-tracking branch 'upstream/master' into header-valid
asraa Dec 26, 2019
a7e22e9
remove fixes for build
asraa Dec 26, 2019
5dba63d
fix merge mistake
asraa Dec 26, 2019
4675686
encode in utf-8 when writing out
asraa Jan 6, 2020
b2d81f7
Merge remote-tracking branch 'upstream/master' into header-valid
asraa Jan 6, 2020
94d5d8d
cleanup
asraa Jan 6, 2020
a41290c
remove unused regex definitions
asraa Jan 7, 2020
b43d32e
add backtick
asraa Jan 24, 2020
87e9ca4
simplify header value regex as blacklist
asraa Jan 24, 2020
a7debef
fixup to match entire string + also fixup dependency
asraa Jan 24, 2020
5a47460
python regex
asraa Jan 27, 2020
bbc4c85
remove pyc
asraa Feb 3, 2020
7995cbb
Merge remote-tracking branch 'upstream/master' into header-valid
asraa Feb 3, 2020
1093391
there's gotta be another way
asraa Feb 4, 2020
6bd356b
Merge remote-tracking branch 'upstream/master' into header-valid
asraa Feb 11, 2020
0b11e92
remove autosaved pgs
asraa Feb 11, 2020
78ca604
Merge branch 'master' into header-valid
akonradi Feb 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,12 @@ Check the [constraint rule comparison matrix](rule_comparison.md) for language-s

// x must be a valid UUID (via RFC 4122)
string x = 1 [(validate.rules).string.uuid = true];

// x must be a valid HTTP header name (via RFC 7230)
string x = 1 [(validate.rules).string.http_header_name = true];
Comment thread
asraa marked this conversation as resolved.
Outdated

// x must be a valid HTTP header value (via RFC 7230)
string x = 1 [(validate.rules).string.http_header_value = true];
```

### Bytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public final class StringValidation {
private static final int UUID_DASH_3 = 18;
private static final int UUID_DASH_4 = 23;
private static final int UUID_LEN = 36;
private static final Pattern HTTP_HEADER_NAME_PATTERN = Pattern.compile("^:?[0-9a-zA-Z!#$%&'*+-.^_|~\u002C]+$");
private static final Pattern HTTP_HEADER_VALUE_PATTERN = Pattern.compile("^[ \t]*(?:[\u0020-\u007E\u0080-\u00FF](?:[ \t]+[\u0020-\u007E\u0080-\u00FF])?)*[ \t]*$");
Comment thread
asraa marked this conversation as resolved.
Outdated

private StringValidation() {
// Intentionally left blank.
Expand Down Expand Up @@ -208,6 +210,22 @@ public static void uuid(final String field, final String value) throws Validatio
throw new ValidationException(field, enquote(value), "invalid UUID string");
}

public static void httpHeaderName(final String field, final String value) throws ValidationException {
if (!HTTP_HEADER_NAME_PATTERN.matches(value)) {
throw new ValidationException(field, enquote(value), "invalid HTTP header name string");
}

return;
}

public static void httpHeaderValue(final String field, final String value) throws ValidationException {
if (!HTTP_HEADER_VALUE_PATTERN.matches(value)) {
throw new ValidationException(field, enquote(value), "invalid HTTP header value string");
}

return;
}

private static String enquote(String value) {
return "\"" + value + "\"";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,34 @@ public void uuidWorks() throws ValidationException {
assertValidationException(() -> uuid("4_dash_at_22", "00000000-0000-0000-000-0000000000000"));
assertValidationException(() -> uuid("4_dash_at_24", "00000000-0000-0000-00000-00000000000"));
}

@Test
public void httpHeaderNameWorks() throws ValidationException {
// Match
StringValidation.httpHeaderName("x", "cluster.name");
StringValidation.httpHeaderName("x", ":path");
StringValidation.httpHeaderName("x", "clustername");
StringValidation.httpHeaderName("x", "!#%&.+");

// No Match
assertThatThrownBy(() -> StringValidation.httpHeaderName("x", "foo\000bar")).isInstanceOf(ValidationException.class);
assertThatThrownBy(() -> StringValidation.httpHeaderName("x", "test/long/url")).isInstanceOf(ValidationException.class);
assertThatThrownBy(() -> StringValidation.httpHeaderName("x", "cluster name")).isInstanceOf(ValidationException.class);
assertThatThrownBy(() -> StringValidation.httpHeaderName("x", "example\r")).isInstanceOf(ValidationException.class);
assertThatThrownBy(() -> StringValidation.httpHeaderName("x", ":")).isInstanceOf(ValidationException.class);
}

@Test
public void httpHeaderValueWorks() throws ValidationException {
// Match
StringValidation.httpHeaderValue("x", "cluster.name");
StringValidation.httpHeaderValue("x", "/TEST/LONG/URL");
StringValidation.httpHeaderValue("x", "cluster name");
StringValidation.httpHeaderValue("x", "!#%&./+");

// No Match
assertThatThrownBy(() -> StringValidation.httpHeaderValue("x", "foo\000bar")).isInstanceOf(ValidationException.class);
assertThatThrownBy(() -> StringValidation.httpHeaderValue("x", "example\r")).isInstanceOf(ValidationException.class);
}

}
8 changes: 5 additions & 3 deletions rule_comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
| ip |✅|✅|✅|✅|✅|
| ipv4 |✅|✅|✅|✅|✅|
| ipv6 |✅|✅|✅|✅|✅|
| uri |✅|✅|✅|✅|✅|
| uri_ref |✅|✅|✅|✅|✅|
| uuid |✅|✅|❌|✅|✅|
| uri |✅|✅|❌|✅|✅|
| uri_ref |✅|✅|❌|✅|✅|
| uuid |✅|✅|✅|✅|✅|
| http_header_name |✅|✅|✅|✅|✅|
| http_header_value |✅|✅|✅|✅|✅|

## Bytes
| Constraint Rule | Go | GoGo | C++ | Java | Python |
Expand Down
9 changes: 9 additions & 0 deletions templates/cc/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ namespace protobuf_wkt = google::protobuf;
namespace validate {
using std::string;

// define the regex for a UUID once up-front
const re2::RE2 _uuidPattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");

// define the regex for an HTTP header name once up-front
const re2::RE2 _httpHeaderName("^:?[0-9a-zA-Z!#$%&'*+-.^_|~\x2C]+$");

// define the regex for an HTTP header value once up-front
const re2::RE2 _httpHeaderValue("^[ \t]*(?:[\x20-\x7E\u0080-\u00FF](?:[ \t]+[\x20-\x7E\u0080-\u00FF])?)*[ \t]*$");

{{ range .AllMessages }}
{{- if not (disabled .) -}}

Expand Down
19 changes: 14 additions & 5 deletions templates/cc/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,20 @@ const strTpl = `
}
*/}}
{{ else if $r.GetUuid }}
{{ unimplemented "C++ UUID validation is not implemented" }}
{{/* TODO(akonradi) implement UUID constraints
if err := m._validateUuid({{ accessor . }}); err != nil {
return {{ errCause . "err" "value must be a valid UUID" }}
if (!RE2::FullMatch(re2::StringPiece({{ accessor . }}), pgv::validate::_uuidPattern)) {
{{ err . "value must be a valid UUID" }}
}
*/}}
{{ else if $r.GetHttpHeaderName }}
{
if (!RE2::FullMatch(re2::StringPiece({{ accessor . }}), pgv::validate::_httpHeaderName)) {
{{ err . "value must be a valid HTTP Header Name" }}
}
}
{{ else if $r.GetHttpHeaderValue }}
{
if (!RE2::FullMatch(re2::StringPiece({{ accessor . }}), pgv::validate::_httpHeaderValue)) {
{{ err . "value must be a valid HTTP Header Value" }}
}
}
{{ end }}
`
7 changes: 7 additions & 0 deletions templates/go/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ var (
// define the regex for a UUID once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_uuidPattern = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")

// define the regex for an HTTP header name once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderName = regexp.MustCompile("^:?[0-9a-zA-Z!#$%&'*+-.^_|~\x2C]+$")

// define the regex for an HTTP header value once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderValue = regexp.MustCompile("^[ \t]*(?:[\x20-\x7E\u0080-\u00FF](?:[ \t]+[\x20-\x7E\u0080-\u00FF])?)*[ \t]*$")


{{ range .AllMessages }}
{{ template "msg" . }}
{{ end }}
Expand Down
8 changes: 8 additions & 0 deletions templates/gogo/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"regexp"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/gogo/protobuf/types"
Expand All @@ -37,6 +38,7 @@ var (
_ = (*url.URL)(nil)
_ = (*mail.Address)(nil)
_ = types.DynamicAny{}
_ = unicode.IsControl

{{ range (externalEnums .) }}
_ = {{ pkg . }}.{{ name . }}(0)
Expand All @@ -46,6 +48,12 @@ var (
// define the regex for a UUID once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_uuidPattern = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")

// define the regex for an HTTP header name once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderName = regexp.MustCompile("^:?[0-9a-zA-Z!#$%&'*+-.^_|~\x2C]+$")
Comment thread
htuch marked this conversation as resolved.
Outdated

// define the regex for an HTTP header value once up-front
var _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderValue = regexp.MustCompile("^[ \t]*(?:[\x20-\x7E\u0080-\u00FF](?:[ \t]+[\x20-\x7E\u0080-\u00FF])?)*[ \t]*$")

{{ range .AllMessages }}
{{ template "msg" . }}
{{ end }}
Expand Down
20 changes: 20 additions & 0 deletions templates/goshared/known.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,23 @@ const uuidTpl = `
return nil
}
`

const httpHeaderNameTpl = `
func (m {{ (msgTyp .).Pointer }}) _validateHttpHeaderName(hdr string) error {
if matched := _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderName.MatchString(hdr); !matched {
return errors.New("invalid HTTP header name format")
}

return nil
}
`

const httpHeaderValueTpl = `
func (m {{ (msgTyp .).Pointer }}) _validateHttpHeaderValue(hdr string) error {
if matched := _{{ snakeCase .File.InputPath.BaseName }}_httpHeaderValue.MatchString(hdr); !matched {
return errors.New("invalid HTTP header name format")
}

return nil
}
`
4 changes: 4 additions & 0 deletions templates/goshared/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func (m {{ (msgTyp .).Pointer }}) Validate() error {

{{ if needs . "uuid" }}{{ template "uuid" . }}{{ end }}

{{ if needs . "httpheadername" }}{{ template "httpheadername" . }}{{ end }}

{{ if needs . "httpheadervalue" }}{{ template "httpheadervalue" . }}{{ end }}

{{ cmt (errname .) " is the validation error returned by " (msgTyp .) ".Validate if the designated constraints aren't met." -}}
type {{ errname . }} struct {
field string
Expand Down
4 changes: 3 additions & 1 deletion templates/goshared/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func Register(tpl *template.Template, params pgs.Parameters) {
tpl.Funcs(map[string]interface{}{
"accessor": fns.accessor,
"byteStr": fns.byteStr,
"snakeCase": fns.snakeCase,
"snakeCase": fns.snakeCase,
"cmt": pgs.C80,
"durGt": fns.durGt,
"durLit": fns.durLit,
Expand Down Expand Up @@ -76,6 +76,8 @@ func Register(tpl *template.Template, params pgs.Parameters) {
template.Must(tpl.New("hostname").Parse(hostTpl))
template.Must(tpl.New("address").Parse(hostTpl))
template.Must(tpl.New("uuid").Parse(uuidTpl))
template.Must(tpl.New("httpheadername").Parse(httpHeaderNameTpl))
template.Must(tpl.New("httpheadervalue").Parse(httpHeaderValueTpl))

template.Must(tpl.New("enum").Parse(enumTpl))
template.Must(tpl.New("repeated").Parse(repTpl))
Expand Down
9 changes: 8 additions & 1 deletion templates/goshared/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,15 @@ const strTpl = `
if err := m._validateUuid({{ accessor . }}); err != nil {
return {{ errCause . "err" "value must be a valid UUID" }}
}
{{ else if $r.GetHttpHeaderName }}
if err := m._validateHttpHeaderName({{ accessor . }}); err != nil {
return {{ errCause . "err" "value must be a valid HTTP header name" }}
}
{{ else if $r.GetHttpHeaderValue }}
if err := m._validateHttpHeaderValue({{ accessor . }}); err != nil {
return {{ errCause . "err" "value must be a valid HTTP header value" }}
}
{{ end }}

{{ if $r.Pattern }}
if !{{ lookup $f "Pattern" }}.MatchString({{ accessor . }}) {
return {{ err . "value does not match regex pattern " (lit $r.GetPattern) }}
Expand Down
6 changes: 6 additions & 0 deletions templates/java/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@ const stringTpl = `{{ $f := .Field }}{{ $r := .Rules -}}
{{- if $r.GetUuid }}
io.envoyproxy.pgv.StringValidation.uuid("{{ $f.FullyQualifiedName }}", {{ accessor . }});
{{- end -}}
{{- if $r.GetHttpHeaderName }}
io.envoyproxy.pgv.StringValidation.httpHeaderName("{{ $f.FullyQualifiedName }}", {{ accessor . }});
{{- end -}}
{{- if $r.GetHttpHeaderValue }}
io.envoyproxy.pgv.StringValidation.httpHeaderValue("{{ $f.FullyQualifiedName }}", {{ accessor . }});
{{- end -}}
`
16 changes: 13 additions & 3 deletions templates/shared/well_known.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
type WellKnown string

const (
Email WellKnown = "email"
Hostname WellKnown = "hostname"
UUID WellKnown = "uuid"
Email WellKnown = "email"
Hostname WellKnown = "hostname"
UUID WellKnown = "uuid"
HttpHeaderName WellKnown = "httpheadername"
HttpHeaderValue WellKnown = "httpheadervalue"
)

// Needs returns true if a well-known string validator is needed for this
Expand Down Expand Up @@ -61,6 +63,14 @@ func strRulesNeeds(rules *validate.StringRules, wk WellKnown) bool {
if rules.GetUuid() {
return true
}
case HttpHeaderName:
if rules.GetHttpHeaderName() {
return true
}
case HttpHeaderValue:
if rules.GetHttpHeaderValue() {
return true
}
}

return false
Expand Down
2 changes: 2 additions & 0 deletions tests/harness/cases/strings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ message StringIPv6 { string val = 1 [(validate.rules).string.ipv6 = tr
message StringURI { string val = 1 [(validate.rules).string.uri = true]; }
message StringURIRef { string val = 1 [(validate.rules).string.uri_ref = true]; }
message StringUUID { string val = 1 [(validate.rules).string.uuid = true]; }
message StringHttpHeaderName { string val = 1 [(validate.rules).string.http_header_name = true]; }
message StringHttpHeaderValue { string val = 1 [(validate.rules).string.http_header_value = true]; }
21 changes: 21 additions & 0 deletions tests/harness/executor/cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,27 @@ var stringCases = []TestCase{
{"string - UUID - valid (v5 - case-insensitive)", &cases.StringUUID{Val: "A6EDC906-2F9F-5FB2-A373-EFAC406F0EF2"}, true},
{"string - UUID - invalid", &cases.StringUUID{Val: "foobar"}, false},
{"string - UUID - invalid (bad UUID)", &cases.StringUUID{Val: "ffffffff-ffff-ffff-ffff-fffffffffffff"}, false},

{"string - http header name - valid", &cases.StringHttpHeaderName{Val: "clustername"}, true},
{"string - http header name - valid", &cases.StringHttpHeaderName{Val: ":path"}, true},
{"string - http header name - valid (nums)", &cases.StringHttpHeaderName{Val: "cluster-123"}, true},
{"string - http header name - valid (special token)", &cases.StringHttpHeaderName{Val: "!+#&.%"}, true},
{"string - http header name - valid (period)", &cases.StringHttpHeaderName{Val: "CLUSTER.NAME"}, true},
{"string - http header name - invalid", &cases.StringHttpHeaderName{Val: ":"}, false},
{"string - http header name - invalid", &cases.StringHttpHeaderName{Val: ":path:"}, false},
{"string - http header name - invalid (space)", &cases.StringHttpHeaderName{Val: "cluster name"}, false},
{"string - http header name - invalid (return)", &cases.StringHttpHeaderName{Val: "example\r"}, false},
{"string - http header name - invalid (tab)", &cases.StringHttpHeaderName{Val: "example\t"}, false},
{"string - http header name - invalid (slash)", &cases.StringHttpHeaderName{Val: "/test/long/url"}, false},

{"string - http header value - valid", &cases.StringHttpHeaderValue{Val: "cluster.name.123"}, true},
{"string - http header value - valid (uppercase)", &cases.StringHttpHeaderValue{Val: "/TEST/LONG/URL"}, true},
{"string - http header value - valid (spaces)", &cases.StringHttpHeaderValue{Val: "cluster name"}, true},
{"string - http header value - valid (tab)", &cases.StringHttpHeaderValue{Val: "example\t"}, true},
{"string - http header value - valid (special token)", &cases.StringHttpHeaderValue{Val: "!#%&./+"}, true},
{"string - http header value - invalid (NUL)", &cases.StringHttpHeaderValue{Val: "foo\000bar"}, false},
{"string - http header value - invalid (DEL)", &cases.StringHttpHeaderValue{Val: "\x7f"}, false},
{"string - http header value - invalid", &cases.StringHttpHeaderValue{Val: "example\r"}, false},
}

var bytesCases = []TestCase{
Expand Down
Loading