2 * Copyright © 2017-2019 AT&T, Bell Canada
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.onap.ccsdk.apps.blueprintsprocessor.functions.netconf.executor.utils
18 import org.apache.commons.lang3.StringUtils
19 import org.onap.ccsdk.apps.blueprintsprocessor.functions.netconf.executor.api.NetconfException
20 import org.slf4j.LoggerFactory
21 import org.xml.sax.InputSource
22 import java.io.StringReader
23 import java.nio.charset.StandardCharsets
24 import java.util.regex.MatchResult
25 import java.util.regex.Pattern
26 import javax.xml.XMLConstants
27 import javax.xml.parsers.DocumentBuilderFactory
28 import kotlin.text.Charsets.UTF_8
31 class NetconfMessageUtils {
34 val log = LoggerFactory.getLogger(NetconfMessageUtils::class.java)
36 const val NEW_LINE = "\n"
37 const val CHUNKED_END_REGEX_PATTERN = "\n##\n"
39 val CAPABILITY_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.CAPABILITY_REGEX)
40 val SESSION_ID_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.SESSION_ID_REGEX)
42 private val CHUNKED_FRAMING_PATTERN: Pattern =
43 Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL)
44 private val CHUNKED_SIZE_PATTERN: Pattern = Pattern.compile("\\n#([1-9][0-9]*)\\n")
45 private val MSG_ID_STRING_PATTERN = Pattern.compile("${RpcMessageUtils.MESSAGE_ID_STRING}=\"(.*?)\"")
47 fun getConfig(messageId: String, configType: String, filterContent: String?): String {
48 val request = StringBuilder()
50 request.append("<get-config>").append(NEW_LINE)
51 request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
52 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
54 request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
56 if (!filterContent.isNullOrEmpty()) {
57 request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE)
58 request.append(filterContent).append(NEW_LINE)
59 request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE)
61 request.append("</get-config>").append(NEW_LINE)
63 return doWrappedRpc(messageId, request.toString())
66 fun doWrappedRpc(messageId: String, request: String): String {
67 val rpc = StringBuilder(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
68 rpc.append(RpcMessageUtils.RPC_OPEN)
69 rpc.append(RpcMessageUtils.MESSAGE_ID_STRING).append(RpcMessageUtils.EQUAL)
70 rpc.append(RpcMessageUtils.QUOTE).append(messageId).append(RpcMessageUtils.QUOTE_SPACE)
71 rpc.append(RpcMessageUtils.NETCONF_BASE_NAMESPACE).append(RpcMessageUtils.CLOSE)
74 rpc.append(RpcMessageUtils.RPC_CLOSE)
75 // rpc.append(NEW_LINE).append(END_PATTERN);
80 fun editConfig(messageId: String, configType: String, defaultOperation: String?,
81 newConfiguration: String): String {
83 val request = StringBuilder()
85 request.append("<edit-config>").append(NEW_LINE)
86 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
87 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
89 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
91 if (defaultOperation != null) {
92 request.append(RpcMessageUtils.DEFAULT_OPERATION_OPEN).append(defaultOperation)
93 .append(RpcMessageUtils.DEFAULT_OPERATION_CLOSE)
94 request.append(NEW_LINE)
97 request.append(RpcMessageUtils.CONFIG_OPEN).append(NEW_LINE)
98 request.append(newConfiguration.trim { it <= ' ' }).append(NEW_LINE)
99 request.append(RpcMessageUtils.CONFIG_CLOSE).append(NEW_LINE)
100 request.append("</edit-config>").append(NEW_LINE)
102 return doWrappedRpc(messageId, request.toString())
105 fun validate(messageId: String, configType: String): String {
106 val request = StringBuilder()
108 request.append("<validate>").append(NEW_LINE)
109 request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE)
110 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
112 request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE)
113 request.append("</validate>").append(NEW_LINE)
115 return doWrappedRpc(messageId, request.toString())
118 fun commit(messageId: String): String {
119 val request = StringBuilder()
121 request.append("<commit>").append(NEW_LINE)
122 request.append("</commit>").append(NEW_LINE)
124 return doWrappedRpc(messageId, request.toString())
128 fun unlock(messageId: String, configType: String): String {
129 val request = StringBuilder()
131 request.append("<unlock>").append(NEW_LINE)
132 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
133 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
135 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
136 request.append("</unlock>").append(NEW_LINE)
138 return doWrappedRpc(messageId, request.toString())
141 @Throws(NetconfException::class)
142 fun deleteConfig(messageId: String, configType: String): String {
143 if (configType == NetconfDatastore.RUNNING.datastore) {
144 log.warn("Target configuration for delete operation can't be \"running\" {}", configType)
145 throw NetconfException("Target configuration for delete operation can't be running")
148 val request = StringBuilder()
150 request.append("<delete-config>").append(NEW_LINE)
151 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
152 request.append(RpcMessageUtils.OPEN).append(configType)
153 .append(RpcMessageUtils.TAG_CLOSE)
155 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
156 request.append("</delete-config>").append(NEW_LINE)
158 return doWrappedRpc(messageId, request.toString())
161 fun discardChanges(messageId: String): String {
162 val request = StringBuilder()
163 request.append("<discard-changes/>").append(NEW_LINE)
164 return doWrappedRpc(messageId, request.toString())
167 fun lock(messageId: String, configType: String): String {
168 val request = StringBuilder()
170 request.append("<lock>").append(NEW_LINE)
171 request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE)
172 request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE)
174 request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE)
175 request.append("</lock>").append(NEW_LINE)
177 return doWrappedRpc(messageId, request.toString())
180 fun closeSession(messageId: String, force: Boolean): String {
181 val request = StringBuilder()
184 request.append("<kill-session/>").append(NEW_LINE)
186 request.append("<close-session/>").append(NEW_LINE)
189 return doWrappedRpc(messageId, request.toString())
192 fun validateRPCXML(rpcRequest: String): Boolean {
194 if (StringUtils.isBlank(rpcRequest)) {
197 val dbf = DocumentBuilderFactory.newInstance()
198 dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
199 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
200 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
201 dbf.newDocumentBuilder()
202 .parse(InputSource(StringReader(rpcRequest.replace(RpcMessageUtils.END_PATTERN, ""))))
204 } catch (e: Exception) {
210 fun getMsgId(message: String): String {
211 val matcher = MSG_ID_STRING_PATTERN.matcher(message)
212 if (matcher.find()) {
213 return matcher.group(1)
215 return if (message.contains(RpcMessageUtils.HELLO)) {
220 fun validateChunkedFraming(reply: String): Boolean {
221 val matcher = CHUNKED_FRAMING_PATTERN.matcher(reply)
222 if (!matcher.matches()) {
223 log.debug("Error Reply: {}", reply)
226 val chunkM = CHUNKED_SIZE_PATTERN.matcher(reply)
227 val chunks = ArrayList<MatchResult>()
228 var chunkdataStr = ""
229 while (chunkM.find()) {
230 chunks.add(chunkM.toMatchResult())
231 // extract chunk-data (and later) in bytes
232 val bytes = Integer.parseInt(chunkM.group(1))
233 val chunkdata = reply.substring(chunkM.end()).toByteArray(StandardCharsets.UTF_8)
234 if (bytes > chunkdata.size) {
235 log.debug("Error Reply - wrong chunk size {}", reply)
238 // convert (only) chunk-data part into String
239 chunkdataStr = String(chunkdata, 0, bytes, StandardCharsets.UTF_8)
240 // skip chunk-data part from next match
241 chunkM.region(chunkM.end() + chunkdataStr.length, reply.length)
243 if (!CHUNKED_END_REGEX_PATTERN.equals(reply.substring(chunks[chunks.size - 1].end() + chunkdataStr.length))) {
244 log.debug("Error Reply: {}", reply)
250 fun createHelloString(capabilities: List<String>): String {
251 val helloMessage = StringBuilder()
252 helloMessage.append(RpcMessageUtils.XML_HEADER).append(NEW_LINE)
253 helloMessage.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">").append(NEW_LINE)
254 helloMessage.append(" <capabilities>").append(NEW_LINE)
255 if (capabilities.isNotEmpty()) {
256 capabilities.forEach { cap ->
257 helloMessage.append(" <capability>").append(cap).append("</capability>").append(NEW_LINE)
260 helloMessage.append(" </capabilities>").append(NEW_LINE)
261 helloMessage.append("</hello>").append(NEW_LINE)
262 helloMessage.append(RpcMessageUtils.END_PATTERN)
263 return helloMessage.toString()
266 fun formatRPCRequest(request: String, messageId: String, deviceCapabilities: Set<String>): String {
267 var request = request
268 request = NetconfMessageUtils.formatNetconfMessage(deviceCapabilities, request)
269 request = NetconfMessageUtils.formatXmlHeader(request)
270 request = NetconfMessageUtils.formatRequestMessageId(request, messageId)
276 * Validate and format netconf message. - NC1.0 if no EOM sequence present on `message`,
277 * append. - NC1.1 chunk-encode given message unless it already is chunk encoded
279 * @param deviceCapabilities Set containing Device Capabilities
280 * @param message to format
281 * @return formated message
283 fun formatNetconfMessage(deviceCapabilities: Set<String>, message: String): String {
284 var message = message
285 if (deviceCapabilities.contains(RpcMessageUtils.NETCONF_11_CAPABILITY)) {
286 message = formatChunkedMessage(message)
287 } else if (!message.endsWith(RpcMessageUtils.END_PATTERN)) {
288 message = message + NEW_LINE + RpcMessageUtils.END_PATTERN
294 * Validate and format message according to chunked framing mechanism.
296 * @param message to format
297 * @return formated message
299 fun formatChunkedMessage(message: String): String {
300 var message = message
301 if (message.endsWith(RpcMessageUtils.END_PATTERN)) {
302 // message given had Netconf 1.0 EOM pattern -> remove
303 message = message.substring(0, message.length - RpcMessageUtils.END_PATTERN.length)
305 if (!message.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
306 // chunk encode message
308 (RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + message.toByteArray(UTF_8).size + RpcMessageUtils.NEW_LINE + message + RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH
309 + RpcMessageUtils.NEW_LINE)
315 * Ensures xml start directive/declaration appears in the `request`.
317 * @param request RPC request message
318 * @return XML RPC message
320 fun formatXmlHeader(request: String): String {
321 var request = request
322 if (!request.contains(RpcMessageUtils.XML_HEADER)) {
323 if (request.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) {
325 request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + RpcMessageUtils.XML_HEADER + request.substring(
326 request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].length)
328 request = RpcMessageUtils.XML_HEADER + "\n" + request
334 fun formatRequestMessageId(request: String, messageId: String): String {
335 var request = request
336 if (request.contains(RpcMessageUtils.MESSAGE_ID_STRING)) {
338 request.replaceFirst((RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.NUMBER_BETWEEN_QUOTES_MATCHER).toRegex(),
339 RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE)
340 } else if (!request.contains(RpcMessageUtils.MESSAGE_ID_STRING) && !request.contains(
341 RpcMessageUtils.HELLO)) {
342 request = request.replaceFirst(RpcMessageUtils.END_OF_RPC_OPEN_TAG.toRegex(),
343 RpcMessageUtils.QUOTE_SPACE + RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE + ">")
345 return updateRequestLength(request)
348 fun updateRequestLength(request: String): String {
349 if (request.contains(NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE)) {
351 Integer.parseInt(request.split(RpcMessageUtils.HASH.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
352 NEW_LINE.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0])
353 val rpcWithEnding = request.substring(request.indexOf('<'))
355 request.split(RpcMessageUtils.MSGLEN_REGEX_PATTERN.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split(
356 (NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE).toRegex()).dropLastWhile(
357 { it.isEmpty() }).toTypedArray()[0]
359 newLen = firstBlock.toByteArray(UTF_8).size
360 if (oldLen != newLen) {
361 return NEW_LINE + RpcMessageUtils.HASH + newLen + NEW_LINE + rpcWithEnding
367 fun checkReply(reply: String?): Boolean {
368 return if (reply != null) {
369 !reply.contains("rpc-error>") || reply.contains("warning") || reply.contains("ok/>")