From 1966bbcca1df3103e5457526e3584bfc2c8c50d4 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Wed, 6 May 2020 08:22:35 +0200 Subject: [PATCH 1/9] WIP add apns4erl & prep for iOS13 push reqs --- apps/roster/src/api/push/ios13.erl | 221 +++++++++++++++++++++++ apps/roster/src/api/push/push_api.erl | 31 +++- apps/roster/src/protocol/roster_push.erl | 29 +-- apps/roster/src/roster.app.src | 2 +- rebar.config | 3 +- rebar.lock | 6 + sys.config | 4 +- 7 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 apps/roster/src/api/push/ios13.erl diff --git a/apps/roster/src/api/push/ios13.erl b/apps/roster/src/api/push/ios13.erl new file mode 100644 index 000000000..a071847c3 --- /dev/null +++ b/apps/roster/src/api/push/ios13.erl @@ -0,0 +1,221 @@ +-module(ios13). +-include_lib("kernel/include/logger.hrl"). +-include("roster.hrl"). +-include_lib("roster/include/static/push_notification_var.hrl"). + +-export([start/0]). +-export([description/0, notify/6, test_push_notification/0]). + +-export([status/0]). + +description() -> "iOS 13.x Push Notifications Module". + +-define(APNS_CERT_DIR, apns_cert_dir()). + +-define(APNS_PORT, proplists:get_value(apns_port, push_api_opts(), 443)). + +-define(GATEWAY_LIST, [ + {<<"SANDBOX">>, "api.sandbox.push.apple.com"}, + {<<"LIVE">>, "api.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"}} + ]). + +start() -> + start(detect_context()). + +start(Context) -> + Conn = #{ name => ios13 + , apple_host => get_host(Context) + , apple_port => ?APNS_PORT + , certfile => certfile(Context) + , keyfile => keyfile(Context) + , type => cert + , gun => #{ transport => tls + , http2_opts => #{ keepalive => 30000 } + } + }, + apns:connect(Conn). + +%% For debugging purposes. Should be replaced by a more +%% structured solution. +status() -> + Ch = supervisor:which_children(apns_sup), + [ch_status(Pid) || {_, Pid, worker, [apns_connection]} <- Ch]. + +detect_context() -> + proplists:get_value(context, push_api_opts(), dev). + +get_host(Context) -> + Key = case Context of + dev -> + <<"SANDBOX">>; + prod -> + <<"LIVE">> + end, + proplists:get_value(Key, ?GATEWAY_LIST). + +push_api_opts() -> + application:get_env(roster, push_api, []). + +certfile(Context) -> + Base = case Context of + dev -> "cert_dev.pem"; + prod -> "cert_prod.pem"; + rc -> "cert_rc.pem" + end, + filename:join(?APNS_CERT_DIR, Base). + +keyfile(Context) -> + Base = case Context of + dev -> "key_dev.pem"; + prod -> "key_prod.pem"; + rc -> "key_rc.pem" + end, + filename:join(?APNS_CERT_DIR, Base). + +apns_cert_dir() -> + D = proplists:get_value(apns_cert_dir, push_api_opts(), + default_cert_dir()), + Priv = code:priv_dir(roster), + filename:join(Priv, D). + +default_cert_dir() -> + filename:join(code:priv_dir(roster), "apns_certificates"). + +%% ------------------------------------------------------------------ +%% Ios Push Notifications +%% ------------------------------------------------------------------ + +notify(Alert, Custom, Type, DeviceId, SessionSettings, ConnSettings) + when is_binary(DeviceId) -> + notify(Alert, Custom, Type, binary_to_list(DeviceId), SessionSettings, ConnSettings); +notify(A, C, T, DeviceId, SessionSettings, ConnSettings) -> + [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], + + Aps = #{ nynja => #{ model => Custom + , type => Type + , title => Alert + , dns => get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS) + , version => <> } }, + + %% Use DeviceId or FormattedDeviceId?? + %% FormattedDeviceId = list_to_integer(DeviceId, 16), + send_push(DeviceId, #{aps => Aps}, ConnSettings), + ok. + +%% ------------------------------------------------------------------ +%% Helpers +%% ------------------------------------------------------------------ + +send_push(DeviceId, Msg, Pid) when is_pid(Pid) -> + apns:push_notification(Pid, DeviceId, Msg). + +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_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. + +ch_status(Pid) -> + case sys:get_status(Pid) of + {status, _, {module, gen_statem}, + [_, running, _, _, + [ {header, _} + , {data, _} + , {data, [{"State", {Status, St}}]} + ]]} -> + ch_status(Pid, Status, St); + _Other -> + {Pid, undefined} + end. + +ch_status(Pid, connected, St) -> + GunPid = maps:get(gun_pid, St), + {Pid, #{ status => connected + , state => St + , gun => gun_status(GunPid) }}; +ch_status(Pid, Other, St) -> + {Pid, Other, St}. + +gun_status(Pid) -> + case sys:get_status(Pid) of + {status, _, {module, _}, + [ _, running, _, _ + , {loop, State}] + } -> + case State of + _ when element(1, State) == state -> + case [M || M <- tuple_to_list(State), + is_map(M)] of + [GunOpts] -> + GunOpts; + _ -> + undefined + end; + _ -> + undefined + end; + _Other -> + undefined + 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"). + +%% TODO: run tests +test_push_notification() -> + {ok, Pid} = start(dev), + 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 = <<"g2gSZAAHTWVzc2FnZWEQZAAFY2hhaW5oA2QAA3AycG0AAAAOMzgw" + "NjM4MDk1MTU4XzdtAAAADjM4MDk5NDM4Mjc5OF84ampqam0AAAAO" + "MzgwOTk0MzgyNzk4XzhtAAAADjM4MDYzODA5NTE1OF83am4GAD5B" + "RlNeAWpqbAAAAAFoBmQABERlc2NqbQAAAARIaGhoYQBqampqamQABHNlbnQ=">>, + notify(Msg, Custom, <<"message">>, ?APNS_TEST_DEVICE_ID, SessionSettings, Pid). diff --git a/apps/roster/src/api/push/push_api.erl b/apps/roster/src/api/push/push_api.erl index 41990451b..a0d3862c9 100644 --- a/apps/roster/src/api/push/push_api.erl +++ b/apps/roster/src/api/push/push_api.erl @@ -1,14 +1,37 @@ -module(push_api). --export([description/0, fcm_notify/3, apns_notify/5]). +-export([start/0]). +-export([description/0, fcm_notify/4, apns_notify/6]). description() -> "Mobile Push Notifications Module. Wrapper for IOS and Android". -compile(export_all). +-include("roster.hrl"). -fcm_notify(MessageTitle, MessageBody, DeviceId) -> +start() -> + {ok, IOS13} = ios13:start(), + {ok, #{ ios13 => IOS13 + , android => [] + , ios => [] }}. + +fcm_notify(MessageTitle, MessageBody, DeviceId, _ConnState) -> 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 +apns_notify(Alert, Custom, Type, DeviceId, SessionSettings, ConnState) -> + case ios_version(SessionSettings) of + ios -> + ios:notify(Alert, Custom, Type, DeviceId, SessionSettings); + ios13 -> + IOS13St = maps:get(ios13, ConnState), + ios13:notify(Alert, Custom, Type, DeviceId, SessionSettings, IOS13St) + end. + +ios_version(Settings) -> + case [X || #'Feature'{key = <<"OS">>, value = <<"iOS 13", _/binary>>} = X + <- Settings] of + [_|_] -> + ios13; + [] -> + ios + end. diff --git a/apps/roster/src/protocol/roster_push.erl b/apps/roster/src/protocol/roster_push.erl index b6e67162c..2e91db72b 100644 --- a/apps/roster/src/protocol/roster_push.erl +++ b/apps/roster/src/protocol/roster_push.erl @@ -5,32 +5,37 @@ -include_lib("kvs/include/kvs.hrl"). -compile(export_all). -start() -> n2o_async:start(#handler{module = ?MODULE, class = system, group = roster, name = ?MODULE, state = []}). +start() -> + {ok, ConnState} = push_api:start(), + n2o_async:start(#handler{module = ?MODULE, class = system, group = roster, name = ?MODULE, state = #{conn_state => ConnState}}). 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) -> +send_push_notification(ios, Push, Payload, PushAlert, <<"calling">>, AuthSettings, HS) -> + push_api:apns_notify(PushAlert, Payload, <<"calling">>, Push, AuthSettings, HS); +send_push_notification(ios, Push, Payload, PushAlert, PushType, AuthSettings, HS) -> DecodedPayload = base64:encode(term_to_binary(Payload)), - push_api:apns_notify(PushAlert, DecodedPayload, PushType, Push, AuthSettings); -send_push_notification(android, Push, Payload, PushAlert, PushType, _) -> + push_api:apns_notify(PushAlert, DecodedPayload, PushType, Push, AuthSettings, HS); +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 + push_api:fcm_notify(PushAlert, AndroidPush, Push, HS); +send_push_notification(_, _, _, _, _, _, _) -> skip. diff --git a/apps/roster/src/roster.app.src b/apps/roster/src/roster.app.src index 05a7bdae9..bf95b6384 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/rebar.config b/rebar.config index a22caab3b..78719e362 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 31235aeb1..08e6c3e55 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 c319338ae..83e75cc9c 100644 --- a/sys.config +++ b/sys.config @@ -95,9 +95,9 @@ {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"}]}, {job_delay, 60}, %% 1 mins {auth_ttl, 900}, %% 15 mins {auth_check_ip, false}, -- GitLab From 8597e4353f6387a769fb83cb2003fe07ce0df998 Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Thu, 28 May 2020 16:39:04 +0200 Subject: [PATCH 2/9] Reboot the apns refactoring effort * remove unnecessary layer push_api.erl * remove split on different ios versions * better naming of roster_apns_api * introduce roster:deployment_context() to find if we are dev/staging/prod --- .../include/static/push_notification_var.hrl | 12 - apps/roster/src/api/push/ios.erl | 118 ---------- apps/roster/src/api/push/ios13.erl | 221 ------------------ apps/roster/src/api/push/push_api.erl | 37 --- apps/roster/src/api/push/roster_apns_api.erl | 209 +++++++++++++++++ apps/roster/src/protocol/roster_push.erl | 57 ++++- .../src/rest/rest_cowboy_push_handler.erl | 4 +- sys.config | 3 +- 8 files changed, 258 insertions(+), 403 deletions(-) delete mode 100644 apps/roster/include/static/push_notification_var.hrl delete mode 100644 apps/roster/src/api/push/ios.erl delete mode 100644 apps/roster/src/api/push/ios13.erl delete mode 100644 apps/roster/src/api/push/push_api.erl create mode 100644 apps/roster/src/api/push/roster_apns_api.erl diff --git a/apps/roster/include/static/push_notification_var.hrl b/apps/roster/include/static/push_notification_var.hrl deleted file mode 100644 index 0af215f1b..000000000 --- a/apps/roster/include/static/push_notification_var.hrl +++ /dev/null @@ -1,12 +0,0 @@ -%% ------------------------------------------------------------------ -%% Static Variables for Push Notification modules -%% ------------------------------------------------------------------ - -%% FGPN - Feature Group for Push Notification -%% FKPN - Feature Key for Push Notification - --define(FGPN_INFO, <<"PUSH_SETTINGS">>). - --define(FKPN_BANDLE, <<"IOS_BANDLE">>). --define(FKPN_GATEWAY, <<"APNS_GATEWAY">>). --define(FKPN_SERVER_DNS, <<"SERVER_DNS">>). \ No newline at end of file diff --git a/apps/roster/src/api/push/ios.erl b/apps/roster/src/api/push/ios.erl deleted file mode 100644 index 07a878027..000000000 --- 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/ios13.erl b/apps/roster/src/api/push/ios13.erl deleted file mode 100644 index a071847c3..000000000 --- a/apps/roster/src/api/push/ios13.erl +++ /dev/null @@ -1,221 +0,0 @@ --module(ios13). --include_lib("kernel/include/logger.hrl"). --include("roster.hrl"). --include_lib("roster/include/static/push_notification_var.hrl"). - --export([start/0]). --export([description/0, notify/6, test_push_notification/0]). - --export([status/0]). - -description() -> "iOS 13.x Push Notifications Module". - --define(APNS_CERT_DIR, apns_cert_dir()). - --define(APNS_PORT, proplists:get_value(apns_port, push_api_opts(), 443)). - --define(GATEWAY_LIST, [ - {<<"SANDBOX">>, "api.sandbox.push.apple.com"}, - {<<"LIVE">>, "api.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"}} - ]). - -start() -> - start(detect_context()). - -start(Context) -> - Conn = #{ name => ios13 - , apple_host => get_host(Context) - , apple_port => ?APNS_PORT - , certfile => certfile(Context) - , keyfile => keyfile(Context) - , type => cert - , gun => #{ transport => tls - , http2_opts => #{ keepalive => 30000 } - } - }, - apns:connect(Conn). - -%% For debugging purposes. Should be replaced by a more -%% structured solution. -status() -> - Ch = supervisor:which_children(apns_sup), - [ch_status(Pid) || {_, Pid, worker, [apns_connection]} <- Ch]. - -detect_context() -> - proplists:get_value(context, push_api_opts(), dev). - -get_host(Context) -> - Key = case Context of - dev -> - <<"SANDBOX">>; - prod -> - <<"LIVE">> - end, - proplists:get_value(Key, ?GATEWAY_LIST). - -push_api_opts() -> - application:get_env(roster, push_api, []). - -certfile(Context) -> - Base = case Context of - dev -> "cert_dev.pem"; - prod -> "cert_prod.pem"; - rc -> "cert_rc.pem" - end, - filename:join(?APNS_CERT_DIR, Base). - -keyfile(Context) -> - Base = case Context of - dev -> "key_dev.pem"; - prod -> "key_prod.pem"; - rc -> "key_rc.pem" - end, - filename:join(?APNS_CERT_DIR, Base). - -apns_cert_dir() -> - D = proplists:get_value(apns_cert_dir, push_api_opts(), - default_cert_dir()), - Priv = code:priv_dir(roster), - filename:join(Priv, D). - -default_cert_dir() -> - filename:join(code:priv_dir(roster), "apns_certificates"). - -%% ------------------------------------------------------------------ -%% Ios Push Notifications -%% ------------------------------------------------------------------ - -notify(Alert, Custom, Type, DeviceId, SessionSettings, ConnSettings) - when is_binary(DeviceId) -> - notify(Alert, Custom, Type, binary_to_list(DeviceId), SessionSettings, ConnSettings); -notify(A, C, T, DeviceId, SessionSettings, ConnSettings) -> - [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], - - Aps = #{ nynja => #{ model => Custom - , type => Type - , title => Alert - , dns => get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS) - , version => <> } }, - - %% Use DeviceId or FormattedDeviceId?? - %% FormattedDeviceId = list_to_integer(DeviceId, 16), - send_push(DeviceId, #{aps => Aps}, ConnSettings), - ok. - -%% ------------------------------------------------------------------ -%% Helpers -%% ------------------------------------------------------------------ - -send_push(DeviceId, Msg, Pid) when is_pid(Pid) -> - apns:push_notification(Pid, DeviceId, Msg). - -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_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. - -ch_status(Pid) -> - case sys:get_status(Pid) of - {status, _, {module, gen_statem}, - [_, running, _, _, - [ {header, _} - , {data, _} - , {data, [{"State", {Status, St}}]} - ]]} -> - ch_status(Pid, Status, St); - _Other -> - {Pid, undefined} - end. - -ch_status(Pid, connected, St) -> - GunPid = maps:get(gun_pid, St), - {Pid, #{ status => connected - , state => St - , gun => gun_status(GunPid) }}; -ch_status(Pid, Other, St) -> - {Pid, Other, St}. - -gun_status(Pid) -> - case sys:get_status(Pid) of - {status, _, {module, _}, - [ _, running, _, _ - , {loop, State}] - } -> - case State of - _ when element(1, State) == state -> - case [M || M <- tuple_to_list(State), - is_map(M)] of - [GunOpts] -> - GunOpts; - _ -> - undefined - end; - _ -> - undefined - end; - _Other -> - undefined - 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"). - -%% TODO: run tests -test_push_notification() -> - {ok, Pid} = start(dev), - 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 = <<"g2gSZAAHTWVzc2FnZWEQZAAFY2hhaW5oA2QAA3AycG0AAAAOMzgw" - "NjM4MDk1MTU4XzdtAAAADjM4MDk5NDM4Mjc5OF84ampqam0AAAAO" - "MzgwOTk0MzgyNzk4XzhtAAAADjM4MDYzODA5NTE1OF83am4GAD5B" - "RlNeAWpqbAAAAAFoBmQABERlc2NqbQAAAARIaGhoYQBqampqamQABHNlbnQ=">>, - notify(Msg, Custom, <<"message">>, ?APNS_TEST_DEVICE_ID, SessionSettings, Pid). 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 a0d3862c9..000000000 --- a/apps/roster/src/api/push/push_api.erl +++ /dev/null @@ -1,37 +0,0 @@ --module(push_api). - --export([start/0]). --export([description/0, fcm_notify/4, apns_notify/6]). - -description() -> "Mobile Push Notifications Module. Wrapper for IOS and Android". - --compile(export_all). - --include("roster.hrl"). - -start() -> - {ok, IOS13} = ios13:start(), - {ok, #{ ios13 => IOS13 - , android => [] - , ios => [] }}. - -fcm_notify(MessageTitle, MessageBody, DeviceId, _ConnState) -> - android:notify(MessageTitle, MessageBody, DeviceId). - -apns_notify(Alert, Custom, Type, DeviceId, SessionSettings, ConnState) -> - case ios_version(SessionSettings) of - ios -> - ios:notify(Alert, Custom, Type, DeviceId, SessionSettings); - ios13 -> - IOS13St = maps:get(ios13, ConnState), - ios13:notify(Alert, Custom, Type, DeviceId, SessionSettings, IOS13St) - end. - -ios_version(Settings) -> - case [X || #'Feature'{key = <<"OS">>, value = <<"iOS 13", _/binary>>} = X - <- Settings] of - [_|_] -> - ios13; - [] -> - ios - end. 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 000000000..fea25cf4a --- /dev/null +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -0,0 +1,209 @@ +%%%------------------------------------------------------------------- +%%% @doc Client interface to apns +%%% +%%% @end +%%%------------------------------------------------------------------- +-module(roster_apns_api). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("roster/include/roster.hrl"). + +-export([ notify/6 + , start/0 + ]). + +-type connection() :: #{ pid => pid() | 'undefined' + , config => maps:map() + }. + +-record(roster_apns_api_state, + { connections :: orddict:orddict('apns_live' | + 'apns_sandbox_prod' | + 'apns_sandbox_dev', + connection()) + }). + + +%% FGPN - Feature Group for Push Notification +%% FKPN - Feature Key for Push Notification +-define(FGPN_INFO, <<"PUSH_SETTINGS">>). + +-define(FKPN_BUNDLE, <<"IOS_BANDLE">>). %% Sic! +-define(FKPN_GATEWAY, <<"APNS_GATEWAY">>). +-define(FKPN_SERVER_DNS, <<"SERVER_DNS">>). +-define(FKPN_GENERIC_TOKEN, <<"GENERIC_TOKEN">>). + + +%%%=================================================================== +%%% API +%%%=================================================================== + +start() -> + start(connection_configs(), []). + +start([#{name := Name} = Config|Left], Connections) -> + ?LOG_INFO("Connecting to apns with config ~p", [Config]), + case apns:connect(Config) of + {ok, Pid} -> + Connection = #{ pid => Pid + , config => Config}, + Connections1 = orddict:store(Name, Connection, Connections), + start(Left, Connections1); + {error, What} -> + error({could_not_start_apns, What}) + end; +start([], Connections) -> + #roster_apns_api_state{ connections = Connections }. + +notify(A, C, T, VoipToken, SessionSettings, State) -> + Connections = State#roster_apns_api_state.connections, + Conn = pick_connection_from_session_settings(SessionSettings, Connections), + [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], + DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), + {Headers, Aps, Token} = token_headers_and_aps(Type, SessionSettings, VoipToken), + Aps1 = Aps#{ nynja => #{ model => Custom + , type => Type + , title => Alert + , dns => DNS + %% TODO: Is this needed? + , version => <> + } + }, + Pid = maps:get(pid, Conn), + case apns:push_notification(Pid, Token, Aps1, Headers) of + {200, _RespHeaders, _RespBody} -> + ok; + {410, RespHeaders, RespBody} -> + %% Expired of invalid token. + ?LOG_INFO("APNS push error ~w:~p:~p", [410, RespHeaders, RespBody]), + {error, bad_token}; + {Status, RespHeaders, RespBody} -> + ?LOG_ERROR("APNS push error ~w:~p:~p", + [Status, RespHeaders, RespBody]), + {error, not_delivered} + end. + +token_headers_and_aps(Type, SessionSettings, VoipToken) -> + %% Note that headers need to be written as strings. + %% The payload content should be numbers. + case push_type_and_token(Type, SessionSettings) of + {background, Token} -> + { #{ apns_push_type => <<"background">> + , apns_priority => <<"5">>} + , #{aps => #{ <<"content-available">> => 1 + } + } + , Token + }; + legacy_voip -> + %% Token from the old system where we used only one token + %% both for voip and background updates. + %% This is not a real voip push. + { #{ apns_push_type => <<"voip">> + , apns_priority => <<"5">>} + , #{} + , VoipToken + }; + voip -> + + { #{ apns_push_type => <<"voip">> + , apns_expiration => <<"0">> %% Deliver immediately or not at all. + , apns_priority => <<"10">>} + , #{} + , VoipToken + } + end. + +push_type_and_token(<<"voip">>, _SessionSettings) -> + %% TODO: Make sure this is the call type given from the rest api. + voip; +push_type_and_token(_Type, SessionSettings) -> + case get_data_from_feature(SessionSettings, ?FKPN_GENERIC_TOKEN) of + [] -> legacy_voip; + Token -> {background, Token} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +%%%=================================================================== +%%% Initialisation of connection + +get_from_config(Key) -> + Env = application:get_env(roster, push_api, []), + proplists:get_value(Key, Env). + +connection_configs() -> + %% TODO: Move the certs away from priv_dir + CertDir = filename:join(code:priv_dir(roster), get_from_config(apns_cert_dir)), + ApnsPort = get_from_config(apns_http_port), + [ #{ name => ConnectionName + , apple_host => host(HostType) + , apple_port => ApnsPort + , certfile => certfile(CertDir, CertType) + , keyfile => keyfile(CertDir, CertType) + , type => cert + , timeout => 10000 + , gun => #{ transport => tls + , http2_opts => #{ keepalive => 30000 } + } + } + || {ConnectionName, HostType, CertType} <- + [ {apns_live, live, prod} + , {apns_sandbox_prod, sandbox, prod} + , {apns_sandbox_dev, sandbox, dev} + ] + ]. + +certfile(CertDir, Context) -> + Base = case Context of + dev -> "cert_dev.pem"; + prod -> "cert_prod.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 + dev -> "key_dev.pem"; + prod -> "key_prod.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 + sandbox -> "api.sandbox.push.apple.com"; + live -> "api.push.apple.com" + end. + +%%%=================================================================== +%%% Client settings + +pick_connection_from_session_settings(SessionSettings, Connections) -> + Bundle = get_data_from_feature(SessionSettings, ?FKPN_BUNDLE), + Gateway = get_data_from_feature(SessionSettings, ?FKPN_GATEWAY), + CertType = cert_type_from_bundle(Bundle), + Name = case {CertType, Gateway} of + {prod, <<"LIVE">>} -> apns_live; + {prod, <<"SANDBOX">>} -> apns_sandbox_prod; + {dev, <<"SANDBOX">>} -> apns_sandbox_dev + end, + orddict:fetch(Name, Connections). + +cert_type_from_bundle(<<"com.nynja.mobile.communicator">>) -> prod; +cert_type_from_bundle(<<"com.nynja.rc.mobile.communicator">>) -> prod; +cert_type_from_bundle(<<"com.nynja.dev.mobile.communicator">>) -> dev. + +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 2e91db72b..ac875a89f 100644 --- a/apps/roster/src/protocol/roster_push.erl +++ b/apps/roster/src/protocol/roster_push.erl @@ -3,21 +3,45 @@ -include("roster.hrl"). -include_lib("n2o/include/n2o.hrl"). -include_lib("kvs/include/kvs.hrl"). --compile(export_all). +-export([ start/0 + , send_push_notification/4 + , proc/2 + ]). start() -> - {ok, ConnState} = push_api:start(), - n2o_async:start(#handler{module = ?MODULE, class = system, group = roster, name = ?MODULE, state = #{conn_state => ConnState}}). + n2o_async:start(#handler{ module = ?MODULE + , class = system + , group = roster + , name = ?MODULE + , state = init}). + +send_push_notification(Session, Payload, Alert, Type) -> + n2o_async:pid(system, ?MODULE) + ! {async_push, Session, Payload, Alert, Type}. + +%% TODO: Handle apns connection messages in proc/2 proc(init, #handler{name = ?MODULE} = Async) -> - ?LOG_INFO("ASYNC", []), - {ok, Async}; + ConnState = #{ android => [] + , ios => roster_apns_api:start()}, + {ok, Async#handler{state = ConnState}}; + +proc({connection_up, _Pid}, #handler{} = H) -> + %% APNS connection. Safe to ignore + {noreply, H}; + +proc({connection_down, _Pid}, #handler{} = H) -> + %% APNS connection. Safe to ignore + {noreply, H}; + +proc({reconnecting, _Pid}, #handler{} = H) -> + %% APNS connection. Safe to ignore + {noreply, H}; 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 @@ -29,13 +53,22 @@ send_push_notification(#'Auth'{ os = OS [PhoneId, OS, binary:part(PushToken, 0, erlang:min(25, size(PushToken))), PushAlert]), send_push_notification(OS, PushToken, Payload, PushAlert, PushType, AuthSettings, HS) end. -send_push_notification(ios, Push, Payload, PushAlert, <<"calling">>, AuthSettings, HS) -> - push_api:apns_notify(PushAlert, Payload, <<"calling">>, Push, AuthSettings, HS); + send_push_notification(ios, Push, Payload, PushAlert, PushType, AuthSettings, HS) -> - DecodedPayload = base64:encode(term_to_binary(Payload)), - push_api:apns_notify(PushAlert, DecodedPayload, PushType, Push, AuthSettings, HS); -send_push_notification(android, Push, Payload, PushAlert, PushType, _AuthSettings, HS) -> + EncodedPayload = base64:encode(term_to_binary(Payload)), + IOS = maps:get(ios, HS), + case roster_apns_api:notify(PushAlert, EncodedPayload, + PushType, Push, AuthSettings, IOS) of + ok -> + ok; + {error, bad_token} -> + %% TODO: Remove auth. + ok; + {error,_What} -> + ok + end; +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, HS); + android:notify(PushAlert, AndroidPush, Push); send_push_notification(_, _, _, _, _, _, _) -> skip. diff --git a/apps/roster/src/rest/rest_cowboy_push_handler.erl b/apps/roster/src/rest/rest_cowboy_push_handler.erl index 96303a561..b16d2cbfa 100644 --- a/apps/roster/src/rest/rest_cowboy_push_handler.erl +++ b/apps/roster/src/rest/rest_cowboy_push_handler.erl @@ -52,14 +52,14 @@ from_json(Req, State) -> Payload = list_to_binary(PS#'PushService'.payload), Recipients = PS#'PushService'.recipients, PushAlert = PushType = list_to_binary(PS#'PushService'.module), - Pid = n2o_async:pid(system, roster_push), lists:foreach( fun(PhoneId0) -> PhoneId = iolist_to_binary(PhoneId0), AuthList = kvs:index('Auth', user_id, PhoneId), lists:foreach( fun(Auth) -> - Pid ! {async_push, Auth, Payload, PushAlert, PushType} + roster_push:send_push_notification( + Auth, Payload, PushAlert, PushType) end, AuthList) end, Recipients), {true, Req1, State} diff --git a/sys.config b/sys.config index 83e75cc9c..4eb389ef4 100644 --- a/sys.config +++ b/sys.config @@ -97,7 +97,8 @@ {push_api,[ {context, dev}, {fcm_server_key,<<"AAAAAzb6_Zg:APA91bGN0jYv_4iqyk8IC4xUdPYXh0yPsTF9YYj_gd9oebRr_ZEoLuC5hCD9RfdqA3Y3AF_P_WbelqvzvgR3RsX_mHBLynV14Q6HakXAtrY_eWLK2xqamF2OC9uBXfKgxTFFqmyr1Kbw">>}, - {apns_cert_dir, "apns_certificates"}]}, + {apns_cert_dir, "apns_certificates"}, + {apns_http_port, 443}]}, {job_delay, 60}, %% 1 mins {auth_ttl, 900}, %% 15 mins {auth_check_ip, false}, -- GitLab From ff8dbc055a6332e15d884d0536c0112e67d5b71c Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Tue, 2 Jun 2020 12:53:10 +0200 Subject: [PATCH 3/9] Adjust push type for calling service --- apps/roster/src/api/push/roster_apns_api.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index fea25cf4a..ded4a6009 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -101,7 +101,8 @@ token_headers_and_aps(Type, SessionSettings, VoipToken) -> %% This is not a real voip push. { #{ apns_push_type => <<"voip">> , apns_priority => <<"5">>} - , #{} + , #{aps => #{ <<"content-available">> => 1 + } , VoipToken }; voip -> @@ -114,8 +115,7 @@ token_headers_and_aps(Type, SessionSettings, VoipToken) -> } end. -push_type_and_token(<<"voip">>, _SessionSettings) -> - %% TODO: Make sure this is the call type given from the rest api. +push_type_and_token(<<"calling">>, _SessionSettings) -> voip; push_type_and_token(_Type, SessionSettings) -> case get_data_from_feature(SessionSettings, ?FKPN_GENERIC_TOKEN) of -- GitLab From 1ddd2f5dd925f9dba0550eb5891aa5c9e16eef11 Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Tue, 2 Jun 2020 14:30:54 +0200 Subject: [PATCH 4/9] Make apns connections anonymous to be able to start more than one --- apps/roster/src/api/push/roster_apns_api.erl | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index ded4a6009..c0f96a385 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -41,7 +41,7 @@ start() -> start(connection_configs(), []). -start([#{name := Name} = Config|Left], Connections) -> +start([{Name, Config}|Left], Connections) -> ?LOG_INFO("Connecting to apns with config ~p", [Config]), case apns:connect(Config) of {ok, Pid} -> @@ -103,10 +103,10 @@ token_headers_and_aps(Type, SessionSettings, VoipToken) -> , apns_priority => <<"5">>} , #{aps => #{ <<"content-available">> => 1 } + } , VoipToken }; voip -> - { #{ apns_push_type => <<"voip">> , apns_expiration => <<"0">> %% Deliver immediately or not at all. , apns_priority => <<"10">>} @@ -138,17 +138,18 @@ connection_configs() -> %% TODO: Move the certs away from priv_dir CertDir = filename:join(code:priv_dir(roster), get_from_config(apns_cert_dir)), ApnsPort = get_from_config(apns_http_port), - [ #{ name => ConnectionName - , apple_host => host(HostType) - , apple_port => ApnsPort - , certfile => certfile(CertDir, CertType) - , keyfile => keyfile(CertDir, CertType) - , type => cert - , timeout => 10000 - , gun => #{ transport => tls - , http2_opts => #{ keepalive => 30000 } - } - } + [{ ConnectionName + , #{ name => undefined %% In order to be able to start more than one + , apple_host => host(HostType) + , apple_port => ApnsPort + , certfile => certfile(CertDir, CertType) + , keyfile => keyfile(CertDir, CertType) + , type => cert + , timeout => 10000 + , gun => #{ transport => tls + , http2_opts => #{ keepalive => 30000 } + } + }} || {ConnectionName, HostType, CertType} <- [ {apns_live, live, prod} , {apns_sandbox_prod, sandbox, prod} -- GitLab From 5b2dd12c2e59d524faf0d7a7cc4999c8bad274a4 Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Wed, 3 Jun 2020 09:28:07 +0200 Subject: [PATCH 5/9] Remove content-available to voip token background pushes --- apps/roster/src/api/push/roster_apns_api.erl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index c0f96a385..7fed8760b 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -101,9 +101,7 @@ token_headers_and_aps(Type, SessionSettings, VoipToken) -> %% This is not a real voip push. { #{ apns_push_type => <<"voip">> , apns_priority => <<"5">>} - , #{aps => #{ <<"content-available">> => 1 - } - } + , #{} , VoipToken }; voip -> -- GitLab From ae9ab30328b035f48160b1948d5ddeda56a43c03 Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Wed, 3 Jun 2020 15:31:02 +0200 Subject: [PATCH 6/9] Add config to enable legacy binary apns protocol --- apps/roster/src/api/push/roster_apns_api.erl | 237 +++++++++++++------ sys.config | 3 +- 2 files changed, 167 insertions(+), 73 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index 7fed8760b..b2cb54ba7 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -12,15 +12,21 @@ , start/0 ]). +%% Temporary manual interface +-export([ set_push_protocol/1 + ]). + -type connection() :: #{ pid => pid() | 'undefined' , config => maps:map() }. +-type connection_name() :: 'apns_live' | + 'apns_sandbox_prod' | + 'apns_sandbox_dev'. + -record(roster_apns_api_state, - { connections :: orddict:orddict('apns_live' | - 'apns_sandbox_prod' | - 'apns_sandbox_dev', - connection()) + { http_connections :: orddict:orddict(connection_name(), connection()) + , binary_configs :: orddict:orddict(connection_name(), map()) }). @@ -38,8 +44,15 @@ %%% API %%%=================================================================== +%% Temporary manual interface +set_push_protocol(Type) when Type =:= binary; Type =:= http -> + {ok, Env} = application:get_env(roster, push_api), + Key = apns_push_type, + Env1 = lists:keyreplace(Key, 1, Env, {Key, Type}), + application:set_env(roster, push_api, Env1). + start() -> - start(connection_configs(), []). + start(http_connection_configs(), []). start([{Name, Config}|Left], Connections) -> ?LOG_INFO("Connecting to apns with config ~p", [Config]), @@ -53,22 +66,79 @@ start([{Name, Config}|Left], Connections) -> error({could_not_start_apns, What}) end; start([], Connections) -> - #roster_apns_api_state{ connections = Connections }. + %% TODO: Extend to long-lived ssl connections. + BinaryConfigs = orddict:from_list(binary_connection_configs()), + #roster_apns_api_state{ http_connections = Connections + , binary_configs = BinaryConfigs + }. notify(A, C, T, VoipToken, SessionSettings, State) -> - Connections = State#roster_apns_api_state.connections, - Conn = pick_connection_from_session_settings(SessionSettings, Connections), [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], + case get_from_config(apns_push_type) of + binary -> + binary_notify(Alert, Custom, Type, VoipToken, SessionSettings, State); + http -> + http_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) + end. + +binary_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) -> + #{ options := Options + , apple_port := Port + , apple_host := Gateway} = pick_binary_config(SessionSettings, State), + DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), + Payload = jsx:encode( + #{ aps => + #{ nynja => + #{ model => Custom + , type => Type + , title => Alert + , dns => DNS + , version => <> + }}}), + Packet = << 0:8, + 32:16/big, + (binary_to_integer(VoipToken, 16)):256/big, + (byte_size(Payload)):16/big, + Payload/binary >>, + binary_push(Gateway, Port, Packet, Options, 1), + ok. + +binary_push(Addr, Port, Payload, Options, Attempt) -> + ?LOG_INFO("Addr: ~p, Options: ~p, Attempt:~p", [Addr, Options, Attempt]), +%% NOTE set Duration = Attempt * 100 for tests + Duration = Attempt * 500, + {Status, Socket} = ssl:connect(Addr, 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]), + binary_push(Addr, Port, Payload, Options, Attempt + 1) + end + end. + + + +http_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) -> + Conn = pick_http_connection(SessionSettings, State), DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), {Headers, Aps, Token} = token_headers_and_aps(Type, SessionSettings, VoipToken), - Aps1 = Aps#{ nynja => #{ model => Custom - , type => Type - , title => Alert - , dns => DNS - %% TODO: Is this needed? - , version => <> - } - }, + Aps1 = #{ aps => Aps + , nynja => #{ model => Custom + , type => Type + , title => Alert + , dns => DNS + %% TODO: Is this needed? + , version => <> + } + }, Pid = maps:get(pid, Conn), case apns:push_notification(Pid, Token, Aps1, Headers) of {200, _RespHeaders, _RespBody} -> @@ -90,8 +160,7 @@ token_headers_and_aps(Type, SessionSettings, VoipToken) -> {background, Token} -> { #{ apns_push_type => <<"background">> , apns_priority => <<"5">>} - , #{aps => #{ <<"content-available">> => 1 - } + , #{ <<"content-available">> => 1 } , Token }; @@ -126,76 +195,100 @@ push_type_and_token(_Type, SessionSettings) -> %%%=================================================================== %%%=================================================================== -%%% Initialisation of connection +%%% Initialisation of connections and configs + +-define(SESSION_NAMES, [apns_live, apns_sandbox_prod, apns_sandbox_dev]). +-define(SANDBOX_HTTP_HOST, "api.sandbox.push.apple.com"). +-define(LIVE_HTTP_HOST, "api.push.apple.com"). +-define(SANDBOX_SSL_HOST, "gateway.sandbox.push.apple.com"). +-define(LIVE_SSL_HOST, "gateway.push.apple.com"). +-define(PROD_CERTS, {"cert_prod.pem", "key_prod.pem"}). +-define(DEV_CERTS, {"cert_dev.pem", "key_dev.pem"}). get_from_config(Key) -> Env = application:get_env(roster, push_api, []), proplists:get_value(Key, Env). -connection_configs() -> - %% TODO: Move the certs away from priv_dir - CertDir = filename:join(code:priv_dir(roster), get_from_config(apns_cert_dir)), +http_connection_configs() -> ApnsPort = get_from_config(apns_http_port), - [{ ConnectionName - , #{ name => undefined %% In order to be able to start more than one - , apple_host => host(HostType) - , apple_port => ApnsPort - , certfile => certfile(CertDir, CertType) - , keyfile => keyfile(CertDir, CertType) - , type => cert - , timeout => 10000 - , gun => #{ transport => tls - , http2_opts => #{ keepalive => 30000 } - } - }} - || {ConnectionName, HostType, CertType} <- - [ {apns_live, live, prod} - , {apns_sandbox_prod, sandbox, prod} - , {apns_sandbox_dev, sandbox, dev} - ] - ]. - -certfile(CertDir, Context) -> - Base = case Context of - dev -> "cert_dev.pem"; - prod -> "cert_prod.pem" - end, - FN = filename:join(CertDir, Base), - case filelib:is_file(FN) of - true -> FN; - false -> error({no_apns_cert_file, FN}) - end. + [ { SessionName, http_config(SessionName, ApnsPort)} + || SessionName <- ?SESSION_NAMES]. -keyfile(CertDir, Context) -> - Base = case Context of - dev -> "key_dev.pem"; - prod -> "key_prod.pem" - end, - FN = filename:join(CertDir, Base), - case filelib:is_file(FN) of - true -> FN; - false -> error({no_apns_key_file, FN}) - end. +http_config(SessionName, ApnsPort) -> + {CertFile, KeyFile} = certfiles_from_session_name(SessionName), + #{ name => undefined %% In order to be able to start more than one + , apple_host => http_host_from_session_name(SessionName) + , apple_port => ApnsPort + , certfile => CertFile + , keyfile => KeyFile + , type => cert + , timeout => 10000 + , gun => #{ transport => tls + , http2_opts => #{ keepalive => 30000 } + } + }. + +binary_connection_configs() -> + ApnsPort = get_from_config(apns_binary_port), + [ { SessionName, binary_config(SessionName, ApnsPort)} + || SessionName <- ?SESSION_NAMES]. + +binary_config(SessionName, BinaryApnsPort) -> + {CertFile, KeyFile} = certfiles_from_session_name(SessionName), + #{ options => [{certfile, CertFile}, + {keyfile, KeyFile}, + {mode, binary}] + , apple_port => BinaryApnsPort + , apple_host => ssl_host_from_session_name(SessionName) + }. + + +http_host_from_session_name(apns_live) -> ?LIVE_HTTP_HOST; +http_host_from_session_name(apns_sandbox_prod) -> ?SANDBOX_HTTP_HOST; +http_host_from_session_name(apns_sandbox_dev) -> ?SANDBOX_HTTP_HOST. + +ssl_host_from_session_name(apns_live) -> ?LIVE_SSL_HOST; +ssl_host_from_session_name(apns_sandbox_prod) -> ?SANDBOX_SSL_HOST; +ssl_host_from_session_name(apns_sandbox_dev) -> ?SANDBOX_SSL_HOST. + +certfiles_from_session_name(apns_live) -> ensure_cert_files(?PROD_CERTS); +certfiles_from_session_name(apns_sandbox_prod) -> ensure_cert_files(?PROD_CERTS); +certfiles_from_session_name(apns_sandbox_dev) -> ensure_cert_files(?DEV_CERTS). -host(Context) -> - case Context of - sandbox -> "api.sandbox.push.apple.com"; - live -> "api.push.apple.com" +ensure_cert_files({CertBase, KeyBase}) -> + %% TODO: Move the certs away from priv_dir + CertDir = filename:join(code:priv_dir(roster), + get_from_config(apns_cert_dir)), + KeyFile = filename:join(CertDir, KeyBase), + CertFile = filename:join(CertDir, CertBase), + case filelib:is_file(KeyFile) andalso filelib:is_file(CertFile) of + true -> {CertFile, KeyFile}; + false -> error({missing_apns_cert_files, CertFile, KeyFile}) end. %%%=================================================================== %%% Client settings -pick_connection_from_session_settings(SessionSettings, Connections) -> +pick_http_connection(SessionSettings, State) -> + Connections = State#roster_apns_api_state.http_connections, + Name = pick_session_name(SessionSettings), + orddict:fetch(Name, Connections). + +pick_binary_config(SessionSettings, State) -> + Configs = State#roster_apns_api_state.binary_configs, + Name = pick_session_name(SessionSettings), + orddict:fetch(Name, Configs). + + +pick_session_name(SessionSettings) -> Bundle = get_data_from_feature(SessionSettings, ?FKPN_BUNDLE), Gateway = get_data_from_feature(SessionSettings, ?FKPN_GATEWAY), CertType = cert_type_from_bundle(Bundle), - Name = case {CertType, Gateway} of - {prod, <<"LIVE">>} -> apns_live; - {prod, <<"SANDBOX">>} -> apns_sandbox_prod; - {dev, <<"SANDBOX">>} -> apns_sandbox_dev - end, - orddict:fetch(Name, Connections). + case {CertType, Gateway} of + {prod, <<"LIVE">>} -> apns_live; + {prod, <<"SANDBOX">>} -> apns_sandbox_prod; + {dev, <<"SANDBOX">>} -> apns_sandbox_dev + end. cert_type_from_bundle(<<"com.nynja.mobile.communicator">>) -> prod; cert_type_from_bundle(<<"com.nynja.rc.mobile.communicator">>) -> prod; diff --git a/sys.config b/sys.config index 4eb389ef4..d4f445bad 100644 --- a/sys.config +++ b/sys.config @@ -95,9 +95,10 @@ {app_credentials, "etc/certs/transcribe-dacb4306ab76.json"} ]}, {push_api,[ - {context, dev}, + {apns_push_type, binary}, %% 'binary' | 'http' {fcm_server_key,<<"AAAAAzb6_Zg:APA91bGN0jYv_4iqyk8IC4xUdPYXh0yPsTF9YYj_gd9oebRr_ZEoLuC5hCD9RfdqA3Y3AF_P_WbelqvzvgR3RsX_mHBLynV14Q6HakXAtrY_eWLK2xqamF2OC9uBXfKgxTFFqmyr1Kbw">>}, {apns_cert_dir, "apns_certificates"}, + {apns_binary_port, 2195}, {apns_http_port, 443}]}, {job_delay, 60}, %% 1 mins {auth_ttl, 900}, %% 15 mins -- GitLab From 33ea1e6c9e66f66d1509f10491b5a81f89763a9a Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Thu, 4 Jun 2020 16:00:36 +0200 Subject: [PATCH 7/9] Choose http or binary apns push depending on client settings and config --- apps/roster/src/api/push/roster_apns_api.erl | 192 +++++++++---------- sys.config | 2 +- 2 files changed, 96 insertions(+), 98 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index b2cb54ba7..9fa62025d 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -13,7 +13,7 @@ ]). %% Temporary manual interface --export([ set_push_protocol/1 +-export([ toggle_force_http/0 ]). -type connection() :: #{ pid => pid() | 'undefined' @@ -40,17 +40,19 @@ -define(FKPN_GENERIC_TOKEN, <<"GENERIC_TOKEN">>). +%% Temporary manual interface +toggle_force_http() -> + {ok, Env} = application:get_env(roster, push_api), + Key = apns_force_http, + Val = not proplists:get_value(Key, Env), + Env1 = lists:keyreplace(Key, 1, Env, {Key, Val}), + application:set_env(roster, push_api, Env1), + Val. + %%%=================================================================== %%% API %%%=================================================================== -%% Temporary manual interface -set_push_protocol(Type) when Type =:= binary; Type =:= http -> - {ok, Env} = application:get_env(roster, push_api), - Key = apns_push_type, - Env1 = lists:keyreplace(Key, 1, Env, {Key, Type}), - application:set_env(roster, push_api, Env1). - start() -> start(http_connection_configs(), []). @@ -74,73 +76,110 @@ start([], Connections) -> notify(A, C, T, VoipToken, SessionSettings, State) -> [Alert, Custom, Type] = [iolist_to_binary([L]) || L <- [A, C, T]], - case get_from_config(apns_push_type) of + DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), + NynjaPayload = #{ model => Custom + , type => Type + , title => Alert + , dns => DNS + %% TODO: Is this needed? + , version => <> + }, + case pick_connection_type(Type, SessionSettings, VoipToken) of + {binary, Token} -> + Config = pick_binary_config(SessionSettings, State), + %% TODO: This is what seems to work here, but + %% it seems wrong according to docs. + Payload = #{ aps => #{nynja => NynjaPayload}}, + binary_notify(Config, Payload, Token); + {http, Headers, Aps, Token} -> + Conn = pick_http_connection(SessionSettings, State), + Payload = #{ aps => Aps + , nynja => NynjaPayload + }, + http_notify(Conn, Payload, Headers, Token) + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +%%%=================================================================== +%%% Notifications + +pick_connection_type(Type, SessionSettings, VoipToken) -> + %% Note that headers need to be written as strings. + %% The payload content should be numbers. + case push_type_and_token(Type, SessionSettings) of + {background, Token} -> + Headers = #{ apns_push_type => <<"background">> + , apns_priority => <<"5">>}, + Aps = #{ <<"content-available">> => 1 }, + {http, Headers, Aps, Token}; binary -> - binary_notify(Alert, Custom, Type, VoipToken, SessionSettings, State); - http -> - http_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) + %% Token from the old system where we used only one token + %% both for voip and background updates. + case get_from_config(apns_force_http) of + true -> + Headers = #{ apns_push_type => <<"voip">> + , apns_priority => <<"5">>}, + Aps = #{}, + {http, Headers, Aps, VoipToken}; + false -> + {binary, VoipToken} + end; + voip -> + Headers = #{ apns_push_type => <<"voip">> + , apns_expiration => <<"0">> %% Deliver immediately or not at all. + , apns_priority => <<"10">>}, + Aps = #{}, + {http, Headers, Aps, VoipToken} end. -binary_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) -> - #{ options := Options - , apple_port := Port - , apple_host := Gateway} = pick_binary_config(SessionSettings, State), - DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), - Payload = jsx:encode( - #{ aps => - #{ nynja => - #{ model => Custom - , type => Type - , title => Alert - , dns => DNS - , version => <> - }}}), +push_type_and_token(<<"calling">>, _SessionSettings) -> + voip; +push_type_and_token(_Type, SessionSettings) -> + case get_data_from_feature(SessionSettings, ?FKPN_GENERIC_TOKEN) of + [] -> binary; + Token -> {background, Token} + end. + +binary_notify(Config, Payload, Token) -> + BinPayload = jsx:encode(Payload), Packet = << 0:8, 32:16/big, - (binary_to_integer(VoipToken, 16)):256/big, - (byte_size(Payload)):16/big, - Payload/binary >>, - binary_push(Gateway, Port, Packet, Options, 1), - ok. - -binary_push(Addr, Port, Payload, Options, Attempt) -> - ?LOG_INFO("Addr: ~p, Options: ~p, Attempt:~p", [Addr, Options, Attempt]), -%% NOTE set Duration = Attempt * 100 for tests + (binary_to_integer(Token, 16)):256/big, + (byte_size(BinPayload)):16/big, + BinPayload/binary >>, + binary_push(Config, Packet, 1). + +binary_push(Config, Packet, Attempt) -> + #{options:= Options, apple_port := Port, apple_host := Addr} = Config, + ?LOG_INFO("Binary apns push: Addr: ~p, Options: ~p, Attempt:~p", [Addr, Options, Attempt]), Duration = Attempt * 500, {Status, Socket} = ssl:connect(Addr, Port, Options, Duration), case Status of ok -> - ssl:send(Socket, Payload), + ssl:send(Socket, Packet), ssl:close(Socket), - ?LOG_INFO("Push sent", []); + ?LOG_INFO("Push sent", []), + ok; error -> if Attempt > 10 -> - ?LOG_INFO("Final error", []); + ?LOG_INFO("Final error", []), + ok; true -> timer:sleep(Duration), ?LOG_INFO("Error with socket opening. Reason:~p", [Socket]), - binary_push(Addr, Port, Payload, Options, Attempt + 1) + binary_push(Config, Packet, Attempt + 1) end end. - - -http_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) -> - Conn = pick_http_connection(SessionSettings, State), - DNS = get_data_from_feature(SessionSettings, ?FKPN_SERVER_DNS), - {Headers, Aps, Token} = token_headers_and_aps(Type, SessionSettings, VoipToken), - Aps1 = #{ aps => Aps - , nynja => #{ model => Custom - , type => Type - , title => Alert - , dns => DNS - %% TODO: Is this needed? - , version => <> - } - }, +http_notify(Conn, Payload, Headers, Token) -> Pid = maps:get(pid, Conn), - case apns:push_notification(Pid, Token, Aps1, Headers) of + #{config := #{ apple_host := Addr}} = Conn, + ?LOG_INFO("Http apns push: Addr: ~p", [Addr]), + case apns:push_notification(Pid, Token, Payload, Headers) of {200, _RespHeaders, _RespBody} -> ok; {410, RespHeaders, RespBody} -> @@ -153,47 +192,6 @@ http_notify(Alert, Custom, Type, VoipToken, SessionSettings, State) -> {error, not_delivered} end. -token_headers_and_aps(Type, SessionSettings, VoipToken) -> - %% Note that headers need to be written as strings. - %% The payload content should be numbers. - case push_type_and_token(Type, SessionSettings) of - {background, Token} -> - { #{ apns_push_type => <<"background">> - , apns_priority => <<"5">>} - , #{ <<"content-available">> => 1 - } - , Token - }; - legacy_voip -> - %% Token from the old system where we used only one token - %% both for voip and background updates. - %% This is not a real voip push. - { #{ apns_push_type => <<"voip">> - , apns_priority => <<"5">>} - , #{} - , VoipToken - }; - voip -> - { #{ apns_push_type => <<"voip">> - , apns_expiration => <<"0">> %% Deliver immediately or not at all. - , apns_priority => <<"10">>} - , #{} - , VoipToken - } - end. - -push_type_and_token(<<"calling">>, _SessionSettings) -> - voip; -push_type_and_token(_Type, SessionSettings) -> - case get_data_from_feature(SessionSettings, ?FKPN_GENERIC_TOKEN) of - [] -> legacy_voip; - Token -> {background, Token} - end. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - %%%=================================================================== %%% Initialisation of connections and configs @@ -206,7 +204,7 @@ push_type_and_token(_Type, SessionSettings) -> -define(DEV_CERTS, {"cert_dev.pem", "key_dev.pem"}). get_from_config(Key) -> - Env = application:get_env(roster, push_api, []), + {ok, Env} = application:get_env(roster, push_api), proplists:get_value(Key, Env). http_connection_configs() -> diff --git a/sys.config b/sys.config index d4f445bad..5a6283243 100644 --- a/sys.config +++ b/sys.config @@ -95,7 +95,7 @@ {app_credentials, "etc/certs/transcribe-dacb4306ab76.json"} ]}, {push_api,[ - {apns_push_type, binary}, %% 'binary' | 'http' + {apns_force_http, false}, {fcm_server_key,<<"AAAAAzb6_Zg:APA91bGN0jYv_4iqyk8IC4xUdPYXh0yPsTF9YYj_gd9oebRr_ZEoLuC5hCD9RfdqA3Y3AF_P_WbelqvzvgR3RsX_mHBLynV14Q6HakXAtrY_eWLK2xqamF2OC9uBXfKgxTFFqmyr1Kbw">>}, {apns_cert_dir, "apns_certificates"}, {apns_binary_port, 2195}, -- GitLab From 260ef2fb919e389ea21f4d96dafa45da8069422c Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Fri, 5 Jun 2020 08:23:00 +0200 Subject: [PATCH 8/9] Rename android push module --- apps/roster/src/api/push/{android.erl => roster_fcm_api.erl} | 4 ++-- apps/roster/src/protocol/roster_push.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/roster/src/api/push/{android.erl => roster_fcm_api.erl} (96%) diff --git a/apps/roster/src/api/push/android.erl b/apps/roster/src/api/push/roster_fcm_api.erl similarity index 96% rename from apps/roster/src/api/push/android.erl rename to apps/roster/src/api/push/roster_fcm_api.erl index db15d4c91..fb07f73a1 100644 --- a/apps/roster/src/api/push/android.erl +++ b/apps/roster/src/api/push/roster_fcm_api.erl @@ -1,4 +1,4 @@ --module(android). +-module(roster_fcm_api). -include_lib("kernel/include/logger.hrl"). -include("roster.hrl"). @@ -42,4 +42,4 @@ notify(_, MessageBody, DeviceId) -> test_push_notification() -> MessageBody = "Notify Liubov about this push", - notify(MessageBody, MessageBody, ?FCM_TEST_DEVICE_ID). \ No newline at end of file + notify(MessageBody, MessageBody, ?FCM_TEST_DEVICE_ID). diff --git a/apps/roster/src/protocol/roster_push.erl b/apps/roster/src/protocol/roster_push.erl index ac875a89f..053dfd63b 100644 --- a/apps/roster/src/protocol/roster_push.erl +++ b/apps/roster/src/protocol/roster_push.erl @@ -70,5 +70,5 @@ send_push_notification(ios, Push, Payload, PushAlert, PushType, AuthSettings, HS 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)))), - android:notify(PushAlert, AndroidPush, Push); + roster_fcm_api:notify(PushAlert, AndroidPush, Push); send_push_notification(_, _, _, _, _, _, _) -> skip. -- GitLab From e905ab17207400b24d494340d5f867df02df5a54 Mon Sep 17 00:00:00 2001 From: Tobias Lindahl Date: Fri, 5 Jun 2020 11:19:48 +0200 Subject: [PATCH 9/9] Better logging --- apps/roster/src/api/push/roster_apns_api.erl | 40 ++++++++++---------- apps/roster/src/protocol/roster_push.erl | 4 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/roster/src/api/push/roster_apns_api.erl b/apps/roster/src/api/push/roster_apns_api.erl index 9fa62025d..b7935fe57 100644 --- a/apps/roster/src/api/push/roster_apns_api.erl +++ b/apps/roster/src/api/push/roster_apns_api.erl @@ -150,45 +150,45 @@ binary_notify(Config, Payload, Token) -> (binary_to_integer(Token, 16)):256/big, (byte_size(BinPayload)):16/big, BinPayload/binary >>, - binary_push(Config, Packet, 1). + binary_push(Config, Packet, Token, 1). -binary_push(Config, Packet, Attempt) -> +binary_push(Config, Packet, Token, Attempt) -> #{options:= Options, apple_port := Port, apple_host := Addr} = Config, ?LOG_INFO("Binary apns push: Addr: ~p, Options: ~p, Attempt:~p", [Addr, Options, Attempt]), Duration = Attempt * 500, - {Status, Socket} = ssl:connect(Addr, Port, Options, Duration), - case Status of - ok -> + case ssl:connect(Addr, Port, Options, Duration) of + {ok, Socket} -> ssl:send(Socket, Packet), ssl:close(Socket), - ?LOG_INFO("Push sent", []), + ?LOG_INFO("Push sent. Token: ~P", [Token, 5]), ok; - error -> - if - Attempt > 10 -> - ?LOG_INFO("Final error", []), - ok; - true -> - timer:sleep(Duration), - ?LOG_INFO("Error with socket opening. Reason:~p", [Socket]), - binary_push(Config, Packet, Attempt + 1) - end + {error, Reason} when Attempt > 10 -> + ?LOG_INFO("Socket error, giving up: ~p, Token ~P", + [Reason, Token, 5]), + ok; + {error, Reason} -> + ?LOG_INFO("Socket error: ~p, Retrying on ~P", + [Reason, Token, 5]), + timer:sleep(Duration), + binary_push(Config, Packet, Token, Attempt + 1) end. http_notify(Conn, Payload, Headers, Token) -> Pid = maps:get(pid, Conn), #{config := #{ apple_host := Addr}} = Conn, - ?LOG_INFO("Http apns push: Addr: ~p", [Addr]), case apns:push_notification(Pid, Token, Payload, Headers) of {200, _RespHeaders, _RespBody} -> + ?LOG_INFO("APNS: Push done. Addr: ~p, Token: ~P", + [Addr, Token, 5]), ok; {410, RespHeaders, RespBody} -> %% Expired of invalid token. - ?LOG_INFO("APNS push error ~w:~p:~p", [410, RespHeaders, RespBody]), + ?LOG_INFO("APNS Invalid token. Addr: ~p Resp: ~p:~p Token: ~p", + [Addr, RespHeaders, RespBody, Token]), {error, bad_token}; {Status, RespHeaders, RespBody} -> - ?LOG_ERROR("APNS push error ~w:~p:~p", - [Status, RespHeaders, RespBody]), + ?LOG_ERROR("APNS push error Resp: ~w:~p:~p, Token: ~p", + [Status, RespHeaders, RespBody, Token]), {error, not_delivered} end. diff --git a/apps/roster/src/protocol/roster_push.erl b/apps/roster/src/protocol/roster_push.erl index 053dfd63b..5b675aca6 100644 --- a/apps/roster/src/protocol/roster_push.erl +++ b/apps/roster/src/protocol/roster_push.erl @@ -49,8 +49,8 @@ send_push_notification(#'Auth'{ os = OS case PushToken of [] -> skip; _ -> - ?LOG_INFO("~p:~p:~pPushAlert:~p", - [PhoneId, OS, binary:part(PushToken, 0, erlang:min(25, size(PushToken))), PushAlert]), + ?LOG_INFO("~p:~p:~PPushAlert:~p", + [PhoneId, OS, PushToken, 5, PushAlert]), send_push_notification(OS, PushToken, Payload, PushAlert, PushType, AuthSettings, HS) end. -- GitLab