Skip to content

Commit

Permalink
WHISTLE-788: support reseller limits and inbound authz tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
k-anderson authored and James Aimonetti committed May 18, 2012
1 parent ffc994b commit eb00fa2
Show file tree
Hide file tree
Showing 9 changed files with 553 additions and 210 deletions.
39 changes: 22 additions & 17 deletions ecallmgr/src/ecallmgr.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,25 @@
,timestamp=wh_util:current_tstamp()
}).

-record(channel, {uuid=undefined
,destination=undefined
,direction=undefined
,account_id=undefined
,authorizing_id=undefined
,authorizing_type=undefined
,owner_id=undefined
,resource_id=undefined
,presence_id=undefined
,billing_id=undefined
,bridge_id=undefined
,realm=undefined
,username=undefined
,import_moh=true
,node=undefined
,per_minute=false
,timestamp=wh_util:current_tstamp()
-record(channel, {uuid = '_'
,destination = '_'
,direction = '_'
,account_id = '_'
,account_billing = '_'
,authorizing_id = '_'
,authorizing_type = '_'
,owner_id = '_'
,resource_id = '_'
,presence_id = '_'
,billing_id = '_'
,bridge_id = '_'
,reseller_id = '_'
,reseller_billing = '_'
,realm = '_'
,username = '_'
,import_moh = '_'
,node = '_'
,timestamp = '_'
}).

-define(DEFAULT_DOMAIN, <<"whistle.2600hz.org">>).
Expand Down Expand Up @@ -71,6 +73,9 @@
%% When an event occurs, we include all prefixed vars in the API message
-define(CHANNEL_VAR_PREFIX, "ecallmgr_").

-define(GET_CCV(Key), <<"variable_", ?CHANNEL_VAR_PREFIX, Key/binary>>).
-define(SET_CCV(Key, Value), <<?CHANNEL_VAR_PREFIX, Key/binary, "=", Value/binary>>).

%% Call and Channel Vars that have a special prefix instead of the standard CHANNEL_VAR_PREFIX prefix
%% [{AMQP-Header, FS-var-name}]
%% so FS-var-name of "foo_var" would become "foo_var=foo_val" in the channel/call string
Expand Down
270 changes: 191 additions & 79 deletions ecallmgr/src/ecallmgr_authz.erl

Large diffs are not rendered by default.

175 changes: 108 additions & 67 deletions ecallmgr/src/ecallmgr_fs_nodes.erl

Large diffs are not rendered by default.

51 changes: 27 additions & 24 deletions ecallmgr/src/ecallmgr_fs_route.erl
Original file line number Diff line number Diff line change
Expand Up @@ -165,39 +165,22 @@ code_change(_OldVsn, State, _Extra) ->
%%% Internal functions
%%%===================================================================
-spec process_route_req/4 :: (atom(), ne_binary(), ne_binary(), proplist()) -> 'ok'.
process_route_req(Node, FSID, CallId, FSData) ->
process_route_req(Node, FSID, CallId, Props) ->
put(callid, CallId),
lager:debug("processing fetch request ~s (call ~s) from ~s", [FSID, CallId, Node]),

DefProp = [{<<"Msg-ID">>, FSID}
,{<<"Caller-ID-Name">>, props:get_value(<<"variable_effective_caller_id_name">>, FSData,
props:get_value(<<"Caller-Caller-ID-Name">>, FSData, <<"Unknown">>))}
,{<<"Caller-ID-Number">>, props:get_value(<<"variable_effective_caller_id_number">>, FSData,
props:get_value(<<"Caller-Caller-ID-Number">>, FSData, <<"0000000000">>))}
,{<<"To">>, ecallmgr_util:get_sip_to(FSData)}
,{<<"From">>, ecallmgr_util:get_sip_from(FSData)}
,{<<"Request">>, ecallmgr_util:get_sip_request(FSData)}
,{<<"From-Network-Addr">>,props:get_value(<<"Caller-Network-Addr">>, FSData)}
,{<<"Call-ID">>, CallId}
,{<<"Custom-Channel-Vars">>, wh_json:from_list(ecallmgr_util:custom_channel_vars(FSData))}
| wh_api:default_headers(?APP_NAME, ?APP_VERSION)],
route(Node, FSID, CallId, DefProp).

-spec route/4 :: (atom(), ne_binary(), ne_binary(), proplist()) -> 'ok'.
route(Node, FSID, CallId, DefProp) ->
lager:debug("starting route request from node ~s", [Node]),
ReqResp = wh_amqp_worker:call(?ECALLMGR_AMQP_POOL
,DefProp
,route_req(CallId, FSID, Props)
,fun wapi_route:publish_req/1
,fun wapi_route:is_actionable_resp/1),
case ReqResp of
{error, _R} -> lager:debug("did not receive route response: ~p", [_R]);
{ok, RespJObj} ->
true = wapi_route:resp_v(RespJObj),
RouteCCV = wh_json:get_value(<<"Custom-Channel-Vars">>, RespJObj, wh_json:new()),
case wh_cache:wait_for_key_local(?ECALLMGR_UTIL_CACHE, ?AUTHZ_RESPONSE_KEY(CallId)) of
{ok, true} -> reply_affirmative(Node, FSID, CallId, RespJObj, RouteCCV);
{ok, false} -> reply_forbidden(Node, FSID)
AuthzEnabled = wh_util:is_true(ecallmgr_config:get(<<"authz_enabled">>, false)),
case AuthzEnabled andalso wh_cache:wait_for_key_local(?ECALLMGR_UTIL_CACHE, ?AUTHZ_RESPONSE_KEY(CallId)) of
{ok, false} -> reply_forbidden(Node, FSID);
_Else -> reply_affirmative(Node, FSID, CallId, RespJObj, RouteCCV)
end
end.

Expand All @@ -210,7 +193,9 @@ reply_forbidden(Node, FSID) ->
]),
lager:debug("sending XML to ~s: ~s", [Node, XML]),
case freeswitch:fetch_reply(Node, FSID, iolist_to_binary(XML)) of
ok -> lager:debug("node ~s accepted our route unauthz", [Node]);
ok ->
_ = ecallmgr_util:fs_log(Node, "whistle node ~s won control arbitration with forbidden reply", [node()]),
lager:debug("node ~s accepted our route unauthz", [Node]);
{error, Reason} -> lager:debug("node ~s rejected our route unauthz, ~p", [Node, Reason]);
timeout -> lager:debug("received no reply from node ~s, timeout", [Node])
end.
Expand All @@ -223,6 +208,7 @@ reply_affirmative(Node, FSID, CallId, RespJObj, CCVs) ->
case freeswitch:fetch_reply(Node, FSID, iolist_to_binary(XML)) of
ok ->
lager:debug("node ~s accepted our route (authzed), starting control and events", [Node]),
_ = ecallmgr_util:fs_log(Node, "whistle node ~s won control arbitration with affimative reply", [node()]),
start_control_and_events(Node, CallId, ServerQ, CCVs);
{error, Reason} -> lager:debug("node ~s rejected our route response, ~p", [Node, Reason]);
timeout -> lager:debug("received no reply from node ~s, timeout", [Node])
Expand Down Expand Up @@ -250,3 +236,20 @@ start_control_and_events(Node, CallId, SendTo, CCVs) ->
send_control_queue(SendTo, CtlProp) ->
lager:debug("sending route_win to ~s", [SendTo]),
wapi_route:publish_win(SendTo, CtlProp).

-spec route_req/3 :: (ne_binary(), ne_binary(), proplist()) -> proplist().
route_req(CallId, FSID, Props) ->
[{<<"Msg-ID">>, FSID}
,{<<"Caller-ID-Name">>, props:get_value(<<"variable_effective_caller_id_name">>, Props,
props:get_value(<<"Caller-Caller-ID-Name">>, Props, <<"Unknown">>))}
,{<<"Caller-ID-Number">>, props:get_value(<<"variable_effective_caller_id_number">>, Props,
props:get_value(<<"Caller-Caller-ID-Number">>, Props, <<"0000000000">>))}
,{<<"To">>, ecallmgr_util:get_sip_to(Props)}
,{<<"From">>, ecallmgr_util:get_sip_from(Props)}
,{<<"Request">>, ecallmgr_util:get_sip_request(Props)}
,{<<"From-Network-Addr">>,props:get_value(<<"Caller-Network-Addr">>, Props)}
,{<<"Call-ID">>, CallId}
,{<<"Custom-Channel-Vars">>, wh_json:from_list(ecallmgr_util:custom_channel_vars(Props))}
| wh_api:default_headers(?APP_NAME, ?APP_VERSION)
].

60 changes: 46 additions & 14 deletions ecallmgr/src/ecallmgr_util.erl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

%%%-------------------------------------------------------------------
%%% @copyright (C) 2010-2012, VoIP INC
%%% @doc
Expand Down Expand Up @@ -34,34 +35,65 @@
%%--------------------------------------------------------------------
-type send_cmd_ret() :: fs_sendmsg_ret() | fs_api_ret().
-spec send_cmd/4 :: (atom(), ne_binary(), ne_binary() | string(), ne_binary() | string()) -> send_cmd_ret().
send_cmd(Node, UUID, <<"hangup">>, _) ->
lager:debug("terminate call on node ~s", [Node]),
_ = ecallmgr_util:fs_log(Node, "whistle terminating call", []),
freeswitch:api(Node, uuid_kill, wh_util:to_list(UUID));
send_cmd(Node, _UUID, <<"record_call">>, Cmd) ->
lager:debug("execute on node ~s: uuid_record(~s)", [Node, Cmd]),
Ret = freeswitch:api(Node, uuid_record, wh_util:to_list(Cmd)),
lager:debug("executing uuid_record returned ~p", [Ret]),
Ret;
send_cmd(Node, UUID, <<"playstop">>, Args) ->
lager:debug("execute on node ~s: uuid_break(~s)", [Node, UUID]),
freeswitch:api(Node, uuid_break, wh_util:to_list(Args));
send_cmd(Node, UUID, <<"xferext">>, Dialplan) ->
send_cmd(Node, UUID, App, Args) when not is_list(App) ->
send_cmd(Node, UUID, wh_util:to_list(App), Args);
send_cmd(Node, UUID, "xferext", Dialplan) ->
XferExt = [begin
_ = ecallmgr_util:fs_log(Node, "whistle queuing command in 'xferext' extension: ~s", [V]),
lager:debug("building xferext on node ~s: ~s", [Node, V]),
{wh_util:to_list(K), wh_util:to_list(V)}
end || {K, V} <- Dialplan],
ok = freeswitch:sendmsg(Node, UUID, [{"call-command", "xferext"} | XferExt]),
ecallmgr_util:fs_log(Node, "whistle transfered call to 'xferext' extension", []);
send_cmd(Node, UUID, App, Args) when not is_list(Args) ->
send_cmd(Node, UUID, App, wh_util:to_list(Args));
send_cmd(Node, UUID, "hangup", _) ->
lager:debug("terminate call on node ~s", [Node]),
_ = ecallmgr_util:fs_log(Node, "whistle terminating call", []),
freeswitch:api(Node, uuid_kill, wh_util:to_list(UUID));
send_cmd(Node, _UUID, "record_call", Args) ->
lager:debug("execute on node ~s: uuid_record(~s)", [Node, Args]),
Ret = freeswitch:api(Node, uuid_record, Args),
lager:debug("executing uuid_record returned ~p", [Ret]),
Ret;
send_cmd(Node, UUID, "playstop", Args) ->
lager:debug("execute on node ~s: uuid_break(~s)", [Node, UUID]),
freeswitch:api(Node, uuid_break, Args);
send_cmd(Node, UUID, AppName, Args) ->
lager:debug("execute on node ~s: ~s(~s)", [Node, AppName, Args]),
_ = ecallmgr_util:fs_log(Node, "whistle executing ~s ~s", [AppName, Args]),
case AppName of
"set" -> maybe_update_channel_cache(Args, UUID);
"export" -> maybe_update_channel_cache(Args, UUID);
_Else -> ok
end,
freeswitch:sendmsg(Node, UUID, [{"call-command", "execute"}
,{"execute-app-name", wh_util:to_list(AppName)}
,{"execute-app-name", AppName}
,{"execute-app-arg", wh_util:to_list(Args)}
]).

-spec maybe_update_channel_cache/2 :: (string(), ne_binary()) -> 'ok'.
maybe_update_channel_cache("ecallmgr_Account-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_account_id(UUID, Value);
maybe_update_channel_cache("ecallmgr_Account-Billing=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_account_billing(UUID, Value);
maybe_update_channel_cache("ecallmgr_Reseller-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_reseller_id(UUID, Value);
maybe_update_channel_cache("ecallmgr_Reseller-Billing=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_reseller_billing(UUID, Value);
maybe_update_channel_cache("ecallmgr_Authorizing-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_authorizing_id(UUID, Value);
maybe_update_channel_cache("ecallmgr_Resource-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_resource_id(UUID, Value);
maybe_update_channel_cache("ecallmgr_Authorizing-Type=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_authorizing_type(UUID, Value);
maybe_update_channel_cache("ecallmgr_Owner-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_owner_id(UUID, Value);
maybe_update_channel_cache("ecallmgr_Presence-ID=" ++ Value, UUID) ->
ecallmgr_fs_nodes:channel_set_presence_id(UUID, Value);
maybe_update_channel_cache(_, _) ->
ok.

-spec get_expires/1 :: (proplist()) -> number().
get_expires(Props) ->
Expiry = wh_util:to_integer(props:get_value(<<"Expires">>, Props, 300)),
Expand Down
104 changes: 96 additions & 8 deletions lib/whistle-1.0.0/src/api/wapi_authz.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
%%% Handles authorization requests, responses, queue bindings
%%% @end
%%% @contributors
%%% James Aimonetti
%%%-------------------------------------------------------------------
-module(wapi_authz).

-compile({no_auto_import, [error/1]}).

-export([req/1, req_v/1
,resp/1, resp_v/1
,identify_req/1, identify_req_v/1
,identify_resp/1, identify_resp_v/1
,win/1, win_v/1
,update/1, update_v/1
,bind_q/2, unbind_q/2
,publish_req/1, publish_req/2
,publish_resp/2, publish_resp/3
,publish_identify_req/1, publish_identify_req/2
,publish_identify_resp/2, publish_identify_resp/3
,publish_win/2, publish_win/3
,publish_update/1, publish_update/2
]).
Expand All @@ -27,6 +28,7 @@
-define(KEY_AUTHZ_REQ, <<"authz.req">>).
-define(KEY_AUTHZ_UPDATE, <<"authz.update">>).
-define(KEY_AUTHZ_CALL_COMMAND, <<"authz.call_command">>).
-define(KEY_AUTHZ_IDENT_REQ, <<"authz.ident_req">>).

%% Authorization Requests
-define(AUTHZ_REQ_HEADERS, [<<"Msg-ID">>, <<"To">>, <<"From">>, <<"Call-ID">>
Expand Down Expand Up @@ -58,6 +60,35 @@
]).
-define(AUTHZ_RESP_TYPES, [{<<"Custom-Channel-Vars">>, ?IS_JSON_OBJECT}]).

%% Authorization Identify Requests
-define(AUTHZ_IDENT_REQ_HEADERS, [<<"Msg-ID">>, <<"To">>, <<"From">>, <<"Request">>, <<"Call-ID">>
,<<"Caller-ID-Name">>, <<"Caller-ID-Number">>
]).
-define(OPTIONAL_AUTHZ_IDENT_REQ_HEADERS, [<<"Custom-Channel-Vars">>]).
-define(AUTHZ_IDENT_REQ_VALUES, [{<<"Event-Category">>, ?EVENT_CATEGORY}
,{<<"Event-Name">>, <<"identify_req">>}
]).
-define(AUTHZ_IDENT_REQ_TYPES, [{<<"Msg-ID">>, fun is_binary/1}
,{<<"To">>, fun is_binary/1}
,{<<"From">>, fun is_binary/1}
,{<<"Call-ID">>, fun is_binary/1}
,{<<"Caller-ID-Name">>, fun is_binary/1}
,{<<"Caller-ID-Number">>, fun is_binary/1}
,{<<"Custom-Channel-Vars">>, ?IS_JSON_OBJECT}
]).

%% Authorization Identify Responses
-define(AUTHZ_IDENT_RESP_HEADERS, [<<"Msg-ID">>, <<"Call-ID">>, <<"Account-ID">>]).
-define(OPTIONAL_AUTHZ_IDENT_RESP_HEADERS, [<<"Reseller-ID">>]).
-define(AUTHZ_IDENT_RESP_VALUES, [{<<"Event-Category">>, ?EVENT_CATEGORY}
,{<<"Event-Name">>, <<"identify_resp">>}
]).
-define(AUTHZ_IDENT_RESP_TYPES, [{<<"Msg-ID">>, fun is_binary/1}
,{<<"Call-ID">>, fun is_binary/1}
,{<<"Account-ID">>, fun is_binary/1}
,{<<"Reseller-ID">>, fun is_binary/1}
]).

%% Authorization Wins
-define(AUTHZ_WIN_HEADERS, [<<"Call-ID">>]).
-define(OPTIONAL_AUTHZ_WIN_HEADERS, [<<"Switch-Hostname">>]).
Expand Down Expand Up @@ -143,11 +174,46 @@ resp_v(Prop) when is_list(Prop) ->
resp_v(JObj) ->
resp_v(wh_json:to_proplist(JObj)).

-spec is_authorized/1 :: (api_terms()) -> boolean().
is_authorized(Prop) when is_list(Prop) ->
wh_util:is_true(props:get_value(<<"Is-Authorized">>, Prop, false));
is_authorized(JObj) ->
wh_json:is_true(<<"Is-Authorized">>, JObj, false).
%%--------------------------------------------------------------------
%% @doc Authorization Request - see wiki
%% Takes proplist, creates JSON iolist or error
%% @end
%%--------------------------------------------------------------------
-spec identify_req/1 :: (api_terms()) -> {'ok', iolist()} | {'error', string()}.
identify_req(Prop) when is_list(Prop) ->
case identify_req_v(Prop) of
true -> wh_api:build_message(Prop, ?AUTHZ_IDENT_REQ_HEADERS, ?OPTIONAL_AUTHZ_IDENT_REQ_HEADERS);
false -> {error, "Proplist failed validation for authz_identify_req"}
end;
identify_req(JObj) ->
identify_req(wh_json:to_proplist(JObj)).

-spec identify_req_v/1 :: (api_terms()) -> boolean().
identify_req_v(Prop) when is_list(Prop) ->
wh_api:validate(Prop, ?AUTHZ_IDENT_REQ_HEADERS, ?AUTHZ_IDENT_REQ_VALUES, ?AUTHZ_IDENT_REQ_TYPES);
identify_req_v(JObj) ->
identify_req_v(wh_json:to_proplist(JObj)).

%%--------------------------------------------------------------------
%% @doc Authorization Response - see wiki
%% Takes proplist, creates JSON iolist or error
%% @end
%%--------------------------------------------------------------------
-spec identify_resp/1 :: (api_terms()) -> {'ok', iolist()} | {'error', string()}.
identify_resp(Prop) when is_list(Prop) ->
case identify_resp_v(Prop) of
true -> wh_api:build_message(Prop, ?AUTHZ_IDENT_RESP_HEADERS, ?OPTIONAL_AUTHZ_IDENT_RESP_HEADERS);
false -> {error, "Proplist failed validation for authz_identify_resp"}
end;
identify_resp(JObj) ->
identify_resp(wh_json:to_proplist(JObj)).

-spec identify_resp_v/1 :: (api_terms()) -> boolean().
identify_resp_v(Prop) when is_list(Prop) ->
wh_api:validate(Prop, ?AUTHZ_IDENT_RESP_HEADERS, ?AUTHZ_IDENT_RESP_VALUES, ?AUTHZ_IDENT_RESP_TYPES);
identify_resp_v(JObj) ->
identify_resp_v(wh_json:to_proplist(JObj)).


%%--------------------------------------------------------------------
%% @doc Authorization Win - see wiki
Expand Down Expand Up @@ -190,6 +256,9 @@ bind_to_q(Q, [updates|T]) ->
bind_to_q(Q, [call_command|T]) ->
ok = amqp_util:bind_q_to_callmgr(Q, ?KEY_AUTHZ_CALL_COMMAND),
bind_to_q(Q, T);
bind_to_q(Q, [identify|T]) ->
ok = amqp_util:bind_q_to_callmgr(Q, ?KEY_AUTHZ_IDENT_REQ),
bind_to_q(Q, T);
bind_to_q(_Q, []) ->
ok.

Expand All @@ -208,6 +277,9 @@ unbind_q_from(Q, [updates|T]) ->
unbind_q_from(Q, [call_command|T]) ->
ok = amqp_util:unbind_q_from_callmgr(Q, ?KEY_AUTHZ_CALL_COMMAND),
unbind_q_from(Q, T);
unbind_q_from(Q, [identify|T]) ->
ok = amqp_util:unbind_q_from_callmgr(Q, ?KEY_AUTHZ_IDENT_REQ),
unbind_q_from(Q, T);
unbind_q_from(_Q, []) ->
ok.

Expand Down Expand Up @@ -239,6 +311,22 @@ publish_resp(Queue, Resp, ContentType) ->
{ok, Payload} = wh_api:prepare_api_payload(Resp, ?AUTHZ_RESP_VALUES, fun ?MODULE:resp/1),
amqp_util:targeted_publish(Queue, Payload, ContentType).

-spec publish_identify_req/1 :: (api_terms()) -> 'ok'.
-spec publish_identify_req/2 :: (api_terms(), ne_binary()) -> 'ok'.
publish_identify_req(JObj) ->
publish_identify_req(JObj, ?DEFAULT_CONTENT_TYPE).
publish_identify_req(Req, ContentType) ->
{ok, Payload} = wh_api:prepare_api_payload(Req, ?AUTHZ_IDENT_REQ_VALUES, fun ?MODULE:identify_req/1),
amqp_util:callmgr_publish(Payload, ContentType, ?KEY_AUTHZ_IDENT_REQ).

-spec publish_identify_resp/2 :: (ne_binary(), api_terms()) -> 'ok'.
-spec publish_identify_resp/3 :: (ne_binary(), api_terms(), ne_binary()) -> 'ok'.
publish_identify_resp(Queue, JObj) ->
publish_identify_resp(Queue, JObj, ?DEFAULT_CONTENT_TYPE).
publish_identify_resp(Queue, Resp, ContentType) ->
{ok, Payload} = wh_api:prepare_api_payload(Resp, ?AUTHZ_IDENT_RESP_VALUES, fun ?MODULE:identify_resp/1),
amqp_util:targeted_publish(Queue, Payload, ContentType).

-spec publish_win/2 :: (ne_binary(), api_terms()) -> 'ok'.
-spec publish_win/3 :: (ne_binary(), api_terms(), ne_binary()) -> 'ok'.
publish_win(Queue, JObj) ->
Expand Down
2 changes: 1 addition & 1 deletion whistle_apps/apps/crossbar/src/modules/cb_clicktocall.erl
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ originate_call(Contact, JObj, AccountId) ->
,{<<"Export-Custom-Channel-Vars">>, [<<"Account-ID">>, <<"Retain-CID">>, <<"Authorizing-ID">>, <<"Authorizing-Type">>]}
| wh_api:default_headers(Amqp, <<"resource">>, <<"originate_req">>, ?APP_NAME, ?APP_VERSION)
],
amqp_mgr:register_return_handler(),
wh_amqp_mgr:register_return_handler(),
wapi_resource:publish_originate_req(Request),
lager:debug("published click to call request ~s", [MsgId]),
wait_for_originate(MsgId).
Expand Down
Loading

0 comments on commit eb00fa2

Please sign in to comment.