summaryrefslogtreecommitdiff
path: root/deps
diff options
context:
space:
mode:
authorUlf Wiger <ulf@feuerlabs.com>2015-07-27 22:30:15 +0200
committerUlf Wiger <ulf@feuerlabs.com>2015-07-27 22:30:15 +0200
commitbeaa827117c8ec897e7e110e885b041643ddd750 (patch)
treedbccb79bb8adba439cd9c6158a8246192022bb11 /deps
parent6eb6551e73f136ad8266f5904f695a6f77381f67 (diff)
downloadrvi_core-beaa827117c8ec897e7e110e885b041643ddd750.tar.gz
au and sa working with sms simulator
Diffstat (limited to 'deps')
-rw-r--r--deps/gsms/Makefile15
-rw-r--r--deps/gsms/ebin/.gitignore2
-rw-r--r--deps/gsms/include/gsms.hrl2
-rw-r--r--deps/gsms/rebar.config1
-rw-r--r--deps/gsms/src/gsms_lib.erl25
-rw-r--r--deps/gsms/src/gsms_plivo.erl373
-rw-r--r--deps/gsms/src/gsms_plivo_sim.erl353
-rw-r--r--deps/gsms/src/gsms_router.erl136
-rw-r--r--deps/gsms/src/gsms_session.erl123
9 files changed, 964 insertions, 66 deletions
diff --git a/deps/gsms/Makefile b/deps/gsms/Makefile
new file mode 100644
index 0000000..cc5afdf
--- /dev/null
+++ b/deps/gsms/Makefile
@@ -0,0 +1,15 @@
+.PHONY: all compile deps clean shell
+
+all: compile
+
+deps:
+ rebar get-deps
+
+compile: deps
+ rebar compile
+
+clean:
+ rebar clean
+
+shell: compile
+ ERL_LIBS=$(PWD)/deps erl -pa ebin
diff --git a/deps/gsms/ebin/.gitignore b/deps/gsms/ebin/.gitignore
new file mode 100644
index 0000000..120fe3a
--- /dev/null
+++ b/deps/gsms/ebin/.gitignore
@@ -0,0 +1,2 @@
+*.beam
+*.app
diff --git a/deps/gsms/include/gsms.hrl b/deps/gsms/include/gsms.hrl
index 72b0dea..0adfd81 100644
--- a/deps/gsms/include/gsms.hrl
+++ b/deps/gsms/include/gsms.hrl
@@ -75,7 +75,7 @@
scts, %% :7/binary
udh=[] :: [gsms_ie()], %% user data header
udl, %% length in septets/octets (depend on dcs)
- ud
+ ud %% user data
}).
-define(MTI_SMS_SUBMIT, 2#01).
diff --git a/deps/gsms/rebar.config b/deps/gsms/rebar.config
index 5f6d3b4..ec9b7c4 100644
--- a/deps/gsms/rebar.config
+++ b/deps/gsms/rebar.config
@@ -1,6 +1,7 @@
%% -*- erlang -*-
%% Config file for gsms application
{deps, [ {uart, ".*", {git, "git@github.com:tonyrog/uart.git"}},
+ {exo, ".*", {git, "git@github.com:Feuerlabs/exo.git"}},
{lager, ".*", {git, "git://github.com/Feuerlabs/lager.git"}}]}.
diff --git a/deps/gsms/src/gsms_lib.erl b/deps/gsms/src/gsms_lib.erl
new file mode 100644
index 0000000..187aefd
--- /dev/null
+++ b/deps/gsms/src/gsms_lib.erl
@@ -0,0 +1,25 @@
+-module(gsms_lib).
+
+-export([get_opt/2,
+ get_opt/3]).
+
+get_opt(K, Opts) when is_atom(K) ->
+ case lists:keyfind(K, 1, Opts) of
+ false -> erlang:error({mandatory, K});
+ {_, V} -> V
+ end;
+get_opt({K, Def}, Opts) ->
+ get_opt(K, Opts, Def).
+
+get_opt(K, Opts, Def) ->
+ case lists:keyfind(K, 1, Opts) of
+ false when is_function(Def, 0) ->
+ Def();
+ false when Def == '$mandatory' ->
+ error({mandatory, K});
+ false ->
+ Def;
+ {_, V} ->
+ V
+ end.
+
diff --git a/deps/gsms/src/gsms_plivo.erl b/deps/gsms/src/gsms_plivo.erl
new file mode 100644
index 0000000..25be7b6
--- /dev/null
+++ b/deps/gsms/src/gsms_plivo.erl
@@ -0,0 +1,373 @@
+-module(gsms_plivo).
+-behaviour(gsms_session).
+
+-export([start_link/2, % called from gsms_if_sup.erl
+ new/1]).
+
+-export([mandatory_options/0,
+ init/1,
+ handle_send/3,
+ get_signal_strength/1
+ ]).
+ %% handle_call/3,
+ %% subscribe/2]).
+
+-export([decode_body/2,
+ signature/3,
+ uuid/0,
+ http_date/0,
+ get_x_plivo_sig/1]).
+
+%% HTTP callback
+-export([handle_body/4]).
+
+-export([trace/0]).
+-export([test_new/0, test_new/2, simtest/1]).
+
+-record(st, {account,
+ auth_id,
+ auth_token,
+ src,
+ recv_uri,
+ recv_port,
+ recv_pid,
+ send_uri}).
+
+-record(server, {parent,
+ uri,
+ auth_token}).
+
+-include_lib("exo/include/exo_http.hrl").
+-include_lib("exo/src/exo_socket.hrl").
+-include("gsms.hrl").
+-include("log.hrl").
+-define(mandatory, '$mandatory').
+
+start_link(_Id, Opts) ->
+ {ok, new(Opts)}.
+
+new(Opts) ->
+ gsms_session:new(?MODULE, Opts).
+
+mandatory_options() ->
+ [auth_id, auth_token, src_number, recv_port, recv_uri].
+
+init(Opts) ->
+ [Acct, ID, Token, Src, Port, URI, SendURI, Attrs] =
+ [gsms_lib:get_opt(K, Opts)
+ || K <- [acct, auth_id, auth_token,
+ src_number, recv_port, recv_uri,
+ {send_uri, "http://api.plivo.com"},
+ {attributes, []}]],
+ {ok, RPid} = spawn_http_listener(Port, URI, Token),
+ {ok, Src, Attrs, #st{account = Acct,
+ auth_id = ID,
+ auth_token = Token,
+ src = no_plus(Src),
+ recv_uri = URI,
+ recv_port = Port,
+ recv_pid = RPid,
+ send_uri = SendURI}}.
+
+handle_send(Opts, Body, St) ->
+ try
+ Dest = gsms_lib:get_opt({addr, ?mandatory}, Opts),
+ Res = plivo_send_SMS(Dest, Body, St),
+ ?debug("send: Res = ~p~n", [Res]),
+ {ok, Res, St}
+ catch
+ error:E ->
+ {error, E}
+ end.
+
+get_signal_strength(St) ->
+ %% For now, simply return maximum strength (see gsms/src/README)
+ {ok, 30, St}.
+
+plivo_send_SMS(Dest, Msg, #st{auth_id = AuthID,
+ auth_token = AuthTok,
+ src = Src,
+ send_uri = SendURI,
+ recv_uri = RecvURI}) ->
+ URI = lists:flatten([SendURI, "/v1/Account/", AuthID, "/Message/"]),
+ JSON = {struct, [{"src", Src},
+ {"dst", no_plus(Dest)},
+ {"text", Msg},
+ {"url", RecvURI},
+ {"log", true}]},
+ Req = binary_to_list(iolist_to_binary(exo_json:encode(JSON))),
+ Hdrs = [{'Content-Type', "application/json"}
+ | exo_http:make_headers(AuthID, AuthTok)],
+ send_result(exo_http:wpost(URI, {1,1}, Hdrs, Req, 1000)).
+
+send_result({ok, #http_response{status = Status} = R, Body}) ->
+ if Status >= 200, Status =< 299 ->
+ {ok, get_uuid(R, Body)};
+ true ->
+ {error, {Status, R#http_response.phrase}}
+ end;
+send_result({error, _} = E) ->
+ E.
+
+spawn_http_listener(Port, URI, Token) ->
+ Srv = #server{parent = self(),
+ uri = URI,
+ auth_token = Token},
+ exo_http_server:start_link(Port, [{request_handler,
+ {?MODULE, handle_body, [Srv]}}]).
+
+handle_body(Socket, Request, Body, #server{auth_token = Tok, uri = URI}) ->
+ ?debug("handle_body(_, ~p, ~p, _)~n", [Request, Body]),
+ try decode_body(Request, Body) of
+ Result ->
+ case validate_request(URI, Request, Result, Tok) of
+ true ->
+ ?debug("handle_body() -> ~p~n", [Result]),
+ case parse_result(Result) of
+ ok ->
+ response(Socket, ok);
+ error ->
+ response(Socket, error)
+ end;
+ false ->
+ response(Socket, auth)
+ end
+ catch
+ _:_ ->
+ response(Socket, error)
+ end.
+
+validate_request(URI, Request, Result, Tok) ->
+ Sig = get_x_plivo_sig(Request),
+ check_signature(Request, URI, Result, Sig, Tok).
+
+check_signature(#http_request{uri = #url{path = Path}},
+ URI, Result, Sig, Tok) ->
+ URL = uri_join(URI, Path),
+ Sig == signature(URL, Result, Tok).
+
+uri_join(URI, Path) ->
+ strip_trailing_slash(URI) ++ strip_trailing_slash(Path).
+
+no_plus([$+|Num]) -> Num;
+no_plus(Num ) -> Num.
+
+add_plus([$+|_] = Num) -> Num;
+add_plus(Num ) -> "+" ++ Num.
+
+signature(URL, Result, Tok) ->
+ Str = lists:foldl(
+ fun({K, A}, S) when is_atom(A) ->
+ S ++ K ++ atom_to_list(A);
+ ({K, F}, S) when is_float(F) ->
+ S ++ K ++ io_lib_format:fwrite_g(F);
+ ({K, I}, S) when is_integer(I) ->
+ S ++ K ++ integer_to_list(I);
+ ({K, V}, S) ->
+ S ++ K ++ V
+ end, strip_trailing_slash(URL), lists:sort(params(Result))),
+ base64:encode_to_string(crypto:hmac(sha, Tok, Str)).
+
+http_date() ->
+ httpd_util:rfc1123_date().
+
+strip_trailing_slash(S) ->
+ case lists:reverse(S) of
+ "/" ++ Rest ->
+ lists:reverse(Rest);
+ _ ->
+ S
+ end.
+
+params({struct, Params}) ->
+ [params(P) || P <- Params];
+params({K, {array, A}}) ->
+ {K, [params(P) || P <- A]};
+params(Params) ->
+ Params.
+
+decode_body(R, Body) ->
+ case get_content_type(R) of
+ "application/x-www-form-urlencoded" ->
+ decode_www_form_urlencoded(Body);
+ "application/json" ->
+ decode_json(Body)
+ end.
+
+get_content_type(#http_request{headers = #http_chdr{content_type = T}}) -> T;
+get_content_type(#http_response{headers = #http_shdr{content_type = T}}) -> T.
+
+decode_www_form_urlencoded(Body) ->
+ lists:map(
+ fun(L) ->
+ [K,V] = re:split(L, "=", [{return,list}]),
+ {unescape(K), unescape(V)}
+ end, re:split(Body, "&", [{return,list}])).
+
+unescape([$%,A,B|T]) ->
+ [list_to_integer([A,B], 16) | unescape(T)];
+unescape([$+|T]) ->
+ [$\s|unescape(T)];
+unescape([H|T]) ->
+ [H|unescape(T)];
+unescape([]) ->
+ [].
+
+decode_json(Body) ->
+ exo_json:decode_string(to_string(Body)).
+
+to_string(B) when is_binary(B) ->
+ binary_to_list(B);
+to_string(S) when is_list(S) ->
+ S.
+
+uuid() ->
+ %% For now, convert to list (TODO: shouldn't be necessary)
+ binary_to_list(uuid_()).
+
+uuid_() ->
+ %% https://en.wikipedia.org/wiki/Universally_unique_identifier
+ N = 4, M = 2, % version 4 - random bytes
+ <<A:48, _:4, B:12, _:2, C:62>> = crypto:rand_bytes(16),
+ UBin = <<A:48, N:4, B:12, M:2, C:62>>,
+ <<A1:8/binary, B1:4/binary, C1:4/binary, D1:4/binary, E1:12/binary>> =
+ << <<(hex(X)):8>> || <<X:4>> <= UBin >>,
+ <<A1:8/binary, "-",
+ B1:4/binary, "-",
+ C1:4/binary, "-",
+ D1:4/binary, "-",
+ E1:12/binary>>.
+
+hex(X) when X >= 0, X =< 9 ->
+ $0 + X;
+hex(X) when X >= 10, X =< 15 ->
+ $a + X - 10.
+
+get_uuid(R, Body) ->
+ case decode_body(R, Body) of
+ {ok, Decoded} ->
+ case lists:keyfind("message_uuid", 1, params(Decoded)) of
+ false ->
+ io:fwrite("Cannot find message_uuid~n", []),
+ uuid();
+ {_, UUID} ->
+ UUID
+ end;
+ _ ->
+ io:fwrite("Couldn't decode body~n", []),
+ uuid()
+ end.
+
+get_x_plivo_sig(#http_response{headers = H}) ->
+ find_x_sig(other_hdrs(H));
+get_x_plivo_sig(#http_request{headers = H}) ->
+ find_x_sig(other_hdrs(H)).
+
+other_hdrs(#http_chdr{other = Hdrs}) -> Hdrs;
+other_hdrs(#http_shdr{other = Hdrs}) -> Hdrs.
+
+find_x_sig(Hdrs) ->
+ case lists:keyfind("X-Plivo-Signature", 1, Hdrs) of
+ false -> false;
+ {_, Sig} -> Sig
+ end.
+
+response(Socket, Reply) ->
+ {Code, Msg} = case Reply of
+ ok -> {200, "OK"};
+ error -> {404, "Not found"};
+ auth -> {401, "Authorization failed"}
+ end,
+ exo_http_server:response(Socket, undefined, Code, Msg, "").
+
+%% From https://www.plivo.com/docs/api/application/ :
+%% ------------------------------------------------------------
+%% The following parameters will be sent to the Message URL.
+%%
+%% Fromstring The source number of the incoming message.
+%% This will be the number of the person sending a message to a Plivo number.
+%% Tostring The number to which the message was sent.
+%% This will the your Plivo number on which the message has been received.
+%% Typestring Type of the message. This will always be sms
+%% Textstring The content of the message.
+%% MessageUUIDstring A unique ID for the message.
+%% Your message can be uniquely identified on Plivo by this ID.
+%% ------------------------------------------------------------
+parse_result(Result) ->
+ case Result of
+ {struct, Elems} ->
+ parse_result_(Elems);
+ [{_,_}|_] = Elems ->
+ parse_result_(Elems);
+ _ ->
+ error
+ end.
+
+parse_result_(Elems) ->
+ case lists:keyfind("Status", 1, Elems) of
+ {_, "delivered"} ->
+ {_, _UUID} = lists:keyfind("MessageUUID", 1, Elems),
+ %% gsms_router:notify(UUID, ok);
+ ok;
+ {_, _} ->
+ ok; % ignore for now
+ false ->
+ case [From, To, _Type, Text, _UUID] = _Res =
+ [proplists:get_value(K, Elems, "")
+ || K <- ["From", "To", "Type", "Text", "MessageUUID"]] of
+ _ when From =/= undefined ->
+ ?debug("Res = ~p~n", [_Res]),
+ gsms_router:input_from(To, #gsms_deliver_pdu{
+ addr = addr(From),
+ ud = Text}),
+ ok;
+ _ ->
+ ok % ignore for now
+ end
+ end.
+
+addr(A) ->
+ #gsms_addr{type = international,
+ addr = add_plus(A)}.
+
+%%
+test_new() ->
+ test_new("111", 9111).
+
+test_new(Src, Port) ->
+ application:ensure_all_started(gsms),
+ new([{acct, "Acct"},
+ {auth_id,"myacct"},{auth_token,"myauth"},
+ {src_number, Src},
+ {recv_port, Port},
+ {send_uri, "https://localhost:9100"},
+ {recv_uri,"https://localhost"}]).
+
+simtest(1) ->
+ application:ensure_all_started(gsms),
+ R = new([{acct, acct1},
+ {auth_id, "acct1"},
+ {auth_token, "auth1"},
+ {src_number, "111"},
+ {recv_port, 9200},
+ {send_uri, "http://localhost:9100"},
+ {recv_uri, "http://localhost:9200"}]),
+ trace(),
+ R;
+simtest(2) ->
+ application:ensure_all_started(gsms),
+ R = new([{acct, acct2},
+ {auth_id, "acct2"},
+ {auth_token, "auth2"},
+ {src_number, "222"},
+ {recv_port, 9300},
+ {send_uri, "http://localhost:9100"},
+ {recv_uri, "http://localhost:9300"}]),
+ trace(),
+ R.
+
+trace() ->
+ dbg:tracer(),
+ dbg:tpl(?MODULE, x),
+ dbg:tp(exo_http, x),
+ dbg:p(all, [c]).
diff --git a/deps/gsms/src/gsms_plivo_sim.erl b/deps/gsms/src/gsms_plivo_sim.erl
new file mode 100644
index 0000000..a202267
--- /dev/null
+++ b/deps/gsms/src/gsms_plivo_sim.erl
@@ -0,0 +1,353 @@
+-module(gsms_plivo_sim).
+-behaviour(gen_server).
+
+-export([start_link/1]).
+
+-export([send_message/3]).
+
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+-compile(export_all).
+
+-include_lib("exo/include/exo_http.hrl").
+-include("log.hrl").
+
+-define(DEFAULT_PORT, 9100).
+
+%% TODO: A bunch of duplication in the records. Should be cleaned up.
+%% TODO: Should be enough with one HTTP server instance serving all accts.
+-record(service, {acct,
+ type,
+ uri,
+ conn_opts = [],
+ numbers = [],
+ auth_token,
+ auth_string,
+ pid}).
+-record(st, {services = [],
+ server,
+ opts,
+ notify = []}).
+
+-record(server, {parent}).
+
+-define(mandatory, '$mandatory').
+
+test() ->
+ application:ensure_all_started(gsms),
+ start_link([{services, [{plivo, [{type, plivo_sim},
+ {port, 9100},
+ {uri, "http://localhost:9100"},
+ {account, "myacct"},
+ {auth, "myauth"}
+ ]}
+ ]}
+ ]).
+
+simtest() ->
+ application:ensure_all_started(gsms),
+ start_link([{port, 9100},
+ {services,
+ [{s1, [{type, plivo_sim},
+ {numbers, ["111"]},
+ {uri, "http://localhost:9200"},
+ {account, "acct1"},
+ {auth, "auth1"}]},
+ {s2, [{type, plivo_sim},
+ {numbers, ["222"]},
+ {uri, "http://localhost:9300"},
+ {account, "acct2"},
+ {auth, "auth2"}]}]}]).
+
+start_link(Opts) ->
+ case lists:keyfind(reg_name, 1, Opts) of
+ false ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []);
+ {_, Name} when is_tuple(Name) ->
+ gen_server:start_link(Name, ?MODULE, Opts, [])
+ end.
+
+send_message(Opts, Body) ->
+ call(?MODULE, {send_message, Opts, Body}).
+
+send_message(Server, Opts, Body) ->
+ call(Server, {send_message, Opts, Body}).
+
+init(Opts) ->
+ {ok, Pid} = start_server(Opts),
+ S0 = #st{server = Pid, opts = Opts},
+ S = case lists:keyfind(services, 1, Opts) of
+ false -> S0#st{server = Pid};
+ {_, Svcs} ->
+ lists:foldl(
+ fun({Svc, SvcOpts}, Sx) ->
+ {_, Sx1} = do_add_service(Svc, SvcOpts, Sx),
+ Sx1
+ end, S0, Svcs)
+ end,
+ {ok, S}.
+
+handle_call({send_message, Opts, Body}, _From, S) ->
+ %% We should really queue the message for delivery, then send a notification
+ case message_params(Opts, Body, S) of
+ {UUID, URI, Token, Params} ->
+ S1 = maybe_notify(Opts, UUID, S),
+ self() ! {message_sent, UUID, Params},
+ try Res = do_send_message(URI, UUID, Token, Params),
+ {reply, Res, S1}
+ catch
+ error:Reason ->
+ {reply, {error, Reason}, S}
+ end;
+ false ->
+ {reply, {error, not_found}, S}
+ end;
+handle_call({add_service, Svc, Opts}, _From, S) ->
+ {Reply, S1} = do_add_service(Svc, Opts, S),
+ {reply, Reply, S1};
+handle_call({authorize,AuthStr}, _, #st{services = Svcs} = S) ->
+ {reply, lists:keyfind(AuthStr, #service.auth_string, Svcs), S};
+handle_call(_Req, _From, S) ->
+ {reply, {error, badarg}, S}.
+
+handle_cast(_Msg, S) ->
+ {noreply, S}.
+
+handle_info({Evt, UUID, _Params} = Msg, #st{notify = Nfy} = S)
+ when Evt == message_sent; Evt == message_delivered ->
+ Found = [N || {E, ID, _} = N <- Nfy,
+ E == Evt andalso ID == UUID],
+ [notify(Msg, Num, S) || {_, _, Num} <- Found],
+ {noreply, S#st{notify = Nfy -- Found}};
+handle_info(_Msg, S) ->
+ {noreply, S}.
+
+terminate(_Reason, _S) ->
+ ok.
+
+code_change(_FromVsn, State, _Extra) ->
+ {ok, State}.
+
+call(Server, Req) ->
+ gen_server:call(Server, Req).
+
+ask_authorize(Pid, AuthStr) ->
+ call(Pid, {authorize, AuthStr}).
+
+auth_service([#service{conn_opts = Opts}|T], Acct, Token) ->
+ case lists:member({account,Acct}, Opts) of
+ true ->
+ lists:member({auth, Token}, Opts);
+ false ->
+ auth_service(T, Acct, Token)
+ end;
+auth_service([], _, _) ->
+ false.
+
+maybe_notify(Opts, UUID, #st{notify = Nfy} = S) ->
+ case lists:keyfind(notify, 1, Opts) of
+ false ->
+ S;
+ {_, Number, Tags} ->
+ S#st{notify = [{Tag, UUID, Number} || Tag <- Tags] ++ Nfy}
+ end.
+
+notify({Event, UUID, Params0}, Number, S) ->
+ case find_service(Number, S) of
+ #service{uri = URI, auth_token = Token} ->
+ Params =
+ [{"Status", status(Event)},
+ {"ParentMessageUUID", UUID},
+ {"PartInfo", "1 of 1"} | Params0],
+ do_send_message(URI, UUID, Token, Params);
+ false ->
+ ignore
+ end.
+
+status(message_sent ) -> "sent";
+status(message_delivered) -> "delivered".
+
+
+message_params(Opts, Body, #st{} = S) ->
+ [From, To, UUID] = [gsms_lib:get_opt(K, Opts, Def)
+ || {K, Def} <- [{from, ?mandatory},
+ {to, ?mandatory},
+ {uuid, fun gsms_plivo:uuid/0}]],
+ case find_service(To, S) of
+ #service{uri = URI, auth_token = Token} ->
+ Params = [
+ {"To", no_plus(To)},
+ {"From", no_plus(From)},
+ {"TotalRate", 0.0},
+ {"Units", 1},
+ {"Text", Body},
+ {"TotalAmount", 0.0},
+ {"Type", "sms"},
+ {"MessageUUID", UUID}
+ ],
+ {UUID, URI, Token, Params};
+ _ ->
+ false
+ end.
+
+find_service(Number, #st{services = Svcs}) ->
+ case [Svc1 || #service{numbers = Ns} = Svc1 <- Svcs,
+ lists:member(Number, Ns)] of
+ [#service{} = Svc|_] ->
+ Svc;
+ _ ->
+ false
+ end.
+
+no_plus([$+|Num]) -> Num;
+no_plus(Num ) -> Num.
+
+do_send_message(URI, UUID, Token, Params) ->
+ Sig = gsms_plivo:signature(URI, Params, Token),
+ Hs = headers(Sig),
+ Result = exo_http:wpost(URI, Hs, Params),
+ io:fwrite("wpost result = ~p~n", [Result]),
+ self() ! {message_delivered, UUID, Params},
+ {ok, UUID}.
+
+headers(Sig) ->
+ [{'Content-Type', "application/x-www-form-urlencoded"},
+ {'Accept-Encoding', "gzip, deflate"},
+ {"X-Plivo-Signature", Sig}].
+
+do_add_service(_Svc, Opts, S) ->
+ [Type, ConnOpts, Numbers, Acct, URI, Auth] =
+ [gsms_lib:get_opt(K, Opts, Def)
+ || {K, Def} <- [{type, plivo_sim},
+ {connection, []},
+ {numbers, []},
+ {account, ?mandatory},
+ {uri, ?mandatory},
+ {auth, ?mandatory}]],
+ AuthStr = exo_http:auth_basic_encode(Acct, Auth),
+ SvcRec = #service{type = Type,
+ conn_opts = ConnOpts,
+ numbers = [no_plus(N) || N <- Numbers],
+ acct = Acct,
+ uri = URI,
+ auth_token = Auth,
+ auth_string = AuthStr},
+ {ok, S#st{services = [SvcRec | S#st.services]}}.
+
+start_server(Opts) ->
+ Port = gsms_lib:get_opt(port, Opts, ?DEFAULT_PORT),
+ Srv = #server{parent = self()},
+ exo_http_server:start_link(Port, [{request_handler,
+ {?MODULE, handle_body, [Srv]}}]).
+
+handle_body(Socket, Request, Body, #server{parent = P}) ->
+ ?debug("Path = ~p~nBody = ~p~n",
+ [(Request#http_request.uri)#url.path, Body]),
+ case check_auth(Request, P) of
+ false ->
+ response(Socket, authentication_failed, "");
+ #service{acct = Acct, auth_token = AuthTok, uri = URI} ->
+ handle_body_(Socket, Request, Body, Acct, AuthTok, URI, P)
+ end.
+
+handle_body_(Socket, Request, Body, Acct, AuthTok, URI, P) ->
+ case valid_request(Request, Acct) of
+ false ->
+ response(Socket, authentication_failed, "");
+ "Message" ->
+ ?debug("handle_body(_, ~p, ~p, _)~n", [Request, Body]),
+ try exo_json:decode_string(binary_to_list(Body)) of
+ {ok, {struct, Result}} ->
+ ?debug("Decoded = ~p~n", [Result]),
+ {_, Src} = lists:keyfind("src", 1, Result),
+ {_, Dest} = lists:keyfind("dst", 1, Result),
+ {_, Text} = lists:keyfind("text", 1, Result),
+ gsms_router:input_from(Src, Text),
+ UUID = gsms_plivo:uuid(),
+ API_id = gsms_plivo:uuid(),
+ Struct = {struct,
+ [{"api_id", API_id},
+ {"message","message(s) queued"},
+ {"message_uuid", UUID},
+ {"api_id", API_id}]},
+ JSON = to_json(Struct),
+ send_message(P, [{from, Src},
+ {to, Dest},
+ {uuid, UUID},
+ {notify, Src, [message_sent,
+ message_delivered]}],
+ Text),
+ response(Socket, ok, JSON,
+ response_headers(URI, Struct, AuthTok))
+ catch
+ _:_ ->
+ response(Socket, server_error, "")
+ end
+ end.
+
+check_auth(Request, P) ->
+ case get_basic_auth(Request) of
+ false -> false;
+ AuthStr ->
+ case ask_authorize(P, AuthStr) of
+ false -> false;
+ Auth -> Auth
+ end
+ end.
+
+to_json(Struct) ->
+ exo_json:encode(Struct).
+
+valid_request(#http_request{uri = #url{path = Path}}, Acct) ->
+ case filename:split(Path) of
+ ["/", "v1","Account",Acct,"Message"] ->
+ "Message";
+ _Split ->
+ io:fwrite("unrecognized: ~p~n", [_Split]),
+ false
+ end.
+
+get_basic_auth(#http_request{headers = #http_chdr{
+ authorization = "Basic " ++ Auth}}) ->
+ Auth;
+get_basic_auth(_) ->
+ false.
+
+
+response(Socket, Res, Body) ->
+ response(Socket, Res, Body, [{"Date", gsms_plivo:http_date()}]).
+
+response(Socket, Res, Body, Hdrs) ->
+ Opts = [{content_type, "application/json"} | Hdrs],
+ {Code, _} = lists:keyfind(Res, 2, responses()),
+ exo_http_server:response(Socket, undefined, Code,
+ atom_to_list(Res), Body, Opts).
+
+response_headers(URI, Params, Token) ->
+ [{"Date", gsms_plivo:http_date()},
+ {"X-Plivo-Signature", gsms_plivo:signature(URI, Params, Token)}].
+
+
+responses() ->
+ [{200, ok},
+ {201, resource_created},
+ {202, resource_changed},
+ {204, resource_deleted},
+ {400, parameter_missing}, % ... or invalid
+ {401, authentication_failed},
+ {404, resource_not_found},
+ {405, method_not_allowed},
+ {500, server_error}].
+
+args(send_message) ->
+ [{"src" , string, mandatory},
+ {"dst" , string, mandatory},
+ {"text" , string, mandatory},
+ {"type" , string, optional, "sms"},
+ {"url" , string, optional, ""},
+ {"method", string, optional, "POST"},
+ {"log" , boolean, optional, true}].
diff --git a/deps/gsms/src/gsms_router.erl b/deps/gsms/src/gsms_router.erl
index bb027f8..eee6c79 100644
--- a/deps/gsms/src/gsms_router.erl
+++ b/deps/gsms/src/gsms_router.erl
@@ -30,25 +30,26 @@
%% API
-export([start_link/1,
- send/2,
- subscribe/1,
- unsubscribe/1,
- join/2,
- input_from/2]).
+ send/2,
+ subscribe/1,
+ unsubscribe/1,
+ join/2, % Module defaults to gsms_0705
+ join/3,
+ input_from/2]).
%% gen_server callbacks
--export([init/1,
- handle_call/3,
- handle_cast/2,
- handle_info/2,
- terminate/2,
- code_change/3]).
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
%% testing
-export([dump/0]).
--define(SERVER, ?MODULE).
-
+-define(SERVER, ?MODULE).
+
-record(subscription,
{
pid :: pid(), %% subscriber process
@@ -59,13 +60,14 @@
-record(interface,
{
pid :: pid(), %% interface pid
+ module :: module(), %% callback module
mon :: reference(), %% monitor reference
bnumber :: gsms_addr(), %% modem msisdn
rssi = 99 :: integer(), %% last known rssi value
attributes = [] :: [{atom(),term()}] %% general match keys
}).
--record(state,
+-record(state,
{
csq_ival = 0,
csq_tmr,
@@ -77,14 +79,14 @@
%%% API
%%%===================================================================
-spec send(Options::list({Key::atom(), Value::term()}), Body::string()) ->
- {ok, Ref::reference()} |
- {error, Reason::term()}.
+ {ok, Ref::reference()} |
+ {error, Reason::term()}.
send(Opts, Body) ->
gen_server:call(?SERVER, {send, Opts, Body}).
-spec subscribe(Filter::[filter()]) -> {ok,Ref::reference()} |
- {error,Reason::term()}.
+ {error,Reason::term()}.
subscribe(Filter) ->
gen_server:call(?SERVER, {subscribe, self(), Filter}).
@@ -94,15 +96,18 @@ subscribe(Filter) ->
unsubscribe(Ref) ->
gen_server:call(?SERVER, {unsubscribe, Ref}).
-join(BNumber,Attributes) ->
- gen_server:call(?SERVER, {join,self(),BNumber,Attributes}).
+join(BNumber, Attributes) ->
+ join(BNumber, gsms_0705, Attributes).
+
+join(BNumber, Module, Attributes) ->
+ gen_server:call(?SERVER, {join,self(),BNumber,Module,Attributes}).
%%
-%% Called from gsms_0705 backend to enter incoming message
+%% Called from session instance to enter incoming message
%%
input_from(BNumber, Sms) ->
lager:debug("message input modem:~s, message = ~p\n",
- [BNumber, Sms]),
+ [BNumber, Sms]),
?SERVER ! {input_from, BNumber, Sms},
ok.
@@ -140,8 +145,8 @@ init(Args) ->
Csq_ival = proplists:get_value(csq_ival, Args, 0),
Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 ->
erlang:start_timer(Csq_ival, self(), csq);
- true -> undefined
- end,
+ true -> undefined
+ end,
process_flag(trap_exit, true),
{ok, #state{ csq_ival=Csq_ival, csq_tmr=Csq_tmr}}.
@@ -164,9 +169,9 @@ handle_call({send,Opts,Body}, _From, State) ->
%% FIXME: add code to match attributes!
case proplists:get_value(bnumber, Opts) of
undefined ->
- case State#state.ifs of
- [I|_] ->
- Reply = gsms_0705:send(I#interface.pid, Opts, Body),
+ case State#state.ifs of
+ [#interface{module = M, pid = Pid}|_] ->
+ Reply = M:send(Pid, Opts, Body),
{reply, Reply, State};
[] ->
{reply, {error,enoent}, State}
@@ -175,37 +180,37 @@ handle_call({send,Opts,Body}, _From, State) ->
case lists:keyfind(BNumber,#interface.bnumber,State#state.ifs) of
false ->
{reply, {error,enoent}, State};
- I ->
- Reply = gsms_0705:send(I#interface.pid, Opts, Body),
+ #interface{module = M, pid = Pid} ->
+ Reply = M:send(Pid, Opts, Body),
{reply, Reply, State}
end
end;
handle_call({subscribe,Pid,Filter}, _From, State) ->
Ref = erlang:monitor(process, Pid),
Subs = [#subscription { pid = Pid,
- ref = Ref,
- filter = Filter } | State#state.subs],
+ ref = Ref,
+ filter = Filter } | State#state.subs],
{reply, {ok,Ref}, State#state { subs = Subs} };
handle_call({unsubscribe,Ref}, _From, State) ->
case lists:keytake(Ref, #subscription.ref, State#state.subs) of
- false ->
- {reply, ok, State};
- {value,_S,Subs} ->
- erlang:demonitor(Ref, [flush]),
- {reply, ok, State#state { subs = Subs} }
+ false ->
+ {reply, ok, State};
+ {value,_S,Subs} ->
+ erlang:demonitor(Ref, [flush]),
+ {reply, ok, State#state { subs = Subs} }
end;
-handle_call({join,Pid,BNumber,Attributes}, _From, State) ->
+handle_call({join,Pid,BNumber,Module,Attributes}, _From, State) ->
case lists:keytake(BNumber, #interface.bnumber, State#state.ifs) of
false ->
?debug("gsms_router: process ~p, bnumber ~p joined.",
[Pid, BNumber]),
- State1 = add_interface(Pid,BNumber,Attributes,State),
+ State1 = add_interface(Pid,BNumber,Module,Attributes,State),
{reply, ok, State1};
{value,I,IFs} ->
receive
{'EXIT', OldPid, _Reason} when I#interface.pid =:= OldPid ->
?debug("join: restart detected", []),
- State1 = add_interface(Pid,BNumber,Attributes,
+ State1 = add_interface(Pid,BNumber,Module,Attributes,
State#state { ifs=IFs} ),
{reply, ok, State1}
after 0 ->
@@ -216,17 +221,18 @@ handle_call(dump, _From, State=#state {subs = Subs, ifs= Ifs}) ->
io:format("LoopData:\n", []),
io:format("Subscriptions:\n", []),
lists:foreach(fun(_Sub=#subscription {pid = Pid, ref = Ref, filter = F}) ->
- io:format("pid ~p, ref ~p, filter ~p~n",
- [Pid, Ref, F])
- end,
- Subs),
+ io:format("pid ~p, ref ~p, filter ~p~n",
+ [Pid, Ref, F])
+ end,
+ Subs),
io:format("Interfaces:\n", []),
- lists:foreach(fun(_If=#interface {pid = Pid, mon = Ref,
- bnumber = B, attributes = A}) ->
- io:format("pid ~p, ref ~p, bnumber ~p, attributes ~p~n",
- [Pid, Ref, B, A])
- end,
- Ifs),
+ lists:foreach(fun(_If=#interface {pid = Pid, mon = Ref, module = Mod,
+ bnumber = B, attributes = A}) ->
+ io:format("pid ~p, ref ~p, bnumber ~p, "
+ "module = ~p, attributes ~p~n",
+ [Pid, Ref, B, Mod, A])
+ end,
+ Ifs),
{reply, ok, State};
handle_call(_Request, _From, State) ->
@@ -258,12 +264,12 @@ handle_cast(_Msg, State) ->
%%--------------------------------------------------------------------
handle_info({'DOWN',Ref,process,Pid,_Reason}, State) ->
case lists:keytake(Ref, #subscription.ref, State#state.subs) of
- false ->
+ false ->
case lists:keytake(Pid, #interface.pid, State#state.ifs) of
false ->
{noreply, State};
{value,_If,Ifs} ->
- ?debug("gsms_router: interface ~p died, reason ~p\n",
+ ?debug("gsms_router: interface ~p died, reason ~p\n",
[_If, _Reason]),
%% Restart done by gsms_if_sup
{noreply,State#state { ifs = Ifs }}
@@ -274,14 +280,14 @@ handle_info({'DOWN',Ref,process,Pid,_Reason}, State) ->
handle_info({input_from, BNumber, Pdu}, State) ->
lager:debug("input bnumber: ~p, pdu=~p\n", [BNumber,Pdu]),
- lists:foreach(fun(S) -> match_filter(S, BNumber, Pdu) end,
- State#state.subs),
+ lists:foreach(fun(S) -> match_filter(S, BNumber, Pdu) end,
+ State#state.subs),
{noreply, State};
handle_info({timeout, Tmr, csq}, State) when State#state.csq_tmr =:= Tmr ->
Is =
lists:map(
- fun(I) ->
- R = gsms_0705:get_signal_strength(I#interface.pid),
+ fun(#interface{module = M, pid = Pid} = I) ->
+ R = M:get_signal_strength(Pid),
lager:debug("csq result: ~p\n", [R]),
case R of
{ok,"+CSQ:"++Params} ->
@@ -311,19 +317,19 @@ handle_info({timeout, Tmr, csq}, State) when State#state.csq_tmr =:= Tmr ->
Csq_ival = State#state.csq_ival,
Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 ->
erlang:start_timer(Csq_ival, self(), csq);
- true -> undefined
- end,
+ true -> undefined
+ end,
{noreply, State#state { ifs = Is, csq_tmr=Csq_tmr }};
handle_info({'EXIT', Pid, Reason}, State) ->
case lists:keytake(Pid, #interface.pid, State#state.ifs) of
{value,_If,Ifs} ->
%% One of our interfaces died, log and ignore
- ?debug("gsms_router: interface ~p died, reason ~p\n",
+ ?debug("gsms_router: interface ~p died, reason ~p\n",
[_If, Reason]),
{noreply,State#state { ifs = Ifs }};
false ->
%% Someone else died, log and terminate
- ?debug("gsms_router: linked process ~p died, reason ~p, terminating\n",
+ ?debug("gsms_router: linked process ~p died, reason ~p, terminating\n",
[Pid, Reason]),
{stop, Reason, State}
end;
@@ -359,16 +365,16 @@ code_change(_OldVsn, State, _Extra) ->
%%% Internal functions
%%%===================================================================
-add_interface(Pid,BNumber,Attributes,State) ->
+add_interface(Pid,BNumber,Module,Attributes,State) ->
Mon = erlang:monitor(process, Pid),
- I = #interface { pid=Pid, mon=Mon,
+ I = #interface { pid=Pid, mon=Mon, module=Module,
bnumber=BNumber, attributes=Attributes },
link(Pid),
State#state { ifs = [I | State#state.ifs ] }.
match_filter(_S=#subscription {filter = Filter,
- pid = Pid,
- ref = Ref},
+ pid = Pid,
+ ref = Ref},
BNumber, Pdu) ->
lager:debug("match filter: ~p", [Filter]),
case match(Filter, BNumber, Pdu) of
@@ -389,7 +395,7 @@ match({'and',A,B}, BNum, Sms) ->
match(A,BNum,Sms) andalso match(B,BNum,Sms);
match({'or',A,B}, BNum, Sms) ->
match(A,BNum,Sms) orelse match(B,BNum,Sms);
-match({bnumber,Addr}, BNum, _Sms) ->
+match({bnumber,Addr}, BNum, _Sms) ->
%% receiving modem
match_addr(Addr, BNum);
match(Match, _BNum, Sms) when is_record(Sms,gsms_deliver_pdu) ->
@@ -403,8 +409,8 @@ match(_, _BNum, _)->
match_clause([A|As], BNum, Sms) ->
case match(A, BNum, Sms) of
- true -> match_clause(As, BNum, Sms);
- false -> false
+ true -> match_clause(As, BNum, Sms);
+ false -> false
end;
match_clause([], _BNum, _Sms) ->
true.
diff --git a/deps/gsms/src/gsms_session.erl b/deps/gsms/src/gsms_session.erl
new file mode 100644
index 0000000..7608d4e
--- /dev/null
+++ b/deps/gsms/src/gsms_session.erl
@@ -0,0 +1,123 @@
+-module(gsms_session).
+-behaviour(gen_server).
+
+-export([new/2,
+ send/3,
+ get_signal_strength/1,
+ subscribe/2,
+ unsubscribe/2
+ ]).
+
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-include("gsms.hrl").
+
+-record(st, {mod,
+ mod_state,
+ subscribers = []}).
+
+-type mod_state() :: any().
+-type dest() :: any().
+-type body() :: any().
+-type option() :: {atom(), any()}.
+-type options() :: [option()].
+
+-type cb_return() :: {ok, mod_state()} | {error, any()}.
+
+-callback init(options()) -> {ok,gsms_addr(),[{atom(), term()}]}
+ | {error, any()}.
+-callback handle_send(dest(), body(), mod_state()) -> cb_return().
+-callback mandatory_options() -> [atom()].
+
+new(Mod, Opts) ->
+ true = valid_opts(Opts, Mod),
+ {ok, Pid} = gen_server:start_link(?MODULE, {Mod, Opts}, []),
+ Pid.
+
+send(Session, Opts, Body) ->
+ call_(Session, {send, Opts, Body}).
+
+get_signal_strength(Session) ->
+ call_(Session, get_signal_strength).
+
+subscribe(Session, Filter) ->
+ case lists:keytake(reg_exp, 1, Filter) of
+ {value, RegExp, Rest} when is_list(RegExp) ->
+ case re:compile(RegExp, [unicode]) of
+ {ok, MP} ->
+ call_(Session, {subscribe, [{reg_exp, MP} | Rest]});
+ {error, _} = E ->
+ E
+ end;
+ {value, _, _} ->
+ %% Assume MP format
+ call_(Session, {subscribe, Filter});
+ false ->
+ call_(Session, {subscribe, Filter})
+ end.
+
+unsubscribe(Session, Ref) ->
+ call_(Session, {unsubscribe, Ref}).
+
+mandatory_options() ->
+ [].
+
+init({Mod, Opts}) ->
+ case Mod:init(Opts) of
+ {ok, BNumber, Attrs, ModSt} ->
+ gsms_router:join(BNumber, ?MODULE, Attrs),
+ {ok, #st{mod = Mod,
+ mod_state = ModSt}};
+ Other ->
+ Other
+ end.
+
+handle_call({send, Opts, Body}, _From, #st{mod = Mod,
+ mod_state = ModSt} = S) ->
+ case Mod:handle_send(Opts, Body, ModSt) of
+ {ok, Reply, ModSt1} ->
+ {reply, Reply, S#st{mod_state = ModSt1}};
+ {error, _} = Error ->
+ {reply, Error, S}
+ end;
+handle_call(get_signal_strength, _From, #st{mod = Mod,
+ mod_state = ModSt} = S) ->
+ case Mod:get_signal_strength(ModSt) of
+ {ok, Res, St1} ->
+ {reply, Res, S#st{mod_state = St1}};
+ {error, _} = E ->
+ {reply, E, S}
+ end;
+handle_call({subscribe, _Pattern}, _From, #st{subscribers = _Subs} = S) ->
+ {reply, {error, nyi}, S}.
+
+handle_cast(_Msg, S) ->
+ {noreply, S}.
+
+handle_info(_Msg, S) ->
+ {noreply, S}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_FromVsn, State, _Extra) ->
+ {ok, State}.
+
+valid_opts(Opts, Mod) ->
+ Mandatory = mandatory_options() ++ Mod:mandatory_options(),
+ case [O || O <- Mandatory,
+ not lists:keymember(O, 1, Opts)] of
+ [] ->
+ true;
+ [_|_] = Missing ->
+ erlang:error({mandatory, lists:usort(Missing)})
+ end.
+
+call_(Session, Req) ->
+ gen_server:call(Session, Req).
+