diff --git a/apps/roster/src/api/push/ios.erl b/apps/roster/src/api/push/ios.erl deleted file mode 100644 index 07a878027b7739383fde6341ecb1d2535fe98df8..0000000000000000000000000000000000000000 --- a/apps/roster/src/api/push/ios.erl +++ /dev/null @@ -1,118 +0,0 @@ --module(ios). --include_lib("kernel/include/logger.hrl"). --include("roster.hrl"). --include_lib("roster/include/static/push_notification_var.hrl"). - --export([description/0, notify/5, test_push_notification/0]). - -description() -> "IOS Push Notifications Module". - --define(APNS_CERT_DIR, proplists:get_value(apns_cert_dir, application:get_env(roster, push_api, []))). --define(APNS_PORT, proplists:get_value(apns_port, application:get_env(roster, push_api, []))). - --define(GATEWAY_LIST, [ - {<<"SANDBOX">>, "gateway.sandbox.push.apple.com"}, - {<<"LIVE">>, "gateway.push.apple.com"}]). - --define(BANDLE_LIST, [ - {<<"com.nynja.mobile.communicator">>, {"cert_prod.pem", "key_prod.pem"}}, - {<<"com.nynja.rс.mobile.communicator">>, {"cert_prod.pem", "key_prod.pem"}}, - {<<"com.nynja.dev.mobile.communicator">>, {"cert_dev.pem", "key_dev.pem"}} - ]). - -%% ------------------------------------------------------------------ -%% Ios Push Notifications -%% ------------------------------------------------------------------ - -notify(Alert, Custom, Type, DeviceId, SessionSettings) when is_binary(DeviceId) -> - notify(Alert, Custom, Type, binary_to_list(DeviceId), SessionSettings); -notify(A, C, T, DeviceId, SessionSettings) -> - [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], - application:ensure_started(ssl), - -%% create aps json - Aps = jsx:encode([{<<"model">>, Custom}, {<<"type">>, Type}, {<<"title">>, Alert}, - {<<"dns">>, get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS)}, {<<"version">>, <>}]), - -%% create ios payload string - PayloadString = binary_to_list(iolist_to_binary(["{\"aps\": {\"nynja\": ", Aps, "}}"])), -% ?LOG_INFO("PayloadString ~p~n~n", [PayloadString]), - -%% prepare push data - Payload = list_to_binary(PayloadString), - PayloadLength = size(Payload), - FormattedDeviceId = list_to_integer(DeviceId, 16), - Packet = <<0:8, 32:16/big, FormattedDeviceId:256/big, PayloadLength:16/big, Payload/binary>>, - {_, {CertFile, KeyFile}} = get_bandle(SessionSettings), - ?LOG_INFO("CertFile: ~p", [CertFile]), - Options = [{certfile, path_to_pem_file(CertFile)}, {keyfile, path_to_pem_file(KeyFile)}, {mode, binary}], - [send_push(Addr, Packet, Options, 1) || {_, Addr} <- get_gateway(SessionSettings)], - ok. - -%% ------------------------------------------------------------------ -%% Helpers -%% ------------------------------------------------------------------ - -send_push(Addr, Payload, Options, Attempt) -> - ?LOG_INFO("Addr: ~p, Attempt:~p", [Addr, Attempt]), -%% NOTE set Duration = Attempt * 100 for tests - Duration = Attempt * 500, - {Status, Socket} = ssl:connect(Addr, ?APNS_PORT, Options, Duration), - case Status of - ok -> - ssl:send(Socket, Payload), - ssl:close(Socket), - ?LOG_INFO("Push sent", []); - error -> - if - Attempt > 10 -> - ?LOG_INFO("Final error", []); - true -> - timer:sleep(Duration), - ?LOG_INFO("Error with socket opening. Reason:~p", [Socket]), - send_push(Addr, Payload, Options, Attempt + 1) - end - end. - -path_to_pem_file(FileName) -> - PrivDir = code:priv_dir(roster), - filename:join([PrivDir,?APNS_CERT_DIR, FileName]). - -get_data_from_feature(SessionSettings, Key) -> - case lists:keyfind(Key, #'Feature'.key, SessionSettings) of - #'Feature'{value = Value} -> Value; - _ -> [] - end. - -get_bandle(SessionSettings) -> - [H|_]=get_from_session(SessionSettings, ?FKPN_BANDLE, ?BANDLE_LIST), - H. - -get_gateway(SessionSettings) -> - get_from_session(SessionSettings, ?FKPN_GATEWAY, ?GATEWAY_LIST). - -get_from_session(SessionSettings, Key, AcceptedValues) -> - case get_data_from_feature(SessionSettings, Key) of - [] -> AcceptedValues; - FoundValue -> Filtered = lists:filter(fun(X) -> element(1,X) == FoundValue end, AcceptedValues), - case Filtered of [] -> AcceptedValues; _ -> Filtered end - end. - -%% ------------------------------------------------------------------ -%% Tests -%% ------------------------------------------------------------------ - -%% Liubov's phone --define(APNS_TEST_DEVICE_ID, "f9e7bedd8d46079c51a5aee1f951bbafc68ec541d68a56a0aa709214263cf138"). -%% Anton's phone dev -%% -define(APNS_TEST_DEVICE_ID, "55e9a60ffde1701ba701ea653ba6c0dfa4e515de7d56aa2039d72a904f353e54"). -%% Anton's phone rc -%% -define(APNS_TEST_DEVICE_ID, "a34830e7199ff499e986d2bc3ab0555b0acebe499f9a9f0445701b7dbe2a6722"). - -test_push_notification() -> - SessionSettings = [#'Feature'{id = <<"ID_Sandbox">>, key = <<"APNS_GATEWAY">>, value = <<"SANDBOX">>, group = <<"AUTH_DATA">>}, - #'Feature'{id = <<"ID_Dns">>, key = <<"SERVER_DNS">>, value = <<"SomeDNSValue">>, group = <<"AUTH_DATA">>}, - #'Feature'{id = <<"ID_Bandle">>, key = <<"IOS_BANDLE">>,value = <<"com.nynja.mobile.communicator">>, group = <<"AUTH_DATA">>}], - Msg = lists:concat(["Test it! ", vox_api:generate_random_data(4)]), - Custom = <<"g2gSZAAHTWVzc2FnZWEQZAAFY2hhaW5oA2QAA3AycG0AAAAOMzgwNjM4MDk1MTU4XzdtAAAADjM4MDk5NDM4Mjc5OF84ampqam0AAAAOMzgwOTk0MzgyNzk4XzhtAAAADjM4MDYzODA5NTE1OF83am4GAD5BRlNeAWpqbAAAAAFoBmQABERlc2NqbQAAAARIaGhoYQBqampqamQABHNlbnQ=">>, - notify(Msg, Custom, <<"message">>, ?APNS_TEST_DEVICE_ID, SessionSettings). diff --git a/apps/roster/src/api/push/push_api.erl b/apps/roster/src/api/push/push_api.erl deleted file mode 100644 index 41990451b09e3d82bb2d23b3d6a7123af708b514..0000000000000000000000000000000000000000 --- a/apps/roster/src/api/push/push_api.erl +++ /dev/null @@ -1,14 +0,0 @@ --module(push_api). - --export([description/0, fcm_notify/3, apns_notify/5]). - -description() -> "Mobile Push Notifications Module. Wrapper for IOS and Android". - --compile(export_all). - - -fcm_notify(MessageTitle, MessageBody, DeviceId) -> - android:notify(MessageTitle, MessageBody, DeviceId). - -apns_notify(Alert, Custom, Type, DeviceId, SessionSettings) -> - ios:notify(Alert, Custom, Type, DeviceId, SessionSettings). \ No newline at end of file diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl new file mode 100644 index 0000000000000000000000000000000000000000..9ec8c202a4307b8dbed54575bcd658995c41572e --- /dev/null +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -0,0 +1,149 @@ +%%%------------------------------------------------------------------- +%%% @doc Client interface to apns +%%% +%%% @end +%%%------------------------------------------------------------------- +-module(roster_apns_api). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("roster/include/roster.hrl"). +-include_lib("roster/include/static/push_notification_var.hrl"). + +-export([ notify/6 + , start/1 + ]). + +-type context() :: 'development' | 'production' | 'staging'. +-record(roster_apns_api_state, { conn_pid :: pid() + , context :: context() + }). + +-define(BUNDLE_ENV, + #{ production => <<"com.nynja.mobile.communicator">> + , staging => <<"com.nynja.rс.mobile.communicator">> + , development => <<"com.nynja.dev.mobile.communicator">>}). + +-define(GATEWAY_ENV, + #{ production => <<"LIVE">> + , staging => <<"LIVE">> + , development => <<"SANDBOX">>}). + +-define(DNS_ENV, + #{ production => <<"im.nynja.net">> + , staging => <<"im.staging.nynja.net">> + , development => <<"im.dev.nynja.net">>}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +start(Context) -> + %% TODO: Move the certs away from priv_dir + CertDir = filename:join(code:priv_dir(roster), get_from_config(apns_cert_dir)), + Conn = #{ name => ios_http2 + , apple_host => host(Context) + , apple_port => get_from_config(apns_http_port) + , certfile => certfile(CertDir, Context) + , keyfile => keyfile(CertDir, Context) + , type => cert + , gun => #{ transport => tls + , http2_opts => #{ keepalive => 30000 } + } + }, + ?LOG_INFO("Connecting to apns with config ~p", [Conn]), + case apns:connect(Conn) of + {ok, ConnPid} -> + %% TODO: We should monitor the connection, + %% or at least deal with apns monitor messages + {ok, #roster_apns_api_state{ context = Context, conn_pid = ConnPid}}; + {error, What} -> + error({could_not_start_apns, What}) + end. + + +notify(A, C, T, DeviceId, SessionSettings, State) -> + Context = State#roster_apns_api_state.context, + case session_settings_errors(SessionSettings, Context) of + [] -> + [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], + DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), + Aps = #{ nynja => #{ model => Custom + , type => Type + , title => Alert + , dns => DNS + %% TODO: Is this needed? + , version => <> + } + }, + ConnPid = State#roster_apns_api_state.conn_pid, + %% TODO: Handle response codes + apns:push_notification(ConnPid, DeviceId, Aps); + What -> + ?LOG_INFO("Bad session apns settings: ~p (~p)", + [What, SessionSettings]), + ok + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +%%%=================================================================== +%%% Initialisation of connection + +get_from_config(Key) -> + Env = application:get_env(roster, push_api, []), + proplists:get_value(Key, Env). + +certfile(CertDir, Context) -> + Base = case Context of + development -> "cert_dev.pem"; + production -> "cert_prod.pem"; + staging -> "cert_rc.pem" + end, + FN = filename:join(CertDir, Base), + case filelib:is_file(FN) of + true -> FN; + false -> error({no_apns_cert_file, FN}) + end. + +keyfile(CertDir, Context) -> + Base = case Context of + development -> "key_dev.pem"; + production -> "key_prod.pem"; + staging -> "key_rc.pem" + end, + FN = filename:join(CertDir, Base), + case filelib:is_file(FN) of + true -> FN; + false -> error({no_apns_key_file, FN}) + end. + +host(Context) -> + case Context of + development -> "api.sandbox.push.apple.com"; + staging -> "api.sandbox.push.apple.com"; + production -> "api.push.apple.com" + end. + +%%%=================================================================== +%%% Client verification + +%% TODO: This could probably go away once the settings have been tested +%% on all environments +session_settings_errors(SessionSettings, Context) -> + Bundle = get_data_from_feature(SessionSettings, ?FKPN_BANDLE), + Gateway = get_data_from_feature(SessionSettings, ?FKPN_GATEWAY), + DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), + ExpectedBundle = maps:get(Context, ?BUNDLE_ENV), + ExpectedGateway = maps:get(Context, ?GATEWAY_ENV), + ExpectedDNS = maps:get(Context, ?DNS_ENV), + [bad_bundle || ExpectedBundle /= Bundle andalso Bundle /= []] ++ + [bad_gateway || ExpectedGateway /= Gateway andalso Gateway /= []] ++ + [bad_dns || ExpectedDNS /= DNS andalso DNS /= []]. + +get_data_from_feature(SessionSettings, Key) -> + case lists:keyfind(Key, #'Feature'.key, SessionSettings) of + #'Feature'{value = Value} -> Value; + _ -> [] + end. diff --git a/apps/roster/src/protocol/roster_push.erl b/apps/roster/src/protocol/roster_push.erl index b6e67162c5aee7831287e778536091a670e3a239..b228df808c11c66ecf7e3a856a61ab478cba74f2 100644 --- a/apps/roster/src/protocol/roster_push.erl +++ b/apps/roster/src/protocol/roster_push.erl @@ -5,32 +5,45 @@ -include_lib("kvs/include/kvs.hrl"). -compile(export_all). -start() -> n2o_async:start(#handler{module = ?MODULE, class = system, group = roster, name = ?MODULE, state = []}). +start() -> + Context = roster:deployment_context(), + {ok, Ios} = roster_apns_api:start(Context), + ConnState = #{ android => [] + , ios => Ios}, + n2o_async:start(#handler{ module = ?MODULE + , class = system + , group = roster + , name = ?MODULE + , state = #{conn_state => ConnState}}). + +%% TODO: Handle apns connection messages in proc/2 proc(init, #handler{name = ?MODULE} = Async) -> ?LOG_INFO("ASYNC", []), {ok, Async}; -proc({async_push, Session, Payload, PushAlert, PushType}, #handler{} = H) -> - send_push_notification(Session, Payload, PushAlert, PushType), +proc({async_push, Session, Payload, PushAlert, PushType}, #handler{state = HS} = H) -> + send_push_notification(Session, Payload, PushAlert, PushType, HS), {reply, [], H}. -%% TODO prettify variables naming -send_push_notification(#'Auth'{os = OS, push = PushToken, user_id = PhoneId, settings = AuthSettings}, Payload, PushAlert, PushType) -> +send_push_notification(#'Auth'{ os = OS + , push = PushToken + , user_id = PhoneId + , settings = AuthSettings}, Payload, PushAlert, PushType, HS) -> case PushToken of [] -> skip; _ -> ?LOG_INFO("~p:~p:~pPushAlert:~p", [PhoneId, OS, binary:part(PushToken, 0, erlang:min(25, size(PushToken))), PushAlert]), - send_push_notification(OS, PushToken, Payload, PushAlert, PushType, AuthSettings) + send_push_notification(OS, PushToken, Payload, PushAlert, PushType, AuthSettings, HS) end. -send_push_notification(ios, Push, Payload, PushAlert, <<"calling">>, AuthSettings) -> - push_api:apns_notify(PushAlert, Payload, <<"calling">>, Push, AuthSettings); -send_push_notification(ios, Push, Payload, PushAlert, PushType, AuthSettings) -> - DecodedPayload = base64:encode(term_to_binary(Payload)), - push_api:apns_notify(PushAlert, DecodedPayload, PushType, Push, AuthSettings); -send_push_notification(android, Push, Payload, PushAlert, PushType, _) -> + +send_push_notification(ios, Push, Payload, PushAlert, PushType, AuthSettings, HS) -> + EncodedPayload = base64:encode(term_to_binary(Payload)), + IOS = maps:get(ios, HS), + roster_apns_api:notify(PushAlert, EncodedPayload, PushType, Push, AuthSettings, IOS); +send_push_notification(android, Push, Payload, PushAlert, PushType, _AuthSettings,_HS) -> PushModel = #push{model = Payload, type = PushType, alert = PushAlert, title = PushAlert, badge = 1}, AndroidPush = http_uri:encode(binary_to_list(base64:encode(term_to_binary(PushModel)))), - push_api:fcm_notify(PushAlert, AndroidPush, Push); -send_push_notification(_, _, _, _, _, _) -> skip. \ No newline at end of file + android:notify(PushAlert, AndroidPush, Push); +send_push_notification(_, _, _, _, _, _, _) -> skip. diff --git a/apps/roster/src/roster.app.src b/apps/roster/src/roster.app.src index 05a7bdae98dd0f58a38dde424cc2c55d7fabaf81..bf95b63841c3ebf24a9bb1576438485b590d7208 100644 --- a/apps/roster/src/roster.app.src +++ b/apps/roster/src/roster.app.src @@ -5,7 +5,7 @@ {applications, [kernel,stdlib, mnesia, crypto, inets, ssl, ibrowse, cowboy, mochiweb, gen_smtp, kvs, nitro, n2o, emqttc, emqttd, bpe, - jose, jsx, uuid, erlydtl, jwt, + jose, jsx, uuid, erlydtl, jwt, apns, mini_s3, qdate, rest, enenra, locus, prometheus, libphonenumber_erlang]}, {mod, {roster, []}}, diff --git a/apps/roster/src/roster.erl b/apps/roster/src/roster.erl index b40dafc87da70cfe9a715ee919936a6bdeb0f454..0ed71f31701a1f559dfe1f8aa93cb834cbf05b21 100644 --- a/apps/roster/src/roster.erl +++ b/apps/roster/src/roster.erl @@ -59,6 +59,7 @@ atoms() -> [android, ios, contact, signup, signin, welcome]. init([]) -> {ok, {{one_for_one, 5, 10}, []}}. start(_, _) -> atoms(), + ensure_deployment_context_is_set(), try load([]) catch Error:Reason -> @@ -96,6 +97,21 @@ start(_, _) -> google_api:start(), A. +ensure_deployment_context_is_set() -> + case deployment_context() of + development -> ok; + staging -> ok; + production -> ok; + Other -> + error({illegal_deployment_context, Other, + <<"Set this using os environment" + " variable DEPLOYMENT_CONTEXT">>}) + end. + +-spec deployment_context() -> 'development' | 'staging' | 'production'. +deployment_context() -> + application:get_env(roster, deployment_context, undefined). + execution_time(StartTime) -> TimeInMicroSec = (os:system_time() - StartTime)/1000, list_to_integer(float_to_list(TimeInMicroSec,[{decimals,0}])). diff --git a/rebar.config b/rebar.config index 530a432ced62a5f4eda7b27f3109740945af81ee..7e9ad4546782ce55e73d96a7e67ce7d6c03fb7bc 100644 --- a/rebar.config +++ b/rebar.config @@ -15,6 +15,7 @@ {emqttc, {git, "git://github.com/NYNJA-MC/emqttc", {branch,"master"}}}, {rest, {git, "git://github.com/synrc/rest", {tag,"5.10"}}}, {gen_smtp, {git, "git://github.com/voxoz/gen_smtp", {branch,"master"}}}, + {apns, {git, "git://github.com/NYNJA-MC/apns4erl", {ref, "6724edcf073f512a01a0b7652223d173db2f2fe0"}}}, {emq_dashboard, {git, "https://github.com/synrc/emq_dashboard", {branch,"master"}}}, {opencensus, {git, "https://github.com/census-instrumentation/opencensus-erlang", {ref, "7fb276f"}}}, {libphonenumber_erlang, {git, "https://github.com/marinakr/libphonenumber_erlang.git", {branch,"master"}}}, @@ -82,7 +83,7 @@ certifi,ibrowse,asn1,xmerl,counters,ctx, wts,syntax_tools,qdate_localtime, libphonenumber_erlang,syn,cowlib,jiffy,idna,parse_trans, - goldrush, public_key,bpe,{lager,load},ssl,ranch, + goldrush, public_key,bpe,{lager,load},ssl,ranch,gun,apns, ssl_verify_fun,locus,emqttd,hackney,roster,service,active, cowboy,emq_dashboard,emqttc,enenra,envy,uuid,erlydtl,forms, gen_smtp, jwt, mini_s3, nitro, opencensus, diff --git a/rebar.lock b/rebar.lock index 1b30e662360e2520fdfc65fd8e3c64180cefa5ff..198e300be83c8efc517c2a5b6ed1dac4b4554135 100644 --- a/rebar.lock +++ b/rebar.lock @@ -3,6 +3,10 @@ {git,"git://github.com/synrc/active", {ref,"cdd8f2b0f62b9785673bdbea7be90e1ae1ca1c02"}}, 0}, + {<<"apns">>, + {git,"git://github.com/NYNJA-MC/apns4erl", + {ref,"6724edcf073f512a01a0b7652223d173db2f2fe0"}}, + 0}, {<<"base64url">>, {git,"https://github.com/dvv/base64url.git", {ref,"f2c64ed8b9bebc536fad37ad97243452b674b837"}}, @@ -86,6 +90,7 @@ {git,"https://github.com/uwiger/gproc", {ref,"1d16f5e6d7cf616eec4395f2385e3a680a4ffc9f"}}, 0}, + {<<"gun">>,{pkg,<<"gun">>,<<"1.3.0">>},1}, {<<"hackney">>, {git,"https://github.com/benoitc/hackney", {ref,"3c32f04ff0783479992a5d11ec0f4a2d09ba922a"}}, @@ -203,6 +208,7 @@ {<<"certifi">>, <<"75424FF0F3BAACCFD34B1214184B6EF616D89E420B258BB0A5EA7D7BC628F7F0">>}, {<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}, {<<"erlware_commons">>, <<"0CE192AD69BC6FD0880246D852D0ECE17631E234878011D1586E053641ED4C04">>}, + {<<"gun">>, <<"18E5D269649C987AF95AEC309F68A27FFC3930531DD227A6EAA0884D6684286E">>}, {<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>}, {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, diff --git a/sys.config b/sys.config index c319338aed5340fe3b3a83a3bcd2413ff790dc41..368dc10f60ce1866da40fa62f35dd866dbec30cb 100644 --- a/sys.config +++ b/sys.config @@ -50,6 +50,9 @@ {review,[{host,"ns.synrc.com"}]}, {roster, [ + %% TODO: Move this to sys.config.src and use environment variable. + %% Don't merge with this still here! + {deployment_context, development}, %% Must be set to 'development' | 'staging' | 'production'. {health_endpoint_accept_traffic, true}, {freeze_time, 1000}, {upload,"./storage"}, @@ -95,9 +98,10 @@ {app_credentials, "etc/certs/transcribe-dacb4306ab76.json"} ]}, {push_api,[ + {context, dev}, {fcm_server_key,<<"AAAAAzb6_Zg:APA91bGN0jYv_4iqyk8IC4xUdPYXh0yPsTF9YYj_gd9oebRr_ZEoLuC5hCD9RfdqA3Y3AF_P_WbelqvzvgR3RsX_mHBLynV14Q6HakXAtrY_eWLK2xqamF2OC9uBXfKgxTFFqmyr1Kbw">>}, - {apns_cert_dir,<<"apns_certificates">>}, - {apns_port,2195}]}, + {apns_cert_dir, "apns_certificates"}, + {apns_http_port, 443}]}, {job_delay, 60}, %% 1 mins {auth_ttl, 900}, %% 15 mins {auth_check_ip, false},