diff --git a/lib/cacophony/message.ex b/lib/cacophony/message.ex index bd621d9..8548902 100644 --- a/lib/cacophony/message.ex +++ b/lib/cacophony/message.ex @@ -1,324 +1,339 @@ defmodule Cacophony.Message do @moduledoc """ Encoding and decoding of ASN.1 LDAP messages to high-level types. ## Known Limitations * Cannot interpret ExtendedResponse objects. Extended response objects are encapsulated as an ExtendedResponse type. Matching up OIDs is up to you. """ require Logger defmodule BindRequest do @moduledoc "A type representing a BindRequest." defstruct [:id, :version, :dn, :credentials] end defmodule BindResponse do @moduledoc "A type representing a BindResponse." defstruct [ :id, :result_code, :matched_dn, {:diagnostic_message, ""}, {:referral, :asn1_NOVALUE}, {:server_sasl_creds, :asn1_NOVALUE} ] end defmodule UnbindRequest do @moduledoc "A type representing an UnbindRequest." defstruct [:id] end @whoami_oid "1.3.6.1.4.1.4203.1.11.3" defmodule WhoAmIRequest do @moduledoc "A type representing a Who Am I request (RFC 4532)." defstruct [:id] end defmodule WhoAmIResponse do @moduledoc "A type representing a Who Am I response (RFC 4532)." defstruct [ :id, {:result_code, :success}, {:matched_dn, ""}, {:diag_msg, ""}, {:referral, :asn1_NOVALUE}, :authzid ] end defmodule ExtendedRequest do @moduledoc "A catch-all type for any extended request." defstruct [:id, :request_name, :request_value] end defmodule ExtendedResponse do @moduledoc "A catch-all type for any extended responses." defstruct [ :id, {:result_code, :success}, {:matched_dn, ""}, {:diag_msg, ""}, {:referral, :asn1_NOVALUE}, :response_name, :response_value ] end defmodule SearchRequest do @moduledoc "A type representing a Search request." defstruct [ :id, :base, {:scope, :baseObject}, {:deref_aliases, :neverDerefAliases}, :size_limit, :time_limit, :types_only, :filter, :attributes ] end defmodule SearchResultDone do @moduledoc "A type representing the end of a set of SearchResultEntry items." defstruct [ :id, {:result_code, :success}, {:matched_dn, ""}, {:diag_msg, ""}, {:referral, :asn1_NOVALUE} ] end defmodule PartialAttribute do @moduledoc "A type representing a PartialAttribute: key to 0 or more values." defstruct [ :name, {:values, []} ] end defmodule SearchResultEntry do @moduledoc "A type representing a SearchResultEntry: DN to 0 or more PartialAttribute items." defstruct [ :id, :object_name, {:attributes, []} ] end + defmodule AbandonRequest do + @moduledoc "A type representing an AbandonRequest: an ID of a request to be abandoned." + + defstruct [ + :id, + :abandon_id + ] + end + + def interpret(:abandonRequest, abandon_id, message_id), + do: {:ok, %AbandonRequest{id: message_id, abandon_id: abandon_id}} + def interpret(:bindRequest, {:BindRequest, vsn, dn, creds}, message_id), do: {:ok, %BindRequest{id: message_id, version: vsn, dn: dn, credentials: creds}} def interpret( :bindResponse, {:BindResponse, result, matched_dn, diag_msg, referral, sasl_creds}, message_id ), do: {:ok, %BindResponse{ id: message_id, result_code: result, matched_dn: matched_dn, diagnostic_message: diag_msg, referral: referral, server_sasl_creds: sasl_creds }} def interpret(:unbindRequest, _, message_id), do: {:ok, %UnbindRequest{id: message_id}} def interpret(:extendedReq, {:ExtendedRequest, @whoami_oid, :asn1_NOVALUE}, message_id), do: {:ok, %WhoAmIRequest{id: message_id}} def interpret(:extendedReq, {:ExtendedRequest, request_name, request_value}, message_id), do: {:ok, %ExtendedRequest{id: message_id, request_name: request_name, request_value: request_value}} def interpret( :extendedResp, {:ExtendedResponse, result, matched_dn, diag_msg, referral, response_name, response_value}, message_id ), do: {:ok, %ExtendedResponse{ id: message_id, result_code: result, matched_dn: matched_dn, diag_msg: diag_msg, referral: referral, response_name: response_name, response_value: response_value }} def interpret( :searchRequest, {:SearchRequest, base, scope, deref_aliases, size_limit, time_limit, types_only, filter, attributes}, message_id ), do: {:ok, %SearchRequest{ id: message_id, base: base, scope: scope, deref_aliases: deref_aliases, size_limit: size_limit, time_limit: time_limit, types_only: types_only, filter: filter, attributes: attributes }} def interpret( :searchResDone, {:LDAPResult, result, matched_dn, diag_msg, referral}, message_id ), do: {:ok, %SearchResultDone{ id: message_id, result_code: result, matched_dn: matched_dn, diag_msg: diag_msg, referral: referral }} def interpret(:searchResEntry, {:SearchResultEntry, matched_dn, attributes}, message_id), do: {:ok, %SearchResultEntry{ id: message_id, object_name: matched_dn, attributes: Enum.map( attributes, fn {:PartialAttribute, key, values} -> %PartialAttribute{name: key, values: values} end ) }} def decode({:LDAPMessage, message_id, {type, payload}, _}), do: interpret(type, payload, message_id) def decode({:LDAPMessage, message_id, {type, payload}}), do: interpret(type, payload, message_id) def decode(message) when is_binary(message) do with {:ok, data} <- :LDAP.decode(:LDAPMessage, message), {:ok, struct} <- decode(data) do {:ok, struct} else e -> {:error, e} end end def decode(_), do: {:error, :badmatch} def encode({:asn1, message}), do: :LDAP.encode(:LDAPMessage, message) def encode({:envelope, message_id, payload}), do: encode({:asn1, {:LDAPMessage, message_id, payload, :asn1_NOVALUE}}) def encode(%BindRequest{} = msg) do payload = {:BindRequest, msg.version, msg.dn, msg.credentials} encode({:envelope, msg.id, {:bindRequest, payload}}) end def encode(%BindResponse{} = msg) do payload = {:BindResponse, msg.result_code, msg.matched_dn, msg.diagnostic_message, msg.referral, msg.server_sasl_creds} encode({:envelope, msg.id, {:bindResponse, payload}}) end def encode(%UnbindRequest{} = msg), do: encode({:envelope, msg.id, {:unbindRequest, :asn1_NOVALUE}}) def encode(%WhoAmIRequest{} = msg), do: encode({:envelope, msg.id, {:extendedReq, {:ExtendedRequest, @whoami_oid, :asn1_NOVALUE}}}) def encode(%WhoAmIResponse{} = msg), do: encode( {:envelope, msg.id, {:extendedResp, {:ExtendedResponse, msg.result_code, msg.matched_dn, msg.diag_msg, msg.referral, :asn1_NOVALUE, msg.authzid}}} ) def encode(%ExtendedRequest{} = msg), do: encode( {:envelope, msg.id, {:extendedReq, {:ExtendedRequest, msg.request_name, msg.request_value}}} ) def encode(%ExtendedResponse{} = msg), do: encode( {:envelope, msg.id, {:extendedResp, {:ExtendedResponse, msg.result_code, msg.matched_dn, msg.diag_msg, msg.referral, msg.response_name, msg.response_value}}} ) def encode(%SearchRequest{} = msg), do: encode( {:envelope, msg.id, {:searchRequest, {:SearchRequest, msg.base, msg.scope, msg.deref_aliases, msg.size_limit, msg.time_limit, msg.types_only, msg.filter, msg.attributes}}} ) def encode(%SearchResultDone{} = msg), do: encode( {:envelope, msg.id, {:searchResDone, {:LDAPResult, msg.result_code, msg.matched_dn, msg.diag_msg, msg.referral}}} ) def encode(%SearchResultEntry{} = msg), do: encode( {:envelope, msg.id, {:searchResEntry, {:SearchResultEntry, msg.object_name, Enum.map( msg.attributes, fn %PartialAttribute{name: name, values: values} -> {:PartialAttribute, name, values} end )}}} ) + def encode(%AbandonRequest{} = msg), + do: encode({:envelope, msg.id, {:abandonRequest, msg.abandon_id}}) + def encode(_), do: {:error, :badmatch} end diff --git a/test/cacophony/message_test.exs b/test/cacophony/message_test.exs index 1e530a4..2fb0b2e 100644 --- a/test/cacophony/message_test.exs +++ b/test/cacophony/message_test.exs @@ -1,231 +1,254 @@ defmodule Cacophony.MessageTest do use ExUnit.Case @whoami_oid "1.3.6.1.4.1.4203.1.11.3" @cirno_oid "9.99.999.9999.99999.999999" describe "decode/1" do test "successfully decodes a bindRequest" do bindreq = {:BindRequest, 3, "cn=foo", {:simple, "bar"}} message = {:LDAPMessage, 1, {:bindRequest, bindreq}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.BindRequest{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 assert msg.dn == "cn=foo" assert msg.credentials == {:simple, "bar"} end test "successfully decodes a bindResponse" do bindresp = {:BindResponse, :success, "cn=foo", "", :asn1_NOVALUE, :asn1_NOVALUE} message = {:LDAPMessage, 1, {:bindResponse, bindresp}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.BindResponse{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 assert msg.matched_dn == "cn=foo" assert msg.result_code == :success end test "successfully decodes an unbindRequest" do message = {:LDAPMessage, 1, {:unbindRequest, :asn1_NOVALUE}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.UnbindRequest{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 end test "successfully decodes an extended whoAmIRequest" do message = {:LDAPMessage, 1, {:extendedReq, {:ExtendedRequest, @whoami_oid, :asn1_NOVALUE}}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.WhoAmIRequest{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 end test "successfully decodes an unknown extendedRequest" do message = {:LDAPMessage, 1, {:extendedReq, {:ExtendedRequest, @cirno_oid, :asn1_NOVALUE}}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.ExtendedRequest{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 end test "successfully decodes a searchRequest" do searchreq = {:SearchRequest, "", :baseObject, :neverDerefAliases, 0, 0, false, {:present, "objectclass"}, ["supportedSASLMechanisms"]} message = {:LDAPMessage, 1, {:searchRequest, searchreq}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchRequest{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 assert msg.attributes == ["supportedSASLMechanisms"] assert msg.filter == {:present, "objectclass"} assert msg.base == "" assert msg.scope == :baseObject assert msg.deref_aliases == :neverDerefAliases assert msg.time_limit == 0 assert msg.size_limit == 0 refute msg.types_only end test "successfully decodes a searchResDone" do searchresdone = {:SearchResultDone, :success, "", "", :asn1_NOVALUE} message = {:LDAPMessage, 1, {:searchResDone, searchresdone}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchResultDone{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 end test "successfully decodes a searchResEntry" do searchresentry = {:SearchResultEntry, "cn=test", [{:PartialAttribute, "key", ["value1", "value2"]}]} message = {:LDAPMessage, 1, {:searchResEntry, searchresentry}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchResultEntry{} = msg} = Cacophony.Message.decode(bin) assert msg.id == 1 assert msg.object_name == "cn=test" assert msg.attributes == [ %Cacophony.Message.PartialAttribute{name: "key", values: ["value1", "value2"]} ] end + + test "successfully decodes an abandonRequest" do + message = {:LDAPMessage, 2, {:abandonRequest, 1}, :asn1_NOVALUE} + + {:ok, bin} = :LDAP.encode(:LDAPMessage, message) + + {:ok, %Cacophony.Message.AbandonRequest{} = msg} = Cacophony.Message.decode(bin) + + assert msg.id == 2 + assert msg.abandon_id == 1 + end end describe "encode/1" do test "successfully encodes a bindRequest" do bindreq = {:BindRequest, 3, "cn=foo", {:simple, "bar"}} message = {:LDAPMessage, 1, {:bindRequest, bindreq}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.BindRequest{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes a bindResponse" do bindresp = {:BindResponse, :success, "cn=foo", "", :asn1_NOVALUE, :asn1_NOVALUE} message = {:LDAPMessage, 1, {:bindResponse, bindresp}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.BindResponse{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes an unbindRequest" do message = {:LDAPMessage, 1, {:unbindRequest, :asn1_NOVALUE}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.UnbindRequest{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes an extended whoAmIRequest" do message = {:LDAPMessage, 1, {:extendedReq, {:ExtendedRequest, @whoami_oid, :asn1_NOVALUE}}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.WhoAmIRequest{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes an unknown extendedRequest" do message = {:LDAPMessage, 1, {:extendedReq, {:ExtendedRequest, @cirno_oid, :asn1_NOVALUE}}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.ExtendedRequest{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes a searchRequest" do searchreq = {:SearchRequest, "", :baseObject, :neverDerefAliases, 0, 0, false, {:present, "objectclass"}, ["supportedSASLMechanisms"]} message = {:LDAPMessage, 1, {:searchRequest, searchreq}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchRequest{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes a searchResDone" do searchresdone = {:SearchResultDone, :success, "", "", :asn1_NOVALUE} message = {:LDAPMessage, 1, {:searchResDone, searchresdone}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchResultDone{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end test "successfully encodes a searchResEntry" do searchresentry = {:SearchResultEntry, "cn=test", [{:PartialAttribute, "key", ["value1", "value2"]}]} message = {:LDAPMessage, 1, {:searchResEntry, searchresentry}, :asn1_NOVALUE} {:ok, bin} = :LDAP.encode(:LDAPMessage, message) {:ok, %Cacophony.Message.SearchResultEntry{} = msg} = Cacophony.Message.decode(bin) {:ok, other_bin} = Cacophony.Message.encode(msg) assert bin == other_bin end + + test "successfully encodes an abandonRequest" do + message = {:LDAPMessage, 2, {:abandonRequest, 1}, :asn1_NOVALUE} + + {:ok, bin} = :LDAP.encode(:LDAPMessage, message) + + {:ok, %Cacophony.Message.AbandonRequest{} = msg} = Cacophony.Message.decode(bin) + + {:ok, other_bin} = Cacophony.Message.encode(msg) + + assert bin == other_bin + end end end