/*
* 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
}
}
}