/* * Copyright © 2017-2019 AT&T, Bell Canada * Modifications Copyright (c) 2019 IBM. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.utils import org.apache.commons.lang3.StringUtils import org.onap.ccsdk.cds.blueprintsprocessor.functions.netconf.executor.api.NetconfException import org.slf4j.LoggerFactory import org.xml.sax.InputSource import java.io.StringReader import java.nio.charset.StandardCharsets import java.util.regex.MatchResult import java.util.regex.Pattern import javax.xml.XMLConstants import javax.xml.parsers.DocumentBuilderFactory import kotlin.text.Charsets.UTF_8 class NetconfMessageUtils { companion object { val log = LoggerFactory.getLogger(NetconfMessageUtils::class.java) const val NEW_LINE = "\n" const val CHUNKED_END_REGEX_PATTERN = "\n##\n" val CAPABILITY_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.CAPABILITY_REGEX) val SESSION_ID_REGEX_PATTERN: Pattern = Pattern.compile(RpcMessageUtils.SESSION_ID_REGEX) private val CHUNKED_FRAMING_PATTERN: Pattern = Pattern.compile("(\\n#([1-9][0-9]*)\\n(.+))+\\n##\\n", Pattern.DOTALL) private val CHUNKED_SIZE_PATTERN: Pattern = Pattern.compile("\\n#([1-9][0-9]*)\\n") private val MSG_ID_STRING_PATTERN = Pattern.compile("${RpcMessageUtils.MESSAGE_ID_STRING}=\"(.*?)\"") fun get(messageId: String, filterContent: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) if (!filterContent.isNullOrEmpty()) { request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE) request.append(filterContent).append(NEW_LINE) request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE) } request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun getConfig(messageId: String, configType: String, filterContent: String?): String { val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE) if (!filterContent.isNullOrEmpty()) { request.append(RpcMessageUtils.SUBTREE_FILTER_OPEN).append(NEW_LINE) request.append(filterContent).append(NEW_LINE) request.append(RpcMessageUtils.SUBTREE_FILTER_CLOSE).append(NEW_LINE) } request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun doWrappedRpc(messageId: String, request: String): String { val rpc = StringBuilder(RpcMessageUtils.XML_HEADER).append(NEW_LINE) rpc.append(RpcMessageUtils.RPC_OPEN) rpc.append(RpcMessageUtils.MESSAGE_ID_STRING).append(RpcMessageUtils.EQUAL) rpc.append(RpcMessageUtils.QUOTE).append(messageId).append(RpcMessageUtils.QUOTE_SPACE) rpc.append(RpcMessageUtils.NETCONF_BASE_NAMESPACE).append(RpcMessageUtils.CLOSE) .append(NEW_LINE) rpc.append(request) rpc.append(RpcMessageUtils.RPC_CLOSE) // rpc.append(NEW_LINE).append(END_PATTERN); return rpc.toString() } fun editConfig(messageId: String, configType: String, defaultOperation: String?, newConfiguration: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE) if (defaultOperation != null) { request.append(RpcMessageUtils.DEFAULT_OPERATION_OPEN).append(defaultOperation) .append(RpcMessageUtils.DEFAULT_OPERATION_CLOSE) request.append(NEW_LINE) } request.append(RpcMessageUtils.CONFIG_OPEN).append(NEW_LINE) request.append(newConfiguration.trim { it <= ' ' }).append(NEW_LINE) request.append(RpcMessageUtils.CONFIG_CLOSE).append(NEW_LINE) request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun validate(messageId: String, configType: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.SOURCE_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.SOURCE_CLOSE).append(NEW_LINE) request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun commit(messageId: String, confirmed: Boolean, confirmTimeout: Int, persist: String, persistId: String): String { if (!persist.isEmpty() && !persistId.isEmpty()) { throw NetconfException("Can't proceed with both persist($persist) and " + "persistId($persistId) specified. Only one should be specified.") } if (confirmed && !persistId.isEmpty()) { throw NetconfException("Can't proceed with both confirmed flag and " + "persistId($persistId) specified. Only one should be specified.") } val request = StringBuilder() request.append("").append(NEW_LINE) if (confirmed) { request.append("") request.append("$confirmTimeout") if (!persist.isEmpty()) { request.append("$persist") } } if (!persistId.isEmpty()) { request.append("$persistId") } request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun cancelCommit(messageId: String, persistId: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) if (!persistId.isEmpty()) { request.append("$persistId") } request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun unlock(messageId: String, configType: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE) request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } @Throws(NetconfException::class) fun deleteConfig(messageId: String, configType: String): String { if (configType == NetconfDatastore.RUNNING.datastore) { log.warn("Target configuration for delete operation can't be \"running\" {}", configType) throw NetconfException("Target configuration for delete operation can't be running") } val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType) .append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE) request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun discardChanges(messageId: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun lock(messageId: String, configType: String): String { val request = StringBuilder() request.append("").append(NEW_LINE) request.append(RpcMessageUtils.TARGET_OPEN).append(NEW_LINE) request.append(RpcMessageUtils.OPEN).append(configType).append(RpcMessageUtils.TAG_CLOSE) .append(NEW_LINE) request.append(RpcMessageUtils.TARGET_CLOSE).append(NEW_LINE) request.append("").append(NEW_LINE) return doWrappedRpc(messageId, request.toString()) } fun closeSession(messageId: String, force: Boolean): String { val request = StringBuilder() if (force) { request.append("").append(NEW_LINE) } else { request.append("").append(NEW_LINE) } return doWrappedRpc(messageId, request.toString()) } fun validateRPCXML(rpcRequest: String): Boolean { try { if (StringUtils.isBlank(rpcRequest)) { return false } val dbf = DocumentBuilderFactory.newInstance() dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) dbf.setFeature("http://xml.org/sax/features/external-general-entities", false) dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false) dbf.newDocumentBuilder() .parse(InputSource(StringReader(rpcRequest.replace(RpcMessageUtils.END_PATTERN, "")))) return true } catch (e: Exception) { return false } } fun getMsgId(message: String): String { val matcher = MSG_ID_STRING_PATTERN.matcher(message) if (matcher.find()) { return matcher.group(1) } return if (message.contains(RpcMessageUtils.HELLO)) { (-1).toString() } else "" } fun validateChunkedFraming(reply: String): Boolean { val matcher = CHUNKED_FRAMING_PATTERN.matcher(reply) if (!matcher.matches()) { log.debug("Error Reply: {}", reply) return false } val chunkM = CHUNKED_SIZE_PATTERN.matcher(reply) val chunks = ArrayList() var chunkdataStr = "" while (chunkM.find()) { chunks.add(chunkM.toMatchResult()) // extract chunk-data (and later) in bytes val bytes = Integer.parseInt(chunkM.group(1)) val chunkdata = reply.substring(chunkM.end()).toByteArray(StandardCharsets.UTF_8) if (bytes > chunkdata.size) { log.debug("Error Reply - wrong chunk size {}", reply) return false } // convert (only) chunk-data part into String chunkdataStr = String(chunkdata, 0, bytes, StandardCharsets.UTF_8) // skip chunk-data part from next match chunkM.region(chunkM.end() + chunkdataStr.length, reply.length) } if (!CHUNKED_END_REGEX_PATTERN.equals(reply.substring(chunks[chunks.size - 1].end() + chunkdataStr.length))) { log.debug("Error Reply: {}", reply) return false } return true } fun createHelloString(capabilities: List): String { val helloMessage = StringBuilder() helloMessage.append(RpcMessageUtils.XML_HEADER).append(NEW_LINE) helloMessage.append("").append(NEW_LINE) helloMessage.append(" ").append(NEW_LINE) if (capabilities.isNotEmpty()) { capabilities.forEach { cap -> helloMessage.append(" ").append(cap).append("").append(NEW_LINE) } } helloMessage.append(" ").append(NEW_LINE) helloMessage.append("").append(NEW_LINE) helloMessage.append(RpcMessageUtils.END_PATTERN) return helloMessage.toString() } fun formatRPCRequest(request: String, messageId: String, deviceCapabilities: Set): String { var request = request request = NetconfMessageUtils.formatNetconfMessage(deviceCapabilities, request) request = NetconfMessageUtils.formatXmlHeader(request) request = NetconfMessageUtils.formatRequestMessageId(request, messageId) return request } /** * Validate and format netconf message. - NC1.0 if no EOM sequence present on `message`, * append. - NC1.1 chunk-encode given message unless it already is chunk encoded * * @param deviceCapabilities Set containing Device Capabilities * @param message to format * @return formated message */ fun formatNetconfMessage(deviceCapabilities: Set, message: String): String { var message = message if (deviceCapabilities.contains(RpcMessageUtils.NETCONF_11_CAPABILITY)) { message = formatChunkedMessage(message) } else if (!message.endsWith(RpcMessageUtils.END_PATTERN)) { message = message + NEW_LINE + RpcMessageUtils.END_PATTERN } return message } /** * Validate and format message according to chunked framing mechanism. * * @param message to format * @return formated message */ fun formatChunkedMessage(message: String): String { var message = message if (message.endsWith(RpcMessageUtils.END_PATTERN)) { // message given had Netconf 1.0 EOM pattern -> remove message = message.substring(0, message.length - RpcMessageUtils.END_PATTERN.length) } if (!message.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) { // chunk encode message message = (RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + message.toByteArray(UTF_8).size + RpcMessageUtils.NEW_LINE + message + RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + RpcMessageUtils.NEW_LINE) } return message } /** * Ensures xml start directive/declaration appears in the `request`. * * @param request RPC request message * @return XML RPC message */ fun formatXmlHeader(request: String): String { var request = request if (!request.contains(RpcMessageUtils.XML_HEADER)) { if (request.startsWith(RpcMessageUtils.NEW_LINE + RpcMessageUtils.HASH)) { request = request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + RpcMessageUtils.XML_HEADER + request.substring( request.split("<".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].length) } else { request = RpcMessageUtils.XML_HEADER + "\n" + request } } return request } fun formatRequestMessageId(request: String, messageId: String): String { var request = request if (request.contains(RpcMessageUtils.MESSAGE_ID_STRING)) { request = request.replaceFirst((RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.NUMBER_BETWEEN_QUOTES_MATCHER).toRegex(), RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE) } else if (!request.contains(RpcMessageUtils.MESSAGE_ID_STRING) && !request.contains( RpcMessageUtils.HELLO)) { request = request.replaceFirst(RpcMessageUtils.END_OF_RPC_OPEN_TAG.toRegex(), RpcMessageUtils.QUOTE_SPACE + RpcMessageUtils.MESSAGE_ID_STRING + RpcMessageUtils.EQUAL + RpcMessageUtils.QUOTE + messageId + RpcMessageUtils.QUOTE + ">") } return updateRequestLength(request) } fun updateRequestLength(request: String): String { if (request.contains(NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE)) { val oldLen = Integer.parseInt(request.split(RpcMessageUtils.HASH.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split( NEW_LINE.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0]) val rpcWithEnding = request.substring(request.indexOf('<')) val firstBlock = request.split(RpcMessageUtils.MSGLEN_REGEX_PATTERN.toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1].split( (NEW_LINE + RpcMessageUtils.HASH + RpcMessageUtils.HASH + NEW_LINE).toRegex()).dropLastWhile( { it.isEmpty() }).toTypedArray()[0] var newLen = 0 newLen = firstBlock.toByteArray(UTF_8).size if (oldLen != newLen) { return NEW_LINE + RpcMessageUtils.HASH + newLen + NEW_LINE + rpcWithEnding } } return request } fun checkReply(reply: String?): Boolean { return if (reply != null) { !reply.contains("rpc-error>") || reply.contains("ok/>") } else false } } }