@@ -342,13 +342,57 @@ func (s *SVCBMandatory) copy() SVCBKeyValue {
342
342
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
343
343
// e := new(dns.SVCBAlpn)
344
344
// e.Alpn = []string{"h2", "http/1.1"}
345
- // h.Value = append(o .Value, e)
345
+ // h.Value = append(h .Value, e)
346
346
type SVCBAlpn struct {
347
347
Alpn []string
348
348
}
349
349
350
- func (* SVCBAlpn ) Key () SVCBKey { return SVCB_ALPN }
351
- func (s * SVCBAlpn ) String () string { return strings .Join (s .Alpn , "," ) }
350
+ func (* SVCBAlpn ) Key () SVCBKey { return SVCB_ALPN }
351
+
352
+ func (s * SVCBAlpn ) String () string {
353
+ // An ALPN value is a comma-separated list of values, each of which can be
354
+ // an arbitrary binary value. In order to allow parsing, the comma and
355
+ // backslash characters are themselves excaped.
356
+ //
357
+ // However, this escaping is done in addition to the normal escaping which
358
+ // happens in zone files, meaning that these values must be
359
+ // double-escaped. This looks terrible, so if you see a never-ending
360
+ // sequence of backslash in a zone file this may be why.
361
+ //
362
+ // https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-08#appendix-A.1
363
+ var str strings.Builder
364
+ for i , alpn := range s .Alpn {
365
+ // 4*len(alpn) is the worst case where we escape every character in the alpn as \123, plus 1 byte for the ',' separating the alpn from others
366
+ str .Grow (4 * len (alpn ) + 1 )
367
+ if i > 0 {
368
+ str .WriteByte (',' )
369
+ }
370
+ for j := 0 ; j < len (alpn ); j ++ {
371
+ e := alpn [j ]
372
+ if ' ' > e || e > '~' {
373
+ str .WriteString (escapeByte (e ))
374
+ continue
375
+ }
376
+ switch e {
377
+ // We escape a few characters which may confuse humans or parsers.
378
+ case '"' , ';' , ' ' :
379
+ str .WriteByte ('\\' )
380
+ str .WriteByte (e )
381
+ // The comma and backslash characters themselves must be
382
+ // doubly-escaped. We use `\\` for the first backslash and
383
+ // the escaped numeric value for the other value. We especially
384
+ // don't want a comma in the output.
385
+ case ',' :
386
+ str .WriteString (`\\\044` )
387
+ case '\\' :
388
+ str .WriteString (`\\\092` )
389
+ default :
390
+ str .WriteByte (e )
391
+ }
392
+ }
393
+ }
394
+ return str .String ()
395
+ }
352
396
353
397
func (s * SVCBAlpn ) pack () ([]byte , error ) {
354
398
// Liberally estimate the size of an alpn as 10 octets
@@ -383,7 +427,47 @@ func (s *SVCBAlpn) unpack(b []byte) error {
383
427
}
384
428
385
429
func (s * SVCBAlpn ) parse (b string ) error {
386
- s .Alpn = strings .Split (b , "," )
430
+ if len (b ) == 0 {
431
+ s .Alpn = []string {}
432
+ return nil
433
+ }
434
+
435
+ alpn := []string {}
436
+ a := []byte {}
437
+ for p := 0 ; p < len (b ); {
438
+ c , q := nextByte (b , p )
439
+ if q == 0 {
440
+ return errors .New ("dns: svcbalpn: unterminated escape" )
441
+ }
442
+ p += q
443
+ // If we find a comma, we have finished reading an alpn.
444
+ if c == ',' {
445
+ if len (a ) == 0 {
446
+ return errors .New ("dns: svcbalpn: empty protocol identifier" )
447
+ }
448
+ alpn = append (alpn , string (a ))
449
+ a = []byte {}
450
+ continue
451
+ }
452
+ // If it's a backslash, we need to handle a comma-separated list.
453
+ if c == '\\' {
454
+ dc , dq := nextByte (b , p )
455
+ if dq == 0 {
456
+ return errors .New ("dns: svcbalpn: unterminated escape decoding comma-separated list" )
457
+ }
458
+ if dc != '\\' && dc != ',' {
459
+ return errors .New ("dns: svcbalpn: bad escaped character decoding comma-separated list" )
460
+ }
461
+ p += dq
462
+ c = dc
463
+ }
464
+ a = append (a , c )
465
+ }
466
+ // Add the final alpn.
467
+ if len (a ) == 0 {
468
+ return errors .New ("dns: svcbalpn: last protocol identifier empty" )
469
+ }
470
+ s .Alpn = append (alpn , string (a ))
387
471
return nil
388
472
}
389
473
0 commit comments