diff options
-rw-r--r-- | components/dlink_sms/LICENSE | 354 | ||||
-rw-r--r-- | components/dlink_sms/Makefile | 37 | ||||
-rw-r--r-- | components/dlink_sms/README.md | 80 | ||||
-rw-r--r-- | components/dlink_sms/src/dlink_sms.app.src | 23 | ||||
-rw-r--r-- | components/dlink_sms/src/dlink_sms_app.erl | 39 | ||||
-rw-r--r-- | components/dlink_sms/src/dlink_sms_rpc.erl | 773 | ||||
-rw-r--r-- | components/dlink_sms/src/dlink_sms_sup.erl | 39 | ||||
-rw-r--r-- | components/dlink_sms/src/sms_connection.erl | 270 | ||||
-rw-r--r-- | components/dlink_sms/src/sms_connection_manager.erl | 259 | ||||
-rw-r--r-- | components/rvi_common/src/rvi_common.erl | 11 | ||||
-rw-r--r-- | deps/gsms/Makefile | 15 | ||||
-rw-r--r-- | deps/gsms/ebin/.gitignore | 2 | ||||
-rw-r--r-- | deps/gsms/include/gsms.hrl | 2 | ||||
-rw-r--r-- | deps/gsms/rebar.config | 1 | ||||
-rw-r--r-- | deps/gsms/src/gsms_lib.erl | 25 | ||||
-rw-r--r-- | deps/gsms/src/gsms_plivo.erl | 373 | ||||
-rw-r--r-- | deps/gsms/src/gsms_plivo_sim.erl | 353 | ||||
-rw-r--r-- | deps/gsms/src/gsms_router.erl | 136 | ||||
-rw-r--r-- | deps/gsms/src/gsms_session.erl | 123 | ||||
-rw-r--r-- | priv/config/rvi_common.config | 3 | ||||
-rw-r--r-- | rebar.config | 3 |
21 files changed, 2854 insertions, 67 deletions
diff --git a/components/dlink_sms/LICENSE b/components/dlink_sms/LICENSE new file mode 100644 index 0000000..c33dcc7 --- /dev/null +++ b/components/dlink_sms/LICENSE @@ -0,0 +1,354 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + diff --git a/components/dlink_sms/Makefile b/components/dlink_sms/Makefile new file mode 100644 index 0000000..a67e511 --- /dev/null +++ b/components/dlink_sms/Makefile @@ -0,0 +1,37 @@ +.PHONY: all deps compile setup clean doc + + +SETUP_GEN=$(shell ./find_setup_gen.sh) + +all: deps compile + +deps: + rebar get-deps + +compile: + rebar compile + +recomp: + rebar compile skip_deps=true + +setup: + ERL_LIBS=$(PWD)/deps:$(ERL_LIBS):$(PWD) \ + $(SETUP_GEN) $(NAME) priv/setup.config setup + +target: + ERL_LIBS=$(PWD)/deps:$(ERL_LIBS) \ + $(SETUP_GEN) $(NAME) priv/setup.config setup -pz $(PWD)/ebin \ + -target rel -vsn 0.1 + +run: setup + erl -boot setup/start -config setup/sys + +doc: + REBAR_DOC=1 rebar skip_deps=true get-deps doc + +clean: + rebar clean + + + + diff --git a/components/dlink_sms/README.md b/components/dlink_sms/README.md new file mode 100644 index 0000000..cc131c5 --- /dev/null +++ b/components/dlink_sms/README.md @@ -0,0 +1,80 @@ +# Exosense Device - Jaguar Land Rover Tizen IVI HVAC control demo + +# Building the Exosense JLR demo. + +Follow the instructions under: + + `https://github.com/Feuerlabs/exosense_specs/blob/master/doc/exosense_demo_tutorial.pdf` + +Replace meta-exodemo with meta-jlrdemo (the Yocto build layerfor this +demo). The meta-sbc6845 layer, mentioned in the tutorial, will not be +needed. + +## Setting up configuration files +After setting up the basic environment, as described in the tutorial, +init the build environment with this command: + + `. oe-init-build-env ../build` + +Copy `meta-jlrdemo/build_conf/*.conf` into the `conf` subdirectory of +the build directory you are currently in. + + cp ../meta-jlrdemo/build_conf/*.conf conf + +## Build the RPM +Since the demo is installed on Tizen, we will not create a complete +image, but rather a set of RPMs that can be installed on the standard +demo. + +Build the RPMs with: + + + bitbake erlang-jlrdemo + +The rpms will be deposited in + + build/tmp/deploy/rpm/i586 + +# Installing the Exosense JLR demo RPMs on the target system + +All rpms to be copied over from the directory above to the target system are listed in + + https://github.com/Feuerlabs/jlrdemo/blob/master/tizen_rpm_list.txt + +Once copied, install them all using a single `rpm -i` command + + +# Setting up automatic launch during boot + +There is a start script for the Exosense JLR demo installed on the target under: + + /usr/lib/erlang/jlrdemo-???/priv/jlrdemo_ctl.sh + +This script also installs the pcan driver kernel module (unless already loaded). + +Copy this script to /usr/sbin + + /usr/lib/erlang/jlrdemo-*/priv/jlrdemo_ctl.sh /usr/sbin + +Edit the uxlaunch systemd service in file: + + /etc/systemd/system/display-manager.service + +Edit the ExecStart= line so that it looks like this: + + ExecStart=/bin/sh /usr/sbin/tizenctl.sh start + +**Note** The /usr/sbin/tizenctl.sh, which will start the dashboard UI, + and the `/usr/sbin/jlrdemo.sh` script is not provided by the + Exosense RPMs. Please see the Tizen documentation for details on + where to source this. + +# Upgrading the Exosense JLR demo + +Be sure to remove the old package using `rpm -e` before installing the new version. +Also be sure to execute the following command to wipe any old setup data. + + rm -rf /root/setup + + + diff --git a/components/dlink_sms/src/dlink_sms.app.src b/components/dlink_sms/src/dlink_sms.app.src new file mode 100644 index 0000000..2d1d8ec --- /dev/null +++ b/components/dlink_sms/src/dlink_sms.app.src @@ -0,0 +1,23 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% + + +%% -*- erlang -*- +{application, dlink_sms, + [ + {description, ""}, + {vsn, "0.1"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + rvi_common + ]}, + {mod, { dlink_sms_app, []}}, + {start_phases, [{json_rpc, []}, {connection_manager, []}]} + ]}. diff --git a/components/dlink_sms/src/dlink_sms_app.erl b/components/dlink_sms/src/dlink_sms_app.erl new file mode 100644 index 0000000..309cf0e --- /dev/null +++ b/components/dlink_sms/src/dlink_sms_app.erl @@ -0,0 +1,39 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% + + +-module(dlink_sms_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, + start_phase/3, + stop/1]). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + dlink_sms_sup:start_link(). + +start_phase(init, _, _) -> + dlink_sms_rpc:init_rvi_component(); + +start_phase(json_rpc, _, _) -> + dlink_sms_rpc:start_json_server(), + ok; + +start_phase(connection_manager, _, _) -> + dlink_sms_rpc:start_connection_manager(), + ok. + + +stop(_State) -> + ok. diff --git a/components/dlink_sms/src/dlink_sms_rpc.erl b/components/dlink_sms/src/dlink_sms_rpc.erl new file mode 100644 index 0000000..78630ae --- /dev/null +++ b/components/dlink_sms/src/dlink_sms_rpc.erl @@ -0,0 +1,773 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% + + +-module(dlink_sms_rpc). +-behavior(gen_server). + +-export([handle_rpc/2]). +-export([handle_notification/2]). +-export([handle_sms/4]). +-export([handle_sms/5]). + +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-export([start_json_server/0]). +-export([start_connection_manager/0]). + +%% Invoked by service discovery +%% FIXME: Should be rvi_service_discovery behavior +-export([service_available/3, + service_unavailable/3]). + +-export([setup_data_link/3, + disconnect_data_link/2, + send_data/5]). + + +-include_lib("lager/include/log.hrl"). +-include_lib("rvi_common/include/rvi_common.hrl"). +-include_lib("rvi_common/include/rvi_dlink.hrl"). +-include_lib("gsms/include/gsms.hrl"). + +-define(PERSISTENT_CONNECTIONS, persistent_connections). +-define(SERVER_OPTS, server_opts). +-define(DEFAULT_SMS_PORT, 0). +-define(DEFAULT_RECONNECT_INTERVAL, 5000). +-define(DEFAULT_SMS_ADDRESS, ""). +-define(DEFAULT_PING_INTERVAL, 300000). %% Five minutes +-define(SERVER, ?MODULE). +-define(DLINK_SMS_VERSION, "1.0"). + +-define(CONNECTION_TABLE, rvi_dlink_sms_connections). +-define(SERVICE_TABLE, rvi_dlink_sms_services). + +%% Multiple registrations of the same service, each with a different connection, +%% is possible. +-record(service_entry, { + service = [], %% Name of service + connections = undefined %% PID of connection that can reach this service + }). + +-record(connection_entry, { + connection = undefined, %% PID of connection that has a set of services. + services = [] %% List of service names available through this connection + }). + +-record(st, { + cs = #component_spec{} + }). + + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +init([]) -> + ?info("dlink_sms:init(): Called"), + gsms:subscribe([{reg_exp, "RVI:{.*\"cmd\":[[:space:]]*\"au\""}]), + %% Dig out the bert rpc server setup + + ets:new(?SERVICE_TABLE, [ set, public, named_table, + { keypos, #service_entry.service }]), + + ets:new(?CONNECTION_TABLE, [ set, public, named_table, + { keypos, #connection_entry.connection }]), + + CS = rvi_common:get_component_specification(), + service_discovery_rpc:subscribe(CS, ?MODULE), + + {ok, #st { + cs = CS + } + }. + +start_json_server() -> + rvi_common:start_json_rpc_server(data_link, ?MODULE, dlink_sms_sup). + + +start_connection_manager() -> + CompSpec = rvi_common:get_component_specification(), + ?info("dlink_sms:init_rvi_component(~p): Starting listener.", [self()]), + sms_connection_manager:start_link(), + ?info("dlink_sms:init_rvi_component(): Setting up persistent connections."), + {ok, PersistentConnections } = + rvi_common:get_module_config(data_link, + ?MODULE, + ?PERSISTENT_CONNECTIONS, + [], + CompSpec), + setup_persistent_connections_(PersistentConnections, CompSpec), + ok. + +setup_persistent_connections_([], _CompSpec) -> + ok; +setup_persistent_connections_([NetworkAddress|T], CompSpec) -> + ?debug("~p: Will persistently connect connect : ~p", + [self(), NetworkAddress]), + Addr = case string:tokens(NetworkAddress, ":") of + [A] -> A; + [A, _Port] -> A + end, + connect_and_retry_remote(Addr, CompSpec), + setup_persistent_connections_(T, CompSpec), + ok. + +service_available(CompSpec, SvcName, DataLinkModule) -> + rvi_common:notification(data_link, ?MODULE, + service_available, + [{ service, SvcName }, + { data_link_module, DataLinkModule }], + CompSpec). + +service_unavailable(CompSpec, SvcName, DataLinkModule) -> + rvi_common:notification(data_link, ?MODULE, + service_unavailable, + [{ service, SvcName }, + { data_link_module, DataLinkModule }], + CompSpec). + + +setup_data_link(CompSpec, Service, Opts) -> + rvi_common:request(data_link, ?MODULE, setup_data_link, + [ { service, Service }, + { opts, Opts }], + [status, timeout], CompSpec). + +disconnect_data_link(CompSpec, NetworkAddress) -> + rvi_common:request(data_link, ?MODULE, disconnect_data_link, + [ {network_address, NetworkAddress} ], + [status], CompSpec). + + +send_data(CompSpec, ProtoMod, Service, DataLinkOpts, Data) -> + rvi_common:request(data_link, ?MODULE, send_data, + [ { proto_mod, ProtoMod }, + { service, Service }, + { data, Data }, + { opts, DataLinkOpts } + ], + [status], CompSpec). + + +%% End of behavior + +%% +%% Connect to a remote RVI node. +%% +connect_remote(Addr, CompSpec) -> + ?info("connect_remote(~p)~n", [Addr]), + case sms_connection_manager:find_connection_by_address(Addr) of + { ok, _Pid } -> + already_connected; + + not_found -> + %% Setup a new outbound connection + ?info("dlink_sms:connect_remote(): Connecting ~p", + [Addr]), + + %% We don't actually connect, since SMS is not session-based + %% Set up a genserver around the new connection. + {ok, Pid } = setup_connection(Addr, CompSpec), + %% Send authorize + LocalAddr = rvi_common:node_msisdn(), + sms_connection:send_auth( + Pid, + term_to_json( + {struct, [{ ?DLINK_ARG_TRANSACTION_ID, 1 }, + { ?DLINK_ARG_CMD, ?DLINK_CMD_AUTHORIZE }, + { ?DLINK_ARG_ADDRESS, LocalAddr }, + { ?DLINK_ARG_VERSION, ?DLINK_SMS_VERSION }, + { ?DLINK_ARG_CERTIFICATES, + {array, get_certificates(CompSpec)} }, + { ?DLINK_ARG_SIGNATURE, get_authorize_jwt(CompSpec) } + ]})), + ok; + {error, Err } -> + ?info("dlink_sms:connect_remote(): Failed ~p: ~p", + [Addr, Err]), + not_available + end. + +setup_connection(Addr, CompSpec) -> + setup_connection(Addr, [], CompSpec). + +setup_connection(Addr, Msgs, CompSpec) -> + sms_connection:setup(Addr, Msgs, ?MODULE, handle_sms, [CompSpec]). + +connect_and_retry_remote(Addr, CompSpec) -> + ?info("dlink_sms:connect_and_retry_remote(): ~p", + [Addr]), + + case connect_remote(Addr, CompSpec) of + ok -> ok; + + Err -> %% Failed to connect. Sleep and try again + ?notice("dlink_sms:connect_and_retry_remote(~p): Failed: ~p", + [Addr, Err]), + + ?notice("dlink_sms:connect_and_retry_remote(~p): Will try again in ~p sec", + [Addr, ?DEFAULT_RECONNECT_INTERVAL]), + + setup_reconnect_timer(?DEFAULT_RECONNECT_INTERVAL, Addr, CompSpec), + + not_available + end. + + +announce_local_service_(_CompSpec, [], _Service, _Availability) -> + ok; + +announce_local_service_(CompSpec, + [ConnPid | T], + Service, Availability) -> + + [ ok, JWT ] = authorize_rpc:sign_message( + CompSpec, availability_msg(Availability, [Service])), + Res = sms_connection:send( + ConnPid, + term_to_json( + {struct, + [ { ?DLINK_ARG_TRANSACTION_ID, 1 }, + { ?DLINK_ARG_CMD, ?DLINK_CMD_SERVICE_ANNOUNCE }, + { ?DLINK_ARG_SIGNATURE, JWT } + ]})), + + ?debug("dlink_sms:announce_local_service(~p: ~p) -> ~p Res: ~p", + [ Availability, Service, ConnPid, Res]), + + %% Move on to next connection. + announce_local_service_(CompSpec, + T, + Service, Availability). + +announce_local_service_(CompSpec, Service, Availability) -> + announce_local_service_(CompSpec, + get_connections(), + Service, Availability). + +%% We lost the socket connection. +%% Unregister all services that were routed to the remote end that just died. +handle_sms(FromPid, Addr, closed, [CompSpec]) -> + ?info("dlink_sms:closed(): Address: ~p", [Addr]), + + %% Get all service records associated with the given connection + LostSvcNameList = get_services_by_connection(FromPid), + + delete_connection(FromPid), + + %% Check if this was our last connection supporting each given service. + lists:map( + fun(SvcName) -> + case get_connections_by_service(SvcName) of + [] -> + service_discovery_rpc: + unregister_services(CompSpec, + [SvcName], + ?MODULE); + _ -> ok + end + end, LostSvcNameList), + + {ok, PersistentConnections } = rvi_common:get_module_config(data_link, + ?MODULE, + persistent_connections, + [], + CompSpec), + %% Check if this is a static node. If so, setup a timer for a reconnect + case lists:member(Addr, PersistentConnections) of + true -> + ?info("dlink_sms:closed(): Reconnect address: ~p", [Addr]), + ?info("dlink_sms:closed(): Reconnect interval: ~p", [ ?DEFAULT_RECONNECT_INTERVAL ]), + setup_reconnect_timer(?DEFAULT_RECONNECT_INTERVAL, + Addr, CompSpec); + false -> ok + end, + ok; + +handle_sms(_FromPid, Addr, error, _ExtraArgs) -> + ?info("dlink_sms:socket_error(): Address: ~p", [Addr]), + ok. + +handle_sms(FromPid, Addr, data, Payload, [CompSpec]) -> + + ?debug("dlink_sms:data(): Payload ~p", [Payload ]), + {ok, {struct, Elems}} = exo_json:decode_string(Payload), + + ?debug("dlink_sms:data(): Got ~p", [ Elems ]), + + case opt(?DLINK_ARG_CMD, Elems, undefined) of + ?DLINK_CMD_AUTHORIZE -> + [ TransactionID, + RemoteAddress, + ProtoVersion, + CertificatesTmp, + Signature ] = + opts([?DLINK_ARG_TRANSACTION_ID, + ?DLINK_ARG_ADDRESS, + ?DLINK_ARG_VERSION, + ?DLINK_ARG_CERTIFICATES, + ?DLINK_ARG_SIGNATURE], + Elems, undefined), + + Certificates = + case CertificatesTmp of + {array, C} -> C; + undefined -> [] + end, + process_authorize(FromPid, Addr, TransactionID, RemoteAddress, + ProtoVersion, Signature, Certificates, CompSpec); + + ?DLINK_CMD_SERVICE_ANNOUNCE -> + [ TransactionID, + ProtoVersion, + Signature ] = + opts([?DLINK_ARG_TRANSACTION_ID, + ?DLINK_ARG_VERSION, + ?DLINK_ARG_SIGNATURE], + Elems, undefined), + + Conn = {Addr, 0}, + case authorize_rpc:validate_message(CompSpec, Signature, Conn) of + [ok, Msg] -> + process_announce(Msg, FromPid, Addr, + TransactionID, ProtoVersion, CompSpec); + _ -> + ?debug("Couldn't validate availability msg from ~p", [Conn]) + end; + + ?DLINK_CMD_RECEIVE -> + [ _TransactionID, + ProtoMod, + Data ] = + opts([?DLINK_ARG_TRANSACTION_ID, + ?DLINK_ARG_MODULE, + ?DLINK_ARG_DATA], + Elems, undefined), + process_data(FromPid, Addr, ProtoMod, Data, CompSpec); + + ?DLINK_CMD_PING -> + ?info("dlink_sms:ping(): Pinged from: ~p", [Addr]), + ok; + + undefined -> + ?warning("dlink_sms:data() cmd undefined, ~p", [Elems]), + ok + end. + +%% JSON-RPC entry point +%% CAlled by local exo http server +handle_notification("service_available", Args) -> + {ok, SvcName} = rvi_common:get_json_element(["service"], Args), + {ok, DataLinkModule} = rvi_common:get_json_element(["data_link_module"], Args), + + gen_server:cast(?SERVER, { rvi, service_available, + [ SvcName, + DataLinkModule ]}), + + ok; +handle_notification("service_unavailable", Args) -> + {ok, SvcName} = rvi_common:get_json_element(["service"], Args), + {ok, DataLinkModule} = rvi_common:get_json_element(["data_link_module"], Args), + + gen_server:cast(?SERVER, { rvi, service_unavailable, + [ SvcName, + DataLinkModule ]}), + + ok; + +handle_notification(Other, _Args) -> + ?info("dlink_sms:handle_notification(~p): unknown", [ Other ]), + ok. + +handle_rpc("setup_data_link", Args) -> + { ok, Service } = rvi_common:get_json_element(["service"], Args), + + { ok, Opts } = rvi_common:get_json_element(["opts"], Args), + + [ Res, Timeout ] = gen_server:call(?SERVER, { rvi, setup_data_link, + [ Service, Opts ] }), + + {ok, [ {status, rvi_common:json_rpc_status(Res)} , { timeout, Timeout }]}; + +handle_rpc("disconnect_data_link", Args) -> + { ok, NetworkAddress} = rvi_common:get_json_element(["network_address"], Args), + [Res] = gen_server:call(?SERVER, { rvi, disconnect_data_link, [NetworkAddress]}), + {ok, [ {status, rvi_common:json_rpc_status(Res)} ]}; + +handle_rpc("send_data", Args) -> + { ok, ProtoMod } = rvi_common:get_json_element(["proto_mod"], Args), + { ok, Service } = rvi_common:get_json_element(["service"], Args), + { ok, Data } = rvi_common:get_json_element(["data"], Args), + { ok, DataLinkOpts } = rvi_common:get_json_element(["opts"], Args), + [ Res ] = gen_server:call(?SERVER, { rvi, send_data, [ProtoMod, Service, Data, DataLinkOpts]}), + {ok, [ {status, rvi_common:json_rpc_status(Res)} ]}; + + +handle_rpc(Other, _Args) -> + ?info("dlink_sms:handle_rpc(~p): unknown", [ Other ]), + { ok, [ { status, rvi_common:json_rpc_status(invalid_command)} ] }. + + +handle_cast( {rvi, service_available, [SvcName, local]}, St) -> + ?debug("dlink_sms:service_available(): ~p (local)", [ SvcName ]), + announce_local_service_(St#st.cs, SvcName, available), + {noreply, St}; + + +handle_cast( {rvi, service_available, [SvcName, Mod]}, St) -> + ?debug("dlink_sms:service_available(): ~p (~p) ignored", [ SvcName, Mod ]), + %% We don't care about remote services available through + %% other data link modules + {noreply, St}; + + +handle_cast( {rvi, service_unavailable, [SvcName, local]}, St) -> + announce_local_service_(St#st.cs, SvcName, unavailable), + {noreply, St}; + +handle_cast( {rvi, service_unavailable, [_SvcName, _]}, St) -> + %% We don't care about remote services available through + %% other data link modules + {noreply, St}; + + +handle_cast(Other, St) -> + ?warning("dlink_sms:handle_cast(~p): unknown", [ Other ]), + {noreply, St}. + + +handle_call({rvi, setup_data_link, [ Service, Opts ]}, _From, St) -> + %% Do we already have a connection that support service? + ?info("dlink_sms: setup_data_link (~p, ~p)~n", [Service, Opts]), + case get_connections_by_service(Service) of + [] -> %% Nop[e + case proplists:get_value(target, Opts, undefined) of + undefined -> + ?info("dlink_sms:setup_data_link(~p) Failed: no target given in options.", + [Service]), + { reply, [ok, -1 ], St }; + + Addr -> + case connect_remote(Addr, St#st.cs) of + ok -> + { reply, [ok, 2000], St }; %% 2 second timeout + + already_connected -> %% We are already connected + { reply, [already_connected, -1], St }; + + Err -> + { reply, [Err, 0], St } + end + end; + + _ -> %% Yes - We do have a connection that knows of service + { reply, [already_connected, -1], St } + end; + + +handle_call({rvi, disconnect_data_link, [Address] }, _From, St) -> + Res = sms_connection:terminate_connection(Address), + { reply, [ Res ], St }; + + +handle_call({rvi, send_data, [ProtoMod, Service, Data, _DataLinkOpts]}, _From, St) -> + + %% Resolve connection pid from service + case get_connections_by_service(Service) of + [] -> + { reply, [ no_route ], St}; + + %% FIXME: What to do if we have multiple connections to the same service? + [ConnPid | _T] -> + Res = sms_connection:send(ConnPid, {receive_data, ProtoMod, Data}), + { reply, [ Res ], St} + end; + + + + +handle_call({setup_initial_ping, Address, Pid}, _From, St) -> + %% Create a timer to handle periodic pings. + {ok, ServerOpts } = rvi_common:get_module_config(data_link, + ?MODULE, + ?SERVER_OPTS, [], + St#st.cs), + Timeout = proplists:get_value( + ping_interval, ServerOpts, ?DEFAULT_PING_INTERVAL), + ?info("dlink_sms:setup_ping(): ~p will be pinged every ~p msec", + [Address, Timeout]), + erlang:send_after(Timeout, self(), {rvi_ping, Pid, Address, Timeout}), + {reply, ok, St}; + +handle_call(Other, _From, St) -> + ?warning("dlink_sms:handle_rpc(~p): unknown", [Other]), + {reply, {ok, [{status, rvi_common:json_rpc_status(invalid_command)}]}, St}. + +%% Ping time +handle_info({rvi_ping, Pid, Address, Timeout}, St) -> + %% Check that connection is up + case sms_connection:is_connection_up(Pid) of + true -> + ?info("dlink_sms:ping(): Pinging: ~p", [Address]), + sms_connection:send(Pid, ping), + erlang:send_after(Timeout, self(), + {rvi_ping, Pid, Address, Timeout}); + false -> + ok + end, + {noreply, St}; + +%% Setup static nodes +handle_info({ rvi_setup_persistent_connection, Addr, CompSpec }, St) -> + ?info("rvi_setup_persistent_connection, ~p~n", [Addr]), + connect_and_retry_remote(Addr, CompSpec), + {noreply, St}; + +handle_info({gsms, _Ref, #gsms_deliver_pdu{ + addr = #gsms_addr{addr = Addr}}} = Msg, + #st{cs = CompSpec} = St) -> + case sms_connection_manager:find_connection_by_address(Addr) of + not_found -> + {ok, _} = setup_connection(Addr, [Msg], CompSpec); + Pid when is_pid(Pid) -> + ignore %% connection should have its own subscription + end, + {noreply, St}; + +handle_info(Info, St) -> + ?notice("dlink_sms(): Unknown message: ~p", [ Info]), + {noreply, St}. + +terminate(_Reason, _St) -> + ok. +code_change(_OldVsn, St, _Extra) -> + {ok, St}. + +setup_reconnect_timer(MSec, Addr, CompSpec) -> + erlang:send_after(MSec, ?MODULE, + { rvi_setup_persistent_connection, + Addr, CompSpec }), + ok. + +get_services_by_connection(ConnPid) -> + case ets:lookup(?CONNECTION_TABLE, ConnPid) of + [ #connection_entry { services = SvcNames } ] -> + SvcNames; + [] -> [] + end. + + +get_connections_by_service(Service) -> + case ets:lookup(?SERVICE_TABLE, Service) of + [ #service_entry { connections = Connections } ] -> + Connections; + [] -> [] + end. + + +add_services(SvcNameList, ConnPid) -> + %% Create or replace existing connection table entry + %% with the sum of new and old services. + ets:insert(?CONNECTION_TABLE, + #connection_entry { + connection = ConnPid, + services = SvcNameList ++ get_services_by_connection(ConnPid) + }), + + %% Add the connection to the service entry for each servic. + [ ets:insert(?SERVICE_TABLE, + #service_entry { + service = SvcName, + connections = [ConnPid | get_connections_by_service(SvcName)] + }) || SvcName <- SvcNameList ], + ok. + + +delete_services(ConnPid, SvcNameList) -> + ets:insert(?CONNECTION_TABLE, + #connection_entry { + connection = ConnPid, + services = get_services_by_connection(ConnPid) -- SvcNameList + }), + + %% Loop through all services and update the conn table + %% Update them with a new version where ConnPid has been removed + [ ets:insert(?SERVICE_TABLE, + #service_entry { + service = SvcName, + connections = get_connections_by_service(SvcName) -- [ConnPid] + }) || SvcName <- SvcNameList ], + ok. + +availability_msg(Availability, Services) -> + {struct, [{ ?DLINK_ARG_STATUS, status_string(Availability) }, + { ?DLINK_ARG_SERVICES, {array, Services} }]}. + +status_string(available ) -> ?DLINK_ARG_AVAILABLE; +status_string(unavailable) -> ?DLINK_ARG_UNAVAILABLE. + +process_authorize(FromPid, PeerAddr, TransactionID, RemoteAddress, + ProtoVersion, Signature, Certificates, CompSpec) -> + ?info("dlink_sms:authorize(): Peer Address: ~p" , [PeerAddr]), + ?info("dlink_sms:authorize(): Remote Address: ~p" , [RemoteAddress]), + ?info("dlink_sms:authorize(): Protocol Ver: ~p" , [ProtoVersion]), + ?debug("dlink_sms:authorize(): TransactionID: ~p", [TransactionID]), + ?debug("dlink_sms:authorize(): Signature: ~p", [Signature]), + + Conn = {PeerAddr, 0}, % add dummy port (necessary?) + case validate_auth_jwt(Signature, Certificates, Conn, CompSpec) of + true -> + connection_authorized(FromPid, Conn, CompSpec); + false -> + %% close connection (how?) + false + end. + +send_authorize(Pid, CompSpec) -> + LocalAddr = rvi_common:node_msisdn(), + sms_connection:send_auth( + Pid, + term_to_json( + {struct, + [ { ?DLINK_ARG_TRANSACTION_ID, 1 }, + { ?DLINK_ARG_CMD, ?DLINK_CMD_AUTHORIZE }, + { ?DLINK_ARG_ADDRESS, LocalAddr }, + { ?DLINK_ARG_VERSION, ?DLINK_SMS_VERSION }, + { ?DLINK_ARG_CERTIFICATES, {array, get_certificates(CompSpec)} }, + { ?DLINK_ARG_SIGNATURE, get_authorize_jwt(CompSpec) } ]})). + +connection_authorized(FromPid, {RemoteAddr, 0} = Conn, CompSpec) -> + %% If FromPid (the genserver managing the socket) is not yet registered + %% with the conneciton manager, this is an incoming connection + %% from the client. We should respond with our own authorize followed by + %% a service announce + case sms_connection:is_auth_sent(FromPid) of + false -> + ?debug("dlink_sms:authorize(): Sending authorize."), + Res = send_authorize(FromPid, CompSpec), + ?debug("dlink_sms:authorize(): Sending authorize: ~p", [ Res]), + ok; + true -> ok + end, + + %% Send our own servide announcement to the remote server + %% that just authorized to us. + [ok, LocalServices] = service_discovery_rpc:get_services_by_module(CompSpec, local), + + [ok, FilteredServices] = authorize_rpc:filter_by_destination( + CompSpec, LocalServices, Conn), + + %% Send an authorize back to the remote node + ?info("dlink_sms:authorize(): Announcing local services: ~p to remote ~p", + [FilteredServices, RemoteAddr]), + + [ok, JWT] = authorize_rpc:sign_message( + CompSpec, availability_msg(available, FilteredServices)), + sms_connection:send(FromPid, + term_to_json( + {struct, + [ { ?DLINK_ARG_TRANSACTION_ID, 1 }, + { ?DLINK_ARG_CMD, ?DLINK_CMD_SERVICE_ANNOUNCE }, + { ?DLINK_ARG_SIGNATURE, JWT } ]})), + + %% Setup ping interval + gen_server:call(?SERVER, {setup_initial_ping, RemoteAddr, FromPid }), + ok. + +process_data(_FromPid, RemoteAddr, ProtocolMod, Data, CompSpec) -> + ?debug("dlink_sms:receive_data(): RemoteAddr: ~p", [RemoteAddr]), + ?debug("dlink_sms:receive_data(): ~p:receive_message(~p)", [ ProtocolMod, Data ]), + Proto = list_to_existing_atom(ProtocolMod), + Proto:receive_message(CompSpec, RemoteAddr, + base64:decode_to_string(Data)). + +process_announce({struct, Elems}, FromPid, Addr, TID, _Vsn, CompSpec) -> + [ Avail, + {array, Svcs} ] = + opts([ ?DLINK_ARG_STATUS, ?DLINK_ARG_SERVICES ], Elems, undefined), + ?debug("dlink_sms:service_announce(~p): Address: ~p", [Avail,Addr]), + ?debug("dlink_sms:service_announce(~p): TransactionID: ~p", [Avail,TID]), + ?debug("dlink_sms:service_announce(~p): Services: ~p", [Avail,Svcs]), + case Avail of + ?DLINK_ARG_AVAILABLE -> + add_services(Svcs, FromPid), + service_discovery_rpc:register_services(CompSpec, Svcs, ?MODULE); + ?DLINK_ARG_UNAVAILABLE -> + delete_services(FromPid, Svcs), + service_discovery_rpc:unregister_services(CompSpec, Svcs, ?MODULE) + end, + ok. + +delete_connection(Conn) -> + %% Create or replace existing connection table entry + %% with the sum of new and old services. + SvcNameList = get_services_by_connection(Conn), + + %% Replace each existing connection entry that has + %% SvcName with a new one where the SvcName is removed. + lists:map(fun(SvcName) -> + Existing = get_connections_by_service(SvcName), + ets:insert(?SERVICE_TABLE, # + service_entry { + service = SvcName, + connections = Existing -- [ Conn ] + }) + end, SvcNameList), + + %% Delete the connection + ets:delete(?CONNECTION_TABLE, Conn), + ok. + +get_connections('$end_of_table', Acc) -> + Acc; + +get_connections(Key, Acc) -> + get_connections(ets:next(?CONNECTION_TABLE, Key), [ Key | Acc ]). + +get_connections() -> + get_connections(ets:first(?CONNECTION_TABLE), []). + +get_authorize_jwt(CompSpec) -> + case authorize_rpc:get_authorize_jwt(CompSpec) of + [ok, JWT] -> + JWT; + [not_found] -> + ?error("No authorize JWT~n", []), + error(cannot_authorize) + end. + +get_certificates(CompSpec) -> + case authorize_rpc:get_certificates(CompSpec) of + [ok, Certs] -> + Certs; + [not_found] -> + ?error("No certificate found~n", []), + error(no_certificate_found) + end. + +validate_auth_jwt(JWT, Certs, Conn, CompSpec) -> + case authorize_rpc:validate_authorization(CompSpec, JWT, Certs, Conn) of + [ok] -> + true; + [not_found] -> + false + end. + +term_to_json(Term) -> + binary_to_list(iolist_to_binary(exo_json:encode(Term))). + +opt(K, L, Def) -> + case lists:keyfind(K, 1, L) of + {_, V} -> V; + false -> Def + end. + +opts(Keys, Elems, Def) -> + [ opt(K, Elems, Def) || K <- Keys]. diff --git a/components/dlink_sms/src/dlink_sms_sup.erl b/components/dlink_sms/src/dlink_sms_sup.erl new file mode 100644 index 0000000..cfe77b5 --- /dev/null +++ b/components/dlink_sms/src/dlink_sms_sup.erl @@ -0,0 +1,39 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% + + +-module(dlink_sms_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%% Helper macro for declaring children of supervisor +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + {ok, { {one_for_one, 5, 10}, + [ + ?CHILD(dlink_sms_rpc, worker) + ]} }. + diff --git a/components/dlink_sms/src/sms_connection.erl b/components/dlink_sms/src/sms_connection.erl new file mode 100644 index 0000000..ba60625 --- /dev/null +++ b/components/dlink_sms/src/sms_connection.erl @@ -0,0 +1,270 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% + +%%%------------------------------------------------------------------- +%%% @author magnus <magnus@t520.home> +%%% @copyright (C) 2014, magnus +%%% @doc +%%% +%%% @end +%%% Created : 12 Sep 2014 by magnus <magnus@t520.home> +%%%------------------------------------------------------------------- +-module(sms_connection). + +-behaviour(gen_server). +-include_lib("lager/include/log.hrl"). + + +%% API + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-export([setup/4, setup/5]). +-export([send/2, send_auth/2]). +-export([is_auth_sent/1]). +-export([is_connection_up/1]). +-export([terminate_connection/1]). + +-include_lib("gsms/include/gsms.hrl"). + +-define(SERVER, ?MODULE). + +-record(st, { + addr = "", + mod = undefined, + func = undefined, + args = undefined, + pst = undefined, %% Payload state + auth_sent = false + }). + +%%%=================================================================== +%%% API +%%%=================================================================== +%% MFA is to deliver data received on the socket. + +setup(Addr, Mod, Fun, Arg) -> + setup(Addr, [], Mod, Fun, Arg). + +setup(Addr, Msgs, Mod, Fun, Arg) -> + gen_server:start_link(?MODULE, {Addr, Msgs, Mod, Fun, Arg},[]). + +send(Conn, Data) -> + send(Conn, Data, false). + +send_auth(Conn, Data) -> + send(Conn, Data, true). + +send(Pid, Data, IsAuth) when is_pid(Pid), is_boolean(IsAuth) -> + gen_server:cast(Pid, {send, "RVI:" ++ Data, IsAuth}); +send(Addr, Data, IsAuth) when is_list(Addr), is_boolean(IsAuth) -> + case sms_connection_manager:find_connection_by_address(Addr) of + {ok, Pid} -> + gen_server:cast(Pid, {send, "RVI:" ++ Data, IsAuth}); + + _Err -> + ?info("connection:send(): Connection ~p not found for data: ~p", + [Addr, Data]), + not_found + + end. + +is_auth_sent(Pid) when is_pid(Pid) -> + gen_server:call(Pid, is_auth_sent). + +terminate_connection(Pid) when is_pid(Pid) -> + gen_server:call(Pid, terminate_connection); +terminate_connection(Addr) when is_list(Addr) -> + case sms_connection_manager:find_connection_by_address(Addr) of + {ok, Pid} -> + gen_server:call(Pid, terminate_connection); + + _Err -> not_found + end. + +is_connection_up(Pid) when is_pid(Pid) -> + is_process_alive(Pid); +is_connection_up(Addr) when is_list(Addr) -> + case sms_connection_manager:find_connection_by_address(Addr) of + {ok, Pid} -> + is_connection_up(Pid); + + _Err -> + false + end. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% +%% @spec init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @end +%%-------------------------------------------------------------------- +%% MFA used to handle socket closed, socket error and received data +%% When data is received, a separate process is spawned to handle +%% the MFA invocation. +init({Addr, Msgs, Mod, Fun, Arg}) -> + [self() ! M || M <- Msgs], + case Addr of + undefined -> ok; + _ -> + sms_connection_manager:add_connection(Addr, self()) + end, + ?debug("connection:init(): self(): ~p", [self()]), + ?debug("connection:init(): Addr: ~p", [Addr]), + ?debug("connection:init(): Msgs: ~p", [Msgs]), + ?debug("connection:init(): Module: ~p", [Mod]), + ?debug("connection:init(): Function: ~p", [Fun]), + ?debug("connection:init(): Arg: ~p", [Arg]), + gsms:subscribe([{anumber, Addr}]), + {ok, #st{ + addr = Addr, + mod = Mod, + func = Fun, + args = Arg, + pst = undefined + }}. + + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @spec handle_call(Request, From, State) -> +%% {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- + + +handle_call(terminate_connection, _From, #st{addr = Addr, + mod = Mod, + func = Fun, + args = Arg} = St) -> + ?debug("~p:handle_call(terminate_connection): Terminating: ~p", + [?MODULE, St#st.addr]), + Mod:Fun(self(), Addr, closed, Arg), + sms_connection_manager:delete_connection_by_pid(self()), + {stop, normal, ok, St}; + +handle_call(is_auth_sent, _From, #st{auth_sent = AuthSent} = St) -> + {reply, AuthSent, St}; + +handle_call(_Request, _From, State) -> + ?warning("~p:handle_call(): Unknown call: ~p", [ ?MODULE, _Request]), + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @spec handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_cast({send, Data, IsAuth}, #st{addr = A} = St) -> + ?debug("~p:handle_call(send): Sending: ~p", + [ ?MODULE, Data]), + gsms:send([{addr, A}], Data), + {noreply, St#st{auth_sent = St#st.auth_sent or IsAuth}}; + +handle_cast({activate_socket, Sock}, State) -> + Res = inet:setopts(Sock, [{active, once}]), + ?debug("connection:activate_socket(): ~p", [Res]), + {noreply, State}; + + +handle_cast(_Msg, State) -> + ?warning("~p:handle_cast(): Unknown call: ~p", [ ?MODULE, _Msg]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @spec handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- + +%% Fill in peername if empty. +handle_info({gsms, _Ref, #gsms_deliver_pdu{addr = #gsms_addr{addr = Addr}, + ud = "RVI:" ++ Data}}, + #st{addr = Addr, pst = PST, + mod = Mod, func = Fun, args = Arg} = State) -> + ?debug("~p:handle_info(data): Data: ~p", [?MODULE, Data]), + ?debug("~p:handle_info(data): From: ~p", [?MODULE, Addr]), + + case rvi_common:extract_json(Data, PST) of + {[], NPST} -> + ?debug("~p:handle_info(data incomplete)", [?MODULE]), + {noreply, State#st {pst = NPST}}; + {JSONElements, NPST} -> + ?debug("~p:handle_info(data complete): Processed: ~p", + [?MODULE, JSONElements]), + FromPid = self(), + spawn(fun() -> + [Mod:Fun(FromPid, Addr, data, SingleElem, Arg) + || SingleElem <- JSONElements] + end), + {noreply, State#st{pst = NPST}} + end; + +handle_info(_Info, State) -> + ?warning("~p:handle_cast(): Unknown info: ~p", [ ?MODULE, _Info]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ?debug("~p:terminate(): Reason: ~p ", [ ?MODULE, _Reason]), + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/components/dlink_sms/src/sms_connection_manager.erl b/components/dlink_sms/src/sms_connection_manager.erl new file mode 100644 index 0000000..0e59b0b --- /dev/null +++ b/components/dlink_sms/src/sms_connection_manager.erl @@ -0,0 +1,259 @@ +%% +%% Copyright (C) 2014, Jaguar Land Rover +%% +%% This program is licensed under the terms and conditions of the +%% Mozilla Public License, version 2.0. The full text of the +%% Mozilla Public License is at https://www.mozilla.org/MPL/2.0/ +%% +%% +%%%------------------------------------------------------------------- +%%% @author magnus <magnus@t520.home> +%%% @copyright (C) 2014, magnus +%%% @doc +%%% +%%% @end +%%% Created : 12 Sep 2014 by magnus <magnus@t520.home> +%%%------------------------------------------------------------------- +-module(sms_connection_manager). + +-behaviour(gen_server). +-include_lib("lager/include/log.hrl"). + +-include_lib("gsms/include/gsms.hrl"). + +%% API +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-export([add_connection/2]). +-export([delete_connection_by_pid/1]). +-export([delete_connection_by_address/1]). +-export([find_connection_by_pid/1]). +-export([find_connection_by_address/1]). + +-define(SERVER, ?MODULE). + +-record(st, { + conn_by_pid = undefined, + conn_by_addr = undefined + }). + +%%%=================================================================== +%%% API +%%%=================================================================== + +add_connection(Addr, Pid) -> + gen_server:call(?SERVER, {add_connection, Addr, Pid}). + +delete_connection_by_pid(Pid) -> + gen_server:call(?SERVER, {delete_connection_by_pid, Pid} ). + +delete_connection_by_address(Addr) -> + gen_server:call(?SERVER, {delete_connection_by_address, Addr}). + +find_connection_by_pid(Pid) -> + gen_server:call(?SERVER, {find_connection_by_pid, Pid} ). + +find_connection_by_address(Addr) -> + gen_server:call(?SERVER, {find_connection_by_address, Addr} ). + + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% +%% @spec start_link() -> {ok, Pid} | ignore | {error, Error} +%% @end +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% +%% @spec init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @end +%%-------------------------------------------------------------------- +init([]) -> + {ok, #st{ + conn_by_pid = dict:new(), %% All managed connection stored by pid + conn_by_addr = dict:new() %% All managed connection stored by address + }}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @spec handle_call(Request, From, State) -> +%% {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_call({add_connection, Addr, Pid}, _From, + #st{conn_by_pid = ConPid, + conn_by_addr = ConAddr} = St) -> + ?debug("~p:handle_call(add): Adding Pid: ~p, Address: ~p", + [?MODULE, Pid, Addr]), + %% Store so that we can find connection both by pid and by address + NConPid = dict:store(Pid, Addr, ConPid), + NConAddr = dict:store(Addr, Pid, ConAddr), + NSt = St#st {conn_by_pid = NConPid, + conn_by_addr = NConAddr}, + {reply, ok, NSt}; + +%% Delete connection by pid +handle_call({delete_connection_by_pid, Pid}, _From, + #st{conn_by_pid = ConPid, + conn_by_addr = ConAddr} = St) when is_pid(Pid)-> + %% Find address associated with Pid + case dict:find(Pid, ConPid) of + error -> + ?debug("~p:handle_call(del_by_pid): not found: ~p", + [?MODULE, Pid]), + {reply, not_found, St}; + + {ok, Addr} -> + ?debug("~p:handle_call(del_by_pid): deleted Pid: ~p, Address: ~p", + [?MODULE, Pid, Addr]), + NConPid = dict:erase(Pid, ConPid), + NConAddr = dict:erase(Addr, ConAddr), + + NSt = St#st{conn_by_pid = NConPid, + conn_by_addr = NConAddr}, + {reply, ok, NSt} + end; + + +%% Delete connection by address +handle_call({delete_connection_by_address, Addr}, _From, + #st{conn_by_pid = ConPid, + conn_by_addr = ConAddr} = St) -> + + %% Find Pid associated with Address + case dict:find(Addr, ConAddr) of + error -> + ?debug("~p:handle_call(del_by_addr): not found: ~p", + [?MODULE, Addr]), + {reply, not_found, St}; + + {ok, Pid} -> + ?debug("~p:handle_call(del_by_addr): deleted Pid: ~p, Address: ~p", + [?MODULE, Pid, Addr]), + NConPid = dict:erase(Pid, ConPid), + NConAddr = dict:erase(Addr, ConAddr), + NSt = St#st {conn_by_pid = NConPid, + conn_by_addr = NConAddr}, + {reply, ok, NSt} + end; + +%% Find connection by pid +handle_call({find_connection_by_pid, Pid}, _From, + #st{conn_by_pid = ConPid} = St) when is_pid(Pid)-> + %% Find address associated with Pid + case dict:find(Pid, ConPid) of + error -> + ?debug("~p:handle_call(find_by_pid): not found: ~p", + [?MODULE, Pid]), + {reply, not_found, St}; + + {ok, Addr} -> + ?debug("~p:handle_call(find_by_addr): Pid: ~p ->: ~p", + [?MODULE, Pid, Addr]), + {reply, {ok, Addr}, St} + end; + +%% Find connection by address +handle_call({find_connection_by_address, Addr}, _From, + #st{conn_by_addr = ConAddr} = St) -> + + %% Find address associated with Pid + case dict:find(Addr, ConAddr) of + error -> + ?debug("~p:handle_call(find_by_addr): not found: ~p", + [?MODULE, Addr]), + {reply, not_found, St}; + + {ok, Pid} -> + ?debug("~p:handle_call(find_by_addr): Addr: ~p ->: ~p", + [?MODULE, Addr, Pid]), + {reply, {ok, Pid}, St} + end; + +handle_call(_Request, _From, State) -> + ?warning("~p:handle_call(): Unknown call: ~p", [?MODULE, _Request]), + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @spec handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + ?warning("~p:handle_cast(): Unknown call: ~p", [ ?MODULE, _Msg]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @spec handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + ?warning("~p:handle_info(): Unknown info: ~p", [?MODULE, _Info]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/components/rvi_common/src/rvi_common.erl b/components/rvi_common/src/rvi_common.erl index 111566f..8028917 100644 --- a/components/rvi_common/src/rvi_common.erl +++ b/components/rvi_common/src/rvi_common.erl @@ -28,6 +28,7 @@ -export([local_service_prefix/0]). -export([node_address_string/0]). -export([node_address_tuple/0]). +-export([node_msisdn/0]). -export([get_request_result/1]). -export([get_component_specification/0, get_component_modules/1, @@ -46,6 +47,7 @@ -define(NODE_SERVICE_PREFIX, node_service_prefix). -define(NODE_ADDRESS, node_address). +-define(NODE_MSISDN, node_msisdn). -record(pst, { buffer = [], @@ -435,6 +437,15 @@ node_address_tuple() -> { Address, list_to_integer(Port) } end. +node_msisdn() -> + case application:get_env(rvi_core, ?NODE_MSISDN) of + {ok, M} when is_list(M) -> M; + undefined -> + ?warning("WARNING: Please set application rvi environment ~p", + [?NODE_MSISDN]), + error({missing_env, ?NODE_MSISDN}) + end. + get_component_config_(Component, Default, CompList) -> case proplists:get_value(Component, CompList, undefined) of undefined -> diff --git a/deps/gsms/Makefile b/deps/gsms/Makefile new file mode 100644 index 0000000..cc5afdf --- /dev/null +++ b/deps/gsms/Makefile @@ -0,0 +1,15 @@ +.PHONY: all compile deps clean shell + +all: compile + +deps: + rebar get-deps + +compile: deps + rebar compile + +clean: + rebar clean + +shell: compile + ERL_LIBS=$(PWD)/deps erl -pa ebin diff --git a/deps/gsms/ebin/.gitignore b/deps/gsms/ebin/.gitignore new file mode 100644 index 0000000..120fe3a --- /dev/null +++ b/deps/gsms/ebin/.gitignore @@ -0,0 +1,2 @@ +*.beam +*.app diff --git a/deps/gsms/include/gsms.hrl b/deps/gsms/include/gsms.hrl index 72b0dea..0adfd81 100644 --- a/deps/gsms/include/gsms.hrl +++ b/deps/gsms/include/gsms.hrl @@ -75,7 +75,7 @@ scts, %% :7/binary udh=[] :: [gsms_ie()], %% user data header udl, %% length in septets/octets (depend on dcs) - ud + ud %% user data }). -define(MTI_SMS_SUBMIT, 2#01). diff --git a/deps/gsms/rebar.config b/deps/gsms/rebar.config index 5f6d3b4..ec9b7c4 100644 --- a/deps/gsms/rebar.config +++ b/deps/gsms/rebar.config @@ -1,6 +1,7 @@ %% -*- erlang -*- %% Config file for gsms application {deps, [ {uart, ".*", {git, "git@github.com:tonyrog/uart.git"}}, + {exo, ".*", {git, "git@github.com:Feuerlabs/exo.git"}}, {lager, ".*", {git, "git://github.com/Feuerlabs/lager.git"}}]}. diff --git a/deps/gsms/src/gsms_lib.erl b/deps/gsms/src/gsms_lib.erl new file mode 100644 index 0000000..187aefd --- /dev/null +++ b/deps/gsms/src/gsms_lib.erl @@ -0,0 +1,25 @@ +-module(gsms_lib). + +-export([get_opt/2, + get_opt/3]). + +get_opt(K, Opts) when is_atom(K) -> + case lists:keyfind(K, 1, Opts) of + false -> erlang:error({mandatory, K}); + {_, V} -> V + end; +get_opt({K, Def}, Opts) -> + get_opt(K, Opts, Def). + +get_opt(K, Opts, Def) -> + case lists:keyfind(K, 1, Opts) of + false when is_function(Def, 0) -> + Def(); + false when Def == '$mandatory' -> + error({mandatory, K}); + false -> + Def; + {_, V} -> + V + end. + diff --git a/deps/gsms/src/gsms_plivo.erl b/deps/gsms/src/gsms_plivo.erl new file mode 100644 index 0000000..25be7b6 --- /dev/null +++ b/deps/gsms/src/gsms_plivo.erl @@ -0,0 +1,373 @@ +-module(gsms_plivo). +-behaviour(gsms_session). + +-export([start_link/2, % called from gsms_if_sup.erl + new/1]). + +-export([mandatory_options/0, + init/1, + handle_send/3, + get_signal_strength/1 + ]). + %% handle_call/3, + %% subscribe/2]). + +-export([decode_body/2, + signature/3, + uuid/0, + http_date/0, + get_x_plivo_sig/1]). + +%% HTTP callback +-export([handle_body/4]). + +-export([trace/0]). +-export([test_new/0, test_new/2, simtest/1]). + +-record(st, {account, + auth_id, + auth_token, + src, + recv_uri, + recv_port, + recv_pid, + send_uri}). + +-record(server, {parent, + uri, + auth_token}). + +-include_lib("exo/include/exo_http.hrl"). +-include_lib("exo/src/exo_socket.hrl"). +-include("gsms.hrl"). +-include("log.hrl"). +-define(mandatory, '$mandatory'). + +start_link(_Id, Opts) -> + {ok, new(Opts)}. + +new(Opts) -> + gsms_session:new(?MODULE, Opts). + +mandatory_options() -> + [auth_id, auth_token, src_number, recv_port, recv_uri]. + +init(Opts) -> + [Acct, ID, Token, Src, Port, URI, SendURI, Attrs] = + [gsms_lib:get_opt(K, Opts) + || K <- [acct, auth_id, auth_token, + src_number, recv_port, recv_uri, + {send_uri, "http://api.plivo.com"}, + {attributes, []}]], + {ok, RPid} = spawn_http_listener(Port, URI, Token), + {ok, Src, Attrs, #st{account = Acct, + auth_id = ID, + auth_token = Token, + src = no_plus(Src), + recv_uri = URI, + recv_port = Port, + recv_pid = RPid, + send_uri = SendURI}}. + +handle_send(Opts, Body, St) -> + try + Dest = gsms_lib:get_opt({addr, ?mandatory}, Opts), + Res = plivo_send_SMS(Dest, Body, St), + ?debug("send: Res = ~p~n", [Res]), + {ok, Res, St} + catch + error:E -> + {error, E} + end. + +get_signal_strength(St) -> + %% For now, simply return maximum strength (see gsms/src/README) + {ok, 30, St}. + +plivo_send_SMS(Dest, Msg, #st{auth_id = AuthID, + auth_token = AuthTok, + src = Src, + send_uri = SendURI, + recv_uri = RecvURI}) -> + URI = lists:flatten([SendURI, "/v1/Account/", AuthID, "/Message/"]), + JSON = {struct, [{"src", Src}, + {"dst", no_plus(Dest)}, + {"text", Msg}, + {"url", RecvURI}, + {"log", true}]}, + Req = binary_to_list(iolist_to_binary(exo_json:encode(JSON))), + Hdrs = [{'Content-Type', "application/json"} + | exo_http:make_headers(AuthID, AuthTok)], + send_result(exo_http:wpost(URI, {1,1}, Hdrs, Req, 1000)). + +send_result({ok, #http_response{status = Status} = R, Body}) -> + if Status >= 200, Status =< 299 -> + {ok, get_uuid(R, Body)}; + true -> + {error, {Status, R#http_response.phrase}} + end; +send_result({error, _} = E) -> + E. + +spawn_http_listener(Port, URI, Token) -> + Srv = #server{parent = self(), + uri = URI, + auth_token = Token}, + exo_http_server:start_link(Port, [{request_handler, + {?MODULE, handle_body, [Srv]}}]). + +handle_body(Socket, Request, Body, #server{auth_token = Tok, uri = URI}) -> + ?debug("handle_body(_, ~p, ~p, _)~n", [Request, Body]), + try decode_body(Request, Body) of + Result -> + case validate_request(URI, Request, Result, Tok) of + true -> + ?debug("handle_body() -> ~p~n", [Result]), + case parse_result(Result) of + ok -> + response(Socket, ok); + error -> + response(Socket, error) + end; + false -> + response(Socket, auth) + end + catch + _:_ -> + response(Socket, error) + end. + +validate_request(URI, Request, Result, Tok) -> + Sig = get_x_plivo_sig(Request), + check_signature(Request, URI, Result, Sig, Tok). + +check_signature(#http_request{uri = #url{path = Path}}, + URI, Result, Sig, Tok) -> + URL = uri_join(URI, Path), + Sig == signature(URL, Result, Tok). + +uri_join(URI, Path) -> + strip_trailing_slash(URI) ++ strip_trailing_slash(Path). + +no_plus([$+|Num]) -> Num; +no_plus(Num ) -> Num. + +add_plus([$+|_] = Num) -> Num; +add_plus(Num ) -> "+" ++ Num. + +signature(URL, Result, Tok) -> + Str = lists:foldl( + fun({K, A}, S) when is_atom(A) -> + S ++ K ++ atom_to_list(A); + ({K, F}, S) when is_float(F) -> + S ++ K ++ io_lib_format:fwrite_g(F); + ({K, I}, S) when is_integer(I) -> + S ++ K ++ integer_to_list(I); + ({K, V}, S) -> + S ++ K ++ V + end, strip_trailing_slash(URL), lists:sort(params(Result))), + base64:encode_to_string(crypto:hmac(sha, Tok, Str)). + +http_date() -> + httpd_util:rfc1123_date(). + +strip_trailing_slash(S) -> + case lists:reverse(S) of + "/" ++ Rest -> + lists:reverse(Rest); + _ -> + S + end. + +params({struct, Params}) -> + [params(P) || P <- Params]; +params({K, {array, A}}) -> + {K, [params(P) || P <- A]}; +params(Params) -> + Params. + +decode_body(R, Body) -> + case get_content_type(R) of + "application/x-www-form-urlencoded" -> + decode_www_form_urlencoded(Body); + "application/json" -> + decode_json(Body) + end. + +get_content_type(#http_request{headers = #http_chdr{content_type = T}}) -> T; +get_content_type(#http_response{headers = #http_shdr{content_type = T}}) -> T. + +decode_www_form_urlencoded(Body) -> + lists:map( + fun(L) -> + [K,V] = re:split(L, "=", [{return,list}]), + {unescape(K), unescape(V)} + end, re:split(Body, "&", [{return,list}])). + +unescape([$%,A,B|T]) -> + [list_to_integer([A,B], 16) | unescape(T)]; +unescape([$+|T]) -> + [$\s|unescape(T)]; +unescape([H|T]) -> + [H|unescape(T)]; +unescape([]) -> + []. + +decode_json(Body) -> + exo_json:decode_string(to_string(Body)). + +to_string(B) when is_binary(B) -> + binary_to_list(B); +to_string(S) when is_list(S) -> + S. + +uuid() -> + %% For now, convert to list (TODO: shouldn't be necessary) + binary_to_list(uuid_()). + +uuid_() -> + %% https://en.wikipedia.org/wiki/Universally_unique_identifier + N = 4, M = 2, % version 4 - random bytes + <<A:48, _:4, B:12, _:2, C:62>> = crypto:rand_bytes(16), + UBin = <<A:48, N:4, B:12, M:2, C:62>>, + <<A1:8/binary, B1:4/binary, C1:4/binary, D1:4/binary, E1:12/binary>> = + << <<(hex(X)):8>> || <<X:4>> <= UBin >>, + <<A1:8/binary, "-", + B1:4/binary, "-", + C1:4/binary, "-", + D1:4/binary, "-", + E1:12/binary>>. + +hex(X) when X >= 0, X =< 9 -> + $0 + X; +hex(X) when X >= 10, X =< 15 -> + $a + X - 10. + +get_uuid(R, Body) -> + case decode_body(R, Body) of + {ok, Decoded} -> + case lists:keyfind("message_uuid", 1, params(Decoded)) of + false -> + io:fwrite("Cannot find message_uuid~n", []), + uuid(); + {_, UUID} -> + UUID + end; + _ -> + io:fwrite("Couldn't decode body~n", []), + uuid() + end. + +get_x_plivo_sig(#http_response{headers = H}) -> + find_x_sig(other_hdrs(H)); +get_x_plivo_sig(#http_request{headers = H}) -> + find_x_sig(other_hdrs(H)). + +other_hdrs(#http_chdr{other = Hdrs}) -> Hdrs; +other_hdrs(#http_shdr{other = Hdrs}) -> Hdrs. + +find_x_sig(Hdrs) -> + case lists:keyfind("X-Plivo-Signature", 1, Hdrs) of + false -> false; + {_, Sig} -> Sig + end. + +response(Socket, Reply) -> + {Code, Msg} = case Reply of + ok -> {200, "OK"}; + error -> {404, "Not found"}; + auth -> {401, "Authorization failed"} + end, + exo_http_server:response(Socket, undefined, Code, Msg, ""). + +%% From https://www.plivo.com/docs/api/application/ : +%% ------------------------------------------------------------ +%% The following parameters will be sent to the Message URL. +%% +%% Fromstring The source number of the incoming message. +%% This will be the number of the person sending a message to a Plivo number. +%% Tostring The number to which the message was sent. +%% This will the your Plivo number on which the message has been received. +%% Typestring Type of the message. This will always be sms +%% Textstring The content of the message. +%% MessageUUIDstring A unique ID for the message. +%% Your message can be uniquely identified on Plivo by this ID. +%% ------------------------------------------------------------ +parse_result(Result) -> + case Result of + {struct, Elems} -> + parse_result_(Elems); + [{_,_}|_] = Elems -> + parse_result_(Elems); + _ -> + error + end. + +parse_result_(Elems) -> + case lists:keyfind("Status", 1, Elems) of + {_, "delivered"} -> + {_, _UUID} = lists:keyfind("MessageUUID", 1, Elems), + %% gsms_router:notify(UUID, ok); + ok; + {_, _} -> + ok; % ignore for now + false -> + case [From, To, _Type, Text, _UUID] = _Res = + [proplists:get_value(K, Elems, "") + || K <- ["From", "To", "Type", "Text", "MessageUUID"]] of + _ when From =/= undefined -> + ?debug("Res = ~p~n", [_Res]), + gsms_router:input_from(To, #gsms_deliver_pdu{ + addr = addr(From), + ud = Text}), + ok; + _ -> + ok % ignore for now + end + end. + +addr(A) -> + #gsms_addr{type = international, + addr = add_plus(A)}. + +%% +test_new() -> + test_new("111", 9111). + +test_new(Src, Port) -> + application:ensure_all_started(gsms), + new([{acct, "Acct"}, + {auth_id,"myacct"},{auth_token,"myauth"}, + {src_number, Src}, + {recv_port, Port}, + {send_uri, "https://localhost:9100"}, + {recv_uri,"https://localhost"}]). + +simtest(1) -> + application:ensure_all_started(gsms), + R = new([{acct, acct1}, + {auth_id, "acct1"}, + {auth_token, "auth1"}, + {src_number, "111"}, + {recv_port, 9200}, + {send_uri, "http://localhost:9100"}, + {recv_uri, "http://localhost:9200"}]), + trace(), + R; +simtest(2) -> + application:ensure_all_started(gsms), + R = new([{acct, acct2}, + {auth_id, "acct2"}, + {auth_token, "auth2"}, + {src_number, "222"}, + {recv_port, 9300}, + {send_uri, "http://localhost:9100"}, + {recv_uri, "http://localhost:9300"}]), + trace(), + R. + +trace() -> + dbg:tracer(), + dbg:tpl(?MODULE, x), + dbg:tp(exo_http, x), + dbg:p(all, [c]). diff --git a/deps/gsms/src/gsms_plivo_sim.erl b/deps/gsms/src/gsms_plivo_sim.erl new file mode 100644 index 0000000..a202267 --- /dev/null +++ b/deps/gsms/src/gsms_plivo_sim.erl @@ -0,0 +1,353 @@ +-module(gsms_plivo_sim). +-behaviour(gen_server). + +-export([start_link/1]). + +-export([send_message/3]). + +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). +-compile(export_all). + +-include_lib("exo/include/exo_http.hrl"). +-include("log.hrl"). + +-define(DEFAULT_PORT, 9100). + +%% TODO: A bunch of duplication in the records. Should be cleaned up. +%% TODO: Should be enough with one HTTP server instance serving all accts. +-record(service, {acct, + type, + uri, + conn_opts = [], + numbers = [], + auth_token, + auth_string, + pid}). +-record(st, {services = [], + server, + opts, + notify = []}). + +-record(server, {parent}). + +-define(mandatory, '$mandatory'). + +test() -> + application:ensure_all_started(gsms), + start_link([{services, [{plivo, [{type, plivo_sim}, + {port, 9100}, + {uri, "http://localhost:9100"}, + {account, "myacct"}, + {auth, "myauth"} + ]} + ]} + ]). + +simtest() -> + application:ensure_all_started(gsms), + start_link([{port, 9100}, + {services, + [{s1, [{type, plivo_sim}, + {numbers, ["111"]}, + {uri, "http://localhost:9200"}, + {account, "acct1"}, + {auth, "auth1"}]}, + {s2, [{type, plivo_sim}, + {numbers, ["222"]}, + {uri, "http://localhost:9300"}, + {account, "acct2"}, + {auth, "auth2"}]}]}]). + +start_link(Opts) -> + case lists:keyfind(reg_name, 1, Opts) of + false -> + gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []); + {_, Name} when is_tuple(Name) -> + gen_server:start_link(Name, ?MODULE, Opts, []) + end. + +send_message(Opts, Body) -> + call(?MODULE, {send_message, Opts, Body}). + +send_message(Server, Opts, Body) -> + call(Server, {send_message, Opts, Body}). + +init(Opts) -> + {ok, Pid} = start_server(Opts), + S0 = #st{server = Pid, opts = Opts}, + S = case lists:keyfind(services, 1, Opts) of + false -> S0#st{server = Pid}; + {_, Svcs} -> + lists:foldl( + fun({Svc, SvcOpts}, Sx) -> + {_, Sx1} = do_add_service(Svc, SvcOpts, Sx), + Sx1 + end, S0, Svcs) + end, + {ok, S}. + +handle_call({send_message, Opts, Body}, _From, S) -> + %% We should really queue the message for delivery, then send a notification + case message_params(Opts, Body, S) of + {UUID, URI, Token, Params} -> + S1 = maybe_notify(Opts, UUID, S), + self() ! {message_sent, UUID, Params}, + try Res = do_send_message(URI, UUID, Token, Params), + {reply, Res, S1} + catch + error:Reason -> + {reply, {error, Reason}, S} + end; + false -> + {reply, {error, not_found}, S} + end; +handle_call({add_service, Svc, Opts}, _From, S) -> + {Reply, S1} = do_add_service(Svc, Opts, S), + {reply, Reply, S1}; +handle_call({authorize,AuthStr}, _, #st{services = Svcs} = S) -> + {reply, lists:keyfind(AuthStr, #service.auth_string, Svcs), S}; +handle_call(_Req, _From, S) -> + {reply, {error, badarg}, S}. + +handle_cast(_Msg, S) -> + {noreply, S}. + +handle_info({Evt, UUID, _Params} = Msg, #st{notify = Nfy} = S) + when Evt == message_sent; Evt == message_delivered -> + Found = [N || {E, ID, _} = N <- Nfy, + E == Evt andalso ID == UUID], + [notify(Msg, Num, S) || {_, _, Num} <- Found], + {noreply, S#st{notify = Nfy -- Found}}; +handle_info(_Msg, S) -> + {noreply, S}. + +terminate(_Reason, _S) -> + ok. + +code_change(_FromVsn, State, _Extra) -> + {ok, State}. + +call(Server, Req) -> + gen_server:call(Server, Req). + +ask_authorize(Pid, AuthStr) -> + call(Pid, {authorize, AuthStr}). + +auth_service([#service{conn_opts = Opts}|T], Acct, Token) -> + case lists:member({account,Acct}, Opts) of + true -> + lists:member({auth, Token}, Opts); + false -> + auth_service(T, Acct, Token) + end; +auth_service([], _, _) -> + false. + +maybe_notify(Opts, UUID, #st{notify = Nfy} = S) -> + case lists:keyfind(notify, 1, Opts) of + false -> + S; + {_, Number, Tags} -> + S#st{notify = [{Tag, UUID, Number} || Tag <- Tags] ++ Nfy} + end. + +notify({Event, UUID, Params0}, Number, S) -> + case find_service(Number, S) of + #service{uri = URI, auth_token = Token} -> + Params = + [{"Status", status(Event)}, + {"ParentMessageUUID", UUID}, + {"PartInfo", "1 of 1"} | Params0], + do_send_message(URI, UUID, Token, Params); + false -> + ignore + end. + +status(message_sent ) -> "sent"; +status(message_delivered) -> "delivered". + + +message_params(Opts, Body, #st{} = S) -> + [From, To, UUID] = [gsms_lib:get_opt(K, Opts, Def) + || {K, Def} <- [{from, ?mandatory}, + {to, ?mandatory}, + {uuid, fun gsms_plivo:uuid/0}]], + case find_service(To, S) of + #service{uri = URI, auth_token = Token} -> + Params = [ + {"To", no_plus(To)}, + {"From", no_plus(From)}, + {"TotalRate", 0.0}, + {"Units", 1}, + {"Text", Body}, + {"TotalAmount", 0.0}, + {"Type", "sms"}, + {"MessageUUID", UUID} + ], + {UUID, URI, Token, Params}; + _ -> + false + end. + +find_service(Number, #st{services = Svcs}) -> + case [Svc1 || #service{numbers = Ns} = Svc1 <- Svcs, + lists:member(Number, Ns)] of + [#service{} = Svc|_] -> + Svc; + _ -> + false + end. + +no_plus([$+|Num]) -> Num; +no_plus(Num ) -> Num. + +do_send_message(URI, UUID, Token, Params) -> + Sig = gsms_plivo:signature(URI, Params, Token), + Hs = headers(Sig), + Result = exo_http:wpost(URI, Hs, Params), + io:fwrite("wpost result = ~p~n", [Result]), + self() ! {message_delivered, UUID, Params}, + {ok, UUID}. + +headers(Sig) -> + [{'Content-Type', "application/x-www-form-urlencoded"}, + {'Accept-Encoding', "gzip, deflate"}, + {"X-Plivo-Signature", Sig}]. + +do_add_service(_Svc, Opts, S) -> + [Type, ConnOpts, Numbers, Acct, URI, Auth] = + [gsms_lib:get_opt(K, Opts, Def) + || {K, Def} <- [{type, plivo_sim}, + {connection, []}, + {numbers, []}, + {account, ?mandatory}, + {uri, ?mandatory}, + {auth, ?mandatory}]], + AuthStr = exo_http:auth_basic_encode(Acct, Auth), + SvcRec = #service{type = Type, + conn_opts = ConnOpts, + numbers = [no_plus(N) || N <- Numbers], + acct = Acct, + uri = URI, + auth_token = Auth, + auth_string = AuthStr}, + {ok, S#st{services = [SvcRec | S#st.services]}}. + +start_server(Opts) -> + Port = gsms_lib:get_opt(port, Opts, ?DEFAULT_PORT), + Srv = #server{parent = self()}, + exo_http_server:start_link(Port, [{request_handler, + {?MODULE, handle_body, [Srv]}}]). + +handle_body(Socket, Request, Body, #server{parent = P}) -> + ?debug("Path = ~p~nBody = ~p~n", + [(Request#http_request.uri)#url.path, Body]), + case check_auth(Request, P) of + false -> + response(Socket, authentication_failed, ""); + #service{acct = Acct, auth_token = AuthTok, uri = URI} -> + handle_body_(Socket, Request, Body, Acct, AuthTok, URI, P) + end. + +handle_body_(Socket, Request, Body, Acct, AuthTok, URI, P) -> + case valid_request(Request, Acct) of + false -> + response(Socket, authentication_failed, ""); + "Message" -> + ?debug("handle_body(_, ~p, ~p, _)~n", [Request, Body]), + try exo_json:decode_string(binary_to_list(Body)) of + {ok, {struct, Result}} -> + ?debug("Decoded = ~p~n", [Result]), + {_, Src} = lists:keyfind("src", 1, Result), + {_, Dest} = lists:keyfind("dst", 1, Result), + {_, Text} = lists:keyfind("text", 1, Result), + gsms_router:input_from(Src, Text), + UUID = gsms_plivo:uuid(), + API_id = gsms_plivo:uuid(), + Struct = {struct, + [{"api_id", API_id}, + {"message","message(s) queued"}, + {"message_uuid", UUID}, + {"api_id", API_id}]}, + JSON = to_json(Struct), + send_message(P, [{from, Src}, + {to, Dest}, + {uuid, UUID}, + {notify, Src, [message_sent, + message_delivered]}], + Text), + response(Socket, ok, JSON, + response_headers(URI, Struct, AuthTok)) + catch + _:_ -> + response(Socket, server_error, "") + end + end. + +check_auth(Request, P) -> + case get_basic_auth(Request) of + false -> false; + AuthStr -> + case ask_authorize(P, AuthStr) of + false -> false; + Auth -> Auth + end + end. + +to_json(Struct) -> + exo_json:encode(Struct). + +valid_request(#http_request{uri = #url{path = Path}}, Acct) -> + case filename:split(Path) of + ["/", "v1","Account",Acct,"Message"] -> + "Message"; + _Split -> + io:fwrite("unrecognized: ~p~n", [_Split]), + false + end. + +get_basic_auth(#http_request{headers = #http_chdr{ + authorization = "Basic " ++ Auth}}) -> + Auth; +get_basic_auth(_) -> + false. + + +response(Socket, Res, Body) -> + response(Socket, Res, Body, [{"Date", gsms_plivo:http_date()}]). + +response(Socket, Res, Body, Hdrs) -> + Opts = [{content_type, "application/json"} | Hdrs], + {Code, _} = lists:keyfind(Res, 2, responses()), + exo_http_server:response(Socket, undefined, Code, + atom_to_list(Res), Body, Opts). + +response_headers(URI, Params, Token) -> + [{"Date", gsms_plivo:http_date()}, + {"X-Plivo-Signature", gsms_plivo:signature(URI, Params, Token)}]. + + +responses() -> + [{200, ok}, + {201, resource_created}, + {202, resource_changed}, + {204, resource_deleted}, + {400, parameter_missing}, % ... or invalid + {401, authentication_failed}, + {404, resource_not_found}, + {405, method_not_allowed}, + {500, server_error}]. + +args(send_message) -> + [{"src" , string, mandatory}, + {"dst" , string, mandatory}, + {"text" , string, mandatory}, + {"type" , string, optional, "sms"}, + {"url" , string, optional, ""}, + {"method", string, optional, "POST"}, + {"log" , boolean, optional, true}]. diff --git a/deps/gsms/src/gsms_router.erl b/deps/gsms/src/gsms_router.erl index bb027f8..eee6c79 100644 --- a/deps/gsms/src/gsms_router.erl +++ b/deps/gsms/src/gsms_router.erl @@ -30,25 +30,26 @@ %% API -export([start_link/1, - send/2, - subscribe/1, - unsubscribe/1, - join/2, - input_from/2]). + send/2, + subscribe/1, + unsubscribe/1, + join/2, % Module defaults to gsms_0705 + join/3, + input_from/2]). %% gen_server callbacks --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% testing -export([dump/0]). --define(SERVER, ?MODULE). - +-define(SERVER, ?MODULE). + -record(subscription, { pid :: pid(), %% subscriber process @@ -59,13 +60,14 @@ -record(interface, { pid :: pid(), %% interface pid + module :: module(), %% callback module mon :: reference(), %% monitor reference bnumber :: gsms_addr(), %% modem msisdn rssi = 99 :: integer(), %% last known rssi value attributes = [] :: [{atom(),term()}] %% general match keys }). --record(state, +-record(state, { csq_ival = 0, csq_tmr, @@ -77,14 +79,14 @@ %%% API %%%=================================================================== -spec send(Options::list({Key::atom(), Value::term()}), Body::string()) -> - {ok, Ref::reference()} | - {error, Reason::term()}. + {ok, Ref::reference()} | + {error, Reason::term()}. send(Opts, Body) -> gen_server:call(?SERVER, {send, Opts, Body}). -spec subscribe(Filter::[filter()]) -> {ok,Ref::reference()} | - {error,Reason::term()}. + {error,Reason::term()}. subscribe(Filter) -> gen_server:call(?SERVER, {subscribe, self(), Filter}). @@ -94,15 +96,18 @@ subscribe(Filter) -> unsubscribe(Ref) -> gen_server:call(?SERVER, {unsubscribe, Ref}). -join(BNumber,Attributes) -> - gen_server:call(?SERVER, {join,self(),BNumber,Attributes}). +join(BNumber, Attributes) -> + join(BNumber, gsms_0705, Attributes). + +join(BNumber, Module, Attributes) -> + gen_server:call(?SERVER, {join,self(),BNumber,Module,Attributes}). %% -%% Called from gsms_0705 backend to enter incoming message +%% Called from session instance to enter incoming message %% input_from(BNumber, Sms) -> lager:debug("message input modem:~s, message = ~p\n", - [BNumber, Sms]), + [BNumber, Sms]), ?SERVER ! {input_from, BNumber, Sms}, ok. @@ -140,8 +145,8 @@ init(Args) -> Csq_ival = proplists:get_value(csq_ival, Args, 0), Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 -> erlang:start_timer(Csq_ival, self(), csq); - true -> undefined - end, + true -> undefined + end, process_flag(trap_exit, true), {ok, #state{ csq_ival=Csq_ival, csq_tmr=Csq_tmr}}. @@ -164,9 +169,9 @@ handle_call({send,Opts,Body}, _From, State) -> %% FIXME: add code to match attributes! case proplists:get_value(bnumber, Opts) of undefined -> - case State#state.ifs of - [I|_] -> - Reply = gsms_0705:send(I#interface.pid, Opts, Body), + case State#state.ifs of + [#interface{module = M, pid = Pid}|_] -> + Reply = M:send(Pid, Opts, Body), {reply, Reply, State}; [] -> {reply, {error,enoent}, State} @@ -175,37 +180,37 @@ handle_call({send,Opts,Body}, _From, State) -> case lists:keyfind(BNumber,#interface.bnumber,State#state.ifs) of false -> {reply, {error,enoent}, State}; - I -> - Reply = gsms_0705:send(I#interface.pid, Opts, Body), + #interface{module = M, pid = Pid} -> + Reply = M:send(Pid, Opts, Body), {reply, Reply, State} end end; handle_call({subscribe,Pid,Filter}, _From, State) -> Ref = erlang:monitor(process, Pid), Subs = [#subscription { pid = Pid, - ref = Ref, - filter = Filter } | State#state.subs], + ref = Ref, + filter = Filter } | State#state.subs], {reply, {ok,Ref}, State#state { subs = Subs} }; handle_call({unsubscribe,Ref}, _From, State) -> case lists:keytake(Ref, #subscription.ref, State#state.subs) of - false -> - {reply, ok, State}; - {value,_S,Subs} -> - erlang:demonitor(Ref, [flush]), - {reply, ok, State#state { subs = Subs} } + false -> + {reply, ok, State}; + {value,_S,Subs} -> + erlang:demonitor(Ref, [flush]), + {reply, ok, State#state { subs = Subs} } end; -handle_call({join,Pid,BNumber,Attributes}, _From, State) -> +handle_call({join,Pid,BNumber,Module,Attributes}, _From, State) -> case lists:keytake(BNumber, #interface.bnumber, State#state.ifs) of false -> ?debug("gsms_router: process ~p, bnumber ~p joined.", [Pid, BNumber]), - State1 = add_interface(Pid,BNumber,Attributes,State), + State1 = add_interface(Pid,BNumber,Module,Attributes,State), {reply, ok, State1}; {value,I,IFs} -> receive {'EXIT', OldPid, _Reason} when I#interface.pid =:= OldPid -> ?debug("join: restart detected", []), - State1 = add_interface(Pid,BNumber,Attributes, + State1 = add_interface(Pid,BNumber,Module,Attributes, State#state { ifs=IFs} ), {reply, ok, State1} after 0 -> @@ -216,17 +221,18 @@ handle_call(dump, _From, State=#state {subs = Subs, ifs= Ifs}) -> io:format("LoopData:\n", []), io:format("Subscriptions:\n", []), lists:foreach(fun(_Sub=#subscription {pid = Pid, ref = Ref, filter = F}) -> - io:format("pid ~p, ref ~p, filter ~p~n", - [Pid, Ref, F]) - end, - Subs), + io:format("pid ~p, ref ~p, filter ~p~n", + [Pid, Ref, F]) + end, + Subs), io:format("Interfaces:\n", []), - lists:foreach(fun(_If=#interface {pid = Pid, mon = Ref, - bnumber = B, attributes = A}) -> - io:format("pid ~p, ref ~p, bnumber ~p, attributes ~p~n", - [Pid, Ref, B, A]) - end, - Ifs), + lists:foreach(fun(_If=#interface {pid = Pid, mon = Ref, module = Mod, + bnumber = B, attributes = A}) -> + io:format("pid ~p, ref ~p, bnumber ~p, " + "module = ~p, attributes ~p~n", + [Pid, Ref, B, Mod, A]) + end, + Ifs), {reply, ok, State}; handle_call(_Request, _From, State) -> @@ -258,12 +264,12 @@ handle_cast(_Msg, State) -> %%-------------------------------------------------------------------- handle_info({'DOWN',Ref,process,Pid,_Reason}, State) -> case lists:keytake(Ref, #subscription.ref, State#state.subs) of - false -> + false -> case lists:keytake(Pid, #interface.pid, State#state.ifs) of false -> {noreply, State}; {value,_If,Ifs} -> - ?debug("gsms_router: interface ~p died, reason ~p\n", + ?debug("gsms_router: interface ~p died, reason ~p\n", [_If, _Reason]), %% Restart done by gsms_if_sup {noreply,State#state { ifs = Ifs }} @@ -274,14 +280,14 @@ handle_info({'DOWN',Ref,process,Pid,_Reason}, State) -> handle_info({input_from, BNumber, Pdu}, State) -> lager:debug("input bnumber: ~p, pdu=~p\n", [BNumber,Pdu]), - lists:foreach(fun(S) -> match_filter(S, BNumber, Pdu) end, - State#state.subs), + lists:foreach(fun(S) -> match_filter(S, BNumber, Pdu) end, + State#state.subs), {noreply, State}; handle_info({timeout, Tmr, csq}, State) when State#state.csq_tmr =:= Tmr -> Is = lists:map( - fun(I) -> - R = gsms_0705:get_signal_strength(I#interface.pid), + fun(#interface{module = M, pid = Pid} = I) -> + R = M:get_signal_strength(Pid), lager:debug("csq result: ~p\n", [R]), case R of {ok,"+CSQ:"++Params} -> @@ -311,19 +317,19 @@ handle_info({timeout, Tmr, csq}, State) when State#state.csq_tmr =:= Tmr -> Csq_ival = State#state.csq_ival, Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 -> erlang:start_timer(Csq_ival, self(), csq); - true -> undefined - end, + true -> undefined + end, {noreply, State#state { ifs = Is, csq_tmr=Csq_tmr }}; handle_info({'EXIT', Pid, Reason}, State) -> case lists:keytake(Pid, #interface.pid, State#state.ifs) of {value,_If,Ifs} -> %% One of our interfaces died, log and ignore - ?debug("gsms_router: interface ~p died, reason ~p\n", + ?debug("gsms_router: interface ~p died, reason ~p\n", [_If, Reason]), {noreply,State#state { ifs = Ifs }}; false -> %% Someone else died, log and terminate - ?debug("gsms_router: linked process ~p died, reason ~p, terminating\n", + ?debug("gsms_router: linked process ~p died, reason ~p, terminating\n", [Pid, Reason]), {stop, Reason, State} end; @@ -359,16 +365,16 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%=================================================================== -add_interface(Pid,BNumber,Attributes,State) -> +add_interface(Pid,BNumber,Module,Attributes,State) -> Mon = erlang:monitor(process, Pid), - I = #interface { pid=Pid, mon=Mon, + I = #interface { pid=Pid, mon=Mon, module=Module, bnumber=BNumber, attributes=Attributes }, link(Pid), State#state { ifs = [I | State#state.ifs ] }. match_filter(_S=#subscription {filter = Filter, - pid = Pid, - ref = Ref}, + pid = Pid, + ref = Ref}, BNumber, Pdu) -> lager:debug("match filter: ~p", [Filter]), case match(Filter, BNumber, Pdu) of @@ -389,7 +395,7 @@ match({'and',A,B}, BNum, Sms) -> match(A,BNum,Sms) andalso match(B,BNum,Sms); match({'or',A,B}, BNum, Sms) -> match(A,BNum,Sms) orelse match(B,BNum,Sms); -match({bnumber,Addr}, BNum, _Sms) -> +match({bnumber,Addr}, BNum, _Sms) -> %% receiving modem match_addr(Addr, BNum); match(Match, _BNum, Sms) when is_record(Sms,gsms_deliver_pdu) -> @@ -403,8 +409,8 @@ match(_, _BNum, _)-> match_clause([A|As], BNum, Sms) -> case match(A, BNum, Sms) of - true -> match_clause(As, BNum, Sms); - false -> false + true -> match_clause(As, BNum, Sms); + false -> false end; match_clause([], _BNum, _Sms) -> true. diff --git a/deps/gsms/src/gsms_session.erl b/deps/gsms/src/gsms_session.erl new file mode 100644 index 0000000..7608d4e --- /dev/null +++ b/deps/gsms/src/gsms_session.erl @@ -0,0 +1,123 @@ +-module(gsms_session). +-behaviour(gen_server). + +-export([new/2, + send/3, + get_signal_strength/1, + subscribe/2, + unsubscribe/2 + ]). + +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-include("gsms.hrl"). + +-record(st, {mod, + mod_state, + subscribers = []}). + +-type mod_state() :: any(). +-type dest() :: any(). +-type body() :: any(). +-type option() :: {atom(), any()}. +-type options() :: [option()]. + +-type cb_return() :: {ok, mod_state()} | {error, any()}. + +-callback init(options()) -> {ok,gsms_addr(),[{atom(), term()}]} + | {error, any()}. +-callback handle_send(dest(), body(), mod_state()) -> cb_return(). +-callback mandatory_options() -> [atom()]. + +new(Mod, Opts) -> + true = valid_opts(Opts, Mod), + {ok, Pid} = gen_server:start_link(?MODULE, {Mod, Opts}, []), + Pid. + +send(Session, Opts, Body) -> + call_(Session, {send, Opts, Body}). + +get_signal_strength(Session) -> + call_(Session, get_signal_strength). + +subscribe(Session, Filter) -> + case lists:keytake(reg_exp, 1, Filter) of + {value, RegExp, Rest} when is_list(RegExp) -> + case re:compile(RegExp, [unicode]) of + {ok, MP} -> + call_(Session, {subscribe, [{reg_exp, MP} | Rest]}); + {error, _} = E -> + E + end; + {value, _, _} -> + %% Assume MP format + call_(Session, {subscribe, Filter}); + false -> + call_(Session, {subscribe, Filter}) + end. + +unsubscribe(Session, Ref) -> + call_(Session, {unsubscribe, Ref}). + +mandatory_options() -> + []. + +init({Mod, Opts}) -> + case Mod:init(Opts) of + {ok, BNumber, Attrs, ModSt} -> + gsms_router:join(BNumber, ?MODULE, Attrs), + {ok, #st{mod = Mod, + mod_state = ModSt}}; + Other -> + Other + end. + +handle_call({send, Opts, Body}, _From, #st{mod = Mod, + mod_state = ModSt} = S) -> + case Mod:handle_send(Opts, Body, ModSt) of + {ok, Reply, ModSt1} -> + {reply, Reply, S#st{mod_state = ModSt1}}; + {error, _} = Error -> + {reply, Error, S} + end; +handle_call(get_signal_strength, _From, #st{mod = Mod, + mod_state = ModSt} = S) -> + case Mod:get_signal_strength(ModSt) of + {ok, Res, St1} -> + {reply, Res, S#st{mod_state = St1}}; + {error, _} = E -> + {reply, E, S} + end; +handle_call({subscribe, _Pattern}, _From, #st{subscribers = _Subs} = S) -> + {reply, {error, nyi}, S}. + +handle_cast(_Msg, S) -> + {noreply, S}. + +handle_info(_Msg, S) -> + {noreply, S}. + +terminate(_Reason, _State) -> + ok. + +code_change(_FromVsn, State, _Extra) -> + {ok, State}. + +valid_opts(Opts, Mod) -> + Mandatory = mandatory_options() ++ Mod:mandatory_options(), + case [O || O <- Mandatory, + not lists:keymember(O, 1, Opts)] of + [] -> + true; + [_|_] = Missing -> + erlang:error({mandatory, lists:usort(Missing)}) + end. + +call_(Session, Req) -> + gen_server:call(Session, Req). + diff --git a/priv/config/rvi_common.config b/priv/config/rvi_common.config index 88e4431..06cee14 100644 --- a/priv/config/rvi_common.config +++ b/priv/config/rvi_common.config @@ -21,7 +21,9 @@ Out = filename:absname(proplists:get_value(outdir, OPTIONS)). crypto, public_key, base64url, + uart, exo, + gsms, compiler, ssl, asn1, @@ -46,6 +48,7 @@ Out = filename:absname(proplists:get_value(outdir, OPTIONS)). bt, dlink_tcp, dlink_bt, + dlink_sms, proto_bert, proto_json %% If adding apps, you can still include this config, and complement diff --git a/rebar.config b/rebar.config index 26fd2b1..0c81b9e 100644 --- a/rebar.config +++ b/rebar.config @@ -9,6 +9,7 @@ "components/rvi_common", "components/authorize", "components/dlink_bt", + "components/dlink_sms", "components/dlink_tcp", "components/proto_bert", "components/proto_json", @@ -30,6 +31,6 @@ {bt, ".*", {git, "git://github.com/magnusfeuer/bt.git", "HEAD"}}, {dthread, ".*", {git, "git://github.com/tonyrog/dthread.git", "HEAD"}}, {uart, ".*", {git, "git://github.com/tonyrog/uart.git", "HEAD"}}, - {gsms, ".*", {git, "git://github.com/tonyrog/gsms.git", "HEAD"}}, + {gsms, ".*", {git, "git://github.com/tonyrog/gsms.git", {branch,"uw-session-behavior"}}}, {base64url, ".*", {git, "git://github.com/dvv/base64url.git", "HEAD"}} ]}. |