@@ -18,6 +18,7 @@ package configuration
1818import (
1919 "errors"
2020 "fmt"
21+ "strings"
2122
2223 strfmt "github.com/go-openapi/strfmt"
2324 parser "github.com/haproxytech/client-native/v6/config-parser"
@@ -159,14 +160,18 @@ func ParseAcmeProvider(p parser.Parser, name string) (*models.AcmeProvider, erro
159160 }
160161 }
161162
163+ var varsStr string
164+
162165 stringAttr := map [string ]* string {
163- "account-key" : & acme .AccountKey ,
164- "challenge" : & acme .Challenge ,
165- "contact" : & acme .Contact ,
166- "curves" : & acme .Curves ,
167- "directory" : & acme .Directory ,
168- "keytype" : & acme .Keytype ,
169- "map" : & acme .Map ,
166+ "account-key" : & acme .AccountKey ,
167+ "acme-provider" : & acme .AcmeProvider ,
168+ "acme-vars" : & varsStr ,
169+ "challenge" : & acme .Challenge ,
170+ "contact" : & acme .Contact ,
171+ "curves" : & acme .Curves ,
172+ "directory" : & acme .Directory ,
173+ "keytype" : & acme .Keytype ,
174+ "map" : & acme .Map ,
170175 }
171176
172177 for kw , dest := range stringAttr {
@@ -198,6 +203,9 @@ func ParseAcmeProvider(p parser.Parser, name string) (*models.AcmeProvider, erro
198203 acme .Bits = misc .Ptr (ic .Value )
199204 }
200205
206+ // acme-vars
207+ acme .AcmeVars = parseAcmeVars (varsStr )
208+
201209 return acme , nil
202210}
203211
@@ -216,14 +224,21 @@ func SerializeAcmeProvider(p parser.Parser, acme *models.AcmeProvider) error {
216224 }
217225 }
218226
227+ acmeVars , err := serializeAcmeVars (acme .AcmeVars )
228+ if err != nil {
229+ return fmt .Errorf ("acme %s: %w" , acme .Name , err )
230+ }
231+
219232 stringAttr := map [string ]string {
220- "account-key" : acme .AccountKey ,
221- "challenge" : acme .Challenge ,
222- "contact" : acme .Contact ,
223- "curves" : acme .Curves ,
224- "directory" : acme .Directory ,
225- "keytype" : acme .Keytype ,
226- "map" : acme .Map ,
233+ "account-key" : acme .AccountKey ,
234+ "acme-provider" : acme .AcmeProvider ,
235+ "acme-vars" : acmeVars ,
236+ "challenge" : acme .Challenge ,
237+ "contact" : acme .Contact ,
238+ "curves" : acme .Curves ,
239+ "directory" : acme .Directory ,
240+ "keytype" : acme .Keytype ,
241+ "map" : acme .Map ,
227242 }
228243
229244 for kw , val := range stringAttr {
@@ -246,3 +261,99 @@ func SerializeAcmeProvider(p parser.Parser, acme *models.AcmeProvider) error {
246261
247262 return nil
248263}
264+
265+ // acme-vars "key=value,foo=\"bar baz\""
266+ func serializeAcmeVars (vars map [string ]string ) (string , error ) {
267+ if len (vars ) == 0 {
268+ return "" , nil
269+ }
270+
271+ var sb strings.Builder
272+ first := true
273+
274+ sb .WriteByte ('"' )
275+ for k , v := range vars {
276+ if len (k ) == 0 {
277+ continue
278+ }
279+ if ! acmeValidKey (k ) {
280+ return "" , fmt .Errorf ("acme-vars: invalid character found in key '%s'" , k )
281+ }
282+ if first {
283+ first = false
284+ } else {
285+ sb .WriteByte (',' )
286+ }
287+ sb .WriteString (k )
288+ sb .WriteByte ('=' )
289+ sb .WriteString (acmeVarEscape (v ))
290+ }
291+ sb .WriteByte ('"' )
292+
293+ return sb .String (), nil
294+ }
295+
296+ func parseAcmeVars (vars string ) map [string ]string {
297+ n := len (vars )
298+ if n == 0 {
299+ return nil
300+ }
301+
302+ if vars [0 ] == '"' && vars [n - 1 ] == '"' {
303+ vars = vars [1 : n - 1 ]
304+ }
305+
306+ vars = strings .TrimSpace (vars )
307+ if len (vars ) == 0 {
308+ return nil
309+ }
310+
311+ vlist := acmeVarSplit (vars )
312+ vmap := make (map [string ]string , len (vlist ))
313+ for _ , keyval := range vlist {
314+ if k , v , found := strings .Cut (strings .TrimSpace (keyval ), "=" ); found {
315+ if len (k ) > 0 {
316+ vmap [k ] = acmeVarUnescape (v )
317+ }
318+ }
319+ }
320+
321+ if len (vmap ) == 0 {
322+ return nil
323+ }
324+ return vmap
325+ }
326+
327+ // Split string by ',' but not escaped commas "\,".
328+ func acmeVarSplit (s string ) []string {
329+ s = strings .ReplaceAll (s , `\,` , "\x00 " )
330+ tokens := strings .Split (s , "," )
331+ for i , token := range tokens {
332+ tokens [i ] = strings .ReplaceAll (token , "\x00 " , `\,` )
333+ }
334+ return tokens
335+ }
336+
337+ func acmeVarEscape (s string ) string {
338+ s = strings .ReplaceAll (s , `"` , `\"` )
339+ s = strings .ReplaceAll (s , `,` , `\,` )
340+ return s
341+ }
342+
343+ func acmeVarUnescape (s string ) string {
344+ s = strings .ReplaceAll (s , `\"` , `"` )
345+ s = strings .ReplaceAll (s , `\,` , `,` )
346+ return s
347+ }
348+
349+ // Variable keys must also be valid Go variable names.
350+ func acmeValidKey (key string ) bool {
351+ for _ , c := range key {
352+ match := ('A' <= c && c <= 'Z' ) || ('a' <= c && c <= 'z' ) ||
353+ ('0' <= c && c <= '9' ) || c == '_'
354+ if ! match {
355+ return false
356+ }
357+ }
358+ return true
359+ }
0 commit comments