use std::{fmt::Debug, io::Write};
use quick_xml::{
events::{BytesStart, Event},
name::{Namespace, ResolveResult},
NsReader, Writer,
};
use crate::{
capabilities::{Base, Capabilities, Capability},
session::SessionId,
};
use super::{xmlns, ClientMsg, ReadError, ReadXml, ServerMsg, WriteError, WriteXml};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ServerHello {
capabilities: Capabilities,
session_id: SessionId,
}
impl ServerHello {
pub(crate) const fn session_id(&self) -> SessionId {
self.session_id
}
pub(crate) fn capabilities(self) -> Capabilities {
self.capabilities
}
}
impl ReadXml for ServerHello {
#[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
let end = start.to_end();
let (mut capabilities, mut session_id) = (None, None);
tracing::debug!("expecting <capabilities> or <session-id>");
loop {
match reader.read_resolved_event()? {
(ResolveResult::Bound(ns), Event::Start(tag))
if ns == xmlns::BASE
&& tag.local_name().as_ref() == b"capabilities"
&& capabilities.is_none() =>
{
capabilities = Some(Capabilities::read_xml(reader, &tag)?);
tracing::debug!(?capabilities);
}
(ResolveResult::Bound(ns), Event::Start(tag))
if ns == xmlns::BASE
&& tag.local_name().as_ref() == b"session-id"
&& session_id.is_none() =>
{
tracing::debug!(?tag);
let span = reader.read_text(tag.to_end().name())?;
tracing::debug!(?span, "trying to parse session_id");
session_id = Some(span.parse()?);
tracing::debug!(?session_id);
}
(_, Event::Comment(_)) => {
continue;
}
(_, Event::End(tag)) if tag == end => break,
(ns, event) => {
tracing::error!(?event, ?ns, "unexpected xml event");
return Err(ReadError::UnexpectedXmlEvent(event.into_owned()));
}
};
}
Ok(Self {
capabilities: capabilities
.ok_or_else(|| ReadError::missing_element("hello", "capabilities"))?,
session_id: session_id
.ok_or_else(|| ReadError::missing_element("hello", "session-id"))?,
})
}
}
impl ServerMsg for ServerHello {
const TAG_NAME: &'static str = "hello";
const TAG_NS: Namespace<'static> = xmlns::BASE;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ClientHello {
capabilities: Capabilities,
}
impl ClientHello {
#[tracing::instrument(level = "debug")]
pub(crate) fn new(capabilities: &[Capability]) -> Self {
Self {
capabilities: capabilities.iter().cloned().collect(),
}
}
#[tracing::instrument(level = "debug")]
pub(crate) fn capabilities(self) -> Capabilities {
self.capabilities
}
}
impl Default for ClientHello {
fn default() -> Self {
const CAPABILITIES: &[Capability] =
&[Capability::Base(Base::V1_0), Capability::Base(Base::V1_1)];
Self::new(CAPABILITIES)
}
}
impl WriteXml for ClientHello {
fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
_ = writer
.create_element("hello")
.write_inner_content(|writer| self.capabilities.write_xml(writer))?;
Ok(())
}
}
impl ClientMsg for ClientHello {}
#[cfg(test)]
mod tests {
use iri_string::types::UriStr;
use super::*;
#[test]
fn server_hello_from_xml() {
let xml = r#"
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
<capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>
<capability>urn:ietf:params:netconf:capability:confirmed-commit:1.0</capability>
<capability>urn:ietf:params:netconf:capability:validate:1.0</capability>
<capability>urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file</capability>
<capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
<capability>urn:ietf:params:xml:ns:netconf:capability:candidate:1.0</capability>
<capability>urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0</capability>
<capability>urn:ietf:params:xml:ns:netconf:capability:validate:1.0</capability>
<capability>urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file</capability>
<capability>urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring</capability>
<capability>http://xml.juniper.net/netconf/junos/1.0</capability>
<capability>http://xml.juniper.net/dmi/system/1.0</capability>
</capabilities>
<session-id>802</session-id>
</hello>
"#;
let expect = ServerHello {
capabilities: [
Capability::Base(Base::V1_0),
Capability::Candidate,
Capability::ConfirmedCommitV1_0,
Capability::ValidateV1_0,
Capability::Url(vec!["http".into(), "ftp".into(), "file".into()]),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:base:1.0")
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:candidate:1.0")
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0")
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:validate:1.0")
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new(
"urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file",
)
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring")
.unwrap()
.into(),
),
#[cfg(feature = "junos")]
Capability::JunosXmlManagementProtocol,
#[cfg(not(feature = "junos"))]
Capability::Unknown(
UriStr::new("http://xml.juniper.net/netconf/junos/1.0")
.unwrap()
.into(),
),
Capability::Unknown(
UriStr::new("http://xml.juniper.net/dmi/system/1.0")
.unwrap()
.into(),
),
]
.into_iter()
.collect(),
session_id: SessionId::new(802).unwrap(),
};
assert_eq!(expect, ServerHello::from_xml(xml).unwrap());
}
#[test]
fn server_hello_with_xmlns_from_xml() {
let xml = r#"
<nc:hello xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
<nc:capabilities>
<nc:capability>urn:ietf:params:netconf:base:1.0</nc:capability>
<nc:capability>urn:ietf:params:netconf:capability:candidate:1.0</nc:capability>
<nc:capability>urn:ietf:params:netconf:capability:confirmed-commit:1.0</nc:capability>
<nc:capability>urn:ietf:params:netconf:capability:validate:1.0</nc:capability>
<nc:capability>urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&revision=2011-06-01</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:netconf:capability:candidate:1.0</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:netconf:capability:validate:1.0</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:yang:ietf-inet-types?module=ietf-inet-types&revision=2013-07-15</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:yang:ietf-yang-metadata?module=ietf-yang-metadata&revision=2016-08-05</nc:capability>
<nc:capability>urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring</nc:capability>
<nc:capability>http://xml.juniper.net/netconf/junos/1.0</nc:capability>
<nc:capability>http://xml.juniper.net/dmi/system/1.0</nc:capability>
<nc:capability>http://yang.juniper.net/junos/jcmd?module=junos-configuration-metadata&revision=2021-09-01</nc:capability>
</nc:capabilities>
<nc:session-id>43129</nc:session-id>
</nc:hello>
]]>]]>
"#;
let expect = ServerHello {
capabilities: [
Capability::Base(Base::V1_0),
Capability::Candidate,
Capability::ConfirmedCommitV1_0,
Capability::ValidateV1_0,
Capability::Url(vec!["http".into(), "ftp".into(), "file".into()]),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&revision=2011-06-01").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:candidate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:validate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-inet-types?module=ietf-inet-types&revision=2013-07-15").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-yang-metadata?module=ietf-yang-metadata&revision=2016-08-05").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring").unwrap().into()),
#[cfg(feature = "junos")]
Capability::JunosXmlManagementProtocol,
#[cfg(not(feature = "junos"))]
Capability::Unknown(UriStr::new("http://xml.juniper.net/netconf/junos/1.0").unwrap().into()),
Capability::Unknown(UriStr::new("http://xml.juniper.net/dmi/system/1.0").unwrap().into()),
Capability::Unknown(UriStr::new("http://yang.juniper.net/junos/jcmd?module=junos-configuration-metadata&revision=2021-09-01").unwrap().into()),
]
.into_iter()
.collect(),
session_id: SessionId::new(43129).unwrap(),
};
assert_eq!(expect, ServerHello::from_xml(xml).unwrap());
}
#[test]
fn client_hello_to_xml() {
let req = ClientHello {
capabilities: std::iter::once(Capability::Base(Base::V1_0)).collect(),
};
let expect = "<hello><capabilities><capability>urn:ietf:params:netconf:base:1.0</capability></capabilities></hello>]]>]]>";
assert_eq!(req.to_xml().unwrap(), expect);
}
}