summaryrefslogtreecommitdiff
path: root/deps/gsms/src/gsms_plivo.erl
diff options
context:
space:
mode:
Diffstat (limited to 'deps/gsms/src/gsms_plivo.erl')
-rw-r--r--deps/gsms/src/gsms_plivo.erl373
1 files changed, 373 insertions, 0 deletions
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]).