diff --git a/response/netconf.go b/response/netconf.go index 6abf88d..f7a2e44 100644 --- a/response/netconf.go +++ b/response/netconf.go @@ -2,6 +2,7 @@ package response import ( "bytes" + "errors" "fmt" "regexp" "strconv" @@ -16,11 +17,21 @@ const ( v1Dot1 = "1.1" v1Dot0Delim = "]]>]]>" xmlHeader = "" + + // 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 ( @@ -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)(.*)`), + rpcErrors: regexp.MustCompile(`(?s)(.*)`), } }) @@ -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 } diff --git a/response/netconf_test.go b/response/netconf_test.go index 7706b52..c253216 100644 --- a/response/netconf_test.go +++ b/response/netconf_test.go @@ -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 { diff --git a/response/test-fixtures/golden/record-response-11b-out.txt b/response/test-fixtures/golden/record-response-11b-out.txt new file mode 100644 index 0000000..f3786b8 --- /dev/null +++ b/response/test-fixtures/golden/record-response-11b-out.txt @@ -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} \ No newline at end of file diff --git a/response/test-fixtures/netconf-output-11b.txt b/response/test-fixtures/netconf-output-11b.txt new file mode 100644 index 0000000..80f2b1d --- /dev/null +++ b/response/test-fixtures/netconf-output-11b.txt @@ -0,0 +1,14 @@ +#279 + + + motd + 8 +###################### +A very cool banner with a bunch of #### in it :) +###################### + +8 + + + +## \ No newline at end of file