Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions spec/multi_set_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require "./helper"

describe SNMP::Session do
it "builds a multi-varbind Set from a Hash (insertion order preserved)" do
values = {
"1.3.6.1.2.1.1.5.0" => "new-sysname",
"1.3.6.1.2.1.1.3.0" => SNMP::TimeTicks.new(9_u32),
}
message = SNMP::Session.new("public").set(values)

message.request.should eq(SNMP::Request::Set)
message.varbinds.map(&.oid).should eq(values.keys.to_a)
message.varbinds[0].value.get_string.should eq("new-sysname")
message.varbinds[1].value.tag_number.should eq(SNMP::AppTags::TimeTicks.to_i)
end
end

describe SNMP::V3::Session do
it "builds a multi-varbind Set from a Hash" do
values = {
"1.3.6.1.2.1.1.5.0" => "name",
"1.3.6.1.2.1.1.6.0" => "location",
}
message = SNMP::V3::Session.new("user").set(values)

message.request.should eq(SNMP::Request::Set)
message.pdu.varbinds.map(&.oid).should eq(values.keys.to_a)
end
end
54 changes: 54 additions & 0 deletions spec/set_values_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require "./helper"

describe "SNMP SET value types" do
it "encodes Counter32 as Application 1" do
ber = SNMP::Counter32.new(0xFFFF_0001_u32).to_ber
ber.tag_class.should eq(ASN1::BER::TagClass::Application)
ber.tag_number.should eq(SNMP::AppTags::Counter32.to_i)
SNMP.get_unsigned32(ber).should eq(0xFFFF_0001_u32)
end

it "encodes Gauge32 as Application 2" do
ber = SNMP::Gauge32.new(42_u32).to_ber
ber.tag_number.should eq(SNMP::AppTags::Gauge32.to_i)
SNMP.get_unsigned32(ber).should eq(42_u32)
end

it "encodes TimeTicks as Application 3" do
ber = SNMP::TimeTicks.new(100_u32).to_ber
ber.tag_number.should eq(SNMP::AppTags::TimeTicks.to_i)
SNMP.get_unsigned32(ber).should eq(100_u32)
end

it "encodes Counter64 as Application 6" do
ber = SNMP::Counter64.new(0xFFFF_FFFF_FFFF_0001_u64).to_ber
ber.tag_class.should eq(ASN1::BER::TagClass::Application)
ber.tag_number.should eq(SNMP::AppTags::Counter64.to_i)
SNMP.get_unsigned64(ber).should eq(0xFFFF_FFFF_FFFF_0001_u64)
end

it "encodes an IpAddress as 4 Application-0 bytes" do
ber = SNMP::IpAddress.new("192.168.1.254").to_ber
ber.tag_class.should eq(ASN1::BER::TagClass::Application)
ber.tag_number.should eq(SNMP::AppTags::IPAddress.to_i)
ber.payload.should eq(Bytes[192, 168, 1, 254])
end

it "encodes Opaque bytes as Application 4" do
ber = SNMP::Opaque.new(Bytes[1, 2, 3]).to_ber
ber.tag_number.should eq(SNMP::AppTags::Opaque.to_i)
ber.payload.should eq(Bytes[1, 2, 3])
end

it "encodes an OID value" do
ber = SNMP::OID.new("1.3.6.1.2.1.1.1.0").to_ber
ber.get_object_id.should eq("1.3.6.1.2.1.1.1.0")
end

it "wires a typed value into Session#set" do
message = SNMP::Session.new("public").set("1.3.6.1.2.1.1.3.0", SNMP::TimeTicks.new(12345_u32))
vb = message.varbinds.first
vb.value.tag_class.should eq(ASN1::BER::TagClass::Application)
vb.value.tag_number.should eq(SNMP::AppTags::TimeTicks.to_i)
end
end
1 change: 1 addition & 0 deletions src/snmp.cr
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class SNMP
end

require "./snmp/error"
require "./snmp/values"
require "./snmp/message"
require "./snmp/session"
require "./snmp/v3/message"
Expand Down
45 changes: 45 additions & 0 deletions src/snmp/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,51 @@ class SNMP::Client
session.parse(sock.read_bytes(ASN1::BER))
end

# Set a single OID to *value* (a typed SNMP value, a Crystal primitive, or a
# raw `ASN1::BER`) and return the agent's Response.
def set(oid : String, value) : SNMP::Message
msg : SNMP::Message? = nil
with_socket do |sock|
msg = set(oid, value, sock)
end

raise Error.new("Failed to read message") if msg.nil?
msg
end

# Multi-varbind Set: assign every OID => value pair in one round-trip.
def set(values : Hash(String, _)) : SNMP::Message
msg : SNMP::Message? = nil
with_socket do |sock|
msg = set(values, sock)
end

raise Error.new("Failed to read message") if msg.nil?
msg
end

private def set(oid : String, value, sock : UDPSocket) : SNMP::Message
check_validation_probe(sock)

message = session.set(oid, value)
message = session.prepare(message) if message.is_a?(SNMP::V3::Message)

sock.write_bytes message
sock.flush
session.parse(sock.read_bytes(ASN1::BER))
end

private def set(values : Hash(String, _), sock : UDPSocket) : SNMP::Message
check_validation_probe(sock)

message = session.set(values)
message = session.prepare(message) if message.is_a?(SNMP::V3::Message)

sock.write_bytes message
sock.flush
session.parse(sock.read_bytes(ASN1::BER))
end

def walk(oid : String) : Array(SNMP::Message)
messages = [] of SNMP::Message
with_socket do |sock|
Expand Down
22 changes: 17 additions & 5 deletions src/snmp/session.cr
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,30 @@ class SNMP::Session
message
end

# TODO:: requires better support for SNMP values such as Counter32, Counter64, Gauge32, OID, Timeticks etc
def set(oid, value, request_id = rand(2147483647))
SNMP::Message.new(@community, Request::Set, to_varbind(oid, value), request_id)
end

# Multi-varbind Set: one SetRequest assigning every OID => value pair. The Hash
# keeps insertion order, so the varbinds go out in the order they were given.
def set(values : Hash(String, _), request_id = rand(2147483647))
varbinds = values.map { |oid, value| to_varbind(oid, value) }
SNMP::Message.new(@community, Request::Set, varbinds, request_id)
end

# Encode a single OID => value assignment into a VarBind. Accepts the typed
# SNMP values (`TypedValue`), the Crystal primitives, a raw `ASN1::BER`, or a
# pre-built `VarBind`.
private def to_varbind(oid, value) : VarBind
data = value.is_a?(VarBind) ? value : VarBind.new(oid)

case value
when TypedValue
data.value = value.to_ber
when String
data.value.set_string(value)
when Int
data.value.set_integer(value)
# TODO::
# when Float
# when Socket::IPAddress
when Bool
data.value.set_boolean(value)
when Nil
Expand All @@ -100,6 +112,6 @@ class SNMP::Session
raise ArgumentError.new("unsupported varbind value. For complex values pass a pre-constructed `ASN1::BER`")
end

SNMP::Message.new(@community, Request::Set, data, request_id)
data
end
end
31 changes: 22 additions & 9 deletions src/snmp/v3/session.cr
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,35 @@ class SNMP::V3::Session
V3::Message.new(scoped_pdu, sec_params, @security, security_model, message_id)
end

# TODO:: requires better support for SNMP values such as Counter32, Counter64, Gauge32, OID, Timeticks etc
def set(oid, value, request_id = rand(2147483647), message_id = rand(2147483647), security_model = @security.security_model)
build_set([to_varbind(oid, value)], request_id, message_id, security_model)
end

# Multi-varbind Set: one SetRequest assigning every OID => value pair (Hash
# keeps insertion order).
def set(values : Hash(String, _), request_id = rand(2147483647), message_id = rand(2147483647), security_model = @security.security_model)
build_set(values.map { |oid, value| to_varbind(oid, value) }, request_id, message_id, security_model)
end

private def build_set(varbinds : Array(VarBind), request_id, message_id, security_model) : V3::Message
pdu = PDU.new(request_id, varbinds)
scoped_pdu = ScopedPDU.new(Request::Set, pdu, @engine_id)
sec_params = SecurityParams.new(@security.username, @engine_id, @engine_boots, @engine_time)

V3::Message.new(scoped_pdu, sec_params, @security, security_model, message_id)
end

# Encode a single OID => value assignment into a VarBind (see `Session#to_varbind`).
private def to_varbind(oid, value) : VarBind
data = value.is_a?(VarBind) ? value : VarBind.new(oid)

case value
when TypedValue
data.value = value.to_ber
when String
data.value.set_string(value)
when Int
data.value.set_integer(value)
# TODO::
# when Float
# when Socket::IPAddress
when Bool
data.value.set_boolean(value)
when Nil
Expand All @@ -209,10 +226,6 @@ class SNMP::V3::Session
raise ArgumentError.new("unsupported varbind value. For complex values pass a pre-constructed `ASN1::BER`")
end

pdu = PDU.new(request_id, data)
scoped_pdu = ScopedPDU.new(Request::Set, pdu, @engine_id)
sec_params = SecurityParams.new(@security.username, @engine_id, @engine_boots, @engine_time)

V3::Message.new(scoped_pdu, sec_params, @security, security_model, message_id)
data
end
end
113 changes: 113 additions & 0 deletions src/snmp/values.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require "./data_types"

# Typed SNMP values for `Session#set` / `PDU` construction. Each wraps a value
# and knows how to encode itself as an application-tagged (RFC 3416) BER, so a
# caller can set e.g. a Counter or a TimeTicks without hand-building the BER.
module SNMP::TypedValue
abstract def to_ber : ASN1::BER

# Build an application-class primitive carrying *payload* under *app_tag*.
private def application_ber(app_tag : AppTags, payload : ASN1::BER) : ASN1::BER
payload.tag_class = ASN1::BER::TagClass::Application
payload.tag_number = app_tag.to_i
payload
end
end

# Counter32 (RFC 2578) — a non-negative wrapping 32-bit counter.
struct SNMP::Counter32
include TypedValue

def initialize(@value : UInt32)
end

def to_ber : ASN1::BER
application_ber(AppTags::Counter32, SNMP.set_unsigned32(@value))
end
end

# Gauge32 / Unsigned32 (RFC 2578) — a non-negative 32-bit value that latches.
struct SNMP::Gauge32
include TypedValue

def initialize(@value : UInt32)
end

def to_ber : ASN1::BER
application_ber(AppTags::Gauge32, SNMP.set_unsigned32(@value))
end
end

# TimeTicks (RFC 2578) — hundredths of a second since an epoch.
struct SNMP::TimeTicks
include TypedValue

def initialize(@value : UInt32)
end

def to_ber : ASN1::BER
application_ber(AppTags::TimeTicks, SNMP.set_unsigned32(@value))
end
end

# Counter64 (RFC 2578) — a non-negative wrapping 64-bit counter (SNMPv2+).
struct SNMP::Counter64
include TypedValue

def initialize(@value : UInt64)
end

def to_ber : ASN1::BER
application_ber(AppTags::Counter64, SNMP.set_unsigned64(@value))
end
end

# IpAddress (RFC 2578) — a 4-octet IPv4 address.
struct SNMP::IpAddress
include TypedValue

@bytes : Bytes

def initialize(@bytes : Bytes)
raise ArgumentError.new("IpAddress requires exactly 4 bytes, got #{@bytes.size}") unless @bytes.size == 4
end

# Accept a dotted-quad string, e.g. "192.168.1.254".
def initialize(dotted : String)
octets = dotted.split('.')
raise ArgumentError.new("invalid IPv4 address #{dotted.inspect}") unless octets.size == 4
@bytes = Bytes.new(4) { |i| octets[i].to_u8 }
end

def to_ber : ASN1::BER
ber = ASN1::BER.new
ber.payload = @bytes
application_ber(AppTags::IPAddress, ber)
end
end

# Opaque (RFC 2578) — an application-wrapped arbitrary byte string.
struct SNMP::Opaque
include TypedValue

def initialize(@bytes : Bytes)
end

def to_ber : ASN1::BER
ber = ASN1::BER.new
ber.payload = @bytes
application_ber(AppTags::Opaque, ber)
end
end

# An OBJECT IDENTIFIER value (universal tag), for setting OID-valued objects.
struct SNMP::OID
include TypedValue

def initialize(@oid : String)
end

def to_ber : ASN1::BER
ASN1::BER.new.set_object_id(@oid)
end
end