diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..fb2f11e --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["*.{ex,exs}", "{lib,priv,test}/**/*.{ex,exs}"] +] diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index 6dfa82f..0000000 --- a/config/config.exs +++ /dev/null @@ -1,24 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. -use Mix.Config - -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for third- -# party users, it should be done in your mix.exs file. - -# Sample configuration: -# -# config :logger, :console, -# level: :info, -# format: "$date $time [$level] $metadata$message\n", -# metadata: [:user_id] - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" diff --git a/example.exs b/example.exs index fcb405c..bc16674 100644 --- a/example.exs +++ b/example.exs @@ -1,72 +1,74 @@ require Logger -#RadiusApp.start :normal,[] +# RadiusApp.start :normal,[] -#IO.puts inspect RadiusDict.Vendor.by_name("Cisco") -#IO.puts inspect RadiusDict.Attribute.by_name("Service-Type") -#IO.puts inspect RadiusDict.Value.by_name("Service-Type","Login-User") -#IO.puts inspect RadiusDict.Value.by_value(6,11) -#IO.puts inspect RadiusDict.Value.by_name("Cisco","Cisco-Disconnect-Cause","Unknown") -#IO.puts inspect RadiusDict.Value.by_value(9,195,11) -#IO.puts inspect RadiusDict.Value.by_value(9,1950,11) +# IO.puts inspect RadiusDict.Vendor.by_name("Cisco") +# IO.puts inspect RadiusDict.Attribute.by_name("Service-Type") +# IO.puts inspect RadiusDict.Value.by_name("Service-Type","Login-User") +# IO.puts inspect RadiusDict.Value.by_value(6,11) +# IO.puts inspect RadiusDict.Value.by_name("Cisco","Cisco-Disconnect-Cause","Unknown") +# IO.puts inspect RadiusDict.Value.by_value(9,195,11) +# IO.puts inspect RadiusDict.Value.by_value(9,1950,11) secret = "112233" attrs = [ - {"User-Password","1234"}, - #tagged attribute (rfc2868) - {"Tunnel-Type","PPTP"}, - #equals - {"Tunnel-Type",{0,"PPTP"}}, - {"Tunnel-Type",{10,"PPTP"}}, - {"Service-Type","Login-User"}, - #tag & value can be integer - {6,1}, - #ipaddr - {"NAS-IP-Address",{1,2,3,4}}, - {"NAS-IP-Address",0x12345678}, - #ipv6addr - {"Login-IPv6-Host",{2003,0xefff,0,0,0,0,0,4}}, - #VSA - {{"Vendor-Specific",9},[ - {"Cisco-Disconnect-Cause",10}, - {195,"Unknown"} - ]}, - #empty VSA? - {{"Vendor-Specific","Microsoft"},[]}, - #some unknown attribute - {255,"123456"} + {"User-Password", "1234"}, + # tagged attribute (rfc2868) + {"Tunnel-Type", "PPTP"}, + # equals + {"Tunnel-Type", {0, "PPTP"}}, + {"Tunnel-Type", {10, "PPTP"}}, + {"Service-Type", "Login-User"}, + # tag & value can be integer + {6, 1}, + # ipaddr + {"NAS-IP-Address", {1, 2, 3, 4}}, + {"NAS-IP-Address", 0x12345678}, + # ipv6addr + {"Login-IPv6-Host", {2003, 0xEFFF, 0, 0, 0, 0, 0, 4}}, + # VSA + {{"Vendor-Specific", 9}, + [ + {"Cisco-Disconnect-Cause", 10}, + {195, "Unknown"} + ]}, + # empty VSA? + {{"Vendor-Specific", "Microsoft"}, []}, + # some unknown attribute + {255, "123456"} ] -#for request packets, leave auth=nil will generate with random bytes +# for request packets, leave auth=nil will generate with random bytes p = %Radius.Packet{code: "Access-Request", id: 12, auth: nil, secret: secret, attrs: attrs} -#will return an iolist -data = Radius.Packet.encode p -Logger.debug "data=#{inspect data}" +# will return an iolist +data = Radius.Packet.encode(p) +Logger.debug("data=#{inspect(data)}") -p = Radius.Packet.decode :erlang.iolist_to_binary(data),secret -Logger.debug inspect p, pretty: true +p = Radius.Packet.decode(:erlang.iolist_to_binary(data), secret) +Logger.debug(inspect(p, pretty: true)) -#for response packets, set auth=request.auth to generate new HMAC-hash with it. +# for response packets, set auth=request.auth to generate new HMAC-hash with it. p = %Radius.Packet{code: "Access-Accept", id: 12, auth: p.auth, secret: secret, attrs: p.attrs} -data = Radius.Packet.encode p -Logger.debug "data=#{inspect data}" -#password decoding SHOULD FAIL here, guess why? -p = Radius.Packet.decode :erlang.iolist_to_binary(data),p.secret -Logger.debug inspect p, pretty: true +data = Radius.Packet.encode(p) +Logger.debug("data=#{inspect(data)}") +# password decoding SHOULD FAIL here, guess why? +p = Radius.Packet.decode(:erlang.iolist_to_binary(data), p.secret) +Logger.debug(inspect(p, pretty: true)) -#wrapper of gen_udp -{:ok,sk} = Radius.listen 1812 +# wrapper of gen_udp +{:ok, sk} = Radius.listen(1812) -loop = fn(loop)-> - #secret can be a string or a function returning a string - #{:ok,host,p} = Radius.recv sk,"123" - {:ok,host,p} = Radius.recv sk,fn(_host) -> secret end +loop = fn loop -> + # secret can be a string or a function returning a string + # {:ok,host,p} = Radius.recv sk,"123" + {:ok, host, p} = Radius.recv(sk, fn _host -> secret end) - IO.puts "From #{inspect host} : \n#{inspect p, pretty: true}" + IO.puts("From #{inspect(host)} : \n#{inspect(p, pretty: true)}") resp = %Radius.Packet{code: "Access-Reject", id: p.id, auth: p.auth, secret: p.secret} - Radius.send sk,host,resp + Radius.send(sk, host, resp) loop.(loop) end + loop.(loop) diff --git a/lib/radius.ex b/lib/radius.ex index d54de99..d209ba2 100644 --- a/lib/radius.ex +++ b/lib/radius.ex @@ -5,7 +5,7 @@ defmodule Radius do wrapper of gen_udp.open """ def listen(port) do - :gen_udp.open(port, [{:active, :false}, {:mode, :binary}]) + :gen_udp.open(port, [{:active, false}, {:mode, :binary}]) end @doc """ @@ -15,8 +15,9 @@ defmodule Radius do secret :: string | fn({host,port}) -> string """ def recv(sk, secret) when is_binary(secret) do - recv(sk, fn(_) -> secret end) + recv(sk, fn _ -> secret end) end + def recv(sk, secret_fn) when is_function(secret_fn) do {:ok, {host, port, data}} = :gen_udp.recv(sk, 5000) secret = secret_fn.({host, port}) diff --git a/lib/radius/dict.ex b/lib/radius/dict.ex index 8528a8b..f2e5e9a 100644 --- a/lib/radius/dict.ex +++ b/lib/radius/dict.ex @@ -1,31 +1,36 @@ defmodule Radius.Dict do - require GenServer + use GenServer require Logger def init(file) do init_ets() load(file) - {:ok,%{file: file}} - end #init/1 + {:ok, %{file: file}} + end + + # init/1 - #def handle_call(:reload,_from,state) do + # def handle_call(:reload,_from,state) do # ctx = load(state.file) # {:reply, {:ok,ctx}, state} - #end #handle_call/3 + # end #handle_call/3 - #exports + # exports def start_link(file) do - GenServer.start_link(__MODULE__,file, name: __MODULE__) - end #start_link/1 + GenServer.start_link(__MODULE__, file, name: __MODULE__) + end + + # start_link/1 - #def reload() do + # def reload() do # GenServer.call(__MODULE__,:reload) - #end #reload + # end #reload defmodule EntryNotFoundError do - defexception [:type,:key] + defexception [:type, :key] + def message(e) do - "Enrty #{e.type} not found for key: #{inspect e.key}" + "Enrty #{e.type} not found for key: #{inspect(e.key)}" end end @@ -35,9 +40,11 @@ defmodule Radius.Dict do def init! do DictEntry.__init__(__MODULE__) end + def insert(val) do - DictEntry.__insert__(__MODULE__,val) + DictEntry.__insert__(__MODULE__, val) end + def lookup(key) do try do lookup!(key) @@ -45,115 +52,147 @@ defmodule Radius.Dict do e in EntryNotFoundError -> [] end end + def lookup!(key) do - DictEntry.__lookup__(__MODULE__,key) + DictEntry.__lookup__(__MODULE__, key) end + def on_insert(val) do val end + def on_load(val) do val end - defoverridable [ on_insert: 1, on_load: 1 ] + + defoverridable on_insert: 1, on_load: 1 end end + def __init__(mod) do - :ets.new mod, [:set, :protected, :named_table] + :ets.new(mod, [:set, :protected, :named_table]) end - def __insert__(mod,val) do - val = mod.on_insert val - keys = val|> mod.index_for - Enum.map keys, fn(x) -> - if not :ets.insert_new mod,{x,val} do - #Logger.warn "ignored duplicat #{mod}: #{inspect x} at #{val.file}:#{val.line} \nObject: #{inspect val, pretty: true}" + + def __insert__(mod, val) do + val = mod.on_insert(val) + keys = val |> mod.index_for + + Enum.map(keys, fn x -> + if not :ets.insert_new(mod, {x, val}) do + # Logger.warn "ignored duplicat #{mod}: #{inspect x} at #{val.file}:#{val.line} \nObject: #{inspect val, pretty: true}" :ok end - end + end) end - def __lookup__(mod,[key]) do - __lookup__ mod,key + + def __lookup__(mod, [key]) do + __lookup__(mod, key) end - def __lookup__(mod,key) do + + def __lookup__(mod, key) do try do - :ets.lookup_element mod, key, 2 + :ets.lookup_element(mod, key, 2) rescue - ArgumentError -> + ArgumentError -> raise EntryNotFoundError, type: mod, key: key end end - end #module DictEntry + end + + # module DictEntry defmodule Vendor do use DictEntry - defstruct id: nil, name: nil, format: {1,1}, file: "(unknown)", line: 0 + defstruct id: nil, name: nil, format: {1, 1}, file: "(unknown)", line: 0 + def index_for(val) do - [ id: val.id, - name: val.name] + [id: val.id, name: val.name] end + def by_name(nil) do %Vendor{} end + def by_name(name) do - lookup! name: name + lookup!(name: name) end + def by_id(id) do - lookup! id: id + lookup!(id: id) end - end #module Vendor + end + + # module Vendor defmodule Attribute do use DictEntry defstruct id: nil, name: nil, type: nil, opts: [], vendor: nil, file: "(unknown)", line: 0 + def on_insert(val) do - v = Vendor.by_name val.vendor - %{val|vendor: v} + v = Vendor.by_name(val.vendor) + %{val | vendor: v} end + def index_for(val) do - [ id: {val.vendor.id, val.id}, - name: val.name] + [id: {val.vendor.id, val.id}, name: val.name] end + def by_name(name) do - lookup! name: name + lookup!(name: name) end + def by_id(vendor \\ nil, id) do - lookup! id: {vendor,id} + lookup!(id: {vendor, id}) end - end #module Attribute + end + + # module Attribute defmodule Value do use DictEntry defstruct name: nil, attr: nil, value: nil, file: "(unknown)", line: 0 + def on_insert(val) do - a = Attribute.lookup! name: val.attr - %{val|attr: a} + a = Attribute.lookup!(name: val.attr) + %{val | attr: a} end + def index_for(val) do - [value: {val.attr.vendor.id, val.attr.id, val.value}, - name: {val.attr.vendor.name, val.attr.name, val.name}] + [ + value: {val.attr.vendor.id, val.attr.id, val.value}, + name: {val.attr.vendor.name, val.attr.name, val.name} + ] end + def by_name(vendor \\ nil, attr, name) do - lookup! name: {vendor,attr,name} + lookup!(name: {vendor, attr, name}) end + def by_value(vendor \\ nil, attr, name) do - lookup! value: {vendor,attr,name} + lookup!(value: {vendor, attr, name}) end - end #module Value - + end + + # module Value + defp init_ets() do - Vendor.init! - Attribute.init! - Value.init! + Vendor.init!() + Attribute.init!() + Value.init!() :ok end defmodule ParserError do - defexception [ file: "", line: -1, msg: nil ] - def exception({:error,{line,_,msg}}) do + defexception file: "", line: -1, msg: nil + + def exception({:error, {line, _, msg}}) do %ParserError{line: line, msg: msg} end - def exception({:error,{line,_,msg},_}) do + + def exception({:error, {line, _, msg}, _}) do %ParserError{line: line, msg: msg} end + def message(e) do "ParserError: at file: #{e.file}:#{e.line} #{e.msg}" end @@ -161,103 +200,128 @@ defmodule Radius.Dict do defp load(path) when is_binary(path) do ctx = %{path: [path], vendor: nil, vendors: [], attrs: [], values: []} - ctx = load ctx - Enum.each ctx.vendors, &Vendor.insert/1 - Enum.each ctx.attrs, &Attribute.insert/1 - Enum.each ctx.values, &Value.insert/1 + ctx = load(ctx) + Enum.each(ctx.vendors, &Vendor.insert/1) + Enum.each(ctx.attrs, &Attribute.insert/1) + Enum.each(ctx.values, &Value.insert/1) ctx end defp load(ctx) when is_map(ctx) do - path = hd ctx.path - #Logger.debug "Loading dict: #{path}" + path = hd(ctx.path) + # Logger.debug "Loading dict: #{path}" try do - File.read!(path) - |> String.to_charlist - |> tokenlize! + File.read!(path) + |> String.to_charlist() + |> tokenlize! |> parse! - |> (&process_dict ctx,&1).() + |> (&process_dict(ctx, &1)).() rescue - e in ParserError -> reraise %{e|file: path}, System.stacktrace - e -> reraise e,System.stacktrace + e in ParserError -> reraise %{e | file: path}, __STACKTRACE__ + e -> reraise e, __STACKTRACE__ after - Logger.flush + Logger.flush() end end defp tokenlize!(string) do - case :radius_dict_lex.string string do - {:ok,tokens, _} -> tokens + case :radius_dict_lex.string(string) do + {:ok, tokens, _} -> tokens e -> raise ParserError, e end end + defp parse!(tokens) do case :radius_dict_parser.parse(tokens) do - {:ok,spec} -> spec + {:ok, spec} -> + spec + e -> - e = ParserError.exception e - Logger.debug inspect _token_near(tokens,[],e.line-2,e.line+2) + e = ParserError.exception(e) + Logger.debug(inspect(_token_near(tokens, [], e.line - 2, e.line + 2))) raise e end end - defp _token_near(acc,[],_,_) do - :lists.reverse acc + defp _token_near(acc, [], _, _) do + :lists.reverse(acc) end - defp _token_near(acc,[h|t],min,max) when :erlang.element(2,h) in min..max do - _token_near [h|acc],t,max,min + + defp _token_near(acc, [h | t], min, max) when :erlang.element(2, h) in min..max do + _token_near([h | acc], t, max, min) end - defp _token_near(acc,[_|t],min,max) do - _token_near acc,t,max,min + + defp _token_near(acc, [_ | t], min, max) do + _token_near(acc, t, max, min) end - defp process_dict(ctx,[]) do + defp process_dict(ctx, []) do ctx end - defp process_dict(ctx,[{:include,name}|tail]) do - target = ctx.path - |> Path.dirname - |> Path.join(name) - ctx = %{ctx| path: [target|ctx.path]} - ctx = load ctx - ctx = %{ctx| path: Enum.drop(ctx.path,1)} - process_dict ctx,tail + + defp process_dict(ctx, [{:include, name} | tail]) do + target = + ctx.path + |> Path.dirname() + |> Path.join(name) + + ctx = %{ctx | path: [target | ctx.path]} + ctx = load(ctx) + ctx = %{ctx | path: Enum.drop(ctx.path, 1)} + process_dict(ctx, tail) end - defp process_dict(ctx,[{:vendor_begin,name,_line}|tail]) do - name = to_string name - ctx = %{ctx| vendor: name} - process_dict ctx,tail + + defp process_dict(ctx, [{:vendor_begin, name, _line} | tail]) do + name = to_string(name) + ctx = %{ctx | vendor: name} + process_dict(ctx, tail) end - defp process_dict(ctx,[{:vendor_end,name,_line}|tail]) do + + defp process_dict(ctx, [{:vendor_end, name, _line} | tail]) do name = to_string(name) + if ctx.vendor == name do - ctx = %{ctx| vendor: nil } - process_dict ctx,tail + ctx = %{ctx | vendor: nil} + process_dict(ctx, tail) else - raise {:error,:end_vendor_not_match, name} + raise {:error, :end_vendor_not_match, name} end end - defp process_dict(ctx,[{:vendor,name,id,format,line}|tail]) do - name = to_string name + + defp process_dict(ctx, [{:vendor, name, id, format, line} | tail]) do + name = to_string(name) v = %Vendor{name: name, id: id, format: format, file: hd(ctx.path), line: line} - ctx = %{ctx| vendors: [v|ctx.vendors]} - process_dict ctx,tail + ctx = %{ctx | vendors: [v | ctx.vendors]} + process_dict(ctx, tail) end - defp process_dict(ctx,[{:attribute,name,id,type,opts,line}|tail]) do + + defp process_dict(ctx, [{:attribute, name, id, type, opts, line} | tail]) do name = to_string(name) - a = %Attribute{name: name, id: id, type: type, opts: opts, vendor: ctx.vendor, file: hd(ctx.path), line: line} - ctx = %{ctx| attrs: [a|ctx.attrs]} - process_dict ctx,tail + + a = %Attribute{ + name: name, + id: id, + type: type, + opts: opts, + vendor: ctx.vendor, + file: hd(ctx.path), + line: line + } + + ctx = %{ctx | attrs: [a | ctx.attrs]} + process_dict(ctx, tail) end - defp process_dict(ctx,[{:value,attr,desc,id,line}|tail]) do + + defp process_dict(ctx, [{:value, attr, desc, id, line} | tail]) do attr = to_string(attr) desc = to_string(desc) v = %Value{name: desc, attr: attr, value: id, file: hd(ctx.path), line: line} - ctx = %{ctx| values: [v|ctx.values]} - process_dict ctx,tail + ctx = %{ctx | values: [v | ctx.values]} + process_dict(ctx, tail) end - defp process_dict(ctx,[head|tail]) do - Logger.warn "Unknown command: #{inspect head}" - process_dict ctx,tail + + defp process_dict(ctx, [head | tail]) do + Logger.warn("Unknown command: #{inspect(head)}") + process_dict(ctx, tail) end end diff --git a/lib/radius/packet.ex b/lib/radius/packet.ex index 9ef6cb3..465146c 100644 --- a/lib/radius/packet.ex +++ b/lib/radius/packet.ex @@ -6,35 +6,38 @@ defmodule Radius.Packet do alias Radius.Dict.Value alias Radius.Dict.EntryNotFoundError - defstruct [ - code: nil, - id: nil, - length: nil, - auth: nil, - attrs: [], - raw: nil, - secret: nil, - ] + defstruct code: nil, + id: nil, + length: nil, + auth: nil, + attrs: [], + raw: nil, + secret: nil @doc """ Decode radius packet """ def decode(data, secret) do - pkt = %{raw: data, secret: secret, attrs: nil} + pkt = + %{raw: data, secret: secret, attrs: nil} |> decode_header |> decode_payload + struct(__MODULE__, pkt) end defp decode_header(%{raw: raw} = ctx) do <> = raw + if byte_size(rest) < length - 20 do {:error, :packet_too_short} else if byte_size(ctx.raw) != length do raise "Packet length not match." end - ctx |> Map.merge(%{ + + ctx + |> Map.merge(%{ code: decode_code(code), id: id, length: length, @@ -44,15 +47,15 @@ defmodule Radius.Packet do end end - defp decode_code(1), do: "Access-Request" - defp decode_code(2), do: "Access-Accept" - defp decode_code(3), do: "Access-Reject" + defp decode_code(1), do: "Access-Request" + defp decode_code(2), do: "Access-Accept" + defp decode_code(3), do: "Access-Reject" defp decode_code(11), do: "Access-Challenge" - defp decode_code(4), do: "Accounting-Request" - defp decode_code(5), do: "Accounting-Response" + defp decode_code(4), do: "Accounting-Request" + defp decode_code(5), do: "Accounting-Response" defp decode_code(12), do: "Status-Server" defp decode_code(13), do: "Status-Client" - defp decode_code(x), do: x + defp decode_code(x), do: x defp decode_payload(ctx) do decode_tlv(ctx.rest, [], {1, 1}) @@ -60,7 +63,9 @@ defmodule Radius.Packet do end defp decode_tlv(<<>>, acc, _), do: acc |> Enum.reverse() - defp decode_tlv(bin, _, {_, 0}), do: bin # not to decode USR style VSAs + # not to decode USR style VSAs + defp decode_tlv(bin, _, {_, 0}), do: bin + defp decode_tlv(bin, acc, {tl, ll} = fmt) when byte_size(bin) > tl + ll do tl = tl * 8 ll = ll * 8 @@ -72,90 +77,108 @@ defmodule Radius.Packet do defp resolve_tlv(attrs, ctx) when is_list(attrs) do attrs = attrs |> Enum.map(&resolve_tlv(&1, ctx, nil)) - Map.put ctx, :attrs, attrs + Map.put(ctx, :attrs, attrs) end - #VSA Entry - defp resolve_tlv({26,value}, ctx, nil) do + # VSA Entry + defp resolve_tlv({26, value}, ctx, nil) do type = "Vendor-Specific" - <>=value + <> = value + try do - v = Vendor.by_id vid - value = case decode_tlv rest,[],v.format do - bin when is_binary(bin) -> bin - tlv when is_list(tlv) -> - Enum.map tlv, fn(x) -> - resolve_tlv(x,ctx,v.id) - end - end - {{type,v.name},value} - rescue _e in EntryNotFoundError -> - {type,value} + v = Vendor.by_id(vid) + + value = + case decode_tlv(rest, [], v.format) do + bin when is_binary(bin) -> + bin + + tlv when is_list(tlv) -> + Enum.map(tlv, fn x -> + resolve_tlv(x, ctx, v.id) + end) + end + + {{type, v.name}, value} + rescue + _e in EntryNotFoundError -> + {type, value} end end defp resolve_tlv({type, value} = tlv, ctx, vendor) do try do - attr = Attribute.by_id vendor,type + attr = Attribute.by_id(vendor, type) type = attr.name - has_tag = Keyword.has_key? attr.opts, :has_tag - {tag, value} = case value do - <<0,rest::binary>> when has_tag==true -> - {nil, rest} + has_tag = Keyword.has_key?(attr.opts, :has_tag) - <> when tag in 1..0x1f and has_tag==true -> - {tag, rest} + {tag, value} = + case value do + <<0, rest::binary>> when has_tag == true -> + {nil, rest} - _ -> - {nil, value} - end + <> when tag in 1..0x1F and has_tag == true -> + {tag, rest} + + _ -> + {nil, value} + end - value = value - |> decode_value(attr.type) - |> resolve_value(vendor,attr.id) - |> decrypt_value(Keyword.get(attr.opts, :encrypt), ctx.auth, ctx.secret) + value = + value + |> decode_value(attr.type) + |> resolve_value(vendor, attr.id) + |> decrypt_value(Keyword.get(attr.opts, :encrypt), ctx.auth, ctx.secret) if tag do - {type,{tag,value}} + {type, {tag, value}} else - {type,value} + {type, value} end - rescue _e in EntryNotFoundError-> + rescue + _e in EntryNotFoundError -> tlv end end - defp decode_value(<>,:byte), do: val - defp decode_value(<>,:short), do: val - defp decode_value(<>,:integer), do: val - defp decode_value(<>,:signed), do: val - defp decode_value(<>,:date), do: val - defp decode_value(<>,:ifid), do: val - defp decode_value(<>,:ipaddr), do: {a,b,c,d} - defp decode_value(<>,:ipv6addr) do - (for <>, do: x) |> :erlang.list_to_tuple + defp decode_value(<>, :byte), do: val + defp decode_value(<>, :short), do: val + defp decode_value(<>, :integer), do: val + defp decode_value(<>, :signed), do: val + defp decode_value(<>, :date), do: val + defp decode_value(<>, :ifid), do: val + defp decode_value(<>, :ipaddr), do: {a, b, c, d} + + defp decode_value(<>, :ipv6addr) do + for(<>, do: x) |> :erlang.list_to_tuple() end - defp decode_value(bin,_t) do + + defp decode_value(bin, _t) do bin end - defp resolve_value(val,vid,aid) do + defp resolve_value(val, vid, aid) do try do - v = Value.by_value vid,aid,val + v = Value.by_value(vid, aid, val) v.name - rescue _e in EntryNotFoundError -> - val + rescue + _e in EntryNotFoundError -> + val end end - defp decrypt_value(bin,nil,_,_), do: bin - defp decrypt_value(bin,1,auth,secret) do - Radius.Util.decrypt_rfc2865 bin,secret,auth + + defp decrypt_value(bin, nil, _, _), do: bin + + defp decrypt_value(bin, 1, auth, secret) do + Radius.Util.decrypt_rfc2865(bin, secret, auth) end - defp decrypt_value(bin,2,auth,secret) do - Radius.Util.decrypt_rfc2868 bin,secret,auth + + defp decrypt_value(bin, 2, auth, secret) do + Radius.Util.decrypt_rfc2868(bin, secret, auth) end - defp decrypt_value(bin,a,_,_) do - Logger.error "Unknown encrypt type: #{inspect a}" + + defp decrypt_value(bin, a, _, _) do + Logger.error("Unknown encrypt type: #{inspect(a)}") bin end @@ -176,21 +199,28 @@ defmodule Radius.Packet do def encode(packet, options \\ []) do sign? = options |> Keyword.get(:sign, false) raw? = options |> Keyword.get(:raw, false) - {auth, reply?} = if packet.auth == nil do - {:crypto.strong_rand_bytes(16), false} - else - {packet.auth, true} - end + + {auth, reply?} = + if packet.auth == nil do + {:crypto.strong_rand_bytes(16), false} + else + {packet.auth, true} + end + packet = %{packet | auth: auth} - packet = if sign? do - attrs = - packet.attrs ++ [ - {"Message-Authenticator", <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>} - ] - %{packet | attrs: attrs} - else - packet - end + + packet = + if sign? do + attrs = + packet.attrs ++ + [ + {"Message-Authenticator", <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>} + ] + + %{packet | attrs: attrs} + else + packet + end attrs = encode_attrs(packet) @@ -198,151 +228,185 @@ defmodule Radius.Packet do length = 20 + :erlang.iolist_size(attrs) header = <> - attrs = if sign? do - signature = :crypto.hmac(:md5, packet.secret, [header, attrs]) - [last | attrs] = attrs |> Enum.reverse() - crop_len = byte_size(last) - 16 - last = <> - [last | attrs] |> Enum.reverse() - else - attrs - end + attrs = + if sign? do + signature = :crypto.mac(:hmac, :md5, packet.secret, [header, attrs]) + [last | attrs] = attrs |> Enum.reverse() + crop_len = byte_size(last) - 16 + last = <> + [last | attrs] |> Enum.reverse() + else + attrs + end - header = if reply? and raw? == false do - resp_auth = - :crypto.hash_init(:md5) - |> :crypto.hash_update(header) - |> :crypto.hash_update(attrs) - |> :crypto.hash_update(packet.secret) - |> :crypto.hash_final() - <> - else - header - end + header = + if reply? and raw? == false do + resp_auth = + :crypto.hash_init(:md5) + |> :crypto.hash_update(header) + |> :crypto.hash_update(attrs) + |> :crypto.hash_update(packet.secret) + |> :crypto.hash_final() + + <> + else + header + end [header, attrs] end - defp encode_attrs(%{attrs: a}=ctx) do - Enum.map a, fn(x) -> + defp encode_attrs(%{attrs: a} = ctx) do + Enum.map(a, fn x -> x |> resolve_attr(ctx) |> encode_attr - end + end) end - #back-door for VSAs, encode_vsa could retuen an iolist - defp encode_attr({26,value}), do: [26,:erlang.iolist_size(value)+2,value] - defp encode_attr({tag,value}) when is_binary(value) do + # back-door for VSAs, encode_vsa could retuen an iolist + defp encode_attr({26, value}), do: [26, :erlang.iolist_size(value) + 2, value] + + defp encode_attr({tag, value}) when is_binary(value) do len = byte_size(value) + 2 - if len > 0xff do - raise "value oversized: #{inspect {tag,value}}" + + if len > 0xFF do + raise "value oversized: #{inspect({tag, value})}" end - <> + + <> end - defp encode_attr({tag,value}) when is_integer(value) do + + defp encode_attr({tag, value}) when is_integer(value) do if value > 0xFFFFFFFF do - Logger.warn "value truncated: #{inspect {tag,value}}" + Logger.warn("value truncated: #{inspect({tag, value})}") end - <> - end - defp encode_attr({type,value,attr}) do - {t,l}=attr.vendor.format - value = if Keyword.has_key? attr.opts, :has_tag do - {tag,value} = case value do - {tag,value} when tag in 0..0x1f -> {tag,value} - {tag,_value} -> raise "Tag over-range, should be [0-0x1f], got: #{tag}" - value -> {0,value} + + <> + end + + defp encode_attr({type, value, attr}) do + {t, l} = attr.vendor.format + + value = + if Keyword.has_key?(attr.opts, :has_tag) do + {tag, value} = + case value do + {tag, value} when tag in 0..0x1F -> {tag, value} + {tag, _value} -> raise "Tag over-range, should be [0-0x1f], got: #{tag}" + value -> {0, value} + end + + value = encode_value(value, attr.type) + <> + else + encode_value(value, attr.type) end - value = encode_value(value,attr.type) - <> - else - encode_value(value,attr.type) - end + length = byte_size(value) + t + l - ll = l*8 - tl = t*8 - <> + ll = l * 8 + tl = t * 8 + <> end - defp encrypt_value({tag,bin},attr,ctx), do: {tag,encrypt_value(bin,attr,ctx)} - defp encrypt_value(bin,attr,ctx), do: encrypt_value(bin,Keyword.get(attr.opts,:encrypt),ctx.auth,ctx.secret) - defp encrypt_value(bin,nil,_,_), do: bin - defp encrypt_value(bin,1,auth,secret) do - Radius.Util.encrypt_rfc2865 bin,secret,auth + defp encrypt_value({tag, bin}, attr, ctx), do: {tag, encrypt_value(bin, attr, ctx)} + + defp encrypt_value(bin, attr, ctx), + do: encrypt_value(bin, Keyword.get(attr.opts, :encrypt), ctx.auth, ctx.secret) + + defp encrypt_value(bin, nil, _, _), do: bin + + defp encrypt_value(bin, 1, auth, secret) do + Radius.Util.encrypt_rfc2865(bin, secret, auth) end - defp encrypt_value(bin,2,auth,secret) do - Radius.Util.encrypt_rfc2868 bin,secret,auth + + defp encrypt_value(bin, 2, auth, secret) do + Radius.Util.encrypt_rfc2868(bin, secret, auth) end - defp encrypt_value(bin,a,_,_) do - Logger.error "Unknown encrypt type: #{inspect a}" + + defp encrypt_value(bin, a, _, _) do + Logger.error("Unknown encrypt type: #{inspect(a)}") bin end + defp encode_value(val, :byte) when is_integer(val), do: <> + defp encode_value(val, :short) when is_integer(val), do: <> + defp encode_value(val, :integer) when is_integer(val), do: <> + defp encode_value(val, :signed) when is_integer(val), do: <> + defp encode_value(val, :date) when is_integer(val), do: <> + defp encode_value(val, :ifid) when is_integer(val), do: <> + defp encode_value({a, b, c, d}, :ipaddr), do: <> + defp encode_value(x, :ipaddr) when is_integer(x), do: <> - defp encode_value(val,:byte) when is_integer(val), do: <> - defp encode_value(val,:short) when is_integer(val), do: <> - defp encode_value(val,:integer) when is_integer(val), do: <> - defp encode_value(val,:signed) when is_integer(val), do: <> - defp encode_value(val,:date) when is_integer(val), do: <> - defp encode_value(val,:ifid) when is_integer(val), do: <> - defp encode_value({a,b,c,d},:ipaddr), do: <> - defp encode_value(x,:ipaddr) when is_integer(x), do: <> - defp encode_value(x,:ipv6addr) when is_tuple(x) and tuple_size(x) == 8 do + defp encode_value(x, :ipv6addr) when is_tuple(x) and tuple_size(x) == 8 do for x <- :erlang.tuple_to_list(x), into: "", do: <> end - defp encode_value(bin,_), do: bin + defp encode_value(bin, _), do: bin - defp resolve_attr({{type,vid},value},ctx) when type=="Vendor-Specific" or type == 26 do - {26,encode_vsa(vid,value,ctx)} + defp resolve_attr({{type, vid}, value}, ctx) when type == "Vendor-Specific" or type == 26 do + {26, encode_vsa(vid, value, ctx)} end - defp resolve_attr(tlv,ctx) do - resolve_attr(tlv,ctx,%Vendor{}) + defp resolve_attr(tlv, ctx) do + resolve_attr(tlv, ctx, %Vendor{}) end - defp resolve_attr({type,value},ctx,vendor) do - case lookup_attr(vendor,type) do - nil -> {type,value} - a -> {a.id,lookup_value(a,value)|>encrypt_value(a,ctx),a} + defp resolve_attr({type, value}, ctx, vendor) do + case lookup_attr(vendor, type) do + nil -> {type, value} + a -> {a.id, lookup_value(a, value) |> encrypt_value(a, ctx), a} end end - defp lookup_attr(vendor,type) when is_integer(type) do + defp lookup_attr(vendor, type) when is_integer(type) do try do - Attribute.by_id vendor.id,type + Attribute.by_id(vendor.id, type) rescue _e in EntryNotFoundError -> nil end end - #Raise an error if attr not defined - defp lookup_attr(_vendor,type) when is_binary(type) do - Attribute.by_name type + + # Raise an error if attr not defined + defp lookup_attr(_vendor, type) when is_binary(type) do + Attribute.by_name(type) end - defp lookup_value(attr,{tag,val}) do - {tag,lookup_value(attr,val)} + defp lookup_value(attr, {tag, val}) do + {tag, lookup_value(attr, val)} end - defp lookup_value(%{type: :integer}=attr,val) when is_binary(val) do + + defp lookup_value(%{type: :integer} = attr, val) when is_binary(val) do try do - v = Value.by_name attr.vendor.name,attr.name,val + v = Value.by_name(attr.vendor.name, attr.name, val) v.value - rescue _e in EntryNotFoundError-> - #raise "Value can not be resolved: #{attr.name}: #{val}" - val + rescue + _e in EntryNotFoundError -> + # raise "Value can not be resolved: #{attr.name}: #{val}" + val end end - defp lookup_value(_,val), do: val - defp encode_vsa(vid,value,ctx) when is_binary(value) and is_binary(vid), do: encode_vsa(Vendor.by_name(vid).id,value,ctx) - defp encode_vsa(vid,value,_) when is_binary(value) and is_integer(vid), do: <> - defp encode_vsa(vid,vsa,ctx) when is_tuple(vsa), do: encode_vsa(vid, [vsa], ctx) - defp encode_vsa(vid,vsa,ctx) when is_binary(vid), do: encode_vsa(Vendor.by_name(vid), vsa, ctx) - defp encode_vsa(vid,vsa,ctx) when is_integer(vid), do: encode_vsa(Vendor.by_id(vid), vsa, ctx) + defp lookup_value(_, val), do: val + + defp encode_vsa(vid, value, ctx) when is_binary(value) and is_binary(vid), + do: encode_vsa(Vendor.by_name(vid).id, value, ctx) + + defp encode_vsa(vid, value, _) when is_binary(value) and is_integer(vid), + do: <> + + defp encode_vsa(vid, vsa, ctx) when is_tuple(vsa), do: encode_vsa(vid, [vsa], ctx) + + defp encode_vsa(vid, vsa, ctx) when is_binary(vid), + do: encode_vsa(Vendor.by_name(vid), vsa, ctx) + + defp encode_vsa(vid, vsa, ctx) when is_integer(vid), do: encode_vsa(Vendor.by_id(vid), vsa, ctx) + defp encode_vsa(vendor, vsa, ctx) do - val = Enum.map vsa, fn(x) -> - x|> resolve_attr(ctx,vendor) |> encode_attr - end - [<>|val] + val = + Enum.map(vsa, fn x -> + x |> resolve_attr(ctx, vendor) |> encode_attr + end) + + [<> | val] end defp encode_code(x) when is_integer(x), do: x @@ -389,7 +453,7 @@ defmodule Radius.Packet do raw = %{packet | attrs: attrs} |> Radius.Packet.encode(raw: true, sign: true) - |> IO.iodata_to_binary + |> IO.iodata_to_binary() crop_len = byte_size(raw) - 16 <<_::bytes-size(crop_len), sig2::binary>> = raw @@ -399,4 +463,6 @@ defmodule Radius.Packet do false end end -end #defmodule Packet +end + +# defmodule Packet diff --git a/lib/radius/supervisor.ex b/lib/radius/supervisor.ex index 86777e7..3d42320 100644 --- a/lib/radius/supervisor.ex +++ b/lib/radius/supervisor.ex @@ -9,10 +9,11 @@ defmodule Radius.Supervisor do dict = [Application.app_dir(:elixir_radius), "priv", "dictionary"] |> Path.join() - + children = [ - worker(Radius.Dict, [dict]) + {Radius.Dict, dict} ] - supervise(children, strategy: :one_for_one) + + Supervisor.init(children, strategy: :one_for_one) end end diff --git a/lib/radius/util.ex b/lib/radius/util.ex index db374ca..efbbd4b 100644 --- a/lib/radius/util.ex +++ b/lib/radius/util.ex @@ -1,7 +1,6 @@ defmodule Radius.Util do require Logger - - use Bitwise + import Bitwise def encrypt_rfc2865(passwd, secret, auth) do passwd @@ -17,10 +16,12 @@ defmodule Radius.Util do def encrypt_rfc2868(passwd, secret, auth) do salt = :crypto.strong_rand_bytes(2) + encrypted = passwd |> pad_to_16() |> hash_xor(auth <> salt, secret, []) + salt <> encrypted end @@ -31,9 +32,11 @@ defmodule Radius.Util do end defp hash_xor(input, hash, secret, acc, opts \\ []) + defp hash_xor(<<>>, _, _, acc, _) do acc |> Enum.reverse() |> :erlang.iolist_to_binary() end + defp hash_xor(<>, hash, secret, acc, opts) do hash = :crypto.hash(:md5, secret <> hash) xor_block = binary_xor(block, hash) @@ -45,22 +48,24 @@ defmodule Radius.Util do s = byte_size(x) * 8 <> = x <> = y - z = x ^^^ y + z = bxor(x, y) <> end defp pad_to_16(bin) do pad_to_16(bin, []) - |> Enum.reverse + |> Enum.reverse() |> :erlang.iolist_to_binary() end defp pad_to_16(bin, acc) when byte_size(bin) == 16, do: [bin | acc] + defp pad_to_16(bin, acc) when byte_size(bin) < 16 do - bin = <> + bin = <> <> = bin [chunk | acc] end + defp pad_to_16(<>, acc), do: pad_to_16(rest, [chunk | acc]) end diff --git a/mix.exs b/mix.exs index 2b1d3e8..eeaadba 100644 --- a/mix.exs +++ b/mix.exs @@ -2,12 +2,14 @@ defmodule RadiusProxy.Mixfile do use Mix.Project def project do - [app: :elixir_radius, - version: "1.0.0", - elixir: "~> 1.0", - description: desc(), - package: package(), - deps: deps()] + [ + app: :elixir_radius, + version: "1.0.0", + elixir: "~> 1.12", + description: desc(), + package: package(), + deps: deps() + ] end # Configuration for the OTP application @@ -15,9 +17,9 @@ defmodule RadiusProxy.Mixfile do # Type `mix help compile.app` for more information def application() do [ - applications: [:logger,:crypto], + applications: [:logger, :crypto], registered: [Radius.Dict], - mod: {Radius.Application,[]}, + mod: {Radius.Application, []} ] end @@ -37,15 +39,17 @@ defmodule RadiusProxy.Mixfile do # {:socket,"~> 0.2.8"} ] end + defp desc() do """ Decode & encode RADIUS packets """ end + defp package() do [ - files: ["lib","src","mix.exs","example.exs","README.md","LICENSE","priv"], - contributors: ["Bearice Ren","Guilherme Balena Versiani"], + files: ["lib", "src", "mix.exs", "example.exs", "README.md", "LICENSE", "priv"], + contributors: ["Bearice Ren", "Guilherme Balena Versiani", "Timmo Verlaan"], licenses: ["MIT License"], links: %{"Github" => "https://github.com/bearice/elixir-radius"} ] diff --git a/test/radius_util_test.exs b/test/radius_util_test.exs index 1fd138d..38310f5 100644 --- a/test/radius_util_test.exs +++ b/test/radius_util_test.exs @@ -1,56 +1,62 @@ defmodule Radius.UtilTest do use ExUnit.Case - @a <<131,203,230,68,225,38,170,174,240,200,112,101,138,46,93,66>> - @e <<47,30,188,38,120,163,223,41,29,48,100,113,255,68,152,156>> + @a <<131, 203, 230, 68, 225, 38, 170, 174, 240, 200, 112, 101, 138, 46, 93, 66>> + @e <<47, 30, 188, 38, 120, 163, 223, 41, 29, 48, 100, 113, 255, 68, 152, 156>> @s "112233" @p "1234" - @a_long <<112,201,55,232,16,34,158,42,134,188,54,96,180,224,125,13>> - @e_long <<3,1,53,143,84,247,176,236,161,76,181,246,222,25,58, - 108,86,46,119,108,134,209,142,31,93,83,184,136,162,52,164,111>> + @a_long <<112, 201, 55, 232, 16, 34, 158, 42, 134, 188, 54, 96, 180, 224, 125, 13>> + @e_long <<3, 1, 53, 143, 84, 247, 176, 236, 161, 76, 181, 246, 222, 25, 58, 108, 86, 46, 119, + 108, 134, 209, 142, 31, 93, 83, 184, 136, 162, 52, 164, 111>> @s_long "abcd" @p_long "12345678901234567890" test "RFC2865 encrypt" do - assert Radius.Util.encrypt_rfc2865(@p,@s,@a) == @e + assert Radius.Util.encrypt_rfc2865(@p, @s, @a) == @e end + test "RFC2865 encrypt (long)" do - assert Radius.Util.encrypt_rfc2865(@p_long,@s_long,@a_long) == @e_long + assert Radius.Util.encrypt_rfc2865(@p_long, @s_long, @a_long) == @e_long end + test "RFC2865 decrypt" do - assert Radius.Util.decrypt_rfc2865(@e,@s,@a) == @p + assert Radius.Util.decrypt_rfc2865(@e, @s, @a) == @p end + test "RFC2865 decrypt (long)" do - assert Radius.Util.decrypt_rfc2865(@e_long,@s_long,@a_long) == @p_long + assert Radius.Util.decrypt_rfc2865(@e_long, @s_long, @a_long) == @p_long end + test "RFC2865 utf8" do p = "测试1234测试1234测试1234" - e = Radius.Util.encrypt_rfc2865(p,@s,@a) - assert Radius.Util.decrypt_rfc2865(e,@s,@a) == p + e = Radius.Util.encrypt_rfc2865(p, @s, @a) + assert Radius.Util.decrypt_rfc2865(e, @s, @a) == p end + test "RFC2865 empty_string" do p = "" - e = Radius.Util.encrypt_rfc2865(p,@s,@a) - assert Radius.Util.decrypt_rfc2865(e,@s,@a) == p + e = Radius.Util.encrypt_rfc2865(p, @s, @a) + assert Radius.Util.decrypt_rfc2865(e, @s, @a) == p end + test "RFC2868" do p = "测试1234测试1234测试1234" - e = Radius.Util.encrypt_rfc2868(p,@s,@a) - assert Radius.Util.decrypt_rfc2868(e,@s,@a) == p + e = Radius.Util.encrypt_rfc2868(p, @s, @a) + assert Radius.Util.decrypt_rfc2868(e, @s, @a) == p end @secret "mykey" - @sample_packet <<1, 118, 0, 173, 55, 91, 232, 245, 150, 233, 11, 207, 252, - 94, 50, 146, 157, 20, 39, 91, 4, 6, 10, 62, 1, 238, 5, 6, 0, 0, 195, 81, 61, - 6, 0, 0, 0, 15, 1, 31, 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, - 55, 116, 114, 97, 99, 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, - 107, 30, 19, 48, 48, 45, 49, 50, 45, 48, 48, 45, 69, 51, 45, 52, 49, 45, 67, - 49, 31, 19, 66, 52, 45, 57, 57, 45, 66, 65, 45, 70, 50, 45, 56, 65, 45, 68, - 54, 6, 6, 0, 0, 0, 2, 12, 6, 0, 0, 5, 220, 79, 36, 2, 0, 0, 34, 1, 104, 111, - 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97, 99, 121, 112, - 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107, 80, 18, 201, 62, 246, 40, - 105, 10, 87, 139, 49, 112, 155, 11, 188, 202, 222, 65>> + @sample_packet <<1, 118, 0, 173, 55, 91, 232, 245, 150, 233, 11, 207, 252, 94, 50, 146, 157, 20, + 39, 91, 4, 6, 10, 62, 1, 238, 5, 6, 0, 0, 195, 81, 61, 6, 0, 0, 0, 15, 1, 31, + 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97, 99, + 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107, 30, 19, 48, 48, + 45, 49, 50, 45, 48, 48, 45, 69, 51, 45, 52, 49, 45, 67, 49, 31, 19, 66, 52, 45, + 57, 57, 45, 66, 65, 45, 70, 50, 45, 56, 65, 45, 68, 54, 6, 6, 0, 0, 0, 2, 12, + 6, 0, 0, 5, 220, 79, 36, 2, 0, 0, 34, 1, 104, 111, 115, 116, 47, 100, 114, 115, + 119, 105, 110, 55, 116, 114, 97, 99, 121, 112, 46, 100, 114, 115, 108, 46, 99, + 111, 46, 117, 107, 80, 18, 201, 62, 246, 40, 105, 10, 87, 139, 49, 112, 155, + 11, 188, 202, 222, 65>> test "Message-Authenticator" do packet = Radius.Packet.decode(@sample_packet, @secret) @@ -72,7 +78,7 @@ defmodule Radius.UtilTest do assert sig1 == sig2 - raw = signed |> Radius.Packet.encode(raw: true) |> IO.iodata_to_binary + raw = signed |> Radius.Packet.encode(raw: true) |> IO.iodata_to_binary() assert raw == @sample_packet