use crate::ber::{Decoder, EncodeBuf, tag};
use crate::error::internal::DecodeErrorKind;
use crate::error::{Error, Result, UNKNOWN_TARGET};
use crate::pdu::{GetBulkPdu, Pdu, PduType, TrapV1Pdu};
use crate::version::Version;
use bytes::Bytes;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommunityPdu {
Standard(Pdu),
TrapV1(TrapV1Pdu),
}
impl CommunityPdu {
pub fn standard(&self) -> Option<&Pdu> {
match self {
Self::Standard(p) => Some(p),
Self::TrapV1(_) => None,
}
}
pub fn trap_v1(&self) -> Option<&TrapV1Pdu> {
match self {
Self::TrapV1(t) => Some(t),
Self::Standard(_) => None,
}
}
pub fn pdu_type(&self) -> PduType {
match self {
Self::Standard(p) => p.pdu_type,
Self::TrapV1(_) => PduType::TrapV1,
}
}
pub(crate) fn encode(&self, buf: &mut EncodeBuf) {
match self {
Self::Standard(p) => p.encode(buf),
Self::TrapV1(t) => t.encode(buf),
}
}
}
impl From<Pdu> for CommunityPdu {
fn from(p: Pdu) -> Self {
Self::Standard(p)
}
}
impl From<TrapV1Pdu> for CommunityPdu {
fn from(t: TrapV1Pdu) -> Self {
Self::TrapV1(t)
}
}
#[derive(Debug, Clone)]
pub struct CommunityMessage {
pub version: Version,
pub community: Bytes,
pub pdu: CommunityPdu,
}
impl CommunityMessage {
pub fn new(version: Version, community: impl Into<Bytes>, pdu: Pdu) -> Self {
assert!(
matches!(version, Version::V1 | Version::V2c),
"CommunityMessage only supports V1/V2c, not {:?}",
version
);
Self {
version,
community: community.into(),
pdu: CommunityPdu::Standard(pdu),
}
}
pub fn v2c(community: impl Into<Bytes>, pdu: Pdu) -> Self {
Self::new(Version::V2c, community, pdu)
}
pub fn v1(community: impl Into<Bytes>, pdu: Pdu) -> Self {
Self::new(Version::V1, community, pdu)
}
pub fn v1_trap(community: impl Into<Bytes>, trap: TrapV1Pdu) -> Self {
Self {
version: Version::V1,
community: community.into(),
pdu: CommunityPdu::TrapV1(trap),
}
}
pub fn encode(&self) -> Bytes {
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
self.pdu.encode(buf);
buf.push_octet_string(&self.community);
buf.push_integer(self.version.as_i32());
});
buf.finish()
}
pub fn decode(data: Bytes) -> Result<Self> {
let mut decoder = Decoder::new(data);
Self::decode_from(&mut decoder)
}
pub(crate) fn decode_from(decoder: &mut Decoder) -> Result<Self> {
let mut seq = decoder.read_sequence()?;
let version_num = seq.read_integer()?;
let version = Version::from_i32(version_num).ok_or_else(|| {
tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(version_num) }, "decode error");
Error::MalformedResponse {
target: UNKNOWN_TARGET,
}
.boxed()
})?;
Self::decode_from_sequence(&mut seq, version)
}
pub(crate) fn decode_from_sequence(seq: &mut Decoder, version: Version) -> Result<Self> {
if version == Version::V3 {
tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::UnknownVersion(3) }, "decode error");
return Err(Error::MalformedResponse {
target: UNKNOWN_TARGET,
}
.boxed());
}
let community = seq.read_octet_string()?;
let pdu_tag = seq.peek_tag().ok_or_else(|| {
tracing::debug!(target: "async_snmp::ber", { offset = seq.offset(), kind = %DecodeErrorKind::TruncatedData }, "truncated community message");
Error::MalformedResponse {
target: UNKNOWN_TARGET,
}
.boxed()
})?;
let pdu = if pdu_tag == tag::pdu::TRAP_V1 {
CommunityPdu::TrapV1(TrapV1Pdu::decode(seq)?)
} else {
CommunityPdu::Standard(Pdu::decode(seq)?)
};
Ok(CommunityMessage {
version,
community,
pdu,
})
}
pub fn into_pdu(self) -> Option<Pdu> {
match self.pdu {
CommunityPdu::Standard(p) => Some(p),
CommunityPdu::TrapV1(_) => None,
}
}
pub fn into_community_pdu(self) -> CommunityPdu {
self.pdu
}
pub fn encode_bulk(version: Version, community: impl Into<Bytes>, pdu: &GetBulkPdu) -> Bytes {
debug_assert!(version != Version::V1, "GETBULK not supported in SNMPv1");
let community = community.into();
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
pdu.encode(buf);
buf.push_octet_string(&community);
buf.push_integer(version.as_i32());
});
buf.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oid;
use crate::pdu::{GenericTrap, TrapV1Pdu};
#[test]
fn test_v1_trap_roundtrip() {
let trap = TrapV1Pdu::new(
oid!(1, 3, 6, 1, 4, 1, 9999),
[192, 168, 1, 1],
GenericTrap::LinkDown,
0,
12345,
vec![],
);
let msg = CommunityMessage::v1_trap(b"public".as_slice(), trap);
let encoded = msg.encode();
let decoded = CommunityMessage::decode(encoded).unwrap();
assert_eq!(decoded.version, Version::V1);
assert_eq!(decoded.community.as_ref(), b"public");
match decoded.pdu {
CommunityPdu::TrapV1(ref t) => {
assert_eq!(t.enterprise, oid!(1, 3, 6, 1, 4, 1, 9999));
assert_eq!(t.agent_addr, [192, 168, 1, 1]);
assert_eq!(t.generic_trap, GenericTrap::LinkDown);
assert_eq!(t.time_stamp, 12345);
}
CommunityPdu::Standard(_) => panic!("expected TrapV1 pdu"),
}
}
#[test]
fn test_v1_roundtrip() {
let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
let msg = CommunityMessage::v1(b"public".as_slice(), pdu);
let encoded = msg.encode();
let decoded = CommunityMessage::decode(encoded).unwrap();
assert_eq!(decoded.version, Version::V1);
assert_eq!(decoded.community.as_ref(), b"public");
assert_eq!(decoded.pdu.standard().unwrap().request_id, 42);
}
#[test]
fn test_v2c_roundtrip() {
let pdu = Pdu::get_request(123, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
let msg = CommunityMessage::v2c(b"private".as_slice(), pdu);
let encoded = msg.encode();
let decoded = CommunityMessage::decode(encoded).unwrap();
assert_eq!(decoded.version, Version::V2c);
assert_eq!(decoded.community.as_ref(), b"private");
assert_eq!(decoded.pdu.standard().unwrap().request_id, 123);
}
#[test]
fn test_version_preserved() {
for version in [Version::V1, Version::V2c] {
let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1)]);
let msg = CommunityMessage::new(version, b"test".as_slice(), pdu);
let encoded = msg.encode();
let decoded = CommunityMessage::decode(encoded).unwrap();
assert_eq!(decoded.version, version);
}
}
}