Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix netconf 1.1 chunk parse #181

Merged
merged 3 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 82 additions & 23 deletions response/netconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package response

import (
"bytes"
"errors"
"fmt"
"regexp"
"strconv"
Expand All @@ -16,11 +17,21 @@ const (
v1Dot1 = "1.1"
v1Dot0Delim = "]]>]]>"
xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"

// https://datatracker.ietf.org/doc/html/rfc6242#section-4.2 max chunk size is 4294967295 so
// for us that is a max of 10 chars that the chunk size could be when we are parsing it out of
// raw bytes.
maxChunkSizeCharLen = 10
)

var errNetconf1Dot1Error = errors.New("unable to parse netconf 1.1 response")

func errNetconf1Dot1ParseError(msg string) error {
return fmt.Errorf("%w: %s", errNetconf1Dot1Error, msg)
}

type netconfPatterns struct {
v1dot1Chunk *regexp.Regexp
rpcErrors *regexp.Regexp
rpcErrors *regexp.Regexp
}

var (
Expand All @@ -31,8 +42,7 @@ var (
func getNetconfPatterns() *netconfPatterns {
netconfPatternsInstanceOnce.Do(func() {
netconfPatternsInstance = &netconfPatterns{
v1dot1Chunk: regexp.MustCompile(`(?ms)(\d+)\n(.*?)^#`), //nolint:gocritic
rpcErrors: regexp.MustCompile(`(?s)<rpc-errors?>(.*)</rpc-errors?>`),
rpcErrors: regexp.MustCompile(`(?s)<rpc-errors?>(.*)</rpc-errors?>`),
}
})

Expand Down Expand Up @@ -120,42 +130,91 @@ func (r *NetconfResponse) record1dot0() {
r.Result = string(bytes.TrimSpace(b))
}

func (r *NetconfResponse) validateChunk(i int, b []byte) {
// does this need more ... "massaging" like scrapli?
// chunk regex matches the newline before the chunk size or end of message delimiter, so we
// subtract one for that newline char
if len(b)-1 != i {
errMsg := fmt.Sprintf("return element lengh invalid, expted: %d, got %d for element: %s\n",
i,
len(b)-1,
b)

func (r *NetconfResponse) record1dot1() {
err := r.record1dot1Chunks()
if err != nil {
r.Failed = &OperationError{
Input: string(r.Input),
Output: r.Result,
ErrorString: errMsg,
ErrorString: err.Error(),
}
}
}

func (r *NetconfResponse) record1dot1() {
patterns := getNetconfPatterns()
func (r *NetconfResponse) record1dot1Chunks() error {
d := bytes.TrimSpace(r.RawResult)

chunkSections := patterns.v1dot1Chunk.FindAllSubmatch(r.RawResult, -1)
if len(d) == 0 || d[0] != byte('#') {
return errNetconf1Dot1ParseError(
"unable to parse netconf response: no chunk marker at start of data",
)
}

var joined []byte

for _, chunkSection := range chunkSections {
chunk := chunkSection[2]
var cursor int

for cursor < len(d) {
if d[cursor] == byte('\n') {
// we don't need this at the start of this loop, but this lets us easily handle newlines
// between chunks
cursor++

continue
}

size, _ := strconv.Atoi(string(chunkSection[1]))
if d[cursor] != byte('#') {
return errNetconf1Dot1ParseError(fmt.Sprintf(
"unable to parse netconf response: chunk marker missing, got '%s'",
string(d[cursor])))
}

r.validateChunk(size, chunk)
cursor++

joined = append(joined, chunk[:len(chunk)-1]...)
if d[cursor] == byte('#') {
break
}

var chunkSizeStr string

for chunkSizeLen := 0; chunkSizeLen <= maxChunkSizeCharLen; chunkSizeLen++ {
if d[cursor+chunkSizeLen] == byte('\n') {
chunkSizeStr = string(d[cursor : cursor+chunkSizeLen])

cursor += chunkSizeLen + 1

break
}
}

if chunkSizeStr == "" {
return errNetconf1Dot1ParseError(
"unable to parse netconf response: failed parsing chunk size",
)
}

chunkSize, err := strconv.Atoi(chunkSizeStr)
if err != nil {
return errNetconf1Dot1ParseError(
fmt.Sprintf(
"unable to parse netconf response: unable to parse chunk size '%s': %s",
chunkSizeStr,
err,
),
)
}

joined = append(joined, d[cursor:cursor+chunkSize]...)

// obviously no reason to iterate over the chunk we just yoinked out, so increment the
// cursor accordingly -- we can ignore newlines after the chunk since we handle that at
// the top of this loop
cursor += chunkSize
}

joined = bytes.TrimPrefix(joined, []byte(xmlHeader))

r.Result = string(bytes.TrimSpace(joined))

return nil
}
5 changes: 5 additions & 0 deletions response/netconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ func TestNetconfRecord(t *testing.T) {
version: "1.1",
payloadFile: "netconf-output-11.txt",
},
"record-response-11b": {
description: "a tricky banner to parse test to test recording netconf 1.1 response",
version: "1.1",
payloadFile: "netconf-output-11b.txt",
},
}

for testName, testCase := range cases {
Expand Down
1 change: 1 addition & 0 deletions response/test-fixtures/golden/record-response-11b-out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Host":"localhost","Port":830,"Input":null,"FramedInput":null,"RawResult":"IzI3OQogIDxiYW5uZXJzIHhtbG5zPSJodHRwOi8vY2lzY28uY29tL25zL3lhbmcvQ2lzY28tSU9TLVhSLWluZnJhLWluZnJhLWNmZyI+CiAgIDxiYW5uZXI+CiAgICA8YmFubmVyLW5hbWU+bW90ZDwvYmFubmVyLW5hbWU+CiAgICA8YmFubmVyLXRleHQ+OAojIyMjIyMjIyMjIyMjIyMjIyMjIyMjCkEgdmVyeSBjb29sIGJhbm5lciB3aXRoIGEgYnVuY2ggb2YgIyMjIyBpbiBpdCA6KQojIyMjIyMjIyMjIyMjIyMjIyMjIyMjCgo4PC9iYW5uZXItdGV4dD4KICAgPC9iYW5uZXI+CiAgPC9iYW5uZXJzPgoKIyM=","Result":"\u003cbanners xmlns=\"http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg\"\u003e\n \u003cbanner\u003e\n \u003cbanner-name\u003emotd\u003c/banner-name\u003e\n \u003cbanner-text\u003e8\n######################\nA very cool banner with a bunch of #### in it :)\n######################\n\n8\u003c/banner-text\u003e\n \u003c/banner\u003e\n \u003c/banners\u003e","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","ElapsedTime":0,"FailedWhenContains":["PHJwYy1lcnJvcj4=","PHJwYy1lcnJvcnM+","PC9ycGMtZXJyb3I+","PC9ycGMtZXJyb3JzPg=="],"Failed":null,"StripNamespaces":false,"NetconfVersion":"1.1","ErrorMessages":null,"SubscriptionID":0}
14 changes: 14 additions & 0 deletions response/test-fixtures/netconf-output-11b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#279
<banners xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-infra-infra-cfg">
<banner>
<banner-name>motd</banner-name>
<banner-text>8
######################
A very cool banner with a bunch of #### in it :)
######################

8</banner-text>
</banner>
</banners>

##