Enabling Code Formatter
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / netconf-executor / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / netconf / executor / utils / NetconfMessageUtils.kt
1 /*
2  * Copyright © 2017-2019 AT&T, Bell Canada
3  * Modifications Copyright (c) 2019 IBM.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils
18
19 import org.apache.commons.lang3.StringUtils
20 import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfException
21 import org.slf4j.LoggerFactory
22 import org.xml.sax.InputSource
23 import java.io.StringReader
24 import java.nio.charset.StandardCharsets
25 import java.util.regex.MatchResult
26 import java.util.regex.Pattern
27 import javax.xml.XMLConstants
28 import javax.xml.parsers.DocumentBuilderFactory
29 import kotlin.text.Charsets.UTF_8
30
31 class NetconfMessageUtils {
32
33     companion object {
34
35         val log = LoggerFactory.getLogger(NetconfMessageUtils::class.java)
36
37         const val NEW_LINE = "\n"
38         const val CHUNKED_END_REGEX_PATTERN = "\n##\n"
39
40         val CAPABILITY_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.CAPABILITY_REGEX)
41         val SESSION_ID_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.SESSION_ID_REGEX)
42
43         private val CHUNKED_FRAMING_PATTERN: Pattern =
44             Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
45         private val CHUNKED_SIZE_PATTERN: Pattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
46         private val MSG_ID_STRING_PATTERN = Pattern.compile("${RpcMessageUtils.MESSAGE_ID_STRING}=\"(.*?)\"")
47
48         fun get(messageId: String, filterContent: String): String {
49             val request = StringBuilder()
50
51             request.append("<get>").append(NEW_LINE)
52             if (!filterContent.isNullOrEmpty()) {
53                 request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE)
54                 request.append(filterContent).append(NEW_LINE)
55                 request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE)
56             }
57             request.append("</get>")
58
59             return doWrappedRpc(messageId, request.toString())
60         }
61
62         fun getConfig(messageId: String, configType: String, filterContent: String?): String {
63             val request = StringBuilder()
64
65             request.append("<get-config>").append(NEW_LINE)
66             request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
67             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
68                 .append(NEW_LINE)
69             request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
70
71             if (!filterContent.isNullOrEmpty()) {
72                 request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE)
73                 request.append(filterContent).append(NEW_LINE)
74                 request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE)
75             }
76             request.append("</get-config>")
77
78             return doWrappedRpc(messageId, request.toString())
79         }
80
81         fun doWrappedRpc(messageId: String, request: String): String {
82             val rpc = StringBuilder(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
83             rpc.append(RpcMessageUtils.RPC_OPEN)
84             rpc.append(RpcMessageUtils.MESSAGE_ID_STRING).append(RpcMessageUtils.EQUAL)
85             rpc.append(RpcMessageUtils.QUOTE).append(messageId).append(RpcMessageUtils.QUOTE_SPACE)
86             rpc.append(RpcMessageUtils.NETCONF_BASE_NAMESPACE).append(RpcMessageUtils.CLOSE)
87             rpc.append(NEW_LINE).append(request).append(NEW_LINE)
88             rpc.append(RpcMessageUtils.RPC_CLOSE)
89             // rpc.append(NEW_LINE).append(END_PATTERN);
90
91             return rpc.toString()
92         }
93
94         fun editConfig(
95             messageId: String,
96             configType: String,
97             defaultOperation: String?,
98             newConfiguration: String
99         ): String {
100             val request = StringBuilder()
101             request.append("<edit-config>").append(NEW_LINE)
102             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
103             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
104                 .append(NEW_LINE)
105             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
106
107             if (defaultOperation != null) {
108                 request.append(RpcMessageUtils.DEFAULT_OPERATION_OPEN).append(defaultOperation)
109                     .append(RpcMessageUtils.DEFAULT_OPERATION_CLOSE)
110                 request.append(NEW_LINE)
111             }
112
113             request.append(RpcMessageUtils.CONFIG_OPEN).append(NEW_LINE)
114             request.append(newConfiguration.trim { it <= ' ' }).append(NEW_LINE)
115             request.append(RpcMessageUtils.CONFIG_CLOSE).append(NEW_LINE)
116             request.append("</edit-config>")
117
118             return doWrappedRpc(messageId, request.toString())
119         }
120
121         fun validate(messageId: String, configType: String): String {
122             val request = StringBuilder()
123
124             request.append("<validate>").append(NEW_LINE)
125             request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
126             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
127                 .append(NEW_LINE)
128             request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
129             request.append("</validate>")
130
131             return doWrappedRpc(messageId, request.toString())
132         }
133
134         fun commit(
135             messageId: String,
136             confirmed: Boolean,
137             confirmTimeout: Int,
138             persist: String,
139             persistId: String
140         ): String {
141
142             if (!persist.isEmpty() && !persistId.isEmpty()) {
143                 throw NetconfException(
144                     "Can't proceed <commit> with both persist($persist) and " +
145                         "persistId($persistId) specified. Only one should be specified."
146                 )
147             }
148             if (confirmed && !persistId.isEmpty()) {
149                 throw NetconfException(
150                     "Can't proceed <commit> with both confirmed flag and " +
151                         "persistId($persistId) specified. Only one should be specified."
152                 )
153             }
154
155             val request = StringBuilder()
156             request.append("<commit>").append(NEW_LINE)
157             if (confirmed) {
158                 request.append("<confirmed/>").append(NEW_LINE)
159                 request.append("<confirm-timeout>$confirmTimeout</confirm-timeout>").append(NEW_LINE)
160                 if (!persist.isEmpty()) {
161                     request.append("<persist>$persist</persist>").append(NEW_LINE)
162                 }
163             }
164             if (!persistId.isEmpty()) {
165                 request.append("<persist-id>$persistId</persist-id>").append(NEW_LINE)
166             }
167             request.append("</commit>")
168
169             return doWrappedRpc(messageId, request.toString())
170         }
171
172         fun cancelCommit(messageId: String, persistId: String): String {
173             val request = StringBuilder()
174             request.append("<cancel-commit>").append(NEW_LINE)
175             if (!persistId.isEmpty()) {
176                 request.append("<persist-id>$persistId</persist-id>").append(NEW_LINE)
177             }
178             request.append("</cancel-commit>")
179
180             return doWrappedRpc(messageId, request.toString())
181         }
182
183         fun unlock(messageId: String, configType: String): String {
184             val request = StringBuilder()
185
186             request.append("<unlock>").append(NEW_LINE)
187             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
188             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
189                 .append(NEW_LINE)
190             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
191             request.append("</unlock>")
192
193             return doWrappedRpc(messageId, request.toString())
194         }
195
196         @Throws(NetconfException::class)
197         fun deleteConfig(messageId: String, configType: String): String {
198             if (configType == NetconfDatastore.RUNNING.datastore) {
199                 log.warn("Target configuration for delete operation can't be \"running\" {}", configType)
200                 throw NetconfException("Target configuration for delete operation can't be running")
201             }
202
203             val request = StringBuilder()
204
205             request.append("<delete-config>").append(NEW_LINE)
206             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
207             request.append(RpcMessageUtils.OPEN).append(configType)
208                 .append(RpcMessageUtils.TAG_CLOSE)
209                 .append(NEW_LINE)
210             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
211             request.append("</delete-config>")
212
213             return doWrappedRpc(messageId, request.toString())
214         }
215
216         fun discardChanges(messageId: String): String {
217             val request = StringBuilder()
218             request.append("<discard-changes/>")
219             return doWrappedRpc(messageId, request.toString())
220         }
221
222         fun lock(messageId: String, configType: String): String {
223             val request = StringBuilder()
224
225             request.append("<lock>").append(NEW_LINE)
226             request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
227             request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
228                 .append(NEW_LINE)
229             request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
230             request.append("</lock>")
231
232             return doWrappedRpc(messageId, request.toString())
233         }
234
235         fun closeSession(messageId: String, force: Boolean): String {
236             val request = StringBuilder()
237             // TODO: kill-session without session-id is a cisco-only variant.
238             // will fail on JUNIPER device.
239             // netconf RFC for kill-session requires session-id
240             // Cisco can accept <kill-session/> for current session
241             // or <kill-session><session-id>####</session-id></kill-session>
242             // as long as session ID is not the same as the current session.
243
244             // Juniperhttps://www.juniper.net/documentation/en_US/junos/topics/task/operational/netconf-session-terminating.html
245             // will accept only with session-id
246             if (force) {
247                 request.append("<kill-session/>")
248             } else {
249                 request.append("<close-session/>")
250             }
251
252             return doWrappedRpc(messageId, request.toString())
253         }
254
255         fun validateRPCXML(rpcRequest: String): Boolean {
256             try {
257                 if (StringUtils.isBlank(rpcRequest)) {
258                     return false
259                 }
260                 val dbf = DocumentBuilderFactory.newInstance()
261                 dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
262                 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
263                 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
264                 dbf.newDocumentBuilder()
265                     .parse(InputSource(StringReader(rpcRequest.replace(RpcMessageUtils.END_PATTERN, ""))))
266                 return true
267             } catch (e: Exception) {
268                 return false
269             }
270         }
271
272         fun getMsgId(message: String): String {
273             val matcher = MSG_ID_STRING_PATTERN.matcher(message)
274             if (matcher.find()) {
275                 return matcher.group(1)
276             }
277             return if (message.contains(RpcMessageUtils.HELLO)) {
278                 (-1).toString()
279             } else ""
280         }
281
282         fun validateChunkedFraming(reply: String): Boolean {
283             val matcher = CHUNKED_FRAMING_PATTERN.matcher(reply)
284             if (!matcher.matches()) {
285                 log.debug("Error Reply: {}", reply)
286                 return false
287             }
288             val chunkM = CHUNKED_SIZE_PATTERN.matcher(reply)
289             val chunks = ArrayList<MatchResult>()
290             var chunkdataStr = ""
291             while (chunkM.find()) {
292                 chunks.add(chunkM.toMatchResult())
293                 // extract chunk-data (and later) in bytes
294                 val bytes = Integer.parseInt(chunkM.group(1))
295                 val chunkdata = reply.substring(chunkM.end()).toByteArray(StandardCharsets.UTF_8)
296                 if (bytes > chunkdata.size) {
297                     log.debug("Error Reply - wrong chunk size {}", reply)
298                     return false
299                 }
300                 // convert (only) chunk-data part into String
301                 chunkdataStr = String(chunkdata, 0, bytes, StandardCharsets.UTF_8)
302                 // skip chunk-data part from next match
303                 chunkM.region(chunkM.end() + chunkdataStr.length, reply.length)
304             }
305             if (!CHUNKED_END_REGEX_PATTERN.equals(reply.substring(chunks[chunks.size - 1].end() + chunkdataStr.length))) {
306                 log.debug("Error Reply: {}", reply)
307                 return false
308             }
309             return true
310         }
311
312         fun createHelloString(capabilities: List<String>): String {
313             val helloMessage = StringBuilder()
314             helloMessage.append(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
315             helloMessage.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">").append(NEW_LINE)
316             helloMessage.append("  <capabilities>").append(NEW_LINE)
317             if (capabilities.isNotEmpty()) {
318                 capabilities.forEach { cap ->
319                     helloMessage.append("    <capability>").append(cap).append("</capability>").append(NEW_LINE)
320                 }
321             }
322             helloMessage.append("  </capabilities>").append(NEW_LINE)
323             helloMessage.append("</hello>").append(NEW_LINE)
324             helloMessage.append(RpcMessageUtils.END_PATTERN)
325             return helloMessage.toString()
326         }
327
328         fun formatRPCRequest(request: String, messageId: String, deviceCapabilities: Set<String>): String {
329             var request = request
330             request = NetconfMessageUtils.formatNetconfMessage(deviceCapabilities, request)
331             request = NetconfMessageUtils.formatXmlHeader(request)
332             request = NetconfMessageUtils.formatRequestMessageId(request, messageId)
333
334             return request
335         }
336
337         /**
338          * Validate and format netconf message. - NC1.0 if no EOM sequence present on `message`,
339          * append. - NC1.1 chunk-encode given message unless it already is chunk encoded
340          *
341          * @param deviceCapabilities Set containing Device Capabilities
342          * @param message to format
343          * @return formated message
344          */
345         fun formatNetconfMessage(deviceCapabilities: Set<String>, message: String): String {
346             var message = message
347             if (deviceCapabilities.contains(RpcMessageUtils.NETCONF_11_CAPABILITY)) {
348                 message = formatChunkedMessage(message)
349             } else if (!message.endsWith(RpcMessageUtils.END_PATTERN)) {
350                 message = message + NEW_LINE + RpcMessageUtils.END_PATTERN
351             }
352             return message
353         }
354
355         /**
356          * Validate and format message according to chunked framing mechanism.
357          *
358          * @param message to format
359          * @return formated message
360          */
361         fun formatChunkedMessage(message: String): String {
362             var message = message
363             if (message.endsWith(RpcMessageUtils.END_PATTERN)) {
364                 // message given had Netconf 1.0 EOM pattern -> remove
365                 message = message.substring(0, message.length - RpcMessageUtils.END_PATTERN.length)
366             }
367             if (!message.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
368                 // chunk encode message
369                 message =
370                     (
371                         RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + message.toByteArray(UTF_8).size + RpcMessageUtils.NEW_LINE + message + RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH +
372                             RpcMessageUtils.NEW_LINE
373                         )
374             }
375             return message
376         }
377
378         /**
379          * Ensures xml start directive/declaration appears in the `request`.
380          *
381          * @param request RPC request message
382          * @return XML RPC message
383          */
384         fun formatXmlHeader(request: String): String {
385             var request = request
386             if (!request.contains(RpcMessageUtils.XML_HEADER)) {
387                 if (request.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
388                     request =
389                         request.split("<".toRegex()).dropLastWhile { it.isEmpty() }
390                         .toTypedArray()[0] + RpcMessageUtils.XML_HEADER + request.substring(
391                         request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].length
392                     )
393                 } else {
394                     request = RpcMessageUtils.XML_HEADER + "\n" + request
395                 }
396             }
397             return request
398         }
399
400         fun formatRequestMessageId(request: String, messageId: String): String {
401             var request = request
402             if (request.contains(RpcMessageUtils.MESSAGE_ID_STRING)) {
403                 request =
404                     request.replaceFirst(
405                         (RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.NUMBER_BETWEEN_QUOTES_MATCHER).toRegex(),
406                         RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE
407                     )
408             } else if (!request.contains(RpcMessageUtils.MESSAGE_ID_STRING) && !request.contains(
409                     RpcMessageUtils.HELLO
410                 )
411             ) {
412                 request = request.replaceFirst(
413                     RpcMessageUtils.END_OF_RPC_OPEN_TAG.toRegex(),
414                     RpcMessageUtils.QUOTE_SPACE + RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE + ">"
415                 )
416             }
417             return updateRequestLength(request)
418         }
419
420         fun updateRequestLength(request: String): String {
421             if (request.contains(NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE)) {
422                 val oldLen =
423                     Integer.parseInt(
424                         request.split(RpcMessageUtils.HASH.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
425                             NEW_LINE.toRegex()
426                         ).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]
427                     )
428                 val rpcWithEnding = request.substring(request.indexOf('<'))
429                 val firstBlock =
430                     request.split(RpcMessageUtils.MSGLEN_REGEX_PATTERN.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
431                         (NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE).toRegex()
432                     ).dropLastWhile(
433                         { it.isEmpty() }).toTypedArray()[0]
434                 var newLen = 0
435                 newLen = firstBlock.toByteArray(UTF_8).size
436                 if (oldLen != newLen) {
437                     return NEW_LINE + RpcMessageUtils.HASH + newLen + NEW_LINE + rpcWithEnding
438                 }
439             }
440             return request
441         }
442
443         fun checkReply(reply: String?): Boolean {
444             return if (reply != null) {
445                 !reply.contains("rpc-error>") || reply.contains("ok/>")
446             } else false
447         }
448     }
449 }