* Invalid NSEC/3 bitmap on non-zero buffer If the PackBuffer is used to encode an NSEC/3 record, the bitmap is xored with the content of the buffer instead of being zeroed first. The algorithm has been changed so it is able zero bytes without losing too much performance (around 2x slower). * Add some comments + rename some vars to make algo clearer * Revert to previous algo with window length compute+0 on new window * Use typeBitMapLen to compute the bitmap length to zero
594 lines
14 KiB
Go
594 lines
14 KiB
Go
package dns
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"testing"
|
|
)
|
|
|
|
// TestPacketDataNsec tests generated using fuzz.go and with a message pack
|
|
// containing the following bytes: 0000\x00\x00000000\x00\x002000000\x0060000\x00\x130000000000000000000"
|
|
// That bytes sequence created the overflow error and further permutations of that sequence were able to trigger
|
|
// the other code paths.
|
|
func TestPackDataNsec(t *testing.T) {
|
|
type args struct {
|
|
bitmap []uint16
|
|
msg []byte
|
|
off int
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantOff int
|
|
wantBytes []byte
|
|
wantErr bool
|
|
wantErrMsg string
|
|
}{
|
|
{
|
|
name: "overflow",
|
|
args: args{
|
|
bitmap: []uint16{
|
|
8962, 8963, 8970, 8971, 8978, 8979,
|
|
8986, 8987, 8994, 8995, 9002, 9003,
|
|
9010, 9011, 9018, 9019, 9026, 9027,
|
|
9034, 9035, 9042, 9043, 9050, 9051,
|
|
9058, 9059, 9066,
|
|
},
|
|
msg: []byte{
|
|
48, 48, 48, 48, 0, 0, 0,
|
|
1, 0, 0, 0, 0, 0, 0, 50,
|
|
48, 48, 48, 48, 48, 48,
|
|
0, 54, 48, 48, 48, 48,
|
|
0, 19, 48, 48,
|
|
},
|
|
off: 48,
|
|
},
|
|
wantErr: true,
|
|
wantErrMsg: "dns: overflow packing nsec",
|
|
wantOff: 48,
|
|
},
|
|
{
|
|
name: "disordered nsec bits",
|
|
args: args{
|
|
bitmap: []uint16{
|
|
8962,
|
|
1,
|
|
},
|
|
msg: []byte{
|
|
48, 48, 48, 48, 0, 0, 0, 1, 0, 0, 0, 0,
|
|
0, 0, 50, 48, 48, 48, 48, 48, 48, 0, 54, 48,
|
|
48, 48, 48, 0, 19, 48, 48, 48, 48, 48, 48, 0,
|
|
0, 0, 1, 0, 0, 0, 0, 0, 0, 50, 48, 48,
|
|
48, 48, 48, 48, 0, 54, 48, 48, 48, 48, 0, 19,
|
|
48, 48, 48, 48, 48, 48, 0, 0, 0, 1, 0, 0,
|
|
0, 0, 0, 0, 50, 48, 48, 48, 48, 48, 48, 0,
|
|
54, 48, 48, 48, 48, 0, 19, 48, 48, 48, 48, 48,
|
|
48, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 50,
|
|
48, 48, 48, 48, 48, 48, 0, 54, 48, 48, 48, 48,
|
|
0, 19, 48, 48, 48, 48, 48, 48, 0, 0, 0, 1,
|
|
0, 0, 0, 0, 0, 0, 50, 48, 48, 48, 48, 48,
|
|
48, 0, 54, 48, 48, 48, 48, 0, 19, 48, 48,
|
|
},
|
|
off: 0,
|
|
},
|
|
wantErr: true,
|
|
wantErrMsg: "dns: nsec bits out of order",
|
|
wantOff: 155,
|
|
},
|
|
{
|
|
name: "simple message with only one window",
|
|
args: args{
|
|
bitmap: []uint16{
|
|
1,
|
|
},
|
|
msg: []byte{
|
|
48, 48, 48, 48, 0, 0,
|
|
0, 1, 0, 0, 0, 0,
|
|
0, 0, 50, 48, 48, 48,
|
|
48, 48, 48, 0, 54, 48,
|
|
48, 48, 48, 0, 19, 48, 48,
|
|
},
|
|
off: 0,
|
|
},
|
|
wantErr: false,
|
|
wantOff: 3,
|
|
wantBytes: []byte{0, 1, 64},
|
|
},
|
|
{
|
|
name: "multiple types",
|
|
args: args{
|
|
bitmap: []uint16{
|
|
TypeNS, TypeSOA, TypeRRSIG, TypeDNSKEY, TypeNSEC3PARAM,
|
|
},
|
|
msg: []byte{
|
|
48, 48, 48, 48, 0, 0,
|
|
0, 1, 0, 0, 0, 0,
|
|
0, 0, 50, 48, 48, 48,
|
|
48, 48, 48, 0, 54, 48,
|
|
48, 48, 48, 0, 19, 48, 48,
|
|
},
|
|
off: 0,
|
|
},
|
|
wantErr: false,
|
|
wantOff: 9,
|
|
wantBytes: []byte{0, 7, 34, 0, 0, 0, 0, 2, 144},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotOff, err := packDataNsec(tt.args.bitmap, tt.args.msg, tt.args.off)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("packDataNsec() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if err != nil && tt.wantErrMsg != err.Error() {
|
|
t.Errorf("packDataNsec() error msg = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
|
|
return
|
|
}
|
|
if gotOff != tt.wantOff {
|
|
t.Errorf("packDataNsec() = %v, want off %v", gotOff, tt.wantOff)
|
|
}
|
|
if err == nil && tt.args.off < len(tt.args.msg) && gotOff < len(tt.args.msg) {
|
|
if want, got := tt.wantBytes, tt.args.msg[tt.args.off:gotOff]; !bytes.Equal(got, want) {
|
|
t.Errorf("packDataNsec() = %v, want bytes %v", got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPackDataNsecDirtyBuffer(t *testing.T) {
|
|
zeroBuf := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0}
|
|
dirtyBuf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
|
off1, _ := packDataNsec([]uint16{TypeNS, TypeSOA, TypeRRSIG}, zeroBuf, 0)
|
|
off2, _ := packDataNsec([]uint16{TypeNS, TypeSOA, TypeRRSIG}, dirtyBuf, 0)
|
|
if off1 != off2 {
|
|
t.Errorf("off1 %v != off2 %v", off1, off2)
|
|
}
|
|
if !bytes.Equal(zeroBuf[:off1], dirtyBuf[:off2]) {
|
|
t.Errorf("dirty buffer differs from zero buffer: %v, %v", zeroBuf[:off1], dirtyBuf[:off2])
|
|
}
|
|
}
|
|
|
|
func BenchmarkPackDataNsec(b *testing.B) {
|
|
benches := []struct {
|
|
name string
|
|
types []uint16
|
|
}{
|
|
{"empty", nil},
|
|
{"typical", []uint16{TypeNS, TypeSOA, TypeRRSIG, TypeDNSKEY, TypeNSEC3PARAM}},
|
|
{"multiple_windows", []uint16{1, 300, 350, 10000, 20000}},
|
|
}
|
|
for _, bb := range benches {
|
|
b.Run(bb.name, func(b *testing.B) {
|
|
buf := make([]byte, 100)
|
|
for n := 0; n < b.N; n++ {
|
|
packDataNsec(bb.types, buf, 0)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestUnpackString(t *testing.T) {
|
|
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm\x7f")
|
|
msg[0] = byte(len(msg) - 1)
|
|
|
|
got, _, err := unpackString(msg, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if want := `abcdef\015\\\"ghi\004mmm\127`; want != got {
|
|
t.Errorf("expected %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUnpackString(b *testing.B) {
|
|
b.Run("Escaped", func(b *testing.B) {
|
|
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm")
|
|
msg[0] = byte(len(msg) - 1)
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
got, _, err := unpackString(msg, 0)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
if want := `abcdef\015\\\"ghi\004mmm`; want != got {
|
|
b.Errorf("expected %q, got %q", want, got)
|
|
}
|
|
}
|
|
})
|
|
b.Run("Unescaped", func(b *testing.B) {
|
|
msg := []byte("\x00large.example.com")
|
|
msg[0] = byte(len(msg) - 1)
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
got, _, err := unpackString(msg, 0)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
if want := "large.example.com"; want != got {
|
|
b.Errorf("expected %q, got %q", want, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPackDataAplPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
negation bool
|
|
ip net.IP
|
|
mask net.IPMask
|
|
expect []byte
|
|
}{
|
|
{
|
|
"1:192.0.2.0/24",
|
|
false,
|
|
net.ParseIP("192.0.2.0").To4(),
|
|
net.CIDRMask(24, 32),
|
|
[]byte{0x00, 0x01, 0x18, 0x03, 192, 0, 2},
|
|
},
|
|
{
|
|
"2:2001:db8:cafe::0/48",
|
|
false,
|
|
net.ParseIP("2001:db8:cafe::"),
|
|
net.CIDRMask(48, 128),
|
|
[]byte{0x00, 0x02, 0x30, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe},
|
|
},
|
|
{
|
|
"with trailing zero bytes 2:2001:db8:cafe::0/64",
|
|
false,
|
|
net.ParseIP("2001:db8:cafe::"),
|
|
net.CIDRMask(64, 128),
|
|
[]byte{0x00, 0x02, 0x40, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe},
|
|
},
|
|
{
|
|
"no non-zero bytes 2::/16",
|
|
false,
|
|
net.ParseIP("::"),
|
|
net.CIDRMask(16, 128),
|
|
[]byte{0x00, 0x02, 0x10, 0x00},
|
|
},
|
|
{
|
|
"!2:2001:db8::/32",
|
|
true,
|
|
net.ParseIP("2001:db8::"),
|
|
net.CIDRMask(32, 128),
|
|
[]byte{0x00, 0x02, 0x20, 0x84, 0x20, 0x01, 0x0d, 0xb8},
|
|
},
|
|
{
|
|
"normalize 1:198.51.103.255/22",
|
|
false,
|
|
net.ParseIP("198.51.103.255").To4(),
|
|
net.CIDRMask(22, 32),
|
|
[]byte{0x00, 0x01, 0x16, 0x03, 198, 51, 100}, // 1:198.51.100.0/22
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ap := &APLPrefix{
|
|
Negation: tt.negation,
|
|
Network: net.IPNet{IP: tt.ip, Mask: tt.mask},
|
|
}
|
|
out := make([]byte, 16)
|
|
off, err := packDataAplPrefix(ap, out, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if !bytes.Equal(tt.expect, out[:off]) {
|
|
t.Fatalf("expected output %02x, got %02x", tt.expect, out[:off])
|
|
}
|
|
// Make sure the packed bytes would be accepted by its own unpack
|
|
_, _, err = unpackDataAplPrefix(out, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPackDataAplPrefix_Failures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ip net.IP
|
|
mask net.IPMask
|
|
}{
|
|
{
|
|
"family mismatch",
|
|
net.ParseIP("2001:db8::"),
|
|
net.CIDRMask(24, 32),
|
|
},
|
|
{
|
|
"unrecognized family",
|
|
[]byte{0x42},
|
|
[]byte{0xff},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ap := &APLPrefix{Network: net.IPNet{IP: tt.ip, Mask: tt.mask}}
|
|
msg := make([]byte, 16)
|
|
off, err := packDataAplPrefix(ap, msg, 0)
|
|
if err == nil {
|
|
t.Fatal("expected error, got none")
|
|
}
|
|
if off != len(msg) {
|
|
t.Fatalf("expected %d, got %d", len(msg), off)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPackDataAplPrefix_BufferBounds(t *testing.T) {
|
|
ap := &APLPrefix{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("2001:db8::"),
|
|
Mask: net.CIDRMask(32, 128),
|
|
},
|
|
}
|
|
wire := []byte{0x00, 0x02, 0x20, 0x04, 0x20, 0x01, 0x0d, 0xb8}
|
|
|
|
t.Run("small", func(t *testing.T) {
|
|
msg := make([]byte, len(wire))
|
|
_, err := packDataAplPrefix(ap, msg, 1) // offset
|
|
if err == nil {
|
|
t.Fatal("expected error, got none")
|
|
}
|
|
})
|
|
|
|
t.Run("exact fit", func(t *testing.T) {
|
|
msg := make([]byte, len(wire))
|
|
off, err := packDataAplPrefix(ap, msg, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if !bytes.Equal(wire, msg[:off]) {
|
|
t.Fatalf("expected %02x, got %02x", wire, msg[:off])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPackDataApl(t *testing.T) {
|
|
in := []APLPrefix{
|
|
{
|
|
Negation: true,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("198.51.0.0").To4(),
|
|
Mask: net.CIDRMask(16, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("2001:db8:beef::"),
|
|
Mask: net.CIDRMask(48, 128),
|
|
},
|
|
},
|
|
}
|
|
expect := []byte{
|
|
// 1:192.51.0.0/16
|
|
0x00, 0x01, 0x10, 0x82, 0xc6, 0x33,
|
|
// 2:2001:db8:beef::0/48
|
|
0x00, 0x02, 0x30, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xbe, 0xef,
|
|
}
|
|
|
|
msg := make([]byte, 32)
|
|
off, err := packDataApl(in, msg, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if !bytes.Equal(expect, msg[:off]) {
|
|
t.Fatalf("expected %02x, got %02x", expect, msg[:off])
|
|
}
|
|
}
|
|
|
|
func TestUnpackDataAplPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wire []byte
|
|
negation bool
|
|
ip net.IP
|
|
mask net.IPMask
|
|
}{
|
|
{
|
|
"1:192.0.2.0/24",
|
|
[]byte{0x00, 0x01, 0x18, 0x03, 192, 0, 2},
|
|
false,
|
|
net.ParseIP("192.0.2.0").To4(),
|
|
net.CIDRMask(24, 32),
|
|
},
|
|
{
|
|
"2:2001:db8::/32",
|
|
[]byte{0x00, 0x02, 0x20, 0x04, 0x20, 0x01, 0x0d, 0xb8},
|
|
false,
|
|
net.ParseIP("2001:db8::"),
|
|
net.CIDRMask(32, 128),
|
|
},
|
|
{
|
|
"!2:2001:db8:8000::/33",
|
|
[]byte{0x00, 0x02, 0x21, 0x85, 0x20, 0x01, 0x0d, 0xb8, 0x80},
|
|
true,
|
|
net.ParseIP("2001:db8:8000::"),
|
|
net.CIDRMask(33, 128),
|
|
},
|
|
{
|
|
"1:0.0.0.0/0",
|
|
[]byte{0x00, 0x01, 0x00, 0x00},
|
|
false,
|
|
net.ParseIP("0.0.0.0").To4(),
|
|
net.CIDRMask(0, 32),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, off, err := unpackDataAplPrefix(tt.wire, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if off != len(tt.wire) {
|
|
t.Fatalf("expected offset %d, got %d", len(tt.wire), off)
|
|
}
|
|
if got.Negation != tt.negation {
|
|
t.Errorf("expected negation %v, got %v", tt.negation, got.Negation)
|
|
}
|
|
if !bytes.Equal(got.Network.IP, tt.ip) {
|
|
t.Errorf("expected IP %02x, got %02x", tt.ip, got.Network.IP)
|
|
}
|
|
if !bytes.Equal(got.Network.Mask, tt.mask) {
|
|
t.Errorf("expected mask %02x, got %02x", tt.mask, got.Network.Mask)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnpackDataAplPrefix_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wire []byte
|
|
want string
|
|
}{
|
|
{
|
|
"incomplete header",
|
|
[]byte{0x00, 0x01, 0x18},
|
|
"dns: overflow unpacking APL prefix",
|
|
},
|
|
{
|
|
"unrecognized family",
|
|
[]byte{0x00, 0x03, 0x00, 0x00},
|
|
"dns: unrecognized APL address family",
|
|
},
|
|
{
|
|
"prefix too large for family IPv4",
|
|
[]byte{0x00, 0x01, 0x21, 0x04, 192, 0, 2, 0},
|
|
"dns: APL prefix too long",
|
|
},
|
|
{
|
|
"prefix too large for family IPv6",
|
|
[]byte{0x00, 0x02, 0x81, 0x85, 0x20, 0x01, 0x0d, 0xb8, 0x80},
|
|
"dns: APL prefix too long",
|
|
},
|
|
{
|
|
"afdlen too long for address family IPv4",
|
|
[]byte{0x00, 0x01, 22, 0x05, 192, 0, 2, 0, 0},
|
|
"dns: APL length too long",
|
|
},
|
|
{
|
|
"overflow unpacking APL address",
|
|
[]byte{0x00, 0x01, 0x10, 0x02, 192},
|
|
"dns: overflow unpacking APL address",
|
|
},
|
|
{
|
|
"address included trailing zeros",
|
|
[]byte{0x00, 0x01, 0x10, 0x04, 192, 0, 2, 0},
|
|
"dns: extra APL address bits",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, _, err := unpackDataAplPrefix(tt.wire, 0)
|
|
if err == nil {
|
|
t.Fatal("expected error, got none")
|
|
}
|
|
|
|
if err.Error() != tt.want {
|
|
t.Errorf("expected %s, got %s", tt.want, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnpackDataApl(t *testing.T) {
|
|
wire := []byte{
|
|
// 2:2001:db8:cafe:4200:0/56
|
|
0x00, 0x02, 0x38, 0x07, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe, 0x42,
|
|
// 1:192.0.2.0/24
|
|
0x00, 0x01, 0x18, 0x03, 192, 0, 2,
|
|
// !1:192.0.2.128/25
|
|
0x00, 0x01, 0x19, 0x84, 192, 0, 2, 128,
|
|
// 1:10.0.0.0/24
|
|
0x00, 0x01, 0x18, 0x01, 0x0a,
|
|
// !1:10.0.0.1/32
|
|
0x00, 0x01, 0x20, 0x84, 0x0a, 0, 0, 1,
|
|
// !1:0.0.0.0/0
|
|
0x00, 0x01, 0x00, 0x80,
|
|
// 2::0/0
|
|
0x00, 0x02, 0x00, 0x00,
|
|
}
|
|
expect := []APLPrefix{
|
|
{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("2001:db8:cafe:4200::"),
|
|
Mask: net.CIDRMask(56, 128),
|
|
},
|
|
},
|
|
{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("192.0.2.0").To4(),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: true,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("192.0.2.128").To4(),
|
|
Mask: net.CIDRMask(25, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("10.0.0.0").To4(),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: true,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("10.0.0.1").To4(),
|
|
Mask: net.CIDRMask(32, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: true,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("0.0.0.0").To4(),
|
|
Mask: net.CIDRMask(0, 32),
|
|
},
|
|
},
|
|
{
|
|
Negation: false,
|
|
Network: net.IPNet{
|
|
IP: net.ParseIP("::").To16(),
|
|
Mask: net.CIDRMask(0, 128),
|
|
},
|
|
},
|
|
}
|
|
|
|
got, off, err := unpackDataApl(wire, 0)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if off != len(wire) {
|
|
t.Fatalf("expected offset %d, got %d", len(wire), off)
|
|
}
|
|
if len(got) != len(expect) {
|
|
t.Fatalf("expected %d prefixes, got %d", len(expect), len(got))
|
|
}
|
|
for i, exp := range expect {
|
|
if got[i].Negation != exp.Negation {
|
|
t.Errorf("[%d] expected negation %v, got %v", i, exp.Negation, got[i].Negation)
|
|
}
|
|
if !bytes.Equal(got[i].Network.IP, exp.Network.IP) {
|
|
t.Errorf("[%d] expected IP %02x, got %02x", i, exp.Network.IP, got[i].Network.IP)
|
|
}
|
|
if !bytes.Equal(got[i].Network.Mask, exp.Network.Mask) {
|
|
t.Errorf("[%d] expected mask %02x, got %02x", i, exp.Network.Mask, got[i].Network.Mask)
|
|
}
|
|
}
|
|
}
|