aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/coap_parse.erl137
-rw-r--r--src/coap_unparse.erl195
-rw-r--r--src/coaperl.app.src11
-rw-r--r--src/coaperl_sup.erl29
-rw-r--r--src/corerl.erl72
5 files changed, 444 insertions, 0 deletions
diff --git a/src/coap_parse.erl b/src/coap_parse.erl
new file mode 100644
index 0000000..bb61a28
--- /dev/null
+++ b/src/coap_parse.erl
@@ -0,0 +1,137 @@
+-module(coap_parse).
+-export([parse/1,
+ get_option_values/2]).
+
+-include("coaperl.hrl").
+
+
+-spec parse(<<>>) -> #coap_request{} | #coap_response{} | #coap_empty_message{}.
+% @doc: parse a binary coap message
+parse(Packet) ->
+ {coap, Parsed} = parse_raw(Packet),
+ parse_to_rec(Parsed).
+
+
+% @doc get the named option values from the given response record
+get_option_values(#coap_response{options=Options}, OptNum) ->
+ [ Val || {coap_option, Num, Val} <- Options, Num =:= OptNum ].
+
+
+% @doc: convert raw message to request, response or empty message records
+parse_to_rec({_Type, Code, MID, _Token, Options, Payload}) when Code > 0, Code =< 31 ->
+ #coap_request{method=coap_unparse:method_name(Code),
+ path=parse_path(Options),
+ query=parse_query(Options),
+ mid=MID,
+ ct=parse_content_type(Options),
+ payload=Payload};
+parse_to_rec({_Type, 0, MID , _Token, _Options, <<>>}) ->
+ #coap_empty_message{mid=MID};
+parse_to_rec({_Type, Code, MID, _Token, Options, Payload}) ->
+ <<Class:3, Detail:5>> = <<Code>>,
+ #coap_response{mid=MID,
+ ct=parse_content_type(Options),
+ options=Options,
+ class=Class,
+ detail=Detail,
+ block2=parse_block2(Options),
+ payload=Payload}.
+
+
+% @doc: parse path from raw options
+parse_path(Options) ->
+ erlang:iolist_to_binary(lists:reverse(parse_path(Options, []))).
+
+parse_path([{coap_option, ?COAP_OPTION_URI_PATH, Comp}|T], Acc) ->
+ parse_path(T, [["/"|Comp]|Acc]);
+parse_path([{coap_option, _, _}|T], Acc) ->
+ parse_path(T, Acc);
+parse_path([], Acc) ->
+ Acc.
+
+
+% @doc: parse query from raw options
+parse_query(Options) ->
+ parse_query(Options, []).
+
+parse_query([{coap_option, ?COAP_OPTION_URI_QUERY, Opt}|T], Acc) ->
+ parse_query(T, [Opt|Acc]);
+parse_query([{coap_option, _, _}|T], Acc) ->
+ parse_query(T, Acc);
+parse_query([], Acc) ->
+ Acc.
+
+
+
+% @doc: parse content type from raw options
+parse_content_type(Options) ->
+ OptVals = [Value || {coap_option, Format, Value} <- Options, Format =:= ?COAP_OPTION_CONTENT_FORMAT],
+ case OptVals of
+ [<<Val:8>>] -> corerl:content_format_type(Val);
+ _ -> undefined
+ end.
+
+
+% @doc: parse block2 option from raw options
+parse_block2(Options) ->
+ OptVals = [Value || {coap_option, Format, Value} <- Options, Format =:= ?COAP_OPTION_BLOCK2],
+ case OptVals of
+ [Val] ->
+ NumLen = bit_size(Val)-4,
+ <<Num:NumLen, M:1, SZX:3>> = Val,
+ {Num, M, 1 bsl (SZX+4)};
+ _ -> undefined
+ end.
+
+
+% @doc: parse raw packet
+parse_raw(<<Version:2, Type:2, TKL: 4, Code:8, MID:16, Token:TKL/bytes, Tail/bytes>>) when Version =:= 1 ->
+ {Options, Payload} = parse_raw_options(Tail),
+ {coap, {Type, Code, MID, Token, Options, Payload}};
+parse_raw(_AnythingElse) ->
+ {bad, "unable to parse packet"}.
+
+% @doc: parse options in raw packet, don't interpret any option types yet
+parse_raw_options(OptionBin) ->
+ parse_raw_options(OptionBin, 0, []).
+
+parse_raw_options(<<>>, _LastNum, OptionList) ->
+ {OptionList, <<>>};
+parse_raw_options(<<16#FF, Payload/bytes>>, _LastNum, OptionList) ->
+ {OptionList, Payload};
+parse_raw_options(<<BaseOptNum:4, BaseOptLen:4, Tail/bytes>>, LastNum, OptionList) ->
+ {Tail1, OptNum} = case BaseOptNum of
+ X when X < 13 ->
+ {Tail, BaseOptNum + LastNum};
+ X when X =:= 13 ->
+ <<ExtOptNum, NewTail/bytes>> = Tail,
+ {NewTail, ExtOptNum + 13 + LastNum};
+ X when X =:= 14 ->
+ <<ExtOptNum:2/bytes, NewTail/bytes>> = Tail,
+ {NewTail, ExtOptNum + 269 + LastNum}
+ end,
+ {OptLen, Tail3} = case BaseOptLen of
+ Y when Y < 13 ->
+ {BaseOptLen, Tail1};
+ Y when Y =:= 13 ->
+ <<ExtOptLen, Tail2/bytes>> = Tail1,
+ {ExtOptLen + 13, Tail2};
+ Y when Y =:= 14 ->
+ <<ExtOptLen:2/bytes, Tail2/bytes>> = Tail1,
+ {ExtOptLen + 269, Tail2}
+ end,
+ <<OptVal:OptLen/bytes, NextOpt/bytes>> = Tail3,
+ parse_raw_options(NextOpt, OptNum, [{coap_option, OptNum, OptVal}|OptionList]).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+parse_to_rec_test() ->
+ % Check if parse to the right rec
+ ?assertEqual(parse_to_rec({egal, 100, 1, egal, [{coap_option, ?COAP_OPTION_CONTENT_FORMAT, <<50:8>>}], payload}),
+ {coap_response,3,4,1,
+ <<"application/json">>,
+ undefined,
+ [{coap_option,12,<<"2">>}],
+ payload}).
+-endif.
diff --git a/src/coap_unparse.erl b/src/coap_unparse.erl
new file mode 100644
index 0000000..aaca450
--- /dev/null
+++ b/src/coap_unparse.erl
@@ -0,0 +1,195 @@
+-module(coap_unparse).
+-export([build_request/2,
+ build_response/3,
+ build_text_response/4,
+ method_name/1]).
+
+-include("coaperl.hrl").
+
+coap_scheme_defaults() ->
+ [{coap, 5683},
+ {coaps, 5684},
+ {'coap+tcp', 5683},
+ {'coaps+tcp', 443}
+ ].
+
+
+% RFC7252, Section 3
+types() ->
+ [{confirmable, 0},
+ {nonconfirmable, 1},
+ {acknowledgement, 2},
+ {reset, 3}].
+
+type_code(Type) ->
+ proplists:get_value(Type, types()).
+
+
+% RFC7252, Section 3
+%% code() ->
+%% [{request, 0},
+%% {success, 2},
+%% {clienterror, 4},
+%% {servererror, 5}].
+
+
+
+% RFC7252, Section 12.1.1
+methods() ->
+ [{get, 1},
+ {post, 2},
+ {put, 3},
+ {delete, 4}
+ ].
+method_code(Method) ->
+ proplists:get_value(Method, methods()).
+method_name(Number) ->
+ {Method, Number} = lists:keyfind(Number, 2, methods()),
+ Method.
+
+
+-spec uri_to_options(string()) -> [{string(), integer(), list()}].
+% convert an URI to the corresponding COAP Options
+uri_to_options(Uri) ->
+ {ok, {_, _, Host, Port, Path, _}} = http_uri:parse(Uri,
+ [{scheme_defaults, coap_scheme_defaults()},
+ {ipv6_host_with_brackets, true}]),
+
+ % Remove brackets from addr if present
+ Host2 = case Host of
+ [$[|T] -> [X || X <- T, X =/= $]];
+ _ -> Host
+ end,
+ {Host2, Port, path_to_options(Path)}.
+
+
+% convert an URI path to the corresponding COAP Options
+path_to_options([$/|Path]) ->
+ lists:reverse(path_to_options(string:tokens(Path, "/"), [])).
+
+path_to_options([], Options) ->
+ Options;
+path_to_options([H|T], Options) ->
+ path_to_options(T, [{?COAP_OPTION_URI_PATH, H}|Options]).
+
+
+-spec content_format_to_options(string()) -> [{integer(), integer()}].
+% @doc: convert a content format into a number (12.3)
+content_format_to_options(ContentFormat) ->
+ FormatNum = corerl:content_format_number(ContentFormat),
+ [{?COAP_OPTION_CONTENT_FORMAT, FormatNum}].
+
+
+-spec num_bytes(integer()) -> integer().
+num_bytes(Val) when is_integer(Val), Val =< 16#ff ->
+ 1;
+num_bytes(Val) when is_integer(Val), Val =< 16#ffff ->
+ 2;
+num_bytes(Val) when is_integer(Val), Val =< 16#ffffff ->
+ 3.
+
+
+-spec option_length(integer() | binary() | list()) -> integer().
+option_length(Val) when is_integer(Val) ->
+ num_bytes(Val);
+option_length(Val) when is_binary(Val) ->
+ byte_size(Val);
+option_length(Val) when is_list(Val) ->
+ string:len(Val).
+
+
+-spec option_format_int(integer()) -> {integer(), integer(), integer()}.
+option_format_int(Val) ->
+ case Val of
+ X when X < 13 ->
+ {X, 0, 0};
+ X when 13 =< X, X < 269 ->
+ {13, X-13, 8};
+ X when X >= 14 ->
+ {14, X-269, 16}
+ end.
+
+
+-spec options_to_bin(list()) -> iolist().
+options_to_bin(Options) ->
+ % FIXME: sort options by number here
+ options_to_bin(Options, 0, []).
+
+options_to_bin([], _, Acc) ->
+ lists:reverse(Acc);
+options_to_bin([{Num, Val}|T], OldNum, Acc) when is_integer(Num)->
+ Diff = Num - OldNum,
+ if Diff < 0 -> erlang:error(badarith);
+ true -> true
+ end,
+ Len = option_length(Val),
+
+ {Delta, ExtDelta, ExtDeltaBits} = option_format_int(Diff),
+ {Length, ExtLength, ExtLengthBits} = option_format_int(Len),
+
+ NewBin = [[<<Delta:4, Length:4,
+ ExtDelta:ExtDeltaBits,
+ ExtLength:ExtLengthBits>>,
+ Val] | Acc],
+ options_to_bin(T, Num, NewBin).
+
+
+build_request([{url, Url}, {method, Method}], Msgid, Options) ->
+ {Host, Port, UriOptions} = uri_to_options(Url),
+ BinOptions = options_to_bin(UriOptions ++ Options),
+ MethodNumber = method_code(Method),
+ Type = type_code(confirmable),
+ {Host, Port, [<<1:2, Type:2, 0:4, 0:3, MethodNumber:5, Msgid:16>>, BinOptions]}.
+
+build_request(Url, Msgid) ->
+ build_request([{url, Url}, {method, get}], Msgid, []).
+
+
+build_response(Class, Detail, Msgid) ->
+ Type = type_code(confirmable),
+ [<<1:2, Type:2, 0:4, Class:3, Detail:5, Msgid:16>>].
+
+build_text_response(Class, Detail, Msgid, Payload) ->
+ Options = content_format_to_options("text/plain"),
+ BinOptions = options_to_bin(Options),
+ Type = type_code(confirmable),
+ [<<1:2, Type:2, 0:4, Class:3, Detail:5, Msgid:16>>, BinOptions, ?COAP_PAYLOAD_MARKER, Payload].
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+option_delta_test() ->
+ % Check if we encode the option delta correctly
+ ?assertEqual(options_to_bin([{11, "foo"}]),
+ [[<<11:4, 3:4>>, "foo"]]),
+ ?assertEqual(options_to_bin([{15, "foo"}]),
+ [[<<13:4, 3:4, 2:8>>, "foo"]]),
+ ?assertEqual(options_to_bin([{275, "foo"}]),
+ [[<<14:4, 3:4, 6:16>>, "foo"]]).
+
+option_length_test() ->
+ % Check if we encode the option value length correctly
+ ?assertEqual(options_to_bin([{11, "12345678901234"}]),
+ [[<<11:4, 13:4, 1:8>>, "12345678901234"]]),
+ % integer represented in one byte
+ ?assertEqual(options_to_bin([{11, 24}]),
+ [[<<11:4, 1:4>>, 24]]),
+ % integer represented in two bytes
+ ?assertEqual(options_to_bin([{11, 324}]),
+ [[<<11:4, 2:4>>, 324]]),
+ % integer represented in three bytes
+ ?assertEqual(options_to_bin([{11, 70024}]),
+ [[<<11:4, 3:4>>, 70024]]).
+
+path_to_options_test() ->
+ Path = "/.well-known/core",
+ Ret = path_to_options(Path),
+ ?assertEqual(Ret, [{11,".well-known"},{11,"core"}]).
+
+uri_to_options_test() ->
+ Uri = "coap://[::1]/.well-known/core",
+ Ret = uri_to_options(Uri),
+ ?assertEqual(Ret, {"::1", 5683, [{11,".well-known"},{11,"core"}]}).
+
+-endif.
diff --git a/src/coaperl.app.src b/src/coaperl.app.src
new file mode 100644
index 0000000..df8c84c
--- /dev/null
+++ b/src/coaperl.app.src
@@ -0,0 +1,11 @@
+{application, coaperl,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {env, []}
+ ]}.
diff --git a/src/coaperl_sup.erl b/src/coaperl_sup.erl
new file mode 100644
index 0000000..b57c918
--- /dev/null
+++ b/src/coaperl_sup.erl
@@ -0,0 +1,29 @@
+-module(coaperl_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type, Args), {I, {I, start_link, [Args]}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, { {one_for_one, 5, 10}
+% [?CHILD(udp, worker, [])]
+ } }.
+
diff --git a/src/corerl.erl b/src/corerl.erl
new file mode 100644
index 0000000..6aa4af5
--- /dev/null
+++ b/src/corerl.erl
@@ -0,0 +1,72 @@
+-module(corerl).
+-export([parse/1,
+ content_format_number/1,
+ content_format_type/1]).
+
+-include("corerl.hrl").
+
+
+content_formats() ->
+ [{"text/plain;charset=utf-8", 0},
+ % shortcut for the above
+ {"text/plain", 0},
+ {"application/link-format", 40},
+ {"application/xml", 41},
+ {"application/octext-stream", 42},
+ {"application/exi", 47},
+ {"application/json", 50}].
+
+content_format_number(ContentFormat) ->
+ proplists:get_value(ContentFormat, content_formats()).
+
+content_format_type(Number) ->
+ list_to_binary(proplists:get_value(Number,
+ [{N,F} || {F,N} <- content_formats()]
+ )).
+
+
+-spec parse(<<>>) -> list(#core_link{}).
+% @doc: parse a core message
+parse(Data) ->
+ parse_link_values(binary:split(Data, <<",">>, [global]), []).
+
+
+parse_link_values([LinkValue|T], Acc) ->
+ Link = parse_link_value(LinkValue),
+ parse_link_values(T, [Link|Acc]);
+parse_link_values([], Acc) ->
+ Acc.
+
+parse_link_value(<<$<, Data/binary>>) ->
+ [Value, Params] = binary:split(Data, <<$>>>),
+ parse_link_params(binary:split(Params, <<$;>>, [global]), #core_link{uri=Value}).
+
+
+parse_link_params([LinkParam|T], Rec) ->
+ NewRec = case binary:split(LinkParam, <<$=>>, [global]) of
+ [<<"title">>, V] ->
+ Rec#core_link{title=V};
+ [<<"ct">>, V] ->
+ Rec#core_link{ct=content_format_type(binary_to_integer(V))};
+ [<<"rt">>, V] ->
+ Rec#core_link{rt=V};
+ [<<"if">>, V] ->
+ Rec#core_link{'if'=V};
+ _ -> Rec
+ end,
+ parse_link_params(T, NewRec);
+parse_link_params([], Rec) ->
+ Rec.
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+parse_link_values_test() ->
+ Data = <<"</>;title=\"General Info\";ct=0,</time>;if=\"clock\";rt=\"Ticks\";title=\"Internal Clock\";ct=0;obs,</async>;ct=50">>,
+ ?assertEqual(parse(Data),
+ [{core_link,<<"/async">>,undefined,undefined,undefined,<<"application/json">>,undefined},
+ {core_link,<<"/time">>,<<"\"Internal Clock\"">>,undefined,<<"\"Ticks\"">>, <<"text/plain;charset=utf-8">>,<<"\"clock\"">>},
+ {core_link,<<"/">>,<<"\"General Info\"">>,undefined,undefined,<<"text/plain;charset=utf-8">>,undefined}
+ ]).
+-endif.