Skip to content

Commit df1681f

Browse files
aiharosmjuraga
authored andcommitted
MINOR: config: add option forwarded support
Add support for the RFC 7239 "option forwarded" directive, which HAProxy uses to insert a Forwarded header field. The config-parser gains an OptionForwarded parser implementing the documented grammar: option forwarded [ proto ] [ host | host-expr <expr> ] [ by | by-expr <expr> ] [ by_port | by_port-expr <expr> ] [ for | for-expr <expr> ] [ for_port | for_port-expr <expr> ] along with "no option forwarded". Each parameter family is mutually exclusive between its boolean and its expression form, and may only appear once; violations are rejected at parse time. The parser is registered for the defaults, frontend, backend and listen sections. The generator learns a //set:validate:<func> directive so a parser can run a validation function before Set() stores data. This keeps the grammar constraints enforced when the type is populated programmatically and not only when a config line is parsed. Only OptionForwarded uses it, so no other generated parser changes. A typed Forwarded model is added to the specification and wired into defaults, frontend and backend, and mapped through the structured configuration in both directions, with the same mutual-exclusion checks applied to the model before it is serialized.
1 parent a33f598 commit df1681f

40 files changed

Lines changed: 2115 additions & 6 deletions

config-parser/generate/config-file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type Data struct {
7676
HasAlias bool
7777
HasDefault bool
7878
HasTable bool
79+
SetValidator string // function used to validate data before Set stores it
7980
}
8081

8182
type ConfigFile struct {

config-parser/generate/normal.tmpl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,18 @@ func (p *{{ .StructName }}) Set(data common.ParserData, index int) error {
184184
{{- else }}
185185
switch newValue := data.(type) {
186186
case *types.{{ .ParserType }}:
187+
{{- if not (eq .SetValidator "") }}
188+
if err := {{ .SetValidator }}(newValue); err != nil {
189+
return err
190+
}
191+
{{- end }}
187192
p.data = newValue
188193
case types.{{ .ParserType }}:
194+
{{- if not (eq .SetValidator "") }}
195+
if err := {{ .SetValidator }}(&newValue); err != nil {
196+
return err
197+
}
198+
{{- end }}
189199
p.data = &newValue
190200
default:
191201
return errors.ErrInvalidData

config-parser/generate/types-normal.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func generateTypes(dir string, dataDir string) { //nolint:gocognit
6868
if strings.HasPrefix(line, "//no:parse") {
6969
parserData.NoParse = true
7070
}
71+
if after, ok := strings.CutPrefix(line, "//set:validate:"); ok {
72+
parserData.SetValidator = strings.TrimSpace(after)
73+
}
7174
if strings.HasPrefix(line, `//test:quote_ok`) && !parserData.Deprecated {
7275
data := strings.SplitN(line, ":", 3)
7376
parserData.TestOKEscaped = append(parserData.TestOKEscaped, data[2])
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/*
2+
Copyright 2026 HAProxy Technologies
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package parsers
18+
19+
import (
20+
"strings"
21+
22+
"github.com/haproxytech/client-native/v6/config-parser/common"
23+
"github.com/haproxytech/client-native/v6/config-parser/errors"
24+
"github.com/haproxytech/client-native/v6/config-parser/types"
25+
)
26+
27+
type OptionForwardedAttributeName string
28+
29+
const (
30+
optionForwardedHost OptionForwardedAttributeName = "host"
31+
optionForwardedHostExpr OptionForwardedAttributeName = "host-expr"
32+
optionForwardedBy OptionForwardedAttributeName = "by"
33+
optionForwardedByExpr OptionForwardedAttributeName = "by-expr"
34+
optionForwardedByPort OptionForwardedAttributeName = "by_port"
35+
optionForwardedByPortExp OptionForwardedAttributeName = "by_port-expr"
36+
optionForwardedFor OptionForwardedAttributeName = "for"
37+
optionForwardedForExpr OptionForwardedAttributeName = "for-expr"
38+
optionForwardedForPort OptionForwardedAttributeName = "for_port"
39+
optionForwardedForPortEx OptionForwardedAttributeName = "for_port-expr"
40+
optionForwardedProto OptionForwardedAttributeName = "proto"
41+
)
42+
43+
type OptionForwarded struct {
44+
data *types.OptionForwarded
45+
preComments []string // comments that appear before the actual line
46+
}
47+
48+
type optionForwardedAttribute struct {
49+
family OptionForwardedAttributeName
50+
needsValue bool
51+
set func(*types.OptionForwarded, string)
52+
}
53+
54+
func optionForwardedAttributeByName(name OptionForwardedAttributeName) (optionForwardedAttribute, bool) {
55+
switch name {
56+
case optionForwardedProto:
57+
return optionForwardedAttribute{
58+
family: optionForwardedProto,
59+
set: func(data *types.OptionForwarded, _ string) {
60+
data.Proto = true
61+
},
62+
}, true
63+
case optionForwardedHost:
64+
return optionForwardedAttribute{
65+
family: optionForwardedHost,
66+
set: func(data *types.OptionForwarded, _ string) {
67+
data.Host = true
68+
},
69+
}, true
70+
case optionForwardedHostExpr:
71+
return optionForwardedAttribute{
72+
family: optionForwardedHost,
73+
needsValue: true,
74+
set: func(data *types.OptionForwarded, value string) { data.HostExpr = value },
75+
}, true
76+
case optionForwardedBy:
77+
return optionForwardedAttribute{
78+
family: optionForwardedBy,
79+
set: func(data *types.OptionForwarded, _ string) {
80+
data.By = true
81+
},
82+
}, true
83+
case optionForwardedByExpr:
84+
return optionForwardedAttribute{
85+
family: optionForwardedBy,
86+
needsValue: true,
87+
set: func(data *types.OptionForwarded, value string) { data.ByExpr = value },
88+
}, true
89+
case optionForwardedByPort:
90+
return optionForwardedAttribute{
91+
family: optionForwardedByPort,
92+
set: func(data *types.OptionForwarded, _ string) {
93+
data.ByPort = true
94+
},
95+
}, true
96+
case optionForwardedByPortExp:
97+
return optionForwardedAttribute{
98+
family: optionForwardedByPort,
99+
needsValue: true,
100+
set: func(data *types.OptionForwarded, value string) { data.ByPortExpr = value },
101+
}, true
102+
case optionForwardedFor:
103+
return optionForwardedAttribute{
104+
family: optionForwardedFor,
105+
set: func(data *types.OptionForwarded, _ string) {
106+
data.For = true
107+
},
108+
}, true
109+
case optionForwardedForExpr:
110+
return optionForwardedAttribute{
111+
family: optionForwardedFor,
112+
needsValue: true,
113+
set: func(data *types.OptionForwarded, value string) { data.ForExpr = value },
114+
}, true
115+
case optionForwardedForPort:
116+
return optionForwardedAttribute{
117+
family: optionForwardedForPort,
118+
set: func(data *types.OptionForwarded, _ string) {
119+
data.ForPort = true
120+
},
121+
}, true
122+
case optionForwardedForPortEx:
123+
return optionForwardedAttribute{
124+
family: optionForwardedForPort,
125+
needsValue: true,
126+
set: func(data *types.OptionForwarded, value string) { data.ForPortExpr = value },
127+
}, true
128+
default:
129+
return optionForwardedAttribute{}, false
130+
}
131+
}
132+
133+
/*
134+
option forwarded [ proto ] [ host | host-expr <host_expr> ] [ by | by-expr <by_expr> ] [ by_port | by_port-expr <by_port_expr>] [ for | for-expr <for_expr> ] [ for_port | for_port-expr <for_port_expr>]
135+
*/
136+
func (s *OptionForwarded) Parse(line string, parts []string, comment string) (string, error) {
137+
if len(parts) > 2 && parts[0] == "no" && parts[1] == "option" && parts[2] == "forwarded" {
138+
if len(parts) != 3 {
139+
return "", errors.ErrInvalidData
140+
}
141+
s.data = &types.OptionForwarded{
142+
NoOption: true,
143+
Comment: comment,
144+
}
145+
return "", nil
146+
}
147+
if len(parts) > 1 && parts[0] == "option" && parts[1] == "forwarded" {
148+
data := &types.OptionForwarded{
149+
Comment: comment,
150+
}
151+
if err := parseOptionForwardedAttributes(data, parts[2:]); err != nil {
152+
return "", err
153+
}
154+
if err := validateOptionForwarded(data); err != nil {
155+
return "", err
156+
}
157+
s.data = data
158+
return "", nil
159+
}
160+
return "", &errors.ParseError{Parser: "option forwarded", Line: line}
161+
}
162+
163+
func parseOptionForwardedAttributes(data *types.OptionForwarded, parts []string) error {
164+
families := map[OptionForwardedAttributeName]bool{}
165+
for index := 0; index < len(parts); index++ {
166+
attribute, ok := optionForwardedAttributeByName(OptionForwardedAttributeName(parts[index]))
167+
if !ok {
168+
return errors.ErrInvalidData
169+
}
170+
if alreadyParsedOptionForwardedFamily(families, attribute.family) {
171+
return errors.ErrInvalidData
172+
}
173+
value := ""
174+
if attribute.needsValue {
175+
index++
176+
if index == len(parts) {
177+
return errors.ErrInvalidData
178+
}
179+
value = parts[index]
180+
}
181+
attribute.set(data, value)
182+
}
183+
return nil
184+
}
185+
186+
func alreadyParsedOptionForwardedFamily(families map[OptionForwardedAttributeName]bool, family OptionForwardedAttributeName) bool {
187+
if families[family] {
188+
return true
189+
}
190+
families[family] = true
191+
return false
192+
}
193+
194+
func validateOptionForwarded(data *types.OptionForwarded) error {
195+
if data == nil {
196+
return errors.ErrInvalidData
197+
}
198+
if data.NoOption {
199+
if data.Proto || data.Host || data.HostExpr != "" || data.By || data.ByExpr != "" ||
200+
data.ByPort || data.ByPortExpr != "" || data.For || data.ForExpr != "" ||
201+
data.ForPort || data.ForPortExpr != "" {
202+
return errors.ErrInvalidData
203+
}
204+
return nil
205+
}
206+
if data.Host && data.HostExpr != "" {
207+
return errors.ErrInvalidData
208+
}
209+
if data.By && data.ByExpr != "" {
210+
return errors.ErrInvalidData
211+
}
212+
if data.ByPort && data.ByPortExpr != "" {
213+
return errors.ErrInvalidData
214+
}
215+
if data.For && data.ForExpr != "" {
216+
return errors.ErrInvalidData
217+
}
218+
if data.ForPort && data.ForPortExpr != "" {
219+
return errors.ErrInvalidData
220+
}
221+
return nil
222+
}
223+
224+
func (s *OptionForwarded) Result() ([]common.ReturnResultLine, error) {
225+
if s.data == nil {
226+
return nil, errors.ErrFetch
227+
}
228+
if err := validateOptionForwarded(s.data); err != nil {
229+
return nil, err
230+
}
231+
var sb strings.Builder
232+
if s.data.NoOption {
233+
sb.WriteString("no ")
234+
}
235+
sb.WriteString("option forwarded")
236+
if !s.data.NoOption {
237+
writeOptionForwardedAttributes(&sb, s.data)
238+
}
239+
return []common.ReturnResultLine{
240+
{
241+
Data: sb.String(),
242+
Comment: s.data.Comment,
243+
},
244+
}, nil
245+
}
246+
247+
func writeOptionForwardedAttributes(sb *strings.Builder, data *types.OptionForwarded) {
248+
if data.Proto {
249+
writeOptionForwardedAttribute(sb, optionForwardedProto)
250+
}
251+
if data.Host {
252+
writeOptionForwardedAttribute(sb, optionForwardedHost)
253+
}
254+
if data.HostExpr != "" {
255+
writeOptionForwardedAttributeValue(sb, optionForwardedHostExpr, data.HostExpr)
256+
}
257+
if data.By {
258+
writeOptionForwardedAttribute(sb, optionForwardedBy)
259+
}
260+
if data.ByExpr != "" {
261+
writeOptionForwardedAttributeValue(sb, optionForwardedByExpr, data.ByExpr)
262+
}
263+
if data.ByPort {
264+
writeOptionForwardedAttribute(sb, optionForwardedByPort)
265+
}
266+
if data.ByPortExpr != "" {
267+
writeOptionForwardedAttributeValue(sb, optionForwardedByPortExp, data.ByPortExpr)
268+
}
269+
if data.For {
270+
writeOptionForwardedAttribute(sb, optionForwardedFor)
271+
}
272+
if data.ForExpr != "" {
273+
writeOptionForwardedAttributeValue(sb, optionForwardedForExpr, data.ForExpr)
274+
}
275+
if data.ForPort {
276+
writeOptionForwardedAttribute(sb, optionForwardedForPort)
277+
}
278+
if data.ForPortExpr != "" {
279+
writeOptionForwardedAttributeValue(sb, optionForwardedForPortEx, data.ForPortExpr)
280+
}
281+
}
282+
283+
func writeOptionForwardedAttribute(sb *strings.Builder, name OptionForwardedAttributeName) {
284+
sb.WriteString(" ")
285+
sb.WriteString(string(name))
286+
}
287+
288+
func writeOptionForwardedAttributeValue(sb *strings.Builder, name OptionForwardedAttributeName, value string) {
289+
writeOptionForwardedAttribute(sb, name)
290+
sb.WriteString(" ")
291+
sb.WriteString(value)
292+
}

0 commit comments

Comments
 (0)