From bd3ae9694c0e8669c8ce53aef685e8afede14f33 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sun, 13 Dec 2015 17:47:14 +0100 Subject: Initial commit --- src/coap_parse.erl | 137 ++++++++++++++++++++++++++++++++++++ src/coap_unparse.erl | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/coaperl.app.src | 11 +++ src/coaperl_sup.erl | 29 ++++++++ src/corerl.erl | 72 +++++++++++++++++++ 5 files changed, 444 insertions(+) create mode 100644 src/coap_parse.erl create mode 100644 src/coap_unparse.erl create mode 100644 src/coaperl.app.src create mode 100644 src/coaperl_sup.erl create mode 100644 src/corerl.erl (limited to 'src') 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}) -> + <> = <>, + #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 + [<>] -> 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, + <> = Val, + {Num, M, 1 bsl (SZX+4)}; + _ -> undefined + end. + + +% @doc: parse raw packet +parse_raw(<>) 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(<>, LastNum, OptionList) -> + {Tail1, OptNum} = case BaseOptNum of + X when X < 13 -> + {Tail, BaseOptNum + LastNum}; + X when X =:= 13 -> + <> = Tail, + {NewTail, ExtOptNum + 13 + LastNum}; + X when X =:= 14 -> + <> = Tail, + {NewTail, ExtOptNum + 269 + LastNum} + end, + {OptLen, Tail3} = case BaseOptLen of + Y when Y < 13 -> + {BaseOptLen, Tail1}; + Y when Y =:= 13 -> + <> = Tail1, + {ExtOptLen + 13, Tail2}; + Y when Y =:= 14 -> + <> = Tail1, + {ExtOptLen + 269, Tail2} + end, + <> = 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 = [[<>, + 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,;if=\"clock\";rt=\"Ticks\";title=\"Internal Clock\";ct=0;obs,;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. -- cgit v1.2.3