diff --git a/.travis.yml b/.travis.yml index 8016c8b..8c4432d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ sudo: false language: erlang otp_release: - - "19.0" + - "19.1" cache: directories: - - $HOME/otp/19.0 + - $HOME/otp/19.1 - $HOME/.cache/rebar3 - _plt install: "true" @@ -15,5 +15,5 @@ branches: - develop notifications: email: - - priestjim@gmail.com + - pj@ezgr.net diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cd2ba2a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +Below is a non-exhaustive list of changes between `gen_rpc` versions. + +## 2.0.0 + +This release boasts a major rengineer/refactor of `gen_rpc` that includes quite a few new features: + +- The server and acceptor FSMs have been converted to `gen_statem` to follow Erlang's best practices + and development. As a result, **support for Erlang releases older than 19.1 has been dropped**. + +- Specific options, leveraging Erlang 19, have been enabled such as off-heap mailboxes for client and acceptor, + and higher priorities for all `gen_rpc` processes. + +- Ports are not dynamically assigned anymore as it shows that, after some research, offers no additional benefits + to having a static port listener. That means less processes to supervise and less moving parts where something can + go wrong. + +- Support for SSL has been added. Please refer to the [README](README.md#ssl-configuration) for more information on + how to use it. + +- Module version control support has been added, effectively allowing you to only make RPC calls to nodes that + run specific versions of modules. + +- `lager` support has been **dropped** in favor of the logging backend-agnostic library `hut`, in order to better support + Elixir installations. The test suite and development profiles still use lager but this doesn't interfere with production + deployments of `gen_rpc`. + +- Tests have been updated to test more edge cases, including new SSL functionality. + +- Some options in `gen_rpc.app.src` have changed names to better describe what they do. Again, pleaserefer to the README to + verify your preexisting settings are consistent with their new names. + +- Various smaller bugs have been fixed and various responses have been massaged for consistency. + +## 1.0.2 + +- Implemented blacklisting/whitelisting of modules available for RPC. + +- Implemented abcast and sbcast support. + +## 1.0.1 + +- Updated documentation + +- Updated/optimized various TCP options + +- Updated tests to include more edge cases + +- Support client-configurable listener port per remote node + +- Small code refactoring and cleanup + +## 1.0.0 + +This release drops the hybrid RPC/TCP approach and uses a separate TCP listener to emulate initial RPC communication. +In addition, this release includes: + +- Updated documentation + +- Added integration tests using Docker diff --git a/Makefile b/Makefile index ccd260a..8629880 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ .PHONY: all test dialyzer xref spec dist coveralls # Run targets -.PHONY: shell +.PHONY: shell shell-master shell-slave # Misc targets .PHONY: clean testclean distclean tags rebar @@ -79,7 +79,6 @@ endif integration: image @export NODES=$(NODES) && cd test/integration && bash -c "./integration-tests.sh $(NODES)" - endif # ============================================================================= @@ -111,8 +110,13 @@ coveralls: $(COVERDATA) # Run targets # ============================================================================= -shell: $(REBAR) epmd - @REBAR_PROFILE=dev $(REBAR) do shell --name gen_rpc@127.0.0.1 --config test/gen_rpc.config +shell: shell-master + +shell-master: $(REBAR) epmd + @REBAR_PROFILE=dev $(REBAR) do shell --name gen_rpc_master@127.0.0.1 --config test/gen_rpc.master.config + +shell-slave: $(REBAR) epmd + @REBAR_PROFILE=dev $(REBAR) do shell --name gen_rpc_slave@127.0.0.1 --config test/gen_rpc.slave.config # ============================================================================= # Misc targets diff --git a/README.md b/README.md index 8086248..25043de 100644 --- a/README.md +++ b/README.md @@ -9,37 +9,17 @@ - License: [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/priestjim/gen_rpc/master/LICENSE) - [Erlang Factory 2016 Talk](https://www.youtube.com/watch?feature=player_embedded&v=xiPnLACtNeo) -## Rationale - -**TL;DR**: `gen_rpc` uses a mailbox-per-node architecture and `gen_tcp` processes to parallelize data reception from multiple nodes without blocking the VM's distributed port. - -The reasons for developing `gen_rpc` became apparent after a lot of trial and error while trying to scale a distributed Erlang infrastructure using the `rpc` library initially and subsequently `erlang:spawn/4` (remote spawn). Both these solutions suffer from very specific issues under a sufficiently high number of requests. - -The `rpc` library operates by shipping data over the wire via Distributed Erlang's ports into a registered `gen_server` on the other side called `rex` (Remote EXecution server), which is running as part of the standard distribution. In high traffic scenarios, this allows the inherent problem of running a single `gen_server` server to manifest: mailbox flooding. As the number of nodes participating in a data exchange with the node in question increases, so do the messages that `rex` has to deal with, eventually becoming too much for the process to handle (don't forget this is confined to a single thread). - -Enter `erlang:spawn/4` (_remote spawn_ from now on). Remote spawn dynamically spawns processes on a remote node, skipping the single-mailbox restriction that `rex` has. The are various libraries written to leverage that loophole (such as [Rexi](https://github.com/cloudant/rexi)), however there's a catch. - -Remote spawn was not designed to ship large amounts of data as part of the call's arguments. Hence, if you want to ship a large binary such as a picture or a transaction log (large can also be small if your network is slow) over remote spawn, sooner or later you'll see this message popping up in your logs if you have subscribed to the system monitor through `erlang:system_monitor/2`: - -```erlang -{monitor,<4685.187.0>,busy_dist_port,#Port<4685.41652>} -``` - -This message essentially means that the VM's distributed port pair was busy while the VM was trying to use it for some other task like _Distributed Erlang heartbeat beacons_ or _mnesia synchronization_. This of course wrecks havoc in certain timing expectations these subsystems have and the results can be very problematic: the VM might detect a node as disconnected even though everything is perfectly healthy and `mnesia` might misdetect a network partition. - -`gen_rpc` solves both these problems by sharding data coming from different nodes to different processes (hence different mailboxes) and by using different `gen_tcp` ports for different nodes (hence not utilizing the Distributed Erlang ports). - -# Build Dependencies +## Build Dependencies To build this project you need to have the following: -* **Erlang/OTP** >= 19.0 +* **Erlang/OTP** >= 19.1 * **git** >= 1.7 * **GNU make** >= 3.80 -* **rebar3** >= 3.0-beta4 +* **rebar3** >= 3.1 ## Usage @@ -51,11 +31,11 @@ Getting started with `gen_rpc` is easy. First, add the appropriate dependency li ]}. ``` -Or if you're using `hex.pm`: +Or if you're using `hex.pm`/`rebar3`: ```erlang {deps [ - {gen_rpc, "1.0.2"} + {gen_rpc, "~> 2.0"} ]}. ``` @@ -65,7 +45,7 @@ Or if you're using Elixir/Mix: def project do [ deps: [ - {:gen_rpc, "~> 1.0.0"} + {:gen_rpc, "~> 2.0"} ] ] ``` @@ -93,6 +73,98 @@ Finally, start a couple of nodes to test it out: 'other_node@1.2.3.4' ``` +## Application settings + +- `tcp_server_port`: The plain TCP port `gen_rpc` will use for incoming connections or `false` if you + do not want plain TCP enabled. + +- `tcp_client_port`: The plain TCP port `gen_rpc` will use for outgoing connections. + +- `ssl_server_port`: The port `gen_rpc` will use for incoming SSL connections or `false` if you do not + want SSL enabled. + +- `ssl_client_port`: The port `gen_rpc` will use for outgoing SSL connections. + +- `ssl_server_options` and `ssl_client_options`: Settings for the `ssl` interface that `gen_rpc` will use to + connect to a remote `gen_rpc` server. + +- `default_client_driver`: The default driver `gen_rpc` is going to use to connect to remote `gen_rpc` nodes. + It should be either `tcp` or `ssl`. + +- `client_config_per_node`: A map of `Node => {Driver, Port}` or `Node => Port` that instructs `gen_rpc` on the `Port` + and/or `Driver` to use when connecting to a `Node`. + +- `rpc_module_control`: Set it to `blacklist` to define a list of modules that will not be exposed to `gen_rpc` or to `whitelist` + to define the list of modules that will be exposed to `gen_rpc`. Set it to `disabled` to disable this feature. + +- `rpc_module_list`: The list of modules that are going to be blacklisted or whitelisted. + +- `authentication_timeout`: Default timeout for the authentication state of an incoming connection in **milliseconds**. + Used to protect against half-open connections in a DoS attack. + +- `connect_timeout`: Default timeout for the initial node-to-node connection in **milliseconds**. + +- `send_timeout`: Default timeout for the transmission of a request (`call`/`cast` etc.) from the local node to the remote node in **milliseconds**. + +- `call_receive_timeout`: Default timeout for the reception of a response in a `call` in **milliseconds**. + +- `sbcast_receive_timeout`: Default timeout for the reception of a response in an `sbcast` in **milliseconds**. + +- `client_inactivity_timeout`: Inactivity period in **milliseconds** after which a client connection to a node will be closed (and hence have the TCP file descriptor freed). + +- `server_inactivity_timeout`: Inactivity period in **milliseconds** after which a server port will be closed (and hence have the TCP file descriptor freed). + +- `async_call_inactivity_timeout`: Inactivity period in **milliseconds** after which a pending process holding an `async_call` return value will exit. This is used for process sanitation purposes so please make sure to set it in a sufficiently high number (or `infinity`). + +## Logging + +`gen_rpc` uses [hut](https://github.com/tolbrino/hut) for logging. This allows the developer to integrate the logging library of their choice by providing the appropriate definition in their `rebar.config`. The default logging facility of `hut` is SASL. + +For more information on how to enable `gen_rpc` to use your own logging facility, consult the [README.md](https://github.com/tolbrino/hut#supported-logging-backends) of `hut`. + +## SSL Configuration + +`gen_rpc` supports SSL for inter-node communication. This allows secure communication and execution over insecure channels such as the internet, essentially allowing a **trully globally distributed Erlang/Elixir** setup. `gen_rpc` is very opinionated on how SSL should be configured and the bundled default options include: + +- A proper PFS-enabled cipher suite + +- Both server and client-based SSL node CN (Common Name) verification + +- Secure renegotiation + +- TLS 1.1/1.2 enforcement + +All of these settings can be found in `include/ssl.hrl` and overriden by redefining the necessary option in `ssl_client_options` and `ssl_server_options`. To actually use SSL support, you'll need to define in both `ssl_client_options` and `ssl_server_options`: + +- The public and private keys in PEM format, for the node you're running `gen_rpc` on, using the usual `certfile`, `keyfile` options. + +- The public key of the CA that signs the node's key and the public key(s) of CA that `gen_rpc` should trust, included in the file `cacertfile` points at. + +- Optionally, a Diffie-Hellman parameter file using the `dhfile` option. + +To generate your own self-signed CA and node certificates, numerous articles can be found online such as [this](https://help.github.com/enterprise/11.10.340/admin/articles/using-self-signed-ssl-certificates/). + +Usually, the CA that will be signing your client and server SSL certificates will be the same so a nominal `sys.confg` that includes SSL support for `gen_rpc` will look like: + +```erlang + {gen_rpc, [ + {ssl_client_options, [ + {certfile, "priv/cert.pem"}, + {keyfile, "priv/cert.key"}, + {cacertfile, "priv/ca.pem"}, + {dhfile, "priv/dhparam.pem"} + ]}, + {ssl_server_options, [ + {certfile, "priv/cert.pem"}, + {keyfile, "priv/cert.key"}, + {cacertfile, "priv/ca.pem"}, + {dhfile, "priv/dhparam.pem"} + ]} + ]} +``` + +For multi-site deployments, a performant setup can be provisioned with edge `gen_rpc` nodes using SSL over the internet and plain TCP for internal data exchange. In that case, non-edge nodes can have `{ssl_server_port, false}` and `{default_client_driver, tcp}` and edge nodes can have their plain TCP port firewalled externally and `{default_client_driver, ssl}`. + ## Build Targets `gen_rpc` bundles a `Makefile` that makes development straightforward. @@ -111,7 +183,13 @@ To run the full test suite, the XRef tool and Dialyzer, run: To build the project and drop in a console while developing, run: - make shell + make shell-master + +or + + make shell-slave + +If you want to run a "master" and a "slave" `gen_rpc` nodes to run tests. To clean every build artifact and log, run: @@ -152,31 +230,29 @@ For more information on what the functions below do, run `erl -man rpc`. - `eval_everywhere(Module, Function, Args)` and `eval_everywhere(Nodes, Module, Function, Args)`: Multi-node version of the `cast` function. -### Application settings +### Module version control -- `tcp_server_port`: The port in which the TCP listener service listens for incoming client requests. +`gen_rpc` supports executing RPC calls on remote nodes that are running only specific module versions. To leverage that feature, in place of `Module` in the section above, use `{Module, Version}`. If the remote `module` is not on the version requested a `{badrpc,incompatible]` will be returned. -- `remote_tcp_server_ports`: A proplist with the nodes that run on alternative `tcp_server_port` configuration and the port - they have configured `gen_rpc` to listen to. Useful when running multiple nodes on the same system and you get port clashes. - -- `rpc_module_control`: Set it to `blacklist` to define a list of modules that will not be exposed to `gen_rpc` or to `whitelist` - to define the list of modules that will be exposed to `gen_rpc`. Set it to `undefined` to disable this feature. +## Rationale -- `rpc_module_list`: The list of modules that are going to be blacklisted or whitelisted. +**TL;DR**: `gen_rpc` uses a mailbox-per-node architecture and `gen_tcp` processes to parallelize data reception from multiple nodes without blocking the VM's distributed port. -- `connect_timeout`: Default timeout for the initial node-to-node connection in **milliseconds**. +The reasons for developing `gen_rpc` became apparent after a lot of trial and error while trying to scale a distributed Erlang infrastructure using the `rpc` library initially and subsequently `erlang:spawn/4` (remote spawn). Both these solutions suffer from very specific issues under a sufficiently high number of requests. -- `send_timeout`: Default timeout for the transmission of a request (`call`/`cast` etc.) from the local node to the remote node in **milliseconds**. +The `rpc` library operates by shipping data over the wire via Distributed Erlang's ports into a registered `gen_server` on the other side called `rex` (Remote EXecution server), which is running as part of the standard distribution. In high traffic scenarios, this allows the inherent problem of running a single `gen_server` server to manifest: mailbox flooding. As the number of nodes participating in a data exchange with the node in question increases, so do the messages that `rex` has to deal with, eventually becoming too much for the process to handle (don't forget this is confined to a single thread). -- `call_receive_timeout`: Default timeout for the reception of a response in a `call` in **milliseconds**. +Enter `erlang:spawn/4` (_remote spawn_ from now on). Remote spawn dynamically spawns processes on a remote node, skipping the single-mailbox restriction that `rex` has. The are various libraries written to leverage that loophole (such as [Rexi](https://github.com/cloudant/rexi)), however there's a catch. -- `sbcast_receive_timeout`: Default timeout for the reception of a response in an `sbcast` in **milliseconds**. +Remote spawn was not designed to ship large amounts of data as part of the call's arguments. Hence, if you want to ship a large binary such as a picture or a transaction log (large can also be small if your network is slow) over remote spawn, sooner or later you'll see this message popping up in your logs if you have subscribed to the system monitor through `erlang:system_monitor/2`: -- `client_inactivity_timeout`: Inactivity period in **milliseconds** after which a client connection to a node will be closed (and hence have the TCP file descriptor freed). +```erlang +{monitor,<4685.187.0>,busy_dist_port,#Port<4685.41652>} +``` -- `server_inactivity_timeout`: Inactivity period in **milliseconds** after which a server port will be closed (and hence have the TCP file descriptor freed). +This message essentially means that the VM's distributed port pair was busy while the VM was trying to use it for some other task like _Distributed Erlang heartbeat beacons_ or _mnesia synchronization_. This of course wrecks havoc in certain timing expectations these subsystems have and the results can be very problematic: the VM might detect a node as disconnected even though everything is perfectly healthy and `mnesia` might misdetect a network partition. -- `async_call_inactivity_timeout`: Inactivity period in **milliseconds** after which a pending process holding an `async_call` return value will exit. This is used for process sanitation purposes so please make sure to set it in a sufficiently high number (or `infinity`). +`gen_rpc` solves both these problems by sharding data coming from different nodes to different processes (hence different mailboxes) and by using a different `gen_tcp` port for different nodes (hence not utilizing the Distributed Erlang ports). ## Architecture @@ -188,21 +264,17 @@ In order to achieve the mailbox-per-node feature, `gen_rpc` uses a very specific - The `dispatcher` process will launch a new `client` process through the client's supervisor. -- The new client process will connect to the remote node's `tcp listener`, submit a requeset for a new server and wait. - -- The `tcp listener` server will ask the `server` supervisor to launch a new `server` process, which in turn will dynamically allocate (`gen_tcp:listen(0)`) a port and return it. - -- The `server` supervisor returns the port to the `tcp listener` which in turn returns it to the `client` through the TCP channel. +- The new client process will connect to the remote node's `gen_rpc server`, submit a request for a new server and wait. -- The `server` then shuts down the TCP channel as its purpose has been fullfilled (which also minimizes file descriptor usage). +- The `gen_rpc server` server will ask the `acceptor` supervisor to launch a new `acceptor` process and hands it off the new socket connection. -- The `client` then connects to the returned port and establishes a TCP session. The `server` on the other node launches a new `acceptor` server as soon as a `client` connects. The relationship between `client`-`server`-`acceptor` is one-to-one-to-one. +- The `acceptor` takes over the new socket and authenticates the `client` with the current Erlang cookie and any extra protocol-level authentication supported by the selected driver. - The `client` finally encodes the request (`call`, `cast` etc.) along with some metadata (the caller's PID and a reference) and sends it over the TCP channel. In case of an `async call`, the `client` also launches a process that will be responsible for handing the server's reply to the requester. -- The `server` on the other side decodes the TCP message received and spawns a new process that will perform the requested function. By spawning a process external to the server, the `server` protects itself from misbehaving function calls. +- The `acceptor` on the other side decodes the TCP message received and spawns a new process that will perform the requested function. By spawning a process external to the server, the `acceptor` protects itself from misbehaving function calls. -- As soon as the reply from the server is ready (only needed in `async_call` and `call`), the `server` spawned process messages the server with the reply, the `server` ships it through the TCP channel to the `client` and the client send the message back to the requester. In the case of `async call`, the `client` messages the spawned worker and the worker replies to the caller with the result. +- As soon as the reply from the server is ready (only needed in `async_call` and `call`), the `acceptor` spawned process messages the server with the reply, the `acceptor` ships it through the TCP channel to the `client` and the client send the message back to the requester. In the case of `async call`, the `client` messages the spawned worker and the worker replies to the caller with the result. All `gen_tcp` processes are properly linked so that any TCP failure will cascade and close the TCP channels and any new connection will allocate a new process and port. diff --git a/TODO.md b/TODO.md index ef9fda0..44a502b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,4 @@ This is a list of pending features or code technical debt for `gen_rpc`: -- Implement SSL connectivity, including CN-based authentication - Implement per-id-and-node tuple connection sharing to spread workload on multiple mailboxes per node -- Implement static port range allocation (instead of listening to 0) to adhere to potential corporate firewall rules \ No newline at end of file diff --git a/include/app.hrl b/include/app.hrl index a8ceb9d..845d6dc 100644 --- a/include/app.hrl +++ b/include/app.hrl @@ -5,35 +5,3 @@ %%% -define(APP, gen_rpc). - -%%% Default TCP options --define(DEFAULT_TCP_OPTS, [binary, - {packet,4}, - {exit_on_close,true}, - {show_econnreset, true}, % Send message for reset connections - {nodelay,true}, % Send our requests immediately - {send_timeout_close,true}, % When the socket times out, close the connection - {delay_send,false}, % Scheduler should favor timely delivery - {linger,{true,2}}, % Allow the socket to flush outgoing data for 2" before closing it - useful for casts - {reuseaddr,true}, % Reuse local port numbers - {keepalive,true}, % Keep our channel open - {tos,72}, % Deliver immediately - {active,false}]). % Retrieve data from socket upon request - -%%% Default TCP options --define(ACCEPTOR_DEFAULT_TCP_OPTS, [binary, - {packet,4}, - {exit_on_close,true}, - {active,once}]). % Retrieve data from socket upon request - -%%% The TCP options that should be copied from the listener to the acceptor --define(ACCEPTOR_TCP_OPTS, [nodelay, - show_econnreset, - send_timeout_close, - delay_send, - linger, - reuseaddr, - keepalive, - tos, - active]). - diff --git a/include/ssl.hrl b/include/ssl.hrl new file mode 100644 index 0000000..bb6a9e4 --- /dev/null +++ b/include/ssl.hrl @@ -0,0 +1,45 @@ +%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. +%%% + +%%% Default SSL options common to client and server +-define(SSL_DEFAULT_COMMON_OPTS, [binary, + {packet,0}, + {header,0}, + {exit_on_close,true}, + {nodelay,true}, % Send our requests immediately + {send_timeout_close,true}, % When the socket times out, close the connection + {delay_send,false}, % Scheduler should favor timely delivery + {linger,{true,2}}, % Allow the socket to flush outgoing data for 2" before closing it - useful for casts + {reuseaddr,true}, % Reuse local port numbers + {keepalive,true}, % Keep our channel open + {tos,72}, % Deliver immediately + {active,false}, + %% SSL options + {ciphers,["ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA", + "ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384", + "ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256", + "AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256", + "ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256", + "ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256", + "AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"]}, + {secure_renegotiate,true}, + {reuse_sessions,true}, + {versions,['tlsv1.2','tlsv1.1']}, + {verify,verify_peer}, + {hibernate_after,600000}, + {active,false}]). + +-define(SSL_DEFAULT_SERVER_OPTS, [{fail_if_no_peer_cert,true}, + {log_alert,false}, + {honor_cipher_order,true}, + {client_renegotiation,true}]). + +-define(SSL_DEFAULT_CLIENT_OPTS, [{server_name_indication,disable}, + {depth,99}]). diff --git a/include/tcp.hrl b/include/tcp.hrl new file mode 100644 index 0000000..f7da013 --- /dev/null +++ b/include/tcp.hrl @@ -0,0 +1,36 @@ +%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. +%%% + +%%% Default TCP options +-define(TCP_DEFAULT_OPTS, [binary, + {packet,4}, + {exit_on_close,true}, + {show_econnreset,true}, % Send message for reset connections + {nodelay,true}, % Send our requests immediately + {send_timeout_close,true}, % When the socket times out, close the connection + {delay_send,false}, % Scheduler should favor timely delivery + {linger,{true,2}}, % Allow the socket to flush outgoing data for 2" before closing it - useful for casts + {reuseaddr,true}, % Reuse local port numbers + {keepalive,true}, % Keep our channel open + {tos,72}, % Deliver immediately + {active,false}]). % Retrieve data from socket upon request + +%%% Default TCP options +-define(ACCEPTOR_DEFAULT_TCP_OPTS, [binary, + {packet,4}, + {exit_on_close,true}, + {active,once}]). % Retrieve data from socket upon request + +%%% The TCP options that should be copied from the listener to the acceptor +-define(ACCEPTOR_COPY_TCP_OPTS, [nodelay, + show_econnreset, + send_timeout_close, + delay_send, + linger, + reuseaddr, + keepalive, + tos, + active]). diff --git a/package.exs b/package.exs index f65081f..9f32170 100644 --- a/package.exs +++ b/package.exs @@ -3,13 +3,13 @@ defmodule GenRPC.Mixfile do def project do [app: :gen_rpc, - version: "1.0.0", + version: "2.0.0", description: "A scalable RPC library for Erlang-VM based languages", package: package] end defp package do - [files: ~w(include src LICENSE Makefile package.exs README.md TODO.md rebar.config), + [files: ~w(include src LICENSE Makefile package.exs README.md TODO.md CHANGELOG.md rebar.config rebar.config.script otp-release.escript), maintainers: ["Panagiotis PJ Papadomitsos"], licenses: ["Apache 2.0"], links: %{"GitHub" => "https://github.com/priestjim/gen_rpc"}] diff --git a/priv/ssl/ca.cert.pem b/priv/ssl/ca.cert.pem new file mode 100644 index 0000000..8e778c0 --- /dev/null +++ b/priv/ssl/ca.cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIJAI7iBfguHJwOMA0GCSqGSIb3DQEBCwUAMGsxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMQ8wDQYDVQQKEwZHZW5SUEMxHjAcBgNVBAMTFUNlcnRpZmljYXRlIEF1dGhv +cml0eTAeFw0xNjA5MTkwNjU0MDVaFw0zNjA5MTQwNjU0MDVaMGsxCzAJBgNVBAYT +AlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv +MQ8wDQYDVQQKEwZHZW5SUEMxHjAcBgNVBAMTFUNlcnRpZmljYXRlIEF1dGhvcml0 +eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPZgNiAtglkGKOiPZ3Ai +Zakd/teUIOwsqVaWBIsYDi1uSmj2UNSe9dFKTBRG6DqaA7+ZasKvJsyhV23woasV +5nqeZnO++U/bbGT06CeoGJSdF1E0WJjZq0LuizQlZ+aJF8hVcYdO4oiI8wPkp8F0 +YPDSzloybRIcXOixUQVXQPwoGL+f8TO+sdcgRdq2pMgTxVV8wPkTif0DjUUkDWTz +KzSMy9ZKv1fYGRMWlXNkgtOuf1vccCwFnoKamwnYIo80oh9DOmWvYR4U5S7Euyes +UZzNzEoFBZDRB3agbzaiutdURBzMGXdOWPxSC7Apdhe6aKhOND6gzW+xLcxTbs9u +21kCAwEAAaOB0DCBzTAdBgNVHQ4EFgQUT1/pE9DF+qIF3fYMB2DH10vi9TwwgZ0G +A1UdIwSBlTCBkoAUT1/pE9DF+qIF3fYMB2DH10vi9Tyhb6RtMGsxCzAJBgNVBAYT +AlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv +MQ8wDQYDVQQKEwZHZW5SUEMxHjAcBgNVBAMTFUNlcnRpZmljYXRlIEF1dGhvcml0 +eYIJAI7iBfguHJwOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB+J +UUlLraMCVzmjpJ51kNAHQeR//c0PR3jboc/K3IvDBGPyTpU5oLS+9Lxv9Bp67XF4 +VOo7V7SjDJCWwFqeMYzU8pgKZZjKUiZkc5Bt6HhgnkBoZ0vOO6UnUcmdyYansDqw +bNq8hQR57Aw9pghOohjwrFSH8g275IU950xvHCsZeyjkB8vOaVLaDbIkipydGa6d +2fTJ3Nsjjd7Fzb5kiuKUwsUAfN8uTRRUimvaX89QEXh1+cr1PkzucssNXhZ0+HBT +OJcO/IjpdVSiCM8W4wf5he5yIL9eIdN7Y+BrN+KcDAWThXpxCRSYTeteyS4qQayi +sBnI4Aea3SDDw9004OI= +-----END CERTIFICATE----- diff --git a/priv/ssl/ca.key.pem b/priv/ssl/ca.key.pem new file mode 100644 index 0000000..009ca54 --- /dev/null +++ b/priv/ssl/ca.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA9mA2IC2CWQYo6I9ncCJlqR3+15Qg7CypVpYEixgOLW5KaPZQ +1J710UpMFEboOpoDv5lqwq8mzKFXbfChqxXmep5mc775T9tsZPToJ6gYlJ0XUTRY +mNmrQu6LNCVn5okXyFVxh07iiIjzA+SnwXRg8NLOWjJtEhxc6LFRBVdA/CgYv5/x +M76x1yBF2rakyBPFVXzA+ROJ/QONRSQNZPMrNIzL1kq/V9gZExaVc2SC065/W9xw +LAWegpqbCdgijzSiH0M6Za9hHhTlLsS7J6xRnM3MSgUFkNEHdqBvNqK611REHMwZ +d05Y/FILsCl2F7poqE40PqDNb7EtzFNuz27bWQIDAQABAoIBABRIGUyO0oMnmaiD +XZ2Ch2HjoT336Lnod0w0b01P/qLIyIFZfY/zQgMnnZmxuJ5SXWqhY4OBTa1cvsjD +HVvrIp0HeyGeIAHt6z9oVdAl61gYknnR2FVE3e9VmcoCJkp5EKciOYVM/iMFOxQf +95taTDym7evI6OerxA43wpaw9x2Kuh1HJRL3okLgdTIdYSjtU2vsOb7TcEf8J8fF +fXr2BMycW+1l7wyCHuHD2xIK2W0ldT7UbLOLt15txKtrv8iK6yvYgSfEx5M3LXvU +nmXJgYssfcMVGyAm6/Rj8UvvYLTPOArAuKjkoOWEt7OwDvsCfRXJvNXLeTTZ/d+g +ySAMDt0CgYEA/RcCZ4CLamNJp5FRUo2Wdw64RLNhD5RvOzepY1ni1JzQlF0eU80+ +qzsOiR/G1WAI0SjophhGapb6ADKW7hmq6cNfXOiIahM53Xh7Q3pHjNfoYl3Mgcgl +lS6CjuVa4pkqiMLF2+xo9hLLvNHe6gv0QhCaH1glA7R4t1TmKdJqWw8CgYEA+TVw +TNCKAmL1gXThoXUp6s5+v2nYq+FShEJ4L1AIzguamHmzuugSCCk7rtoxFi1UbNhJ +10UfuQ/fqbtJNumqYVgoJcqw16E9mdB5qs+EBfuC0BsPvoQx90XkMLS056fXOJO8 +h2UolmPTN1xVVZGQhrdEWgL8tpfP/OoJunA4gxcCgYBU/IADL4ghTnvyN/r95Kut +CRVZhH7IU4jScst/oDRqPspPt8EyVM6Bg1BLPY9wIVJ0oe8VrzseGdqTbGmARMA+ +xStlsHP2YsRgmU+TBiG2qt3PAj5lKkdsg/S1dVECnHpK+FSmmAla8E7X9gWLxD3e +XWLTua6cQuLydSDg1FaAgwKBgGltFwYYDtp1nODYXn+cflAsbWhy0cvc9bioGgoB +9MeeFxe8HLHMbZxwPSR6f9ue76FgWtSsFICI25kFdzK45XBrCAj815VRTCsfC+mc +AoE5Zpy9Zgq2CCFQuvlz6Tg7RAxWS6KHxATjVD6OXDdn7llffJYLv3dLgBzBsb3i +rbWfAoGBALYYSCG9+O0Sb3zSKHSF7QRdw1eaPJeIVDXYHmkZ6zzBBwr6DZ1+DMKF +xSA5R+gsJ6mOpjO9WDfMq2KBanYM9Ow9oUiETugssjO04i5NHB9g+kvqa0A19Bfz +oGrockn8xXmqBrenSZuPFriw3xOltOTEdEyN1bJ5EYZ7OXCqcB7f +-----END RSA PRIVATE KEY----- diff --git a/priv/ssl/ca.srl b/priv/ssl/ca.srl new file mode 100644 index 0000000..fd6c3f3 --- /dev/null +++ b/priv/ssl/ca.srl @@ -0,0 +1 @@ +BD6B6A958A3C4681 diff --git a/priv/ssl/dhparam.pem b/priv/ssl/dhparam.pem new file mode 100644 index 0000000..d291ef3 --- /dev/null +++ b/priv/ssl/dhparam.pem @@ -0,0 +1,13 @@ +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEAroDK5Kfo2pS0vAPo/cNigg2LGH2/q+ss4ZD+UW3yDXKrGwjh5/94 +hSx3pjlnrSvY9bw04gXFoz+FGn7zJ1f128+DcRpNIhD9H9ucvnLgfIthjQil1FWS +m38UTJQJnNCJF2FhT027MQamWaILcZ++1XbrvMovPkMekBuAPs2fnycBW2UqSbxT +ePx6o4PimP2eEIhTMrWmwJF3/pZA0k1MGcXziadpwRJtW3D2yCBNRbDTmFrXMp7J +lsWgtIcictxrI56WMtvwAaGwhKAzK5iaZJCMEi7PqsEltfO3lbSNPzyzXbR8rADA +1moh8HQDsVbPBafSI5qvo4X8+Nk5lSPpVKa4voRD4QWxnw1oc+kK/xCojoCZ2ApG +9foHjoJ+hRq+9gBxuYa9v/5WTgnub7v8HshqAnbjxubTlOgwISkQv5DMSaqtI8mx +JPq+P9rU5g+5H/i/v5GMOvW1r2ZFubM93Nx051RVNOzNBYGPr9gpaMxPiXkH5a+S +m8iYX+XlFUeseV2eCTtK19+154hdDlbGKoBbHsAkc5AiyOU8P0+6jHdL9QS6nSzV +3CSuFZYZq3pSGUPCN1IH74VXPa5hWdlblItVnzlbBHqCjZippZb8evduu+b0Lncw +5eSeY9weGyHHk6XHnwuPqetk3kg4e4wkX0YAwOXuu3E5f0KhLEQO51sCAQI= +-----END DH PARAMETERS----- diff --git a/priv/ssl/gen_rpc_master@127.0.0.1.cert.pem b/priv/ssl/gen_rpc_master@127.0.0.1.cert.pem new file mode 100644 index 0000000..de7c584 --- /dev/null +++ b/priv/ssl/gen_rpc_master@127.0.0.1.cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj0CCQC9a2qVijxGgDANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEP +MA0GA1UEChMGR2VuUlBDMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +HhcNMTYwOTE5MDcwMDEyWhcNMzYwOTE0MDcwMDEyWjBuMQswCQYDVQQGEwJVUzET +MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGR2VuUlBDMSEwHwYDVQQDFBhnZW5fcnBjX21hc3RlckAxMjcuMC4wLjEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQ1dZtyRFzwdjS/5dus+2s +cFgTmCnNups8wduV3PYulHJ5nVOVYYFjNmKyvEDp3LcVfCtrLPkkApJAQP/tnTkl +feRTo+HTtLYQTkoTNqHIkqKmCNqJ+rTIs2EreUOEQVyLVgDOh8emiOl6UqUVb33J +4O03avWcZvp0fWIpXZhkcsLpRkctF3bUCP5YnbXLXHtqR5rkDicqrh/2e2Ml3k1Y +bPBtup+9H2sQ7c9pzPNAYySh/grIHMEgKMJ/qPAkQ6jsrqZxPjtY0NE5FGgbnB+j +4WsJfYfIBP78SZ2i8fRQnA9ceH39c8K7gwGWXw4HaVOmWJjKD5CrGHwsVX6EYl0P +AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACJJ5v6mUOY69lJW0XlWVL1jk7p170sz +IX0OD53CfWQhjCURlpRCmYBvJS3EVbDnJCjdS1hZORwXR5vcSUeaE7htPSyEBgr9 +Ougg2KEODBb5xlZdS8JMuOiwpHVPiyIjJDYskbyGvckYyQdTdPXXjyhQdHsnVJ2F +1csxGlMNI53F14Z1jicUwtzltkEOZg7aQzJ/ivrQptD03qEIZ95q6xn86zTe8sE0 +z94DTEWZRdcZkqmS/94soNidjLjeLGiOwt4pB1e23aVPRStpt5ymkQIv7DiEPuza +gNjp8kz2qS3610GfL0fCAj211j9541GYAcpoOIQVOiGmFlQ+xyH7dv4= +-----END CERTIFICATE----- diff --git a/priv/ssl/gen_rpc_master@127.0.0.1.csr.pem b/priv/ssl/gen_rpc_master@127.0.0.1.csr.pem new file mode 100644 index 0000000..0fa20d5 --- /dev/null +++ b/priv/ssl/gen_rpc_master@127.0.0.1.csr.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICyjCCAbICAQAwbjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkdlblJQQzEhMB8GA1UE +AxQYZ2VuX3JwY19tYXN0ZXJAMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA0NXWbckRc8HY0v+XbrPtrHBYE5gpzbqbPMHbldz2LpRyeZ1T +lWGBYzZisrxA6dy3FXwrayz5JAKSQED/7Z05JX3kU6Ph07S2EE5KEzahyJKipgja +ifq0yLNhK3lDhEFci1YAzofHpojpelKlFW99yeDtN2r1nGb6dH1iKV2YZHLC6UZH +LRd21Aj+WJ21y1x7akea5A4nKq4f9ntjJd5NWGzwbbqfvR9rEO3PaczzQGMkof4K +yBzBICjCf6jwJEOo7K6mcT47WNDRORRoG5wfo+FrCX2HyAT+/EmdovH0UJwPXHh9 +/XPCu4MBll8OB2lTpliYyg+Qqxh8LFV+hGJdDwIDAQABoBcwFQYJKoZIhvcNAQkC +MQgTBkdlblJQQzANBgkqhkiG9w0BAQUFAAOCAQEAtZuXPszqJdw70LYpVhUxRKJe +WPaVj9wt4h30fkhDUwuXbm7Ed1dyKIaB3bTwSox0INn4aFiy2rGSDbywhYxU2KOb +gRsMlHKcDn1HhaifvQRr2fKHM+P7C4e9sOXp3moB9i6JcI+C2vCEjfLeOE3tMUPo +k9NXd3BXlc8EpPfDUFg7gcco7n5lffxCfxJvNsMA7Or6xxt5WyyJm73v9wPj4OLT +Vmmo3lo//AcctLaFPS3wsGFacUkOAt5+5K2P8u3GW4S9IFes9j4CIlBrwHEIuzUl +g6mut8/ic6TiYMhqQfpdslGKdpEgTZuQqs7tHFDPRYOvRsXXxAFJShKAHwlqTA== +-----END CERTIFICATE REQUEST----- diff --git a/priv/ssl/gen_rpc_master@127.0.0.1.key.pem b/priv/ssl/gen_rpc_master@127.0.0.1.key.pem new file mode 100644 index 0000000..8036a16 --- /dev/null +++ b/priv/ssl/gen_rpc_master@127.0.0.1.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0NXWbckRc8HY0v+XbrPtrHBYE5gpzbqbPMHbldz2LpRyeZ1T +lWGBYzZisrxA6dy3FXwrayz5JAKSQED/7Z05JX3kU6Ph07S2EE5KEzahyJKipgja +ifq0yLNhK3lDhEFci1YAzofHpojpelKlFW99yeDtN2r1nGb6dH1iKV2YZHLC6UZH +LRd21Aj+WJ21y1x7akea5A4nKq4f9ntjJd5NWGzwbbqfvR9rEO3PaczzQGMkof4K +yBzBICjCf6jwJEOo7K6mcT47WNDRORRoG5wfo+FrCX2HyAT+/EmdovH0UJwPXHh9 +/XPCu4MBll8OB2lTpliYyg+Qqxh8LFV+hGJdDwIDAQABAoIBAQCL3oztnvu3Yh/M +wi80kAIhumTjSsjE57TNbrGZoBKqg/ZgCcVPiazVPDnCqF95wsBE/ZM/BzIesjIu +ItrG1MTPnY/tjRfVaXV/WlA2qiKVadDSD5//RhELU0lDisZzsCtBj2qe6UYI/i4S +JYQ35SKEscdvlI3IIRiiKNA0EGHe899+1+50gBJLfRJGsc7U90Nc2Lu45qjyIWPg ++eSqFyrPEEKU20TszmcuUvU0hzFuxXPMpcCEdgM62+6E+GUlL8fFLL/M+FxnO5bH +sIMWHL20Hkk2wUgjZZokk5iJ2M3ipjtUhyGCgUwIex7JMQTsHHzuiOtqOQwESQGU +RnMw4CzRAoGBAPm5yix2FnTyhrx+xyqDSd/WFvt+jJIhJ8T57yIaOH4xsqm1+jk4 +xeh8PgZ6jQlft2M1TgsXxvtbkAK4zevWWzGrJjuFA+5rUic/18UFgdMNhaO9BSz8 +AxgFB77e/vEtQpSvY5B2RIVfD1um4uuA7FpXmDhC0mkBFGpxtFytDn9tAoGBANYV +C3b3cnCLUuOjbzwTR107TmAYeMm+iy84JJgV+ML45Eg0TjrePWwQbCg2gWvj06sE +PB4A5f5nz41qgKzD1JWCDsNmPLIDq0HTZsa3twMnjs2CgjqxPS8HIlimkgAew8QT +fjkFyTIhKo0flCDE4oTDEgRWXLG6ht0aZ/q77nTrAoGBAIi5EJihGpByzBPdcVu0 +P7j+NN0CnyHOZ0Cv+h34rx9gREooaNP1Yl1z0F2psRgFwJjfcVgfb4Dk04a5+Xv6 +gzlBVBBYNhvNxE/a415qMncDuzw3W+MSIvt+nYwB+SDAP+i50dAhAEjpD7aFO03m +6wBXXu7Gnv6AadYRZMcstHhNAoGAIJc5lJXdFScQqlkCIRoqPhY+O0DeepAIblrU +r1aA9WWgkyFrW2jj3uStIn4Ru9QGMD3HzSWD3fP90+CH0EVLwqr3BmY1DUCjvLSr +K8tiEBfglIauvrKmxtcGMorBJUWJtfrlt1abJ4eHC87n5Qk0FtP/lSt0lxX3XAee +Fryvc5sCgYBctWaTHptVzgJBsOhSBpbx4Wcbwq+OooiarPHKMbJqHV/V9cnP3/Bs +ik4tcQtrOJ5CIPEI/wAe5kemr6o0NTxVCjyc+iw86qEe5wj1zkVvYEmNblgAF4k+ +e9zhZg7bBp5XR4TlW4kUO0uRhBgBX5ZvQGLIeC0Mx9zJ1jdGoNwQDg== +-----END RSA PRIVATE KEY----- diff --git a/priv/ssl/gen_rpc_slave@127.0.0.1.cert.pem b/priv/ssl/gen_rpc_slave@127.0.0.1.cert.pem new file mode 100644 index 0000000..22c2971 --- /dev/null +++ b/priv/ssl/gen_rpc_slave@127.0.0.1.cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVDCCAjwCCQC9a2qVijxGgTANBgkqhkiG9w0BAQsFADBrMQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEP +MA0GA1UEChMGR2VuUlBDMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +HhcNMTYwOTE5MDcwMDUyWhcNMzYwOTE0MDcwMDUyWjBtMQswCQYDVQQGEwJVUzET +MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEPMA0G +A1UEChMGR2VuUlBDMSAwHgYDVQQDFBdnZW5fcnBjX3NsYXZlQDEyNy4wLjAuMTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALS4Bt96fLOKoaVOhM6sP+wV +FtGiLk8uzbXftK1iUm7fJUv2kK52xOS+iF4hQK9VPYDio1PJCemqdfNmx8UWOYv4 +zCHjRE6E+pj23r/ODmu4yYNCzU1jbuJEqM4jTNW9QBfsR0Uf9fksYu64UsApQbJf +OsXQz6iboJJcsrDf7nABfqHGXp7HnHah1cBGednvQYEoxKgPiAhUSoF51WmGEnpe +8aaK0TJCKicWqHEsAIuBhOAPGWtX38QL3DS5vQZ25pgXRJVxRTj9FM5WTtLA8YXw +Gw6+K8T2RbUvm1dwe7HLxtsMqyaylIF+6MZH4idGLS+UeYLt6sC0tOyORCdocM8C +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAyRXhEo55xx8EP5F9RpTTSwy5Ds5qWC3z +mkNKDXImUZlEcw0QMsAJlBWiE3o9avYanerwYvRskGUB9g4buBklDxa4dbefFYr0 +B0yn7QvSPlEYHy2z+qbXFHyXTjS7YEn4/W/dw0ImtbE7vmyfcrJda42+Ib00+Xzu +pVDTepNPkqpmZswiFBK3LxT3wcspB4JQAwAxkeKX/OJjzvWI8SHlCW/FtYfDxsl0 +Lefbdg/bcyPgd3SFdlIjazR9Ou8a7PmLC2lhWMRq0H3uS/Xc6MTQ9lEjCZnh556V +OZEEyPUXsfCn//fbcravbcvZXXpnAUux9GulfqIj7iyhg1OXOnV2ow== +-----END CERTIFICATE----- diff --git a/priv/ssl/gen_rpc_slave@127.0.0.1.csr.pem b/priv/ssl/gen_rpc_slave@127.0.0.1.csr.pem new file mode 100644 index 0000000..b410201 --- /dev/null +++ b/priv/ssl/gen_rpc_slave@127.0.0.1.csr.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICsjCCAZoCAQAwbTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx +FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xDzANBgNVBAoTBkdlblJQQzEgMB4GA1UE +AxQXZ2VuX3JwY19zbGF2ZUAxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC0uAbfenyziqGlToTOrD/sFRbRoi5PLs2137StYlJu3yVL9pCu +dsTkvoheIUCvVT2A4qNTyQnpqnXzZsfFFjmL+Mwh40ROhPqY9t6/zg5ruMmDQs1N +Y27iRKjOI0zVvUAX7EdFH/X5LGLuuFLAKUGyXzrF0M+om6CSXLKw3+5wAX6hxl6e +x5x2odXARnnZ70GBKMSoD4gIVEqBedVphhJ6XvGmitEyQionFqhxLACLgYTgDxlr +V9/EC9w0ub0GduaYF0SVcUU4/RTOVk7SwPGF8BsOvivE9kW1L5tXcHuxy8bbDKsm +spSBfujGR+InRi0vlHmC7erAtLTsjkQnaHDPAgMBAAGgADANBgkqhkiG9w0BAQUF +AAOCAQEAcK3au5VsfyKxiziA6hUfllQ3s3U+zqLA62OhpVSuZlvgVb1m8Yw5Wahx +C0eTAnFQ0UxChVSj4FSXRU6MmIF23JketsEjDOCLjUdavhhZcguqJZVTUNkNscS9 +l6ayNt2mUIXKbWsTOgQISplS3u3Gl6JS4AxQ6qOLB7puEQBo9EQj7kzaY3++z2k6 +IWeobH0HWp04KQPGMhOjZli2iiBycwqNCUAg3aUV5G6xdPgybZwYlYIB0e0XtFqC +z6hpF9amtmAJbcH0JgwAVgLX8y3Qr7H73LMWMUQqOCgApWlCsH85a0s7EZc73HTZ +AbeqtRLSBJaLOGmKYpEVkQZFfb60mQ== +-----END CERTIFICATE REQUEST----- diff --git a/priv/ssl/gen_rpc_slave@127.0.0.1.key.pem b/priv/ssl/gen_rpc_slave@127.0.0.1.key.pem new file mode 100644 index 0000000..1fdb0d1 --- /dev/null +++ b/priv/ssl/gen_rpc_slave@127.0.0.1.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtLgG33p8s4qhpU6Ezqw/7BUW0aIuTy7Ntd+0rWJSbt8lS/aQ +rnbE5L6IXiFAr1U9gOKjU8kJ6ap182bHxRY5i/jMIeNEToT6mPbev84Oa7jJg0LN +TWNu4kSoziNM1b1AF+xHRR/1+Sxi7rhSwClBsl86xdDPqJugklyysN/ucAF+ocZe +nsecdqHVwEZ52e9BgSjEqA+ICFRKgXnVaYYSel7xporRMkIqJxaocSwAi4GE4A8Z +a1ffxAvcNLm9BnbmmBdElXFFOP0UzlZO0sDxhfAbDr4rxPZFtS+bV3B7scvG2wyr +JrKUgX7oxkfiJ0YtL5R5gu3qwLS07I5EJ2hwzwIDAQABAoIBAQCc2Cbm6Fi4PnZi +kQ4EjUyBKiB+2qIUiQtycPBPlsjyfurQzfLt6BikapTSFUwdn/5cENctzYqz6QSr +tiVlcQ+K4ujP9H4BI7qW7ZOPplevuFX0fRLPup+u82YKWmrCCplI6gFFBiMmGhbX +FtSDAIuLMslAjCTeMgufW6KejgtT2xIekdTNpL7NI3GVC9nJhURkIxnG2SfdbY15 +p8Vret6pxhfLUVJ7YjLRmMgKMIKN6A+lLZcW8+uOG9NrGbZkHk5cqgPDZWdv2Fmy +V7aOaWybosfMDCGDKO4i6xbSm6KthFNyHML8Y1nTqCcQiP2fnvXh4hu29U5BVQov +91GHK8zpAoGBAOU5f6JjCUzDhDaqcMy6KF9m2cQmJJNDxD82ewGeCNjx61uXHbB9 +2pLqZ9LkH104lx4IHxMQCTLc0dh1be5Sjff2A6+Ag07S0e7TNkRmRjS9EsNFuk7v +j7nXDNNvXmcCUcDmruCFOPj5UDKJPaL+1+5ar6w8IXv5HV/s4fLRxWt7AoGBAMnU +D6898kHh/qhE6TFBpSCzy+vs9vcrKkXaCJ7lY829qlTFIsaWPMKqf9Rj0LKdnMLA +vncru+g4CnvnrAuil51G8D6qNfLiA1YlLJNrAf2IWUGqJif0cMMFX2cwyacmM+kF +hJZ76PS4LKWzEFS/+5zc15kvJ+azO5caCCwFVRW9AoGBAMl2v7b5VDMjpBBnvLwy +4Uj4HG1JGNxVsyXbvECvpRcHiNP2/SEhe/mkC5xO40ILesXqXWVjsrIBYm3cx8Gu +zRgiIRse2ElvARPmjsuPGCVCCDuYDSdTZf7wA1XRjCb84t2n4gtzC5/Hf7Tq6kFX +unDDQ6MdNlC/ohGtIOlJdToBAoGAEhoZBgiyT8JrSsBTgf13SAWYRqLMS8LtWbKC +j23Xk75tJok/4zvaW0Nbj7scHskQJE+RRbMAeYV9Ozu+LJT6sDqKlbRCfnRTBEj1 +RIuWdBzklIUkQvUjn/o9Yk336Cv+/+lkyPJ4JGrYZkw3LMLDWIBePfkCxShSMe09 +ymT4CqkCgYA4pn+y9C/63EUPBjMdYpgmtA6nUlqiL47alUgC3+yrYW7A4rsl1bz8 +g5adTECEc9FuKUSxTERe4BwNQ+8CgznUmqKAhMZXepJMJmD7IIqa8m5zbmZd+4Yv +NQWNP0TpwesoiiMPm/ZGiArNqBNzom/S+5MJxSfB5KkS3x8PrZgxag== +-----END RSA PRIVATE KEY----- diff --git a/rebar.config b/rebar.config index 51607c3..f094968 100644 --- a/rebar.config +++ b/rebar.config @@ -2,26 +2,21 @@ %%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: %%% -%%% Require OTP 17.0 at a bare minimum -{require_min_otp_vsn, "19"}. +%%% Require OTP 19.1 at a bare minimum +{minimum_otp_vsn, "19.1"}. %% Plugins {plugins, [rebar3_hex]}. {shell, [ - {apps, [gen_rpc]}, - {config, "gen_rpc.config"} + {apps, [lager, sync, gen_rpc]} ]}. {erl_opts, [debug_info, {warn_format, 1}, - {parse_transform, lager_transform}, - {lager_truncation_size, 8192}, bin_opt_info, inline_list_funcs, - no_debug_info, warnings_as_errors, report_warnings, - stong_validation, warn_untyped_record, warn_export_vars, warn_export_all, @@ -37,19 +32,29 @@ ]}. {deps, [ - {lager, "~> 3.0"} + {hut, "~> 1.2"}, + {ssl_verify_fun, "~> 1.1"} ]}. {profiles, [ {test, [ - {erl_opts, [warnings_as_errors, debug_info, export_all, no_inline_list_funcs]}, - {deps, [ + {erl_opts, [{d,'TEST'}, + {d,'HUT_LAGER'}, + {parse_transform, lager_transform}, + warnings_as_errors, + export_all, + no_inline_list_funcs]}, + {deps, [{lager, "~> 3.0"}, {eunit_formatters, "~> 0.3"} ]} ]}, {dev, [ - {erl_opts, [warnings_as_errors, debug_info, export_all, no_inline_list_funcs]}, - {deps, [ + {erl_opts, [{d,'HUT_LAGER'}, + {parse_transform, lager_transform}, + warnings_as_errors, + export_all, + no_inline_list_funcs]}, + {deps, [{lager, "~> 3.0"}, {sync, {git, "git://github.com/rustyio/sync.git", {branch, "master"}}} ]} ]} @@ -96,5 +101,5 @@ %% XRef {xref_warnings, true}. {xref_extra_paths, []}. -{xref_checks,[undefined_function_calls, undefined_functions, locals_not_used, - deprecated_function_calls, deprecated_functions]}. +{xref_checks, [undefined_function_calls, undefined_functions, locals_not_used, + deprecated_function_calls, deprecated_functions]}. diff --git a/src/driver/gen_rpc_driver_ssl.erl b/src/driver/gen_rpc_driver_ssl.erl new file mode 100644 index 0000000..85100e4 --- /dev/null +++ b/src/driver/gen_rpc_driver_ssl.erl @@ -0,0 +1,224 @@ +%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. +%%% +%%% Original concept inspired and some code copied from +%%% https://erlangcentral.org/wiki/index.php?title=Building_a_Non-blocking_TCP_server_using_OTP_principles + +-module(gen_rpc_driver_ssl). +-author("Panagiotis Papadomitsos "). + +%%% Behaviour +-behaviour(gen_rpc_driver). + +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). +%%% Include this library's name macro +-include("app.hrl"). +%%% Include SSL macros +-include("ssl.hrl"). +%%% Include helpful guard macros +-include("guards.hrl"). + +%%% Public API +-export([connect/2, + listen/1, + accept/1, + get_peer/1, + send/2, + activate_socket/1, + authenticate_server/1, + authenticate_client/3, + copy_sock_opts/2, + set_controlling_process/2, + set_send_timeout/2, + set_acceptor_opts/1]). + +%%% =================================================== +%%% Public API +%%% =================================================== +%% Connect to a node +-spec connect(atom(), inet:port_number()) -> {ok, ssl:sslsocket()} | {error, term()}. +connect(Node, Port) when is_atom(Node) -> + Host = gen_rpc_helper:host_from_node(Node), + ConnTO = gen_rpc_helper:get_connect_timeout(), + SslOpts = merge_ssl_options(client, Node), + case ssl:connect(Host, Port, SslOpts, ConnTO) of + {ok, Socket} -> + ?log(debug, "event=connect_to_remote_server peer=\"~s\" socket=\"~s\" result=success", + [Node, gen_rpc_helper:socket_to_string(Socket)]), + {ok, Socket}; + {error, Reason} -> + ?log(error, "event=connect_to_remote_server peer=\"~s\" result=failure reason=\"~p\"", [Node, Reason]), + {error, {badtcp,Reason}} + end. + + +-spec listen(inet:port_number()) -> {ok, ssl:sslsocket()} | {error, term()}. +listen(Port) when is_integer(Port) -> + SslOpts = merge_ssl_options(server, undefined), + ssl:listen(Port, SslOpts). + +-spec accept(ssl:sslsocket()) -> ok | {error, term()}. +accept(Socket) when is_tuple(Socket) -> + {ok, TSocket} = ssl:transport_accept(Socket, infinity), + case ssl:ssl_accept(TSocket) of + ok -> + {ok, TSocket}; + Error -> + Error + end. + +-spec send(ssl:sslsocket(), binary()) -> ok | {error, term()}. +send(Socket, Data) when is_tuple(Socket), is_binary(Data) -> + case ssl:send(Socket, Data) of + {error, timeout} -> + ?log(error, "event=send_data_failed socket=\"~s\" reason=\"timeout\"", [gen_rpc_helper:socket_to_string(Socket)]), + {error, {badtcp,send_timeout}}; + {error, Reason} -> + ?log(error, "event=send_data_failed socket=\"~s\" reason=\"~p\"", [gen_rpc_helper:socket_to_string(Socket), Reason]), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=send_data_succeeded socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + ok + end. + +-spec activate_socket(ssl:sslsocket()) -> ok. +activate_socket(Socket) when is_tuple(Socket) -> + ok = ssl:setopts(Socket, [{active,once}]), + ok. + +%% Authenticate to a server +-spec authenticate_server(ssl:sslsocket()) -> ok | {error, {badtcp | badrpc, term()}}. +authenticate_server(Socket) -> + Cookie = erlang:get_cookie(), + NodeStr = erlang:atom_to_list(node()), + Packet = erlang:term_to_binary({gen_rpc_authenticate_connection, NodeStr, Cookie}), + SendTO = gen_rpc_helper:get_send_timeout(undefined), + RecvTO = gen_rpc_helper:get_call_receive_timeout(undefined), + ok = set_send_timeout(Socket, SendTO), + case ssl:send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=authentication_connection_failed socket=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), Reason]), + ok = ssl:close(Socket), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=authentication_connection_succeeded socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + case ssl:recv(Socket, 0, RecvTO) of + {ok, RecvPacket} -> + case erlang:binary_to_term(RecvPacket) of + gen_rpc_connection_authenticated -> + ?log(debug, "event=connection_authenticated socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + ok; + {gen_rpc_connection_rejected, Reason} -> + ?log(error, "event=authentication_rejected socket=\"~s\" reason=\"~s\"", [gen_rpc_helper:socket_to_string(Socket), Reason]), + ok = ssl:close(Socket), + {error, {badrpc,Reason}}; + _Else -> + ?log(error, "event=authentication_transmission_error socket=\"~s\" reason=\"invalid_payload\"", + [gen_rpc_helper:socket_to_string(Socket)]), + ok = ssl:close(Socket), + {error, {badrpc,invalid_message}} + end; + {error, Reason} -> + ?log(error, "event=authentication_reception_failed socket=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), Reason]), + ok = ssl:close(Socket), + {error, {badtcp,Reason}} + end + end. + +%% Authenticate a connected client +-spec authenticate_client(ssl:sslsocket(), tuple(), binary()) -> ok | {error, {badtcp | badrpc, term()}}. +authenticate_client(Socket, Peer, Data) -> + Cookie = erlang:get_cookie(), + try erlang:binary_to_term(Data) of + {gen_rpc_authenticate_connection, Node, Cookie} -> + PeerCert = extract_peer_certificate(Socket), + {SocketResponse, AuthResult} = case ssl_verify_hostname:verify_cert_hostname(PeerCert, Node) of + {fail, AuthReason} -> + ?log(error, "event=node_certificate_mismatch socket=\"~s\" peer=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), AuthReason]), + {{gen_rpc_connection_rejected,node_certificate_mismatch}, {error,{badrpc,node_certificate_mismatch}}}; + {valid, _Hostname} -> + ?log(debug, "event=certificate_validated socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + {gen_rpc_connection_authenticated, ok} + end, + Packet = erlang:term_to_binary(SocketResponse), + case send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=transmission_failed socket=\"~s\" peer=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Reason]), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=transmission_succeeded socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + ok = activate_socket(Socket), + AuthResult + end; + {gen_rpc_authenticate_connection, _Node, _IncorrectCookie} -> + ?log(error, "event=invalid_cookie_received socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + Packet = erlang:term_to_binary({gen_rpc_connection_rejected, invalid_cookie}), + ok = case send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=transmission_failed socket=\"~s\" peer=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Reason]); + ok -> + ?log(debug, "event=transmission_succeeded socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]) + end, + {error, {badrpc,invalid_cookie}}; + OtherData -> + ?log(debug, "event=erroneous_data_received socket=\"~s\" peer=\"~s\" data=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), OtherData]), + {error, {badrpc,erroneous_data}} + catch + error:badarg -> + {error, {badtcp,corrupt_data}} + end. + +-spec copy_sock_opts(port(), port()) -> ok | {error, any()}. +copy_sock_opts(_ListSock, _AccSock) -> + ok. % SSL copies the socket's options to the acceptor by default + +-spec get_peer(ssl:sslsocket()) -> {inet:ip4_address(), inet:port_number()}. +get_peer(Socket) when is_tuple(Socket) -> + {ok, Peer} = ssl:peername(Socket), + Peer. + +-spec set_controlling_process(ssl:sslsocket(), pid()) -> ok | {error, term()}. +set_controlling_process(Socket, Pid) when is_tuple(Socket), is_pid(Pid) -> + ssl:controlling_process(Socket, Pid). + +-spec set_send_timeout(ssl:sslsocket(), timeout() | undefined) -> ok. +set_send_timeout(Socket, SendTO) when is_tuple(Socket) -> + ok = ssl:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(SendTO)}]), + ok. + +-spec set_acceptor_opts(ssl:sslsocket()) -> ok. +set_acceptor_opts(Socket) when is_tuple(Socket) -> + ok = ssl:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(undefined)}]), + ok. + +%%% =================================================== +%%% Private functions +%%% =================================================== +merge_ssl_options(client, Node) -> + {ok, ExtraOpts} = application:get_env(?APP, ssl_client_options), + NodeStr = atom_to_list(Node), + DefaultOpts = lists:append(?SSL_DEFAULT_COMMON_OPTS, ?SSL_DEFAULT_CLIENT_OPTS), + VerifyOpts = [{verify_fun, {fun ssl_verify_hostname:verify_fun/3,[{check_hostname,NodeStr}]}}|DefaultOpts], + gen_rpc_helper:merge_sockopt_lists(ExtraOpts, VerifyOpts); + +merge_ssl_options(server, _Node) -> + {ok, ExtraOpts} = application:get_env(?APP, ssl_server_options), + DefaultOpts = lists:append(?SSL_DEFAULT_COMMON_OPTS, ?SSL_DEFAULT_SERVER_OPTS), + gen_rpc_helper:merge_sockopt_lists(ExtraOpts, DefaultOpts). + +extract_peer_certificate(Socket) -> + {ok, Cert} = ssl:peercert(Socket), + public_key:pkix_decode_cert(Cert, otp). diff --git a/src/driver/gen_rpc_driver_tcp.erl b/src/driver/gen_rpc_driver_tcp.erl new file mode 100644 index 0000000..a7064a6 --- /dev/null +++ b/src/driver/gen_rpc_driver_tcp.erl @@ -0,0 +1,201 @@ +%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. +%%% +%%% Original concept inspired and some code copied from +%%% https://erlangcentral.org/wiki/index.php?title=Building_a_Non-blocking_TCP_server_using_OTP_principles + +-module(gen_rpc_driver_tcp). +-author("Panagiotis Papadomitsos "). + +%%% Behaviour +-behaviour(gen_rpc_driver). + +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). +%%% Include this library's name macro +-include("app.hrl"). +%%% Include TCP macros +-include("tcp.hrl"). +%%% Include helpful guard macros +-include("guards.hrl"). + +%%% Public API +-export([connect/2, + listen/1, + accept/1, + get_peer/1, + send/2, + activate_socket/1, + authenticate_server/1, + authenticate_client/3, + copy_sock_opts/2, + set_controlling_process/2, + set_send_timeout/2, + set_acceptor_opts/1]). + +%%% =================================================== +%%% Public API +%%% =================================================== +%% Connect to a node +-spec connect(atom(), inet:port_number()) -> {ok, port()} | {error, term()}. +connect(Node, Port) when is_atom(Node) -> + Host = gen_rpc_helper:host_from_node(Node), + ConnTO = gen_rpc_helper:get_connect_timeout(), + case gen_tcp:connect(Host, Port, ?TCP_DEFAULT_OPTS, ConnTO) of + {ok, Socket} -> + ?log(debug, "event=connect_to_remote_server peer=\"~s\" socket=\"~s\" result=success", + [Node, gen_rpc_helper:socket_to_string(Socket)]), + {ok, Socket}; + {error, Reason} -> + ?log(error, "event=connect_to_remote_server peer=\"~s\" result=failure reason=\"~p\"", [Node, Reason]), + {error, {badtcp,Reason}} + end. + +-spec listen(inet:port_number()) -> {ok, port()} | {error, term()}. +listen(Port) when is_integer(Port) -> + gen_tcp:listen(Port, ?TCP_DEFAULT_OPTS). + +-spec accept(port()) -> ok | {error, term()}. +accept(Socket) when is_port(Socket) -> + gen_tcp:accept(Socket, infinity). + +-spec activate_socket(port()) -> ok. +activate_socket(Socket) when is_port(Socket) -> + ok = inet:setopts(Socket, [{active,once}]), + ok. + +-spec send(port(), binary()) -> ok | {error, term()}. +send(Socket, Data) when is_port(Socket), is_binary(Data) -> + case gen_tcp:send(Socket, Data) of + {error, timeout} -> + ?log(error, "event=send_data_failed socket=\"~s\" reason=\"timeout\"", [gen_rpc_helper:socket_to_string(Socket)]), + {error, {badtcp,send_timeout}}; + {error, Reason} -> + ?log(error, "event=send_data_failed socket=\"~s\" reason=\"~p\"", [gen_rpc_helper:socket_to_string(Socket), Reason]), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=send_data_succeeded socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + ok + end. + +%% Authenticate to a server +-spec authenticate_server(port()) -> ok | {error, {badtcp | badrpc, term()}}. +authenticate_server(Socket) -> + Cookie = erlang:get_cookie(), + Packet = erlang:term_to_binary({gen_rpc_authenticate_connection, Cookie}), + SendTO = gen_rpc_helper:get_send_timeout(undefined), + RecvTO = gen_rpc_helper:get_call_receive_timeout(undefined), + ok = set_send_timeout(Socket, SendTO), + case gen_tcp:send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=authentication_connection_failed socket=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), Reason]), + ok = gen_tcp:close(Socket), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=authentication_connection_succeeded socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + case gen_tcp:recv(Socket, 0, RecvTO) of + {ok, RecvPacket} -> + case erlang:binary_to_term(RecvPacket) of + gen_rpc_connection_authenticated -> + ?log(debug, "event=connection_authenticated socket=\"~s\"", [gen_rpc_helper:socket_to_string(Socket)]), + ok; + {gen_rpc_connection_rejected, invalid_cookie} -> + ?log(error, "event=authentication_rejected socket=\"~s\" reason=\"invalid_cookie\"", + [gen_rpc_helper:socket_to_string(Socket)]), + ok = gen_tcp:close(Socket), + {error, {badrpc,invalid_cookie}}; + _Else -> + ?log(error, "event=authentication_reception_error socket=\"~s\" reason=\"invalid_payload\"", + [gen_rpc_helper:socket_to_string(Socket)]), + ok = gen_tcp:close(Socket), + {error, {badrpc,invalid_message}} + end; + {error, Reason} -> + ?log(error, "event=authentication_reception_failed socket=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), Reason]), + ok = gen_tcp:close(Socket), + {error, {badtcp,Reason}} + end + end. + +%% Authenticate a connected client +-spec authenticate_client(port(), tuple(), binary()) -> ok | {error, {badtcp | badrpc, term()}}. +authenticate_client(Socket, Peer, Data) -> + Cookie = erlang:get_cookie(), + try erlang:binary_to_term(Data) of + {gen_rpc_authenticate_connection, Cookie} -> + Packet = erlang:term_to_binary(gen_rpc_connection_authenticated), + Result = case send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=transmission_failed socket=\"~s\" peer=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Reason]), + {error, {badtcp,Reason}}; + ok -> + ?log(debug, "event=transmission_succeeded socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + ok = activate_socket(Socket), + ok + end, + Result; + {gen_rpc_authenticate_connection, _IncorrectCookie} -> + ?log(error, "event=invalid_cookie_received socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + Packet = erlang:term_to_binary({gen_rpc_connection_rejected, invalid_cookie}), + ok = case send(Socket, Packet) of + {error, Reason} -> + ?log(error, "event=transmission_failed socket=\"~s\" peer=\"~s\" reason=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Reason]); + ok -> + ?log(debug, "event=transmission_succeeded socket=\"~s\" peer=\"~s\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]) + end, + {error, {badrpc,invalid_cookie}}; + OtherData -> + ?log(debug, "event=erroneous_data_received socket=\"~s\" peer=\"~s\" data=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), OtherData]), + {error, {badrpc,erroneous_data}} + catch + error:badarg -> + {error, {badtcp,corrupt_data}} + end. + +%% Taken from prim_inet. We are merely copying some socket options from the +%% listening socket to the new acceptor socket. +-spec copy_sock_opts(port(), port()) -> ok | {error, any()}. +copy_sock_opts(ListSock, AccSock) when is_port(ListSock), is_port(AccSock) -> + true = inet_db:register_socket(AccSock, inet_tcp), + case prim_inet:getopts(ListSock, ?ACCEPTOR_COPY_TCP_OPTS) of + {ok, SockOpts} -> + case prim_inet:setopts(AccSock, SockOpts) of + ok -> ok; + Error -> Error + end; + Error -> + Error + end. + +-spec get_peer(port()) -> {inet:ip4_address(), inet:port_number()}. +get_peer(Socket) when is_port(Socket) -> + {ok, Peer} = inet:peername(Socket), + Peer. + +-spec set_controlling_process(port(), pid()) -> ok | {error, term()}. +set_controlling_process(Socket, Pid) when is_port(Socket), is_pid(Pid) -> + gen_tcp:controlling_process(Socket, Pid). + +-spec set_send_timeout(port(), timeout() | undefined) -> ok. +set_send_timeout(Socket, SendTO) when is_port(Socket) -> + ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(SendTO)}]), + ok. + +-spec set_acceptor_opts(port()) -> ok. +set_acceptor_opts(Socket) when is_port(Socket) -> + ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(undefined)}|?ACCEPTOR_DEFAULT_TCP_OPTS]), + ok. + +%%% =================================================== +%%% Private functions +%%% =================================================== diff --git a/src/gen_rpc.app.src b/src/gen_rpc.app.src index b30fee5..4c420d0 100644 --- a/src/gen_rpc.app.src +++ b/src/gen_rpc.app.src @@ -8,7 +8,7 @@ [{description, "A scalable RPC library for Erlang-VM based languages"}, {vsn, git}, {mod, {gen_rpc_app, []}}, - {registered, [gen_rpc_dispatcher, gen_rpc_listener]}, + {registered, [gen_rpc_dispatcher]}, {maintainers, ["Panagiotis PJ Papadomitsos"]}, {licenses, ["Apache 2.0"]}, {links, [{"Github", "https://github.com/priestjim/gen_rpc"}]}, @@ -20,16 +20,32 @@ "rebar.config", "src" ]}, - {applications, [kernel, stdlib, lager, crypto, asn1, public_key, ssl]}, + {applications, [kernel, stdlib, crypto, asn1, public_key, ssl]}, {env,[ - %% Default TCP listener port + %% TCP server port. Set to false to disable {tcp_server_port, 5369}, - %% Proplist of nodes that listen to a different port - {remote_tcp_server_ports, []}, + %% Default TCP port for outgoing connections + {tcp_client_port, 5369}, + %% SSL server port. Set to false to disable + {ssl_server_port, false}, + %% SSL server options + {ssl_server_options, []}, + %% Default SSL port for outgoing connections + {ssl_client_port, 5370}, + %% SSL client options + {ssl_client_options, []}, + %% Default driver to use for outgoing connections + {default_client_driver, tcp}, + %% Fine-graned driver/port control + %% for each outgoing client connection + %% Describe it as node_name => {driver, port} + %% Or node_name => driver to use the default port + %% for the specified client driver + {client_config_per_node, #{}}, %% List of modules available for RPC - %% This is either whitelist, blacklist or undefined + %% This is either whitelist, blacklist or disabled %% to disable the feature - {rpc_module_control, undefined}, + {rpc_module_control, disabled}, %% This is the list of modules that the %% rule above applies to {rpc_module_list, []}, @@ -37,6 +53,8 @@ {connect_timeout, 5000}, %% Client and Server send timeout {send_timeout, 5000}, + %% Authentication timeout + {authentication_timeout, 5000}, %% Default receive timeout for call() functions {call_receive_timeout, 15000}, %% Default receive timeout for sbcast diff --git a/src/gen_rpc.erl b/src/gen_rpc.erl index 2097e03..fa682a8 100644 --- a/src/gen_rpc.erl +++ b/src/gen_rpc.erl @@ -36,51 +36,52 @@ %%% =================================================== %% All functions are GUARD-ed in the sender module, no %% need for the overhead here --spec async_call(node(), module(), atom()|function()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec async_call(node(), atom() | tuple(), atom() | function()) -> term() | {badrpc, term()} | {badtcp | term()}. async_call(Node, M, F) -> gen_rpc_client:async_call(Node, M, F). --spec async_call(node(), module(), atom()|function(), list()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec async_call(node(), atom() | tuple(), atom() | function(), list()) -> term() | {badrpc, term()} | {badtcp | term()}. async_call(Node, M, F, A) -> gen_rpc_client:async_call(Node, M, F, A). --spec call(node(), module(), atom()|function()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function()) -> term() | {badrpc, term()} | {badtcp | term()}. call(Node, M, F) -> gen_rpc_client:call(Node, M, F). --spec call(node(), module(), atom()|function(), list()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function(), list()) -> term() | {badrpc, term()} | {badtcp | term()}. call(Node, M, F, A) -> gen_rpc_client:call(Node, M, F, A). --spec call(node(), module(), atom()|function(), list(), timeout() | undefined) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function(), list(), timeout() | undefined) -> + term() | {badrpc, term()} | {badtcp | term()}. call(Node, M, F, A, RecvTO) -> gen_rpc_client:call(Node, M, F, A, RecvTO). --spec call(node(), module(), atom()|function(), list(), timeout() | undefined, timeout() | undefined) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function(), list(), timeout() | undefined, timeout() | undefined) -> term() | {badrpc, term()} | {badtcp | term()}. call(Node, M, F, A, RecvTO, SendTO) -> gen_rpc_client:call(Node, M, F, A, RecvTO, SendTO). --spec cast(node(), module(), atom()|function()) -> true. +-spec cast(node(), atom() | tuple(), atom() | function()) -> true. cast(Node, M, F) -> gen_rpc_client:cast(Node, M, F). --spec cast(node(), module(), atom()|function(), list()) -> true. +-spec cast(node(), atom() | tuple(), atom() | function(), list()) -> true. cast(Node, M, F, A) -> gen_rpc_client:cast(Node, M, F, A). --spec cast(node(), module(), atom()|function(), list(), timeout() | undefined) -> true. +-spec cast(node(), atom() | tuple(), atom() | function(), list(), timeout() | undefined) -> true. cast(Node, M, F, A, SendTO) -> gen_rpc_client:cast(Node, M, F, A, SendTO). --spec eval_everywhere([node()], module(), atom()|function()) -> abcast. +-spec eval_everywhere([node()], atom() | tuple(), atom() | function()) -> abcast. eval_everywhere(Nodes, M, F) -> gen_rpc_client:eval_everywhere(Nodes, M, F). --spec eval_everywhere([node()], module(), atom()|function(), list()) -> abcast. +-spec eval_everywhere([node()], atom() | tuple(), atom() | function(), list()) -> abcast. eval_everywhere(Nodes, M, F, A) -> gen_rpc_client:eval_everywhere(Nodes, M, F, A). --spec eval_everywhere([node()], module(), atom()|function(), list(), timeout() | undefined) -> abcast. +-spec eval_everywhere([node()], atom() | tuple(), atom() | function(), list(), timeout() | undefined) -> abcast. eval_everywhere(Nodes, M, F, A, SendTO) -> gen_rpc_client:eval_everywhere(Nodes, M, F, A, SendTO). @@ -96,15 +97,15 @@ nb_yield(Key) -> nb_yield(Key, Timeout) -> gen_rpc_client:nb_yield(Key, Timeout). --spec multicall(module(), atom(), list()) -> {list(), list()}. +-spec multicall(atom() | tuple(), atom(), list()) -> {list(), list()}. multicall(M, F, A) -> gen_rpc_client:multicall(M, F, A). --spec multicall(list() | module(), module() | atom(), atom() | list(), list() | timeout()) -> {list(), list()}. +-spec multicall(list() | atom() | tuple(), atom() | tuple(), atom() | list(), list() | timeout()) -> {list(), list()}. multicall(NodesOrModule, MorF, ForA, AorTimeout) -> gen_rpc_client:multicall(NodesOrModule, MorF, ForA, AorTimeout). --spec multicall(list(), module(), atom(), list(), timeout()) -> {list(), list()}. +-spec multicall(list(), atom() | tuple(), atom(), list(), timeout()) -> {list(), list()}. multicall(Nodes, M, F, A, Timeout) -> gen_rpc_client:multicall(Nodes, M, F, A, Timeout). diff --git a/src/gen_rpc_acceptor.erl b/src/gen_rpc_acceptor.erl index 2672fff..ada610a 100644 --- a/src/gen_rpc_acceptor.erl +++ b/src/gen_rpc_acceptor.erl @@ -12,11 +12,17 @@ %%% Behaviour -behaviour(gen_statem). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). %%% Include this library's name macro -include("app.hrl"). %%% Local state -record(state, {socket = undefined :: port() | undefined, + driver :: atom(), + driver_mod :: atom(), + driver_closed :: atom(), + driver_error :: atom(), peer :: {inet:ip4_address(), inet:port_number()}, control :: whitelist | blacklist | undefined, list :: sets:set() | undefined}). @@ -26,13 +32,13 @@ -dialyzer([{no_return, [call_middleman/3]}]). %%% Server functions --export([start_link/1, set_socket/2, stop/1]). +-export([start_link/2, set_socket/2, stop/1]). %% gen_statem callbacks -export([init/1, handle_event/4, callback_mode/0, terminate/3, code_change/4]). -%% FSM States --export([waiting_for_socket/3, waiting_for_data/3]). +%% State machine states +-export([waiting_for_socket/3, waiting_for_auth/3, waiting_for_data/3]). %%% Process exports -export([call_worker/6, call_middleman/3]). @@ -40,10 +46,10 @@ %%% =================================================== %%% Supervisor functions %%% =================================================== --spec start_link({inet:ip4_address(), inet:port_number()}) -> gen_statem:startlink_ret(). -start_link(Peer) when is_tuple(Peer) -> +-spec start_link(atom(), {inet:ip4_address(), inet:port_number()}) -> gen_statem:startlink_ret(). +start_link(Driver, Peer) when is_atom(Driver), is_tuple(Peer) -> Name = gen_rpc_helper:make_process_name("acceptor", Peer), - gen_statem:start_link({local,Name}, ?MODULE, {Peer}, []). + gen_statem:start_link({local,Name}, ?MODULE, {Driver, Peer}, []). -spec stop(pid()) -> ok. stop(Pid) when is_pid(Pid) -> @@ -53,137 +59,182 @@ stop(Pid) when is_pid(Pid) -> %%% Server functions %%% =================================================== -spec set_socket(pid(), gen_tcp:socket()) -> ok. -set_socket(Pid, Socket) when is_pid(Pid), is_port(Socket) -> - gen_statem:call(Pid, {socket_ready, Socket}, infinity). +set_socket(Pid, Socket) when is_pid(Pid) -> + gen_statem:call(Pid, {socket_ready,Socket}, infinity). %%% =================================================== %%% Behaviour callbacks %%% =================================================== -init({Peer}) -> +init({Driver, Peer}) -> ok = gen_rpc_helper:set_optimal_process_flags(), - {Control, ControlList} = case application:get_env(?APP, rpc_module_control) of - {ok, undefined} -> - {undefined, undefined}; - {ok, Type} when Type =:= whitelist; Type =:= blacklist -> - {ok, List} = application:get_env(?APP, rpc_module_list), - {Type, sets:from_list(List)} - end, - ok = lager:info("event=start peer=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), - %% Store the client's IP and the node in our state - {ok, waiting_for_socket, #state{peer=Peer,control=Control,list=ControlList}}. + {Control, ControlList} = gen_rpc_helper:get_rpc_module_control(), + {DriverMod, _DriverPort, DriverClosed, DriverError} = gen_rpc_helper:get_server_driver_options(Driver), + ?log(info, "event=start driver=~s peer=\"~s\"", [Driver, gen_rpc_helper:peer_to_string(Peer)]), + {ok, waiting_for_socket, #state{driver=Driver, + driver_mod=DriverMod, + driver_error=DriverError, + driver_closed=DriverClosed, + peer=Peer, + control=Control, + list=ControlList}}. callback_mode() -> state_functions. -waiting_for_socket({call, From}, {socket_ready, Socket}, #state{peer=Peer} = State) -> +waiting_for_socket({call,From}, {socket_ready,Socket}, #state{driver=Driver, driver_mod=DriverMod, peer=Peer} = State) -> % Now we own the socket - ok = lager:debug("event=acquiring_socket_ownership socket=\"~p\" peer=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer)]), - ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(undefined)}|?ACCEPTOR_DEFAULT_TCP_OPTS]), + ?log(debug, "event=acquiring_socket_ownership driver=~s socket=\"~s\" peer=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + ok = DriverMod:set_acceptor_opts(Socket), + ok = DriverMod:activate_socket(Socket), ok = gen_statem:reply(From, ok), - {next_state, waiting_for_data, State#state{socket=Socket}}. + {next_state, waiting_for_auth, State#state{socket=Socket}, gen_rpc_helper:get_authentication_timeout()}. -waiting_for_data(info, {tcp, Socket, Data}, #state{socket=Socket,peer=Peer,control=Control,list=List} = State) -> +waiting_for_auth(info, {Driver,Socket,Data}, #state{socket=Socket, driver=Driver, driver_mod=DriverMod, peer=Peer} = State) -> + case DriverMod:authenticate_client(Socket, Peer, Data) of + {error, Reason} -> + {stop, Reason, State}; + ok -> + {next_state, waiting_for_data, State} + end; + +waiting_for_auth(timeout, _Timeout, #state{socket=Socket, driver=Driver, peer=Peer} = State) -> + ?log(notice, "event=timed_out_waiting_for_auth driver=~s socket=\"~s\" peer=\"~s\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), + {stop, timed_out_waiting_for_auth, State}; + +waiting_for_auth(info, {DriverClosed, Socket} = Msg, #state{socket=Socket, driver_closed=DriverClosed} = State) -> + handle_event(info, Msg, waiting_for_auth, State); + +waiting_for_auth(info, {DriverError, Socket, _Reason} = Msg, #state{socket=Socket, driver_error=DriverError} = State) -> + handle_event(info, Msg, waiting_for_auth, State). + +waiting_for_data(info, {Driver,Socket,Data}, + #state{socket=Socket, driver=Driver, driver_mod=DriverMod, peer=Peer, control=Control, list=List} = State) -> %% The meat of the whole project: process a function call and return %% the data try erlang:binary_to_term(Data) of {{CallType,M,F,A}, Caller} when CallType =:= call; CallType =:= async_call -> - case is_allowed(M, Control, List) of + {ModVsnAllowed, RealM} = check_module_version_compat(M), + case check_if_module_allowed(RealM, Control, List) of true -> - WorkerPid = erlang:spawn(?MODULE, call_worker, [self(), CallType, M, F, A, Caller]), - ok = lager:debug("event=call_received socket=\"~p\" peer=\"~s\" caller=\"~p\" worker_pid=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), Caller, WorkerPid]), - ok = inet:setopts(Socket, [{active, once}]), - {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; + case ModVsnAllowed of + true -> + WorkerPid = erlang:spawn(?MODULE, call_worker, [self(), CallType, RealM, F, A, Caller]), + ?log(debug, "event=call_received driver=~s socket=\"~s\" peer=\"~s\" caller=\"~p\" worker_pid=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Caller, WorkerPid]), + ok = DriverMod:activate_socket(Socket), + {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; + false -> + ?log(debug, "event=incompatible_module_version driver=~s socket=\"~s\" method=~s module=~s", + [Driver, gen_rpc_helper:socket_to_string(Socket), CallType, RealM]), + ok = DriverMod:activate_socket(Socket), + waiting_for_data(info, {CallType, Caller, {badrpc,incompatible}}, State) + end; false -> - ok = lager:debug("event=request_not_allowed socket=\"~p\" control=~s method=~s module=\"~s\"", [Socket,Control,CallType,M]), - ok = inet:setopts(Socket, [{active, once}]), + ?log(debug, "event=request_not_allowed driver=~s socket=\"~s\" control=~s method=~s module=~s", + [Driver, gen_rpc_helper:socket_to_string(Socket), Control, CallType, RealM]), + ok = DriverMod:activate_socket(Socket), waiting_for_data(info, {CallType, Caller, {badrpc,unauthorized}}, State) end; {cast, M, F, A} -> - case is_allowed(M, Control, List) of + {ModVsnAllowed, RealM} = check_module_version_compat(M), + _Result = case check_if_module_allowed(RealM, Control, List) of true -> - ok = lager:debug("event=cast_received socket=\"~p\" peer=\"~s\" module=~s function=~s args=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), M, F, A]), - _Pid = erlang:spawn(M, F, A), - ok = inet:setopts(Socket, [{active, once}]), - {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; + case ModVsnAllowed of + true -> + ?log(debug, "event=cast_received driver=~s socket=\"~s\" peer=\"~s\" module=~s function=~s args=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), RealM, F, A]), + _Pid = erlang:spawn(RealM, F, A); + false -> + ?log(debug, "event=incompatible_module_version driver=~s socket=\"~s\" module=~s", + [Driver, gen_rpc_helper:socket_to_string(Socket), RealM]) + end; false -> - ok = lager:debug("event=request_not_allowed socket=\"~p\" control=~s method=cast module=\"~s\"", [Socket,Control,M]), - ok = inet:setopts(Socket, [{active, once}]), - {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)} - end; + ?log(debug, "event=request_not_allowed driver=~s socket=\"~s\" control=~s method=cast module=~s", + [Driver, gen_rpc_helper:socket_to_string(Socket), Control, RealM]) + end, + ok = DriverMod:activate_socket(Socket), + {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; {abcast, Name, Msg} -> - ok = lager:debug("event=abcast_received socket=\"~p\" peer=\"~s\" process=~s message=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), Name, Msg]), + ?log(debug, "event=abcast_received driver=~s socket=\"~s\" peer=\"~s\" process=~s message=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Name, Msg]), Msg = erlang:send(Name, Msg), - ok = inet:setopts(Socket, [{active, once}]), + ok = DriverMod:activate_socket(Socket), {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; {sbcast, Name, Msg, Caller} -> - ok = lager:debug("event=sbcast_received socket=\"~p\" peer=\"~s\" process=~s message=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), Name, Msg]), + ?log(debug, "event=sbcast_received driver=~s socket=\"~s\" peer=\"~s\" process=~s message=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Name, Msg]), Reply = case erlang:whereis(Name) of undefined -> error; - Pid -> Pid ! Msg, success + Pid -> Msg = erlang:send(Pid, Msg), success end, - ok = inet:setopts(Socket, [{active, once}]), + ok = DriverMod:activate_socket(Socket), waiting_for_data(info, {sbcast, Caller, Reply}, State); OtherData -> - ok = lager:debug("event=erroneous_data_received socket=\"~p\" peer=\"~s\" data=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), OtherData]), - {stop, {badrpc, erroneous_data}, State} + ?log(debug, "event=erroneous_data_received driver=~s socket=\"~s\" peer=\"~s\" data=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), OtherData]), + {stop, {badrpc,erroneous_data}, State} catch error:badarg -> - {stop, {badtcp, corrupt_data}, State} + {stop, {badtcp,corrupt_data}, State} end; %% Handle a call worker message -waiting_for_data(info, {CallReply,_Caller,_Reply} = Payload, #state{socket=Socket} = State) when Socket =/= undefined, CallReply =:= call; - Socket =/= undefined, CallReply =:= async_call; - Socket =/= undefined, CallReply =:= sbcast -> +waiting_for_data(info, {CallReply,_Caller,_Reply} = Payload, #state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State) +when CallReply =:= call orelse CallReply =:= async_call orelse CallReply =:= sbcast -> Packet = erlang:term_to_binary(Payload), - ok = lager:debug("message=call_reply event=call_reply_received socket=\"~p\" type=~s", [Socket, CallReply]), - case gen_tcp:send(Socket, Packet) of + ?log(debug, "message=call_reply event=call_reply_received driver=~s socket=\"~s\" type=~s", + [Driver, gen_rpc_helper:socket_to_string(Socket), CallReply]), + case DriverMod:send(Socket, Packet) of ok -> - ok = lager:debug("message=call_reply event=call_reply_sent socket=\"~p\"", [Socket]), + ?log(debug, "message=call_reply event=call_reply_sent driver=~s socket=\"~s\"", [Driver, gen_rpc_helper:socket_to_string(Socket)]), {keep_state_and_data, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; {error, Reason} -> - ok = lager:error("message=call_reply event=failed_to_send_call_reply socket=\"~p\" reason=\"~p\"", [Socket, Reason]), - {stop, {badtcp, Reason}, State} + ?log(error, "message=call_reply event=failed_to_send_call_reply driver=~s socket=\"~s\" reason=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Reason]), + {stop, Reason, State} end; %% Handle the inactivity timeout gracefully -waiting_for_data(timeout, _Undefined, State) -> - ok = lager:info("message=timeout event=server_inactivity_timeout socket=\"~p\" action=stopping", [State#state.socket]), - {stop, normal, State}. +waiting_for_data(timeout, _Undefined, #state{socket=Socket, driver=Driver} = State) -> + ?log(info, "message=timeout event=server_inactivity_timeout driver=~s socket=\"~s\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket)]), + {stop, normal, State}; + +waiting_for_data(info, {DriverClosed, Socket} = Msg, #state{socket=Socket, driver_closed=DriverClosed} = State) -> + handle_event(info, Msg, waiting_for_data, State); -handle_event(info, {tcp_closed, Socket}, _StateName, #state{socket=Socket,peer=Peer} = State) -> - ok = lager:notice("message=tcp_closed event=tcp_socket_closed socket=\"~p\" peer=\"~s\" action=stopping", - [Socket, gen_rpc_helper:peer_to_string(Peer)]), +waiting_for_data(info, {DriverError, Socket, _Reason} = Msg, #state{socket=Socket, driver_error=DriverError} = State) -> + handle_event(info, Msg, waiting_for_data, State). + +handle_event(info, {DriverClosed, Socket}, _StateName, #state{socket=Socket, driver=Driver, driver_closed=DriverClosed, peer=Peer} = State) -> + ?log(notice, "message=channel_closed driver=~s socket=\"~s\" peer=\"~s\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer)]), {stop, normal, State}; -handle_event(info, {tcp_error, Socket, Reason}, _StateName, #state{socket=Socket,peer=Peer} = State) -> - ok = lager:notice("message=tcp_error event=tcp_socket_error socket=\"~p\" peer=\"~s\" reason=\"~p\" action=stopping", - [Socket, gen_rpc_helper:peer_to_string(Peer), Reason]), +handle_event(info, {DriverError, Socket, Reason}, _StateName, #state{socket=Socket, driver=Driver, driver_error=DriverError, peer=Peer} = State) -> + ?log(error, "message=channel_error driver=~s socket=\"~s\" peer=\"~s\" reason=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), gen_rpc_helper:peer_to_string(Peer), Reason]), {stop, normal, State}; -handle_event(EventType, Event, StateName, State) -> - ok = lager:critical("socket=\"~p\" event=uknown_event event_type=\"~p\" payload=\"~p\" action=stopping", - [State#state.socket, EventType, Event]), +handle_event(EventType, Event, StateName, #state{socket=Socket, driver=Driver} = State) -> + ?log(error, "event=uknown_event driver=~s socket=\"~s\" event_type=\"~p\" payload=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), EventType, Event]), {stop, {StateName, undefined_event, Event}, State}. terminate(_Reason, _StateName, _State) -> ok. code_change(_OldVsn, StateName, State, _Extra) -> - {state_functions, StateName, State}. + {ok, StateName, State}. %%% =================================================== %%% Private functions %%% =================================================== -%% Process an RPC call request outside of the FSM +%% Process an RPC call request outside of the state machine call_worker(Server, CallType, M, F, A, Caller) -> - ok = lager:debug("event=call_received caller=\"~p\" module=~s function=~s args=\"~p\"", [Caller, M, F, A]), + ?log(debug, "event=call_received caller=\"~p\" module=~s function=~s args=\"~p\"", [Caller, M, F, A]), % If called MFA return exception, not of type term(). % This fails term_to_binary coversion, crashes process % and manifest as timeout. Wrap inside anonymous function with catch @@ -208,11 +259,38 @@ call_middleman(M, F, A) -> erlang:exit({call_middleman_result, Res}), ok. -is_allowed(_Module, undefined, _List) -> +%% Check if the function is RPC-enabled +check_if_module_allowed(_DriverMod, disabled, _List) -> true; -is_allowed(Module, whitelist, List) when is_atom(Module) -> +check_if_module_allowed(Module, whitelist, List) -> sets:is_element(Module, List); -is_allowed(Module, blacklist, List) when is_atom(Module) -> +check_if_module_allowed(Module, blacklist, List) -> not sets:is_element(Module, List). + +%% Check if the module version called is compatible with the one +%% requested by the caller +check_module_version_compat({M, Version}) -> + try + Attrs = M:module_info(attributes), + {vsn, VsnList} = lists:keyfind(vsn, 1, Attrs), + case VsnList of + [Vsn] when Vsn =:= Version -> + {true, M}; + Vsn when Vsn =:= Version -> + {true, M}; + _Else -> + {false, M} + end + catch + error:undef -> + ?log(debug, "event=module_not_found module=~s", [M]), + {false, M}; + error:badarg -> + ?log(debug, "event=invalid_module_definition module=\"~p\"", [M]), + {false, M} + end; + +check_module_version_compat(M) -> + {true, M}. diff --git a/src/gen_rpc_app.erl b/src/gen_rpc_app.erl index d168cf3..934ac5f 100644 --- a/src/gen_rpc_app.erl +++ b/src/gen_rpc_app.erl @@ -22,11 +22,11 @@ %%% =================================================== %%% Application callbacks %%% =================================================== --spec start(application:start_type(),any()) -> {error,any()} | {ok,pid()} | {ok,pid(),any()}. +-spec start(application:start_type(), term()) -> {error,any()} | {ok,pid()} | {ok,pid(),any()}. start(_StartType, _StartArgs) -> gen_rpc_sup:start_link(). --spec stop(any()) -> ok. +-spec stop(term()) -> ok. stop(_State) -> ok. diff --git a/src/gen_rpc_client.erl b/src/gen_rpc_client.erl index 25de3d2..50b958e 100644 --- a/src/gen_rpc_client.erl +++ b/src/gen_rpc_client.erl @@ -12,23 +12,24 @@ %%% Behaviour -behaviour(gen_server). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). %%% Include this library's name macro -include("app.hrl"). %%% Include helpful guard macros -include("guards.hrl"). -%%% Connection and reply timeouts from the TCP server --define(TCP_SERVER_CONN_TIMEOUT, 5000). --define(TCP_SERVER_SEND_TIMEOUT, 5000). --define(TCP_SERVER_RECV_TIMEOUT, 5000). - %%% Local state --record(state, {socket :: port()}). +-record(state, {socket :: port(), + driver :: atom(), + driver_mod :: atom(), + driver_closed :: atom(), + driver_error :: atom()}). %%% Supervisor functions -export([start_link/1, stop/1]). -%%% FSM functions +%%% Server functions -export([call/3, call/4, call/5, call/6, cast/3, cast/4, cast/5]). -export([async_call/3, async_call/4, yield/1, nb_yield/1, nb_yield/2]). @@ -62,111 +63,117 @@ stop(Node) when is_atom(Node) -> %%% =================================================== %%% Server functions %%% =================================================== - %% Simple server call with no args and default timeout values --spec call(node(), module(), atom()|function()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function()) -> term() | {badrpc,term()} | {badtcp,term()}. call(Node, M, F) -> call(Node, M, F, [], undefined, undefined). %% Simple server call with args and default timeout values --spec call(node(), module(), atom()|function(), list()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function(), list()) -> term() | {badrpc,term()} | {badtcp,term()}. call(Node, M, F, A) -> call(Node, M, F, A, undefined, undefined). %% Simple server call with custom receive timeout value --spec call(node(), module(), atom()|function(), list(), timeout()) -> term() | {badrpc, term()} | {badtcp | term()}. +-spec call(node(), atom() | tuple(), atom() | function(), list(), timeout()) -> term() | {badrpc,term()} | {badtcp,term()}. call(Node, M, F, A, RecvTO) -> call(Node, M, F, A, RecvTO, undefined). %% Simple server call with custom receive and send timeout values %% This is the function that all of the above call --spec call(node(), module(), atom()|function(), list(), timeout() | undefined, timeout() | undefined) -> term() | {badrpc, term()} | {badtcp | term()}. -call(Node, M, F, A, RecvTO, SendTO) when is_atom(Node), is_atom(M), is_atom(F), is_list(A), +-spec call(node(), atom() | tuple(), atom() | function(), list(), timeout() | undefined, timeout() | undefined) -> + term() | {badrpc,term()} | {badtcp,term()}. +call(Node, M, F, A, RecvTO, SendTO) when is_atom(Node), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A), RecvTO =:= undefined orelse ?is_timeout(RecvTO), SendTO =:= undefined orelse ?is_timeout(SendTO) -> %% Create a unique name for the client because we register as such PidName = gen_rpc_helper:make_process_name("client", Node), case erlang:whereis(PidName) of undefined -> - ok = lager:info("event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), + ?log(info, "event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), case gen_rpc_dispatcher:start_client(Node) of {ok, NewPid} -> %% We take care of CALL inside the gen_server %% This is not resilient enough if the caller's mailbox is full %% but it's good enough for now - try gen_server:call(NewPid, {{call,M,F,A}, SendTO}, gen_rpc_helper:get_call_receive_timeout(RecvTO)) - catch exit:{timeout,_Reason} -> {badrpc,timeout} + try + gen_server:call(NewPid, {{call,M,F,A}, SendTO}, gen_rpc_helper:get_call_receive_timeout(RecvTO)) + catch + exit:{timeout,_Reason} -> {badrpc,timeout}; + exit:OtherReason -> {badrpc, {unknown_error, OtherReason}} end; {error, Reason} -> Reason end; Pid -> - ok = lager:debug("event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), - try gen_server:call(Pid, {{call,M,F,A}, SendTO}, gen_rpc_helper:get_call_receive_timeout(RecvTO)) - catch exit:{timeout,_Reason} -> {badrpc,timeout} + ?log(debug, "event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), + try + gen_server:call(Pid, {{call,M,F,A}, SendTO}, gen_rpc_helper:get_call_receive_timeout(RecvTO)) + catch + exit:{timeout,_Reason} -> {badrpc,timeout}; + exit:OtherReason -> {badrpc, {unknown_error, OtherReason}} end end. %% Simple server cast with no args and default timeout values --spec cast(node(), module(), atom()|function()) -> true. +-spec cast(node(), atom() | tuple(), atom() | function()) -> true. cast(Node, M, F) -> cast(Node, M, F, [], undefined). %% Simple server cast with args and default timeout values --spec cast(node(), module(), atom()|function(), list()) -> true. +-spec cast(node(), atom() | tuple(), atom() | function(), list()) -> true. cast(Node, M, F, A) -> cast(Node, M, F, A, undefined). %% Simple server cast with custom send timeout value %% This is the function that all of the above casts call --spec cast(node(), module(), atom()|function(), list(), timeout() | undefined) -> true. -cast(Node, M, F, A, SendTO) when is_atom(Node), is_atom(M), is_atom(F), is_list(A), +-spec cast(node(), atom() | tuple(), atom() | function(), list(), timeout() | undefined) -> true. +cast(Node, M, F, A, SendTO) when is_atom(Node), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A), SendTO =:= undefined orelse ?is_timeout(SendTO) -> _WorkerPid = erlang:spawn(?MODULE, cast_worker, [Node, {cast,M,F,A}, undefined, SendTO]), true. %% Evaluate {M, F, A} on connected nodes. --spec eval_everywhere([node()], module(), atom()|function()) -> abcast. +-spec eval_everywhere([node()], atom() | tuple(), atom() | function()) -> abcast. eval_everywhere(Nodes, M, F) -> eval_everywhere(Nodes, M, F, [], undefined). %% Evaluate {M, F, A} on connected nodes. --spec eval_everywhere([node()], module(), atom()|function(), list()) -> abcast. +-spec eval_everywhere([node()], atom() | tuple(), atom() | function(), list()) -> abcast. eval_everywhere(Nodes, M, F, A) -> eval_everywhere(Nodes, M, F, A, undefined). %% Evaluate {M, F, A} on connected nodes. --spec eval_everywhere([node()], module(), atom()|function(), list(), timeout() | undefined) -> abcast. -eval_everywhere(Nodes, M, F, A, SendTO) when is_list(Nodes), is_atom(M), is_atom(F), is_list(A), +-spec eval_everywhere([node()], atom() | tuple(), atom() | function(), list(), timeout() | undefined) -> abcast. +eval_everywhere(Nodes, M, F, A, SendTO) when is_list(Nodes), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A), SendTO =:= undefined orelse ?is_timeout(SendTO) -> _ = [erlang:spawn(?MODULE, cast_worker, [Node, {cast,M,F,A}, abcast, SendTO]) || Node <- Nodes], abcast. %% Simple server async_call with no args --spec async_call(Node::node(), M::module(), F::atom()|function()) -> term() | {badrpc, term()} | {badtcp | term()}. -async_call(Node, M, F)-> +-spec async_call(node(), atom() | tuple(), atom() | function()) -> term() | {badrpc,term()} | {badtcp,term()}. +async_call(Node, M, F) -> async_call(Node, M, F, []). %% Simple server async_call with args --spec async_call(Node::node(), M::module(), F::atom()|function(), A::list()) -> term() | {badrpc, term()} | {badtcp | term()}. -async_call(Node, M, F, A) when is_atom(Node), is_atom(M), is_atom(F), is_list(A) -> +-spec async_call(node(), atom() | tuple(), atom() | function(), list()) -> term() | {badrpc,term()} | {badtcp,term()}. +async_call(Node, M, F, A) when is_atom(Node), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A) -> Ref = erlang:make_ref(), Pid = erlang:spawn(?MODULE, async_call_worker, [Node, M, F, A, Ref]), {Pid, Ref}. %% Simple server yield with key. Delegate to nb_yield. Default timeout form configuration. --spec yield(tuple()) -> term() | {badrpc, term()}. +-spec yield(tuple()) -> term() | {badrpc,term()}. yield(Key) -> {value,Result} = nb_yield(Key, infinity), Result. %% Simple server non-blocking yield with key, default timeout value of 0 --spec nb_yield(tuple()) -> {value, term()} | {badrpc, term()}. +-spec nb_yield(tuple()) -> {value,term()} | {badrpc,term()}. nb_yield(Key)-> nb_yield(Key, 0). %% Simple server non-blocking yield with key and custom timeout value --spec nb_yield(tuple(), timeout()) -> {value, term()} | {badrpc, term()}. +-spec nb_yield(tuple(), timeout()) -> {value,term()} | {badrpc,term()}. nb_yield({Pid,Ref}, Timeout) when is_pid(Pid), is_reference(Ref), ?is_timeout(Timeout) -> Pid ! {self(), Ref, yield}, receive @@ -174,25 +181,25 @@ nb_yield({Pid,Ref}, Timeout) when is_pid(Pid), is_reference(Ref), ?is_timeout(Ti {value,Result} after Timeout -> - ok = lager:debug("event=nb_yield_timeout async_call_pid=\"~p\" async_call_ref=\"~p\"", [Pid, Ref]), + ?log(debug, "event=nb_yield_timeout async_call_pid=\"~p\" async_call_ref=\"~p\"", [Pid, Ref]), timeout end. %% "Concurrent" call to a set of servers --spec multicall(module(), atom(), list()) -> {list(), list()}. -multicall(M, F, A) when is_atom(M), is_atom(F), is_list(A) -> +-spec multicall(atom() | tuple(), atom(), list()) -> {list(), list()}. +multicall(M, F, A) when is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A) -> multicall([node()|gen_rpc:nodes()], M, F, A). --spec multicall(list() | module(), module() | atom(), atom() | list(), list() | timeout()) -> {list(), list()}. -multicall(M, F, A, Timeout) when is_atom(M), is_atom(F), is_list(A), ?is_timeout(Timeout) -> +-spec multicall(list() | atom() | tuple(), atom() | tuple(), atom() | list(), list() | timeout()) -> {list(), list()}. +multicall(M, F, A, Timeout) when is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A), ?is_timeout(Timeout) -> multicall([node()|gen_rpc:nodes()], M, F, A, Timeout); -multicall(Nodes, M, F, A) when is_list(Nodes), is_atom(M), is_atom(F), is_list(A) -> +multicall(Nodes, M, F, A) when is_list(Nodes), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A) -> Keys = [async_call(Node, M, F, A) || Node <- Nodes], parse_multicall_results(Keys, Nodes, undefined). --spec multicall(list(), module(), atom(), list(), timeout()) -> {list(), list()}. -multicall(Nodes, M, F, A, Timeout) when is_list(Nodes), is_atom(M), is_atom(F), is_list(A), ?is_timeout(Timeout) -> +-spec multicall(list(), atom() | tuple(), atom(), list(), timeout()) -> {list(), list()}. +multicall(Nodes, M, F, A, Timeout) when is_list(Nodes), is_atom(M) orelse is_tuple(M), is_atom(F), is_list(A), ?is_timeout(Timeout) -> Keys = [async_call(Node, M, F, A) || Node <- Nodes], parse_multicall_results(Keys, Nodes, Timeout). @@ -220,52 +227,51 @@ sbcast(Nodes, Name, Msg) when is_list(Nodes), is_atom(Name) -> %%% =================================================== init({Node}) -> ok = gen_rpc_helper:set_optimal_process_flags(), - ok = lager:info("event=initializing_client node=\"~s\"", [Node]), - case connect_to_tcp_server(Node) of - {ok, IpAddress, Port} -> - ok = lager:debug("event=remote_server_started_successfully server_node=\"~s\" remote_port=\"~B\"", [Node, Port]), - ConnTO = gen_rpc_helper:get_connect_timeout(), - case gen_tcp:connect(IpAddress, Port, ?DEFAULT_TCP_OPTS, ConnTO) of - {ok, Socket} -> - ok = lager:debug("event=connecting_to_server server_node=\"~s\" peer=\"~s\" result=success", - [Node, gen_rpc_helper:peer_to_string({IpAddress, Port})]), - {ok, #state{socket=Socket}, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; - {error, Reason} -> - ok = lager:error("event=connecting_to_server server_node=\"~s\" server_ip=\"~s:~B\" result=failure reason=\"~p\"", - [Node, gen_rpc_helper:peer_to_string({IpAddress, Port}), Reason]), - {stop, {badtcp,Reason}} + {Driver, Port} = gen_rpc_helper:get_client_config_per_node(Node), + {DriverMod, DriverClosed, DriverError} = gen_rpc_helper:get_client_driver_options(Driver), + ?log(info, "event=initializing_client driver=~s node=\"~s\" port=~B", [Driver, Node, Port]), + case DriverMod:connect(Node, Port) of + {ok, Socket} -> + case DriverMod:authenticate_server(Socket) of + ok -> + {ok, #state{socket=Socket, + driver=Driver, + driver_mod=DriverMod, + driver_closed=DriverClosed, + driver_error=DriverError}, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; + {error, ReasonTuple} -> + ?log(error, "event=client_authentication_failed driver=~s reason=\"~p\"", [Driver, ReasonTuple]), + {stop, ReasonTuple} end; - {error, Reason} -> + {error, {_Class,Reason}} -> + %% This should be badtcp but to conform with + %% the RPC library we return badrpc {stop, {badrpc,Reason}} end. %% This is the actual CALL handler -handle_call({{call,_M,_F,_A} = PacketTuple, SendTO}, Caller, #state{socket=Socket} = State) -> +handle_call({{call,_M,_F,_A} = PacketTuple, SendTO}, Caller, #state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State) -> Packet = erlang:term_to_binary({PacketTuple, Caller}), - ok = lager:debug("message=call event=constructing_call_term socket=\"~p\" caller=\"~p\"", [Socket, Caller]), - ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(SendTO)}]), - case gen_tcp:send(Socket, Packet) of - {error, timeout} -> - ok = lager:error("message=call event=transmission_failed socket=\"~p\" caller=\"~p\" reason=\"timeout\"", [Socket, Caller]), - {stop, {badtcp,send_timeout}, {badtcp,send_timeout}, State}; + ?log(debug, "message=call event=constructing_call_term driver=~s socket=\"~s\" caller=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller]), + ok = DriverMod:set_send_timeout(Socket, SendTO), + case DriverMod:send(Socket, Packet) of {error, Reason} -> - ok = lager:error("message=call event=transmission_failed socket=\"~p\" caller=\"~p\" reason=\"~p\"", [Socket, Caller, Reason]), - {stop, {badtcp,Reason}, {badtcp,Reason}, State}; + ?log(error, "message=call event=transmission_failed driver=~s socket=\"~s\" caller=\"~p\" reason=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Reason]), + {stop, Reason, Reason, State}; ok -> - ok = lager:debug("message=call event=transmission_succeeded socket=\"~p\" caller=\"~p\"", [Socket, Caller]), + ?log(debug, "message=call event=transmission_succeeded driver=~s socket=\"~s\" caller=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller]), %% We need to enable the socket and perform the call only if the call succeeds - ok = inet:setopts(Socket, [{active,once}]), + ok = DriverMod:activate_socket(Socket), {noreply, State, gen_rpc_helper:get_inactivity_timeout(?MODULE)} end; -%% Gracefully terminate -handle_call(stop, _Caller, State) -> - ok = lager:debug("event=stopping_client socket=\"~p\"", [State#state.socket]), - {stop, normal, ok, State}; - %% Catch-all for calls - die if we get a message we don't expect -handle_call(Msg, _Caller, State) -> - ok = lager:critical("event=uknown_call_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), +handle_call(Msg, _Caller, #state{socket=Socket, driver=Driver} = State) -> + ?log(error, "event=uknown_call_received driver=~s socket=\"~s\" message=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), Msg]), {stop, {unknown_call, Msg}, {unknown_call, Msg}, State}. %% This is the actual CAST handler for CAST @@ -273,79 +279,80 @@ handle_cast({{cast,_M,_F,_A} = PacketTuple, SendTO}, State) -> send_cast(PacketTuple, State, SendTO, false); %% This is the actual CAST handler for ABCAST -handle_cast({{abcast, _Name, _Msg} = PacketTuple, undefined}, State) -> +handle_cast({{abcast,_Name,_Msg} = PacketTuple, undefined}, State) -> send_cast(PacketTuple, State, undefined, false); %% This is the actual CAST handler for SBCAST -handle_cast({{sbcast, _Name, _Msg, _Caller} = PacketTuple, undefined}, State) -> +handle_cast({{sbcast,_Name,_Msg,_Caller} = PacketTuple, undefined}, State) -> send_cast(PacketTuple, State, undefined, true); %% This is the actual ASYNC CALL handler -handle_cast({{async_call,_M,_F,_A} = PacketTuple, Caller, Ref}, #state{socket=Socket} = State) -> +handle_cast({{async_call,_M,_F,_A} = PacketTuple, Caller, Ref}, #state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State) -> Packet = erlang:term_to_binary({PacketTuple, {Caller,Ref}}), - ok = lager:debug("message=call event=constructing_async_call_term socket=\"~p\" worker_pid=\"~p\" async_call_ref=\"~p\"", - [Socket, Caller, Ref]), - ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(undefined)}]), - case gen_tcp:send(Socket, Packet) of - {error, timeout} -> - ok = lager:error("message=async_call event=transmission_failed socket=\"~p\" worker_pid=\"~p\" call_ref=\"~p\" reason=\"timeout\"", - [Socket, Caller, Ref]), - {stop, {badtcp,send_timeout}, {badtcp,send_timeout}, State}; + ?log(debug, "message=async_call event=constructing_async_call_term socket=\"~s\" worker_pid=\"~p\" async_call_ref=\"~p\"", + [gen_rpc_helper:socket_to_string(Socket), Caller, Ref]), + ok = DriverMod:set_send_timeout(Socket, undefined), + case DriverMod:send(Socket, Packet) of {error, Reason} -> - ok = lager:error("message=async_call event=transmission_failed socket=\"~p\" worker_pid=\"~p\" call_ref=\"~p\" reason=\"~p\"", - [Socket, Caller, Ref, Reason]), - {stop, {badtcp,Reason}, {badtcp,Reason}, State}; + ?log(error, "message=async_call event=transmission_failed driver=~s socket=\"~s\" worker_pid=\"~p\" call_ref=\"~p\" reason=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Ref, Reason]), + {stop, Reason, Reason, State}; ok -> - ok = lager:debug("message=async_call event=transmission_succeeded socket=\"~p\" worker_pid=\"~p\" call_ref=\"~p\"", - [Socket, Caller, Ref]), + ?log(debug, "message=async_call event=transmission_succeeded driver=~s socket=\"~s\" worker_pid=\"~p\" call_ref=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Ref]), %% We need to enable the socket and perform the call only if the call succeeds - ok = inet:setopts(Socket, [{active,once}]), + ok = DriverMod:activate_socket(Socket), %% Reply will be handled from the worker {noreply, State, gen_rpc_helper:get_inactivity_timeout(?MODULE)} end; %% Catch-all for casts - die if we get a message we don't expect -handle_cast(Msg, State) -> - ok = lager:critical("event=uknown_cast_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), +handle_cast(Msg, #state{socket=Socket, driver=Driver} = State) -> + ?log(error, "event=uknown_cast_received driver=~s socket=\"~s\" message=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), Msg]), {stop, {unknown_cast, Msg}, State}. %% Handle any TCP packet coming in -handle_info({tcp,Socket,Data}, #state{socket=Socket} = State) -> - _Reply = try erlang:binary_to_term(Data) of +handle_info({Driver,Socket,Data}, #state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State) -> + _Reply = case erlang:binary_to_term(Data) of {call, Caller, Reply} -> - ok = lager:debug("message=tcp event=call_reply_received caller=\"~p\" action=sending_reply", [Caller]), + ?log(debug, "event=call_reply_received driver=~s socket=\"~s\" caller=\"~p\" action=sending_reply", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller]), gen_server:reply(Caller, Reply); {async_call, {Caller, Ref}, Reply} -> - ok = lager:debug("message=tcp event=async_call_reply_received caller=\"~p\" action=sending_reply", [Caller]), + ?log(debug, "event=async_call_reply_received driver=~s socket=\"~s\" caller=\"~p\" action=sending_reply", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller]), Caller ! {self(), Ref, async_call, Reply}; {sbcast, {Caller, Ref, Node}, Reply} -> - ok = lager:debug("message=tcp event=sbcast_reply_received caller=\"~p\" reference=\"~p\" action=sending_reply", [Caller, Ref]), + ?log(debug, "event=sbcast_reply_received driver=~s socket=\"~s\" caller=\"~p\" reference=\"~p\" action=sending_reply", + [Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Ref]), Caller ! {Ref, Node, Reply}; OtherData -> - ok = lager:error("message=tcp event=erroneous_reply_received socket=\"~p\" data=\"~p\" action=ignoring", [Socket, OtherData]) - catch - error:badarg -> - ok = lager:error("message=tcp event=corrupt_data_received socket=\"~p\" action=ignoring", [Socket]) + ?log(error, "event=erroneous_reply_received driver=~s socket=\"~s\" data=\"~p\" action=ignoring", + [Driver, gen_rpc_helper:socket_to_string(Socket), OtherData]) end, - ok = inet:setopts(Socket, [{active,once}]), + ok = DriverMod:activate_socket(Socket), {noreply, State, gen_rpc_helper:get_inactivity_timeout(?MODULE)}; -handle_info({tcp_closed, Socket}, #state{socket=Socket} = State) -> - ok = lager:warning("message=tcp_closed event=tcp_socket_closed socket=\"~p\" action=stopping", [Socket]), +handle_info({DriverClosed, Socket}, #state{socket=Socket, driver=Driver, driver_closed=DriverClosed} = State) -> + ?log(error, "message=channel_closed driver=~s socket=\"~s\" action=stopping", [Driver, gen_rpc_helper:socket_to_string(Socket)]), {stop, normal, State}; -handle_info({tcp_error, Socket, Reason}, #state{socket=Socket} = State) -> - ok = lager:warning("message=tcp_error event=tcp_socket_error socket=\"~p\" reason=\"~p\" action=stopping", [Socket, Reason]), +handle_info({DriverError, Socket, Reason}, #state{socket=Socket, driver=Driver, driver_error=DriverError} = State) -> + ?log(error, "message=channel_error driver=~s socket=\"~s\" reason=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), Reason]), {stop, normal, State}; %% Handle the inactivity timeout gracefully -handle_info(timeout, State) -> - ok = lager:info("message=timeout event=client_inactivity_timeout socket=\"~p\" action=stopping", [State#state.socket]), +handle_info(timeout, #state{socket=Socket, driver=Driver} = State) -> + ?log(info, "message=timeout event=client_inactivity_timeout driver=~s socket=\"~s\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket)]), {stop, normal, State}; %% Catch-all for info - our protocol is strict so die! -handle_info(Msg, State) -> - ok = lager:critical("event=uknown_message_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), +handle_info(Msg, #state{socket=Socket, driver=Driver} = State) -> + ?log(error, "event=uknown_message_received driver=~s socket=\"~s\" message=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), Msg]), {stop, {unknown_info, Msg}, State}. %% Stub functions @@ -358,75 +365,22 @@ terminate(_Reason, _State) -> %%% =================================================== %%% Private functions %%% =================================================== -connect_to_tcp_server(Node) -> - Host = gen_rpc_helper:host_from_node(Node), - Port = gen_rpc_helper:get_remote_tcp_server_port(Node), - case gen_tcp:connect(Host, Port, ?DEFAULT_TCP_OPTS, ?TCP_SERVER_CONN_TIMEOUT) of - {ok, Socket} -> - ok = lager:debug("event=connecting_to_server peer=\"~s\" socket=\"~p\" result=success", [Node, Socket]), - {ok, {IpAddress, _Port}} = inet:peername(Socket), - get_node_port(Socket, IpAddress); - {error, Reason} -> - ok = lager:error("event=connecting_to_server peer=\"~s\" result=failure reason=\"~p\"", [Node, Reason]), - {error, Reason} - end. - -get_node_port(Socket, IpAddress) -> - Cookie = erlang:get_cookie(), - Packet = erlang:term_to_binary({start_gen_rpc_server, Cookie}), - ok = inet:setopts(Socket, [{send_timeout, ?TCP_SERVER_SEND_TIMEOUT}]), - case gen_tcp:send(Socket, Packet) of - {error, Reason} -> - ok = lager:error("event=transmission_failed socket=\"~p\" reason=\"~p\"", [Socket, Reason]), - ok = gen_tcp:close(Socket), - {error, Reason}; - ok -> - ok = lager:debug("event=transmission_succeeded socket=\"~p\"", [Socket]), - case gen_tcp:recv(Socket, 0, ?TCP_SERVER_RECV_TIMEOUT) of - {ok, RecvPacket} -> - try erlang:binary_to_term(RecvPacket) of - {gen_rpc_server_started, Port} -> - ok = lager:debug("event=reception_succeeded socket=\"~p\"", [Socket]), - ok = gen_tcp:close(Socket), - {ok, IpAddress, Port}; - {connection_rejected, invalid_cookie} -> - ok = lager:notice("event=reception_failed socket=\"~p\" reason=\"invalid_cookie\"", [Socket]), - ok = gen_tcp:close(Socket), - {error, invalid_cookie}; - _Else -> - ok = lager:debug("event=reception_failed socket=\"~p\" reason=\"invalid_payload\"", [Socket]), - ok = gen_tcp:close(Socket), - {error, invalid_message} - catch - error:badarg -> - ok = lager:debug("event=reception_failed socket=\"~p\" reason=\"invalid_erlang_term\"", [Socket]), - ok = gen_tcp:close(Socket), - {error, invalid_payload} - end; - {error, Reason} -> - ok = lager:debug("event=reception_failed socket=\"~p\" reason=\"~p\"", [Socket, Reason]), - ok = gen_tcp:close(Socket), - {error, Reason} - end - end. - -send_cast(PacketTuple, #state{socket=Socket} = State, SendTO, Activate) -> +send_cast(PacketTuple, #state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State, SendTO, Activate) -> Packet = erlang:term_to_binary(PacketTuple), - ok = lager:debug("event=constructing_cast_term cast=\"~p\" socket=\"~p\"", [PacketTuple, Socket]), - ok = inet:setopts(Socket, [{send_timeout, gen_rpc_helper:get_send_timeout(SendTO)}]), - case gen_tcp:send(Socket, Packet) of - {error, timeout} -> - %% Terminate will handle closing the socket - ok = lager:error("message=cast event=transmission_failed socket=\"~p\" reason=\"timeout\"", [Socket]), - {stop, {badtcp,send_timeout}, State}; + ?log(debug, "event=constructing_cast_term driver=~s socket=\"~s\" cast=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), PacketTuple]), + ok = DriverMod:set_send_timeout(Socket, SendTO), + case DriverMod:send(Socket, Packet) of {error, Reason} -> - ok = lager:error("message=cast event=transmission_failed socket=\"~p\" reason=\"~p\"", [Socket, Reason]), - {stop, {badtcp,Reason}, State}; + ?log(error, "message=cast event=transmission_failed driver=~s socket=\"~s\" reason=\"~p\"", + [Driver, gen_rpc_helper:socket_to_string(Socket), Reason]), + {stop, Reason, State}; ok -> - if Activate =:= true -> ok = inet:setopts(Socket, [{active,once}]); + ok = if Activate =:= true -> DriverMod:activate_socket(Socket); true -> ok end, - ok = lager:debug("message=cast event=transmission_succeeded socket=\"~p\"", [Socket]), + ?log(debug, "message=cast event=transmission_succeeded driver=~s socket=\"~s\"", + [Driver, gen_rpc_helper:socket_to_string(Socket)]), {noreply, State, gen_rpc_helper:get_inactivity_timeout(?MODULE)} end. @@ -435,7 +389,7 @@ cast_worker(Node, Cast, Ret, SendTO) -> PidName = gen_rpc_helper:make_process_name("client", Node), case erlang:whereis(PidName) of undefined -> - ok = lager:info("event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), + ?log(info, "event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), case gen_rpc_dispatcher:start_client(Node) of {ok, NewPid} -> %% We take care of CALL inside the gen_server @@ -447,7 +401,7 @@ cast_worker(Node, Cast, Ret, SendTO) -> Ret end; Pid -> - ok = lager:debug("event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), + ?log(debug, "event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), ok = gen_server:cast(Pid, {Cast,SendTO}), Ret end. @@ -457,16 +411,16 @@ async_call_worker(Node, M, F, A, Ref) -> PidName = gen_rpc_helper:make_process_name("client", Node), SrvPid = case erlang:whereis(PidName) of undefined -> - ok = lager:info("event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), + ?log(info, "event=client_process_not_found server_node=\"~s\" action=spawning_client", [Node]), case gen_rpc_dispatcher:start_client(Node) of {ok, NewPid} -> ok = gen_server:cast(NewPid, {{async_call,M,F,A}, self(), Ref}), NewPid; - {error, {badrpc, _} = RpcError} -> + {error, {badrpc,_} = RpcError} -> RpcError end; Pid -> - ok = lager:debug("event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), + ?log(debug, "event=client_process_found pid=\"~p\" server_node=\"~s\"", [Pid, Node]), ok = gen_server:cast(Pid, {{async_call,M,F,A}, self(), Ref}), Pid end, @@ -530,4 +484,4 @@ parse_sbcast_results([{_Pid,Node}|WorkerPids], Ref, {Good, Bad}, Timeout) -> end; parse_sbcast_results([], _Ref, Results, _Timeout) -> - Results. \ No newline at end of file + Results. diff --git a/src/gen_rpc_dispatcher.erl b/src/gen_rpc_dispatcher.erl index 4e2a245..67c86dd 100644 --- a/src/gen_rpc_dispatcher.erl +++ b/src/gen_rpc_dispatcher.erl @@ -11,6 +11,8 @@ %%% Behaviour -behaviour(gen_server). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). %%% Include this library's name macro -include("app.hrl"). @@ -43,36 +45,36 @@ start_client(Node) when is_atom(Node) -> %%% Behaviour callbacks %%% =================================================== init([]) -> - ok = lager:info("event=start"), + ?log(info, "event=start"), {ok, undefined}. %% Simply launch a connection to a node through the appropriate %% supervisor. This is a serialization interface so that handle_call({start_client, Node}, _Caller, undefined) -> PidName = gen_rpc_helper:make_process_name("client", Node), - Reply = case whereis(PidName) of + Reply = case erlang:whereis(PidName) of undefined -> - ok = lager:debug("message=start_client event=starting_client_server server_node=\"~s\"", [Node]), + ?log(debug, "message=start_client event=starting_client_server server_node=\"~s\"", [Node]), gen_rpc_client_sup:start_child(Node); Pid -> - ok = lager:debug("message=start_client event=node_already_started server_node=\"~s\"", [Node]), + ?log(debug, "message=start_client event=node_already_started server_node=\"~s\"", [Node]), {ok, Pid} end, {reply, Reply, undefined}; %% Catch-all for calls - die if we get a message we don't expect handle_call(Msg, _Caller, undefined) -> - ok = lager:critical("event=uknown_call_received message=\"~p\" action=stopping", [Msg]), + ?log(error, "event=uknown_call_received message=\"~p\" action=stopping", [Msg]), {stop, {unknown_call, Msg}, undefined}. %% Catch-all for casts - die if we get a message we don't expect handle_cast(Msg, undefined) -> - ok = lager:critical("event=uknown_cast_received message=\"~p\" action=stopping", [Msg]), + ?log(error, "event=uknown_cast_received message=\"~p\" action=stopping", [Msg]), {stop, {unknown_cast, Msg}, undefined}. %% Catch-all for info - our protocol is strict so die! handle_info(Msg, undefined) -> - ok = lager:critical("event=uknown_message_received message=\"~p\" action=stopping", [Msg]), + ?log(error, "event=uknown_message_received message=\"~p\" action=stopping", [Msg]), {stop, {unknown_info, Msg}, undefined}. code_change(_OldVersion, undefined, _Extra) -> diff --git a/src/gen_rpc_driver.erl b/src/gen_rpc_driver.erl new file mode 100644 index 0000000..bffc969 --- /dev/null +++ b/src/gen_rpc_driver.erl @@ -0,0 +1,37 @@ +%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. +%%% + +-module(gen_rpc_driver). + +-callback connect(atom(), inet:port_number()) -> {ok, term()} | {error, term()}. + +-callback listen(inet:port_number()) -> {ok, term()} | {error, term()}. + +-callback accept(term()) -> ok | {error, term()}. + +-callback activate_socket(term()) -> ok. + +-callback authenticate_server(term()) -> ok | {error, {badtcp | badrpc, term()}}. + +-callback authenticate_client(term(), tuple(), binary()) -> ok | {error, {badtcp | badrpc, term()}}. + +-callback send(term(), binary()) -> ok | {error, term()}. + +-callback get_peer(term()) -> {inet:ip4_address(), inet:port_number()}. + +-callback copy_sock_opts(term(), term()) -> ok | {error, any()}. + +-callback set_controlling_process(term(), pid()) -> ok | {error, term()}. + +-callback set_send_timeout(term(), timeout() | undefined) -> ok. + +-callback set_acceptor_opts(term()) -> ok. + +-ifdef(TEST). +%% Stub function to fool code coverage +-export([stub/0]). +stub() -> ok. +-endif. diff --git a/src/gen_rpc_helper.erl b/src/gen_rpc_helper.erl index 77eae9c..6063f9e 100644 --- a/src/gen_rpc_helper.erl +++ b/src/gen_rpc_helper.erl @@ -7,48 +7,37 @@ -module(gen_rpc_helper). -author("Panagiotis Papadomitsos "). +%%% Since a lot of these functions are simple +%%% let's inline them +-compile([inline]). + %%% Include this library's name macro -include("app.hrl"). %%% Public API --export([otp_release/0, - peer_to_string/1, +-export([peer_to_string/1, + socket_to_string/1, host_from_node/1, - set_sock_opt/2, + set_optimal_process_flags/0, make_process_name/2, extract_node_name/1, - set_optimal_process_flags/0, - get_remote_tcp_server_port/1, + is_driver_enabled/1, + merge_sockopt_lists/2, + get_server_driver_options/1, + get_client_config_per_node/1, get_connect_timeout/0, get_send_timeout/1, + get_rpc_module_control/0, + get_authentication_timeout/0, get_call_receive_timeout/1, get_sbcast_receive_timeout/0, + get_control_receive_timeout/0, get_inactivity_timeout/1, get_async_call_inactivity_timeout/0]). %%% =================================================== %%% Public API %%% =================================================== --spec otp_release() -> integer(). -otp_release() -> - try - erlang:list_to_integer(erlang:system_info(otp_release)) - catch - error:badarg -> - %% Before Erlang 17, R was included in the OTP release, - %% which would make the list_to_integer call fail. - %% Since we only use this function to test the availability - %% of the show_econnreset feature, 16 is good enough. - 16 - end. - --spec set_optimal_process_flags() -> ok. -set_optimal_process_flags() -> - _ = erlang:process_flag(trap_exit, true), - _ = erlang:process_flag(priority, high), - _ = erlang:process_flag(message_queue_data, off_heap), - ok. - %% Return the connected peer's IP -spec peer_to_string({inet:ip4_address(), inet:port_number()} | inet:ip4_address()) -> string(). peer_to_string({{A,B,C,D}, Port}) when is_integer(A), is_integer(B), is_integer(C), is_integer(D), is_integer(Port) -> @@ -60,6 +49,20 @@ peer_to_string({{A,B,C,D}, Port}) when is_integer(A), is_integer(B), is_integer( peer_to_string({A,B,C,D} = IpAddress) when is_integer(A), is_integer(B), is_integer(C), is_integer(D) -> peer_to_string({IpAddress, 0}). +-spec socket_to_string(term()) -> string(). +socket_to_string(Socket) when is_port(Socket) -> + io_lib:format("~p", [Socket]); + +socket_to_string(Socket) when is_tuple(Socket) -> + case Socket of + {sslsocket, _, {TcpSock, _}} -> + io_lib:format("~p", [TcpSock]); + {sslsocket,{_, TcpSock, _, _}, _} -> + io_lib:format("~p", [TcpSock]); + _Else -> + ssl_socket + end. + %% Return the remote Erlang hostname -spec host_from_node(node()) -> string(). host_from_node(Node) when is_atom(Node) -> @@ -67,34 +70,26 @@ host_from_node(Node) when is_atom(Node) -> [_Name, Host] = string:tokens(NodeStr, [$@]), Host. -%% Taken from prim_inet. We are merely copying some socket options from the -%% listening socket to the new acceptor socket. --spec set_sock_opt(port(), port()) -> ok | {error, any()}. -set_sock_opt(ListSock, AccSock) when is_port(ListSock), is_port(AccSock) -> - true = inet_db:register_socket(AccSock, inet_tcp), - case prim_inet:getopts(ListSock, ?ACCEPTOR_TCP_OPTS) of - {ok, Opts} -> - case prim_inet:setopts(AccSock, Opts) of - ok -> ok; - Error -> gen_tcp:close(AccSock), Error - end; - Error -> - (try - gen_tcp:close(AccSock) - catch - _:_ -> ok - end), - Error - end. +%% Set optimal process flags +-spec set_optimal_process_flags() -> ok. +set_optimal_process_flags() -> + _ = erlang:process_flag(trap_exit, true), + _ = erlang:process_flag(priority, high), + _ = erlang:process_flag(message_queue_data, off_heap), + ok. %% Return an atom to identify gen_rpc processes -spec make_process_name(list(), {inet:ip4_address(), inet:port_number()} | atom()) -> atom(). make_process_name("client", Node) when is_atom(Node) -> %% This function is going to be called enough to warrant a less pretty %% process name in order to avoid calling costly functions - NodeStr = atom_to_list(Node), + NodeStr = erlang:atom_to_list(Node), list_to_atom(lists:flatten(["gen_rpc.client.", NodeStr])); +make_process_name("server", Driver) when is_atom(Driver) -> + DriverStr = erlang:atom_to_list(Driver), + list_to_atom(lists:flatten(["gen_rpc_server_", DriverStr])); + make_process_name(Prefix, Peer) when is_list(Prefix), is_tuple(Peer) -> list_to_atom(lists:flatten(["gen_rpc.", Prefix, ".", peer_to_string(Peer)])). @@ -106,30 +101,66 @@ extract_node_name(PidName) when is_atom(PidName) -> PidStr = atom_to_list(PidName), list_to_atom(lists:nthtail(15, PidStr)). -%% Retrieves the default connect timeout +%% Merge lists that contain both tuples and simple values observing +%% keys in proplists +-spec merge_sockopt_lists(list(), list()) -> list(). +merge_sockopt_lists(List1, List2) -> + SList1 = lists:usort(fun hybrid_proplist_compare/2, List1), + SList2 = lists:usort(fun hybrid_proplist_compare/2, List2), + lists:umerge(fun hybrid_proplist_compare/2, SList1, SList2). + +-spec is_driver_enabled(atom()) -> boolean(). +is_driver_enabled(Driver) when is_atom(Driver) -> + DriverStr = erlang:atom_to_list(Driver), + Setting = list_to_atom(lists:flatten([DriverStr, "_server_port"])), + case application:get_env(?APP, Setting) of + {ok, false} -> + false; + {ok, _Port} -> + true + end. + +-spec get_server_driver_options(atom()) -> tuple(). +get_server_driver_options(Driver) when is_atom(Driver) -> + DriverStr = erlang:atom_to_list(Driver), + DriverMod = list_to_atom(lists:flatten(["gen_rpc_driver_", DriverStr])), + ClosedMsg = list_to_atom(lists:flatten([DriverStr, "_closed"])), + ErrorMsg = list_to_atom(lists:flatten([DriverStr, "_error"])), + PortSetting = list_to_atom(lists:flatten([DriverStr, "_server_port"])), + {ok, DriverPort} = application:get_env(?APP, PortSetting), + {DriverMod, DriverPort, ClosedMsg, ErrorMsg}. + +-spec get_client_driver_options(atom()) -> tuple(). +get_client_driver_options(Driver) when is_atom(Driver) -> + DriverStr = erlang:atom_to_list(Driver), + DriverMod = list_to_atom(lists:flatten(["gen_rpc_driver_", DriverStr])), + ClosedMsg = list_to_atom(lists:flatten([DriverStr, "_closed"])), + ErrorMsg = list_to_atom(lists:flatten([DriverStr, "_error"])), + {DriverMod, ClosedMsg, ErrorMsg}. + +-spec get_client_config_per_node(atom()) -> {atom(), inet:port_number()}. +get_client_config_per_node(Node) when is_atom(Node) -> + {ok, NodeConfig} = application:get_env(?APP, client_config_per_node), + case maps:find(Node, NodeConfig) of + error -> + {ok, Driver} = application:get_env(?APP, default_client_driver), + DriverStr = erlang:atom_to_list(Driver), + PortSetting = list_to_atom(lists:flatten([DriverStr, "_client_port"])), + {ok, Port} = application:get_env(?APP, PortSetting), + {Driver, Port}; + {ok, Port} when is_integer(Port) -> + {ok, Driver} = application:get_env(?APP, default_client_driver), + {Driver, Port}; + {ok, {Driver,Port}} -> + {Driver, Port} + end. + -spec get_connect_timeout() -> timeout(). get_connect_timeout() -> {ok, ConnTO} = application:get_env(?APP, connect_timeout), ConnTO. -%% Retrieves the specific TCP server port --spec get_remote_tcp_server_port(atom()) -> inet:port_number(). -get_remote_tcp_server_port(Node) -> - case application:get_env(?APP, remote_tcp_server_ports) of - {ok, []} -> - {ok, Port} = application:get_env(?APP, tcp_server_port), - Port; - {ok, Ports} -> - case lists:keyfind(Node, 1, Ports) of - false -> - {ok, Port} = application:get_env(?APP, tcp_server_port), - Port; - {Node, Port} -> - Port - end - end. - -%% Merges user-defined receive timeout values with app timeout values +%% Merges user-defined call receive timeout values with app timeout values -spec get_call_receive_timeout(undefined | timeout()) -> timeout(). get_call_receive_timeout(undefined) -> {ok, RecvTO} = application:get_env(?APP, call_receive_timeout), @@ -138,6 +169,34 @@ get_call_receive_timeout(undefined) -> get_call_receive_timeout(Else) -> Else. +-spec get_rpc_module_control() -> {atom(), atom() | sets:set()}. +get_rpc_module_control() -> + case application:get_env(?APP, rpc_module_control) of + {ok, disabled} -> + {disabled, disabled}; + {ok, Type} when Type =:= whitelist; Type =:= blacklist -> + {ok, List} = application:get_env(?APP, rpc_module_list), + {Type, sets:from_list(List)} + end. + +%% Retrieves the default authentication timeout +-spec get_authentication_timeout() -> timeout(). +get_authentication_timeout() -> + {ok, AuthTO} = application:get_env(?APP, authentication_timeout), + AuthTO. + +%% Returns the default sbcast receive timeout +-spec get_sbcast_receive_timeout() -> timeout(). +get_sbcast_receive_timeout() -> + {ok, RecvTO} = application:get_env(?APP, sbcast_receive_timeout), + RecvTO. + +%% Returns the default dispatch receive timeout +-spec get_control_receive_timeout() -> timeout(). +get_control_receive_timeout() -> + {ok, RecvTO} = application:get_env(?APP, control_receive_timeout), + RecvTO. + %% Merges user-defined send timeout values with app timeout values -spec get_send_timeout(undefined | timeout()) -> timeout(). get_send_timeout(undefined) -> @@ -161,8 +220,11 @@ get_async_call_inactivity_timeout() -> {ok, TTL} = application:get_env(?APP, async_call_inactivity_timeout), TTL. --spec get_sbcast_receive_timeout() -> timeout(). -get_sbcast_receive_timeout() -> - {ok, RecvTO} = application:get_env(?APP, sbcast_receive_timeout), - RecvTO. +%%% =================================================== +%%% Private functions +%%% =================================================== +hybrid_proplist_compare({K1,_V1}, {K2,_V2}) -> + K1 =< K2; +hybrid_proplist_compare(K1, K2) -> + K1 =< K2. diff --git a/src/gen_rpc_server.erl b/src/gen_rpc_server.erl index f00ff79..3e6a0ac 100644 --- a/src/gen_rpc_server.erl +++ b/src/gen_rpc_server.erl @@ -10,116 +10,93 @@ -author("Panagiotis Papadomitsos "). %%% Behaviour --behaviour(gen_server). +-behaviour(gen_statem). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). %%% Include this library's name macro -include("app.hrl"). +%%% Local state %%% Local state -record(state, {socket :: port(), - peer :: tuple(), - acceptor :: prim_inet:insock()}). + driver :: atom(), + driver_mod :: atom()}). -%%% Supervisor functions +%%% Server functions -export([start_link/1, stop/1]). -%%% Server functions --export([get_port/1]). +%% gen_statem callbacks +-export([init/1, handle_event/4, callback_mode/0, terminate/3, code_change/4]). -%%% Behaviour callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). +%% State machine states +-export([waiting_for_connection/3]). %%% =================================================== %%% Supervisor functions %%% =================================================== --spec start_link({inet:ip4_address(), inet:port_number()}) -> gen_sever:startlink_ret(). -start_link(Peer) when is_tuple(Peer) -> - Name = gen_rpc_helper:make_process_name("server", Peer), - gen_server:start_link({local,Name}, ?MODULE, {Peer}, [{spawn_opt, [{priority, high}]}]). - --spec stop(pid()) -> ok. -stop(Pid) when is_pid(Pid) -> - gen_server:stop(Pid, normal, infinity). +-spec start_link(atom()) -> gen_statem:startlink_ret(). +start_link(Driver) when is_atom(Driver) -> + case gen_rpc_helper:is_driver_enabled(Driver) of + false -> ignore; + true -> gen_statem:start_link({local,gen_rpc_helper:make_process_name("server", Driver)}, ?MODULE, {Driver}, []) + end. -%%% =================================================== -%%% Server functions -%%% =================================================== --spec get_port(pid()) -> {ok, inet:port_number()} | {error, term()} | term(). %dialyzer complains without term(). -get_port(Pid) when is_pid(Pid) -> - gen_server:call(Pid, get_port). +-spec stop(atom()) -> ok. +stop(Driver) when is_atom(Driver) -> + gen_statem:stop(gen_rpc_helper:make_process_name("server", Driver), normal, infinity). %%% =================================================== %%% Behaviour callbacks %%% =================================================== -init({Peer}) -> - _OldVal = erlang:process_flag(trap_exit, true), - case gen_tcp:listen(0, ?DEFAULT_TCP_OPTS) of +init({Driver}) -> + ok = gen_rpc_helper:set_optimal_process_flags(), + {DriverMod, DriverPort, _ClosedMsg, _ErrorMsg} = gen_rpc_helper:get_server_driver_options(Driver), + case DriverMod:listen(DriverPort) of {ok, Socket} -> - ok = lager:info("event=listener_started_successfully peer=\"~s\"", - [gen_rpc_helper:peer_to_string(Peer)]), - {ok, Ref} = prim_inet:async_accept(Socket, -1), - {ok, #state{peer=Peer, socket=Socket, acceptor=Ref}}; + %% Launch a new acceptor with a new accept socket + ?log(info, "event=server_setup_successfully driver=~s socket=\"~s\"", [Driver, gen_rpc_helper:socket_to_string(Socket)]), + {ok, waiting_for_connection, #state{socket=Socket, driver=Driver, driver_mod=DriverMod}, {next_event,internal,accept}}; {error, Reason} -> - ok = lager:critical("event=failed_to_start_listener peer=\"~s\" reason=\"~p\"", - [gen_rpc_helper:peer_to_string(Peer), Reason]), + ?log(error, "event=failed_to_setup_server driver=~s reason=\"~p\"", [Driver, Reason]), {stop, Reason} end. -%% Returns the dynamic port the current TCP server listens to -handle_call(get_port, _From, #state{socket=Socket} = State) -> - {ok, Port} = inet:port(Socket), - ok = lager:debug("message=get_port socket=\"~p\" port=~B", [Socket,Port]), - {reply, {ok, Port}, State}; - -%% Catch-all for calls - die if we get a message we don't expect -handle_call(Msg, _From, State) -> - ok = lager:critical("event=unknown_call_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_call, Msg}, {unknown_call, Msg}, State}. - -%% Catch-all for casts - die if we get a message we don't expect -handle_cast(Msg, State) -> - ok = lager:critical("event=unknown_cast_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_cast, Msg}, State}. - -handle_info({inet_async, ListSock, Ref, {ok, AccSocket}}, - #state{peer=Peer, socket=ListSock, acceptor=Ref} = State) -> - try - ok = lager:info("event=client_connection_received peer=\"~s\" socket=\"~p\" action=starting_acceptor", - [gen_rpc_helper:peer_to_string(Peer), ListSock]), - %% Start an acceptor process. We need to provide the acceptor - %% process with our designated node IP and name so enforcement - %% of those attributes can be made for security reasons. - {ok, AccPid} = gen_rpc_acceptor_sup:start_child(Peer), - %% Link to acceptor, if they die so should we, since we are single-receiver - %% to single-acceptor service - case gen_rpc_helper:set_sock_opt(ListSock, AccSocket) of - ok -> ok; - {error, Reason} -> exit({set_sock_opt, Reason}) - end, - ok = gen_tcp:controlling_process(AccSocket, AccPid), - ok = gen_rpc_acceptor:set_socket(AccPid, AccSocket), - {stop, normal, State} - catch - exit:ExitReason -> - ok = lager:error("message=inet_async event=unknown_error socket=\"~p\" error=\"~p\" action=stopping", - [ListSock, ExitReason]), - {stop, ExitReason, State} - end; - -%% Handle async socket errors gracefully -handle_info({inet_async, ListSock, Ref, Error}, #state{socket=ListSock,acceptor=Ref} = State) -> - ok = lager:error("message=inet_async event=listener_error socket=\"~p\" error=\"~p\" action=stopping", - [ListSock, Error]), - {stop, Error, State}; - -%% Catch-all for info - our protocol is strict so die! -handle_info(Msg, State) -> - ok = lager:critical("event=uknown_message_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_message, Msg}, State}. - -terminate(_Reason, _State) -> +callback_mode() -> + state_functions. + +waiting_for_connection(internal, accept, #state{socket=ListSock, driver=Driver, driver_mod=DriverMod} = State) -> + case DriverMod:accept(ListSock) of + {ok, AccSock} -> + ?log(info, "event=client_connection_received driver=~s socket=\"~s\" action=starting_acceptor", + [Driver, gen_rpc_helper:socket_to_string(ListSock)]), + Peer = DriverMod:get_peer(AccSock), + {ok, AccPid} = gen_rpc_acceptor_sup:start_child(Driver, Peer), + case DriverMod:copy_sock_opts(ListSock, AccSock) of + ok -> ok; + {error, Reason} -> exit({set_sock_opt, Reason}) + end, + ok = DriverMod:set_controlling_process(AccSock, AccPid), + ok = gen_rpc_acceptor:set_socket(AccPid, AccSock), + {keep_state_and_data, {next_event,internal,accept}}; + {error, Reason} -> + ?log(error, "event=socket_error_event driver=~s socket=\"~s\" event=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(ListSock), Reason]), + {stop, {socket_error, Reason}, State} + end. + +handle_event(EventType, Event, StateName, #state{socket=Socket, driver=Driver} = State) -> + ?log(error, "event=uknown_event driver=~s socket=\"~s\" event_type=\"~p\" payload=\"~p\" action=stopping", + [Driver, gen_rpc_helper:socket_to_string(Socket), EventType, Event]), + {stop, {StateName, undefined_event, Event}, State}. + +terminate(_Reason, _StateName, _State) -> ok. -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +%%% =================================================== +%%% Private functions +%%% =================================================== + diff --git a/src/gen_rpc_tcp_acceptor.erl b/src/gen_rpc_tcp_acceptor.erl deleted file mode 100644 index 7315455..0000000 --- a/src/gen_rpc_tcp_acceptor.erl +++ /dev/null @@ -1,139 +0,0 @@ -%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- -%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: -%%% -%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. -%%% -%%% Original concept inspired and some code copied from -%%% https://erlangcentral.org/wiki/index.php?title=Building_a_Non-blocking_TCP_server_using_OTP_principles - --module(gen_rpc_tcp_acceptor). --author("Panagiotis Papadomitsos "). - -%%% Behaviour --behaviour(gen_statem). - -%%% Include this library's name macro --include("app.hrl"). - -%%% Receive timeout for lingering clients --define(RECEIVE_TIMEOUT, 5000). -%%% Reply timeout --define(SEND_TIMEOUT, 5000). - -%%% Local state --record(state, {socket = undefined :: port() | undefined, - peer :: {inet:ip4_address(), inet:port_number()}}). - -%%% Server functions --export([start_link/1, set_socket/2, stop/1]). - -%% gen_statem callbacks --export([init/1, handle_event/4, callback_mode/0, terminate/3, code_change/4]). - -%% FSM States --export([waiting_for_socket/3, waiting_for_data/3]). - -%%% =================================================== -%%% Supervisor functions -%%% =================================================== --spec start_link({inet:ip4_address(), inet:port_number()}) -> gen_statem:startlink_ret(). -start_link(Peer) when is_tuple(Peer) -> - Name = gen_rpc_helper:make_process_name("tcp_acceptor", Peer), - gen_statem:start_link({local,Name}, ?MODULE, {Peer}, []). - --spec stop(pid()) -> ok. -stop(Pid) when is_pid(Pid) -> - gen_statem:stop(Pid, normal, infinity). - -%%% =================================================== -%%% Server functions -%%% =================================================== --spec set_socket(pid(), gen_tcp:socket()) -> ok. -set_socket(Pid, Socket) when is_pid(Pid), is_port(Socket) -> - gen_statem:call(Pid, {socket_ready, Socket}). - -%%% =================================================== -%%% Behaviour callbacks -%%% =================================================== -init({Peer}) -> - _OldVal = erlang:process_flag(trap_exit, true), - ok = lager:debug("event=start peer=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), - %% Store the client's IP in our state - {ok, waiting_for_socket, #state{peer=Peer}}. - -callback_mode() -> - state_functions. - -waiting_for_socket({call, From}, {socket_ready, Socket}, #state{peer=Peer} = State) -> - % Now we own the socket - ok = lager:debug("event=acquiring_socket_ownership socket=\"~p\" peer=\"~s\"", [Socket, gen_rpc_helper:peer_to_string(Peer)]), - ok = inet:setopts(Socket, [{send_timeout, ?SEND_TIMEOUT}|?ACCEPTOR_DEFAULT_TCP_OPTS]), - ok = gen_statem:reply(From, ok), - {next_state, waiting_for_data, State#state{socket=Socket}}. - -%% Notification event coming from client -waiting_for_data(info, {tcp, Socket, Data}, #state{socket=Socket,peer=Peer} = State) when Socket =/= undefined -> - Cookie = erlang:get_cookie(), - try erlang:binary_to_term(Data) of - {start_gen_rpc_server, ClientCookie} when ClientCookie =:= Cookie -> - {ok, Pid} = gen_rpc_server_sup:start_child(Peer), - {ok, Port} = gen_rpc_server:get_port(Pid), - Packet = erlang:term_to_binary({gen_rpc_server_started, Port}), - Result = case gen_tcp:send(Socket, Packet) of - {error, Reason} -> - ok = lager:error("event=transmission_failed socket=\"~p\" peer=\"~s\" reason=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), Reason]), - {stop, {badtcp,Reason}, State}; - ok -> - ok = lager:debug("event=transmission_succeeded socket=\"~p\" peer=\"~s\"", - [Socket, gen_rpc_helper:peer_to_string(Peer)]), - {stop, normal, State} - end, - Result; - {start_gen_rpc_server, _IncorrectClientCookie} -> - ok = lager:error("event=invalid_cookie_received socket=\"~p\" peer=\"~s\"", - [Socket, gen_rpc_helper:peer_to_string(Peer)]), - Packet = erlang:term_to_binary({connection_rejected, invalid_cookie}), - ok = case gen_tcp:send(Socket, Packet) of - {error, Reason} -> - ok = lager:error("event=transmission_failed socket=\"~p\" peer=\"~s\" reason=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), Reason]); - ok -> - ok = lager:debug("event=transmission_succeeded socket=\"~p\" peer=\"~s\"", - [Socket, gen_rpc_helper:peer_to_string(Peer)]) - end, - {stop, {badrpc,invalid_cookie}, State}; - OtherData -> - ok = lager:debug("event=erroneous_data_received socket=\"~p\" peer=\"~s\" data=\"~p\"", - [Socket, gen_rpc_helper:peer_to_string(Peer), OtherData]), - {stop, {badrpc, erroneous_data}, State} - catch - error:badarg -> - {stop, {badtcp, corrupt_data}, State} - end; - -%% Handle the inactivity timeout gracefully -waiting_for_data(timeout, _Undefined, State) -> - ok = lager:info("message=timeout event=receive_timeout socket=\"~p\" action=stopping", [State#state.socket]), - {stop, normal, State}. - -handle_event(info, {tcp_closed, Socket}, _StateName, #state{socket=Socket, peer=Peer} = State) -> - ok = lager:info("message=tcp_closed event=tcp_socket_closed socket=\"~p\" peer=\"~s\" action=stopping", - [Socket, gen_rpc_helper:peer_to_string(Peer)]), - {stop, normal, State}; - -handle_event(info, {tcp_error, Socket, Reason}, _StateName, #state{socket=Socket,peer=Peer} = State) -> - ok = lager:notice("message=tcp_error event=tcp_socket_error socket=\"~p\" peer=\"~s\" reason=\"~p\" action=stopping", - [Socket, gen_rpc_helper:peer_to_string(Peer), Reason]), - {stop, normal, State}; - -handle_event(EventType, Event, StateName, State) -> - ok = lager:critical("socket=\"~p\" event=uknown_event event_type=\"~p\" payload=\"~p\" action=stopping", - [State#state.socket, EventType, Event]), - {stop, {StateName, undefined_event, Event}, State}. - -code_change(_OldVsn, StateName, State, _Extra) -> - {state_functions, StateName, State}. - -terminate(_Reason, _StateName, _State) -> - ok. diff --git a/src/gen_rpc_tcp_server.erl b/src/gen_rpc_tcp_server.erl deleted file mode 100644 index a29cb4f..0000000 --- a/src/gen_rpc_tcp_server.erl +++ /dev/null @@ -1,108 +0,0 @@ -%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- -%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: -%%% -%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. -%%% -%%% Original concept inspired and some code copied from -%%% https://erlangcentral.org/wiki/index.php?title=Building_a_Non-blocking_TCP_server_using_OTP_principles - --module(gen_rpc_tcp_server). --author("Panagiotis Papadomitsos "). - -%%% Behaviour --behaviour(gen_server). - -%%% Include this library's name macro --include("app.hrl"). - -%%% Local state --record(state, {socket :: port(), - acceptor :: prim_inet:insock()}). - -%%% Supervisor functions --export([start_link/0, stop/0]). - -%%% Behaviour callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). - -%%% =================================================== -%%% Supervisor functions -%%% =================================================== --spec start_link() -> gen_sever:startlink_ret(). -start_link() -> - gen_server:start_link({local,?MODULE}, ?MODULE, {}, []). - --spec stop() -> ok. -stop() -> - gen_server:stop(?MODULE, normal, infinity). - -%%% =================================================== -%%% Behaviour callbacks -%%% =================================================== -init({}) -> - _OldVal = erlang:process_flag(trap_exit, true), - {ok, Port} = application:get_env(?APP, tcp_server_port), - case gen_tcp:listen(Port, ?DEFAULT_TCP_OPTS) of - {ok, Socket} -> - ok = lager:info("event=listener_started_successfully port=\"~B\"", [Port]), - {ok, Ref} = prim_inet:async_accept(Socket, -1), - {ok, #state{socket=Socket, acceptor=Ref}}; - {error, Reason} -> - ok = lager:critical("event=failed_to_start_listener reason=\"~p\"", [Reason]), - {stop, Reason} - end. - -%% Catch-all for calls - die if we get a message we don't expect -handle_call(Msg, _From, State) -> - ok = lager:critical("event=unknown_call_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_call, Msg}, {unknown_call, Msg}, State}. - -%% Catch-all for casts - die if we get a message we don't expect -handle_cast(Msg, State) -> - ok = lager:critical("event=unknown_cast_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_cast, Msg}, State}. - -handle_info({inet_async, ListSock, Ref, {ok, AccSocket}}, #state{socket=ListSock, acceptor=Ref} = State) -> - try - {ok, Peer} = inet:peername(AccSocket), - ok = lager:info("event=client_connection_received client_ip=\"~s\" socket=\"~p\" action=starting_acceptor", - [gen_rpc_helper:peer_to_string(Peer), ListSock]), - %% Start an acceptor process. We need to provide the acceptor - %% process with our designated client IP and so enforcement - %% of this attribute can be made for security reasons. - {ok, AccPid} = gen_rpc_tcp_acceptor_sup:start_child(Peer), - case gen_rpc_helper:set_sock_opt(ListSock, AccSocket) of - ok -> ok; - {error, Reason} -> exit({set_sock_opt, Reason}) - end, - ok = gen_tcp:controlling_process(AccSocket, AccPid), - ok = gen_rpc_acceptor:set_socket(AccPid, AccSocket), - case prim_inet:async_accept(ListSock, -1) of - {ok, NewRef} -> {noreply, State#state{acceptor=NewRef}, hibernate}; - {error, NewRef} -> exit({async_accept, inet:format_error(NewRef)}) - end - catch - exit:ExitReason -> - ok = lager:error("message=inet_async event=unknown_error socket=\"~p\" error=\"~p\" action=stopping", - [ListSock, ExitReason]), - {stop, ExitReason, State} - end; - -%% Handle async socket errors gracefully -handle_info({inet_async, Socket, _Ref, Error}, #state{socket=Socket} = State) -> - ok = lager:error("message=inet_async event=listener_error socket=\"~p\" error=\"~p\" action=stopping", - [Socket, Error]), - {stop, Error, State}; - -%% Catch-all for info - our protocol is strict so die! -handle_info(Msg, State) -> - ok = lager:critical("event=uknown_message_received socket=\"~p\" message=\"~p\" action=stopping", [State#state.socket, Msg]), - {stop, {unknown_message, Msg}, State}. - -%% Terminate cleanly by closing the listening socket -terminate(_Reason, _Socket) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. diff --git a/src/supervisor/gen_rpc_acceptor_sup.erl b/src/supervisor/gen_rpc_acceptor_sup.erl index 44ea9bb..f57d805 100644 --- a/src/supervisor/gen_rpc_acceptor_sup.erl +++ b/src/supervisor/gen_rpc_acceptor_sup.erl @@ -10,8 +10,11 @@ %%% Behaviour -behaviour(supervisor). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). + %%% Supervisor functions --export([start_link/0, start_child/1, stop_child/1]). +-export([start_link/0, start_child/2, stop_child/1]). %%% Supervisor callbacks -export([init/1]). @@ -23,10 +26,10 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). --spec start_child({inet:ip4_address(), inet:port_number()}) -> {ok, pid()} | {error, any()}. -start_child(Peer) when is_tuple(Peer) -> - ok = lager:debug("event=starting_new_acceptor peer=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), - case supervisor:start_child(?MODULE, [Peer]) of +-spec start_child(atom(), {inet:ip4_address(), inet:port_number()}) -> supervisor:startchild_ret(). +start_child(Driver, Peer) when is_tuple(Peer) -> + ?log(debug, "event=starting_new_acceptor peer=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), + case supervisor:start_child(?MODULE, [Driver,Peer]) of {error, {already_started, CPid}} -> %% If we've already started the child, terminate it and start anew ok = stop_child(CPid), @@ -39,9 +42,8 @@ start_child(Peer) when is_tuple(Peer) -> -spec stop_child(pid()) -> ok. stop_child(Pid) when is_pid(Pid) -> - ok = lager:debug("event=stopping_acceptor acceptor_pid=\"~p\"", [Pid]), + ?log(debug, "event=stopping_acceptor acceptor_pid=\"~p\"", [Pid]), _ = supervisor:terminate_child(?MODULE, Pid), - _ = supervisor:delete_child(?MODULE, Pid), ok. %%% =================================================== diff --git a/src/supervisor/gen_rpc_client_sup.erl b/src/supervisor/gen_rpc_client_sup.erl index 0093e89..48d2af1 100644 --- a/src/supervisor/gen_rpc_client_sup.erl +++ b/src/supervisor/gen_rpc_client_sup.erl @@ -10,6 +10,9 @@ %%% Behaviour -behaviour(supervisor). +%%% Include the HUT library +-include_lib("hut/include/hut.hrl"). + %%% Supervisor functions -export([start_link/0, start_child/1, stop_child/1, children_names/0]). @@ -25,7 +28,7 @@ start_link() -> -spec start_child(node()) -> supervisor:startchild_ret(). start_child(Node) when is_atom(Node) -> - ok = lager:debug("event=starting_new_client server_node=\"~s\"", [Node]), + ?log(debug, "event=starting_new_client server_node=\"~s\"", [Node]), case supervisor:start_child(?MODULE, [Node]) of {error, {already_started, CPid}} -> %% If we've already started the child, terminate it and start anew @@ -39,9 +42,8 @@ start_child(Node) when is_atom(Node) -> -spec stop_child(pid()) -> ok. stop_child(Pid) when is_pid(Pid) -> - ok = lager:debug("event=stopping_client client_pid=\"~p\"", [Pid]), + ?log(debug, "event=stopping_client client_pid=\"~p\"", [Pid]), _ = supervisor:terminate_child(?MODULE, Pid), - _ = supervisor:delete_child(?MODULE, Pid), ok. -spec children_names() -> list(). @@ -67,4 +69,3 @@ init([]) -> {ok, {{simple_one_for_one, 100, 1}, [ {gen_rpc_client, {gen_rpc_client,start_link,[]}, temporary, 5000, worker, [gen_rpc_client]} ]}}. - diff --git a/src/supervisor/gen_rpc_server_sup.erl b/src/supervisor/gen_rpc_server_sup.erl deleted file mode 100644 index 16bfaec..0000000 --- a/src/supervisor/gen_rpc_server_sup.erl +++ /dev/null @@ -1,56 +0,0 @@ -%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- -%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: -%%% -%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. -%%% - --module(gen_rpc_server_sup). --author("Panagiotis Papadomitsos "). - -%%% Behaviour --behaviour(supervisor). - -%%% Supervisor functions --export([start_link/0, start_child/1, stop_child/1]). - -%%% Supervisor callbacks --export([init/1]). - -%%% =================================================== -%%% Supervisor functions -%%% =================================================== --spec start_link() -> supervisor:startlink_ret(). -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% Launch a local receiver and return the port --spec start_child({inet:ip4_address(), inet:port_number()}) -> {ok, any()} | {error, any()}. -start_child(Peer) when is_tuple(Peer) -> - ok = lager:debug("event=starting_new_server peer=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), - case supervisor:start_child(?MODULE, [Peer]) of - {error, {already_started, CPid}} -> - %% If we've already started the child, terminate it and start anew - ok = stop_child(CPid), - supervisor:start_child(?MODULE, [Peer]); - {error, OtherError} -> - {error, OtherError}; - {ok, TPid} -> - {ok, TPid} - end. - -%% Terminate and unregister a child server --spec stop_child(Pid::pid()) -> ok. -stop_child(Pid) when is_pid(Pid) -> - ok = lager:debug("event=stopping_server server_pid=\"~p\"", [Pid]), - %% Terminate the acceptor child first and then - _ = supervisor:terminate_child(?MODULE, Pid), - _ = supervisor:delete_child(?MODULE, Pid), - ok. - -%%% =================================================== -%%% Supervisor callbacks -%%% =================================================== -init([]) -> - {ok, {{simple_one_for_one, 100, 1}, [ - {gen_rpc_server, {gen_rpc_server,start_link,[]}, temporary, 5000, worker, [gen_rpc_server]} - ]}}. diff --git a/src/supervisor/gen_rpc_sup.erl b/src/supervisor/gen_rpc_sup.erl index 3359f21..8fb2af1 100644 --- a/src/supervisor/gen_rpc_sup.erl +++ b/src/supervisor/gen_rpc_sup.erl @@ -28,10 +28,9 @@ start_link() -> %%% =================================================== init([]) -> {ok, {{one_for_one, 100, 1}, [ - {gen_rpc_server_sup, {gen_rpc_server_sup,start_link, []}, permanent, 5000, supervisor, [gen_rpc_server_sup]}, + {gen_rpc_server_tcp, {gen_rpc_server,start_link,[tcp]}, permanent, 5000, worker, [gen_rpc_server]}, + {gen_rpc_server_ssl, {gen_rpc_server,start_link,[ssl]}, permanent, 5000, worker, [gen_rpc_server]}, {gen_rpc_acceptor_sup, {gen_rpc_acceptor_sup,start_link, []}, permanent, 5000, supervisor, [gen_rpc_acceptor_sup]}, - {gen_rpc_tcp_acceptor_sup, {gen_rpc_tcp_acceptor_sup,start_link, []}, permanent, 5000, supervisor, [gen_rpc_tcp_acceptor_sup]}, {gen_rpc_dispatcher, {gen_rpc_dispatcher,start_link, []}, permanent, 5000, worker, [gen_rpc_dispatcher]}, - {gen_rpc_tcp_server, {gen_rpc_tcp_server,start_link, []}, permanent, 5000, worker, [gen_rpc_tcp_server]}, {gen_rpc_client_sup, {gen_rpc_client_sup,start_link, []}, permanent, 5000, supervisor, [gen_rpc_client_sup]} ]}}. diff --git a/src/supervisor/gen_rpc_tcp_acceptor_sup.erl b/src/supervisor/gen_rpc_tcp_acceptor_sup.erl deleted file mode 100644 index a030594..0000000 --- a/src/supervisor/gen_rpc_tcp_acceptor_sup.erl +++ /dev/null @@ -1,53 +0,0 @@ -%%% -*-mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- -%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: -%%% -%%% Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. -%%% - --module(gen_rpc_tcp_acceptor_sup). --author("Panagiotis Papadomitsos "). - -%%% Behaviour --behaviour(supervisor). - -%%% Supervisor functions --export([start_link/0, start_child/1, stop_child/1]). - -%%% Supervisor callbacks --export([init/1]). - -%%% =================================================== -%%% Supervisor functions -%%% =================================================== --spec start_link() -> supervisor:startlink_ret(). -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - --spec start_child({inet:ip4_address(), inet:port_number()}) -> {ok, supervisor:startchild_ret()} | {error, any()}. -start_child(Peer) when is_tuple(Peer) -> - ok = lager:debug("event=starting_new_tcp_acceptor client_ip=\"~s\"", [gen_rpc_helper:peer_to_string(Peer)]), - case supervisor:start_child(?MODULE, [Peer]) of - {error, {already_started, CPid}} -> - %% If we've already started the child, terminate it and start anew - ok = stop_child(CPid), - supervisor:start_child(?MODULE, [Peer]); - {error, OtherError} -> - {error, OtherError}; - {ok, Pid} -> - {ok, Pid} - end. - --spec stop_child(Pid::pid()) -> ok. -stop_child(Pid) when is_pid(Pid) -> - ok = lager:debug("event=stopping_acceptor acceptor_pid=\"~p\"", [Pid]), - _ = supervisor:terminate_child(?MODULE, Pid), - _ = supervisor:delete_child(?MODULE, Pid), - ok. - -%%% =================================================== -%%% Supervisor callbacks -%%% =================================================== -init([]) -> - {ok, {{simple_one_for_one, 1000, 1}, [ - {gen_rpc_tcp_acceptor, {gen_rpc_tcp_acceptor,start_link,[]}, temporary, 5000, worker, [gen_rpc_tcp_acceptor]} - ]}}. diff --git a/test/ct/integration_SUITE.erl b/test/ct/integration_SUITE.erl index d433089..1e724e3 100644 --- a/test/ct/integration_SUITE.erl +++ b/test/ct/integration_SUITE.erl @@ -23,7 +23,9 @@ init_per_suite(Config) -> %% Starting Distributed Erlang on local node {ok, _Pid} = gen_rpc_test_helper:start_distribution(this_node()), %% Setup application logging - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:set_application_environment(?MASTER), + %% Setup the simple TCP driver + ok = gen_rpc_test_helper:set_driver_configuration(tcp, ?MASTER), %% Starting the application locally {ok, _MasterApps} = application:ensure_all_started(?APP), Config. @@ -72,7 +74,7 @@ remote_socket_close(_Config) -> [{_,AccPid,_,_}] = rpc:call(Node, supervisor, which_children, [gen_rpc_acceptor_sup]), true = rpc:call(Node, erlang, exit, [AccPid,normal]) end, peers()), - ok = timer:sleep(100), + ok = timer:sleep(1000), Alive = gen_rpc:nodes(), ok = lists:foreach(fun(Node) -> false = lists:member(Node, Alive) diff --git a/test/ct/local_SUITE.erl b/test/ct/local_SUITE.erl index bb5a0e0..0ef1b24 100644 --- a/test/ct/local_SUITE.erl +++ b/test/ct/local_SUITE.erl @@ -9,6 +9,8 @@ %%% CT Macros -include_lib("test/include/ct.hrl"). +%%% TCP settings +-include("tcp.hrl"). %%% No need to export anything, everything is automatically exported %%% as part of the test profile @@ -22,48 +24,52 @@ all() -> init_per_suite(Config) -> %% Starting Distributed Erlang on local node {ok, _Pid} = gen_rpc_test_helper:start_distribution(?MASTER), - %% Setup application logging - ok = gen_rpc_test_helper:set_application_environment(), - %% Starting the application locally - {ok, _MasterApps} = application:ensure_all_started(?APP), + %% Setup the app locally + ok = gen_rpc_test_helper:start_master(tcp), Config. end_per_suite(_Config) -> ok. +init_per_testcase(authentication_timeout, Config) -> + ok = gen_rpc_test_helper:restart_application(), + ok = gen_rpc_test_helper:start_master(tcp), + ok = application:set_env(?APP, authentication_timeout, 500), + Config; + init_per_testcase(client_inactivity_timeout, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:start_master(tcp), ok = application:set_env(?APP, client_inactivity_timeout, 500), Config; init_per_testcase(server_inactivity_timeout, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:start_master(tcp), ok = application:set_env(?APP, server_inactivity_timeout, 500), Config; init_per_testcase(async_call_inexistent_node, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:start_master(tcp), Config; init_per_testcase(remote_node_call, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + ok = gen_rpc_test_helper:start_master(tcp), + ok = gen_rpc_test_helper:start_slave(tcp), Config; init_per_testcase(rpc_module_whitelist, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:start_master(tcp), ok = application:set_env(?APP, rpc_module_list, [erlang, os]), ok = application:set_env(?APP, rpc_module_control, whitelist), Config; init_per_testcase(rpc_module_blacklist, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), + ok = gen_rpc_test_helper:start_master(tcp), ok = application:set_env(?APP, rpc_module_list, [erlang, os]), ok = application:set_env(?APP, rpc_module_control, blacklist), Config; @@ -71,6 +77,10 @@ init_per_testcase(rpc_module_blacklist, Config) -> init_per_testcase(_OtherTest, Config) -> Config. +end_per_testcase(authentication_timeout, Config) -> + ok = application:set_env(?APP, authentication_timeout, 5000), + Config; + end_per_testcase(client_inactivity_timeout, Config) -> ok = application:set_env(?APP, client_inactivity_timeout, infinity), Config; @@ -80,17 +90,17 @@ end_per_testcase(server_inactivity_timeout, Config) -> Config; end_per_testcase(remote_node_call, Config) -> - ok = gen_rpc_test_helper:stop_slave(?SLAVE), + ok = gen_rpc_test_helper:stop_slave(), Config; end_per_testcase(rpc_module_whitelist, Config) -> ok = application:set_env(?APP, rpc_module_list, []), - ok = application:set_env(?APP, rpc_module_control, undefined), + ok = application:set_env(?APP, rpc_module_control, disabled), Config; end_per_testcase(rpc_module_blacklist, Config) -> ok = application:set_env(?APP, rpc_module_list, []), - ok = application:set_env(?APP, rpc_module_control, undefined), + ok = application:set_env(?APP, rpc_module_control, disabled), Config; end_per_testcase(_OtherTest, Config) -> @@ -101,12 +111,10 @@ end_per_testcase(_OtherTest, Config) -> %%% =================================================== %% Test supervisor's status supervisor_black_box(_Config) -> - true = erlang:is_process_alive(whereis(gen_rpc_server_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_acceptor_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_tcp_acceptor_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_client_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_tcp_server)), - true = erlang:is_process_alive(whereis(gen_rpc_dispatcher)), + true = erlang:is_process_alive(erlang:whereis(gen_rpc_acceptor_sup)), + true = erlang:is_process_alive(erlang:whereis(gen_rpc_client_sup)), + true = erlang:is_process_alive(erlang:whereis(gen_rpc_server_tcp)), + true = erlang:is_process_alive(erlang:whereis(gen_rpc_dispatcher)), ok. %% Test main functions @@ -140,6 +148,16 @@ call_with_receive_timeout(_Config) -> call_with_worker_kill(_Config) -> {badrpc, killed} = gen_rpc:call(?MASTER, timer, kill_after, [0]). +call_module_version_check_success(_Config) -> + stub_function = gen_rpc:call(?MASTER, {gen_rpc_test_helper, "1.0.0"}, stub_function, []). + +call_module_version_check_incompatible(_Config) -> + {badrpc, incompatible} = gen_rpc:call(?MASTER, {gen_rpc_test_helper, "X.Y.Z"}, stub_function, []). + +call_module_version_check_invalid(_Config) -> + {badrpc, incompatible} = gen_rpc:call(?MASTER, {gen_rpc_test_helper1, "X.Y.Z"}, stub_function, []), + {badrpc, incompatible} = gen_rpc:call(?MASTER, {rpc, 1}, cast, []). + interleaved_call(_Config) -> %% Spawn 3 consecutive processes that execute gen_rpc:call %% to the remote node and wait an inversely proportionate time @@ -151,6 +169,18 @@ interleaved_call(_Config) -> ok = interleaved_call_loop(Pid1, Pid2, Pid3, 0), ok. +authentication_timeout(_Config) -> + [] = supervisor:which_children(gen_rpc_acceptor_sup), + {ok, _Sock} = gen_tcp:connect("127.0.0.1", ?MASTER_PORT, ?TCP_DEFAULT_OPTS), + %% Give the server some time to launch the acceptor + ok = timer:sleep(50), + %% The acceptor has been launched + [_Master] = supervisor:which_children(gen_rpc_acceptor_sup), + ok = timer:sleep(600), + %% The acceptor should have shut down + [] = supervisor:which_children(gen_rpc_acceptor_sup), + ok. + cast(_Config) -> true = gen_rpc:cast(?MASTER, erlang, timestamp). @@ -267,9 +297,7 @@ server_inactivity_timeout(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?MASTER, os, timestamp), ok = timer:sleep(600), %% Lookup the client named process, shouldn't be there - [] = supervisor:which_children(gen_rpc_acceptor_sup), - %% The server supervisor should have no children - [] = supervisor:which_children(gen_rpc_server_sup). + [] = supervisor:which_children(gen_rpc_acceptor_sup). random_tcp_close(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?MASTER, os, timestamp), @@ -277,20 +305,22 @@ random_tcp_close(_Config) -> true = erlang:exit(AccPid, normal), ok = timer:sleep(500), % Give some time to the supervisor to kill the children [] = gen_rpc:nodes(), - [] = supervisor:which_children(gen_rpc_server_sup), [] = supervisor:which_children(gen_rpc_acceptor_sup), [] = supervisor:which_children(gen_rpc_client_sup). rpc_module_whitelist(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?MASTER, os, timestamp), ?MASTER = gen_rpc:call(?MASTER, erlang, node), - {badrpc,unauthorized} = gen_rpc:call(?MASTER, application, which_applications). + {badrpc, unauthorized} = gen_rpc:call(?MASTER, application, which_applications). rpc_module_blacklist(_Config) -> {badrpc, unauthorized} = gen_rpc:call(?MASTER, os, timestamp), {badrpc, unauthorized} = gen_rpc:call(?MASTER, erlang, node), 60000 = gen_rpc:call(?MASTER, timer, seconds, [60]). +driver_stub(_Config) -> + ok = gen_rpc_driver:stub(). + %%% =================================================== %%% Auxiliary functions for test cases %%% =================================================== diff --git a/test/ct/multi_rpc_SUITE.erl b/test/ct/multi_rpc_SUITE.erl index 09450ec..6ee52f1 100644 --- a/test/ct/multi_rpc_SUITE.erl +++ b/test/ct/multi_rpc_SUITE.erl @@ -22,10 +22,8 @@ all() -> init_per_suite(Config) -> %% Starting Distributed Erlang on local node {ok, _Pid} = gen_rpc_test_helper:start_distribution(?MASTER), - %% Setup application logging - ok = gen_rpc_test_helper:set_application_environment(), - %% Starting the application locally - {ok, _MasterApps} = application:ensure_all_started(?APP), + %% Setup the app locally + ok = gen_rpc_test_helper:start_master(tcp), Config. end_per_suite(_Config) -> @@ -33,21 +31,20 @@ end_per_suite(_Config) -> init_per_testcase(sbcast_with_bad_server, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + ok = gen_rpc_test_helper:start_master(tcp), + ok = gen_rpc_test_helper:start_slave(tcp), %% Set a low sbcast timeout ok = rpc:call(?SLAVE, application, set_env, [?APP, sbcast_receive_timeout, 500]), Config; init_per_testcase(_OtherTest, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + ok = gen_rpc_test_helper:start_master(tcp), + ok = gen_rpc_test_helper:start_slave(tcp), Config. end_per_testcase(_OtherTest, Config) -> - ok = gen_rpc_test_helper:stop_slave(?SLAVE), + ok = gen_rpc_test_helper:stop_slave(), Config. %%% =================================================== @@ -58,11 +55,8 @@ eval_everywhere_mfa_no_node(_Config) -> ConnectedNodes = [], abcast = gen_rpc:eval_everywhere(ConnectedNodes, erlang, whereis, [node()]), % Nothing catastrophically on sender side after sending call to the ether. - true = erlang:is_process_alive(whereis(gen_rpc_server_sup)), true = erlang:is_process_alive(whereis(gen_rpc_acceptor_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_client_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_tcp_acceptor_sup)), - true = erlang:is_process_alive(whereis(gen_rpc_tcp_server)). + true = erlang:is_process_alive(whereis(gen_rpc_client_sup)). %% Eval_everywhere is fire and forget, which means some test cases need to show %% something has been executed on target nodes. @@ -110,7 +104,6 @@ eval_everywhere_mfa_exit_multiple_nodes(_Config) -> ConnectedNodes = [?SLAVE, ?SLAVE], abcast = gen_rpc:eval_everywhere(ConnectedNodes, erlang, exit, [fatal]), % Nothing blows up on sender side after sending call to nothing - true = erlang:is_process_alive(whereis(gen_rpc_server_sup)), true = erlang:is_process_alive(whereis(gen_rpc_acceptor_sup)), true = erlang:is_process_alive(whereis(gen_rpc_client_sup)). @@ -135,6 +128,18 @@ multicall_multiple_nodes(_Config) -> Nodes = gen_rpc:nodes(), true = lists:member(?SLAVE, Nodes). +multicall_with_bad_module_version(_Config) -> + ConnectedNodes = [?SLAVE, ?SLAVE], + {[], ConnectedNodes} = gen_rpc:multicall(ConnectedNodes, {gen_rpc_test_helper, "X.Y.Z"}, stub_function, []), + Nodes = gen_rpc:nodes(), + true = lists:member(?SLAVE, Nodes). + +multicall_with_good_module_version(_Config) -> + ConnectedNodes = [?SLAVE, ?SLAVE], + {[stub_function, stub_function], []} = gen_rpc:multicall(ConnectedNodes, {gen_rpc_test_helper, "1.0.0"}, stub_function, []), + Nodes = gen_rpc:nodes(), + true = lists:member(?SLAVE, Nodes). + multicall_multiple_nodes_and_local(_Config) -> ConnectedNodes = [?SLAVE, ?SLAVE], {[{_,_,_}, {_,_,_}], []} = gen_rpc:multicall(ConnectedNodes, os, timestamp, []), diff --git a/test/ct/remote_SUITE.erl b/test/ct/remote_SUITE.erl index 4b53914..dff27ee 100644 --- a/test/ct/remote_SUITE.erl +++ b/test/ct/remote_SUITE.erl @@ -17,73 +17,80 @@ %%% CT callback functions %%% =================================================== all() -> - gen_rpc_test_helper:get_test_functions(?MODULE). + [{group, tcp}, {group, ssl}]. + % [{group, tcp}]. -init_per_suite(Config) -> +groups() -> + Cases = gen_rpc_test_helper:get_test_functions(?MODULE), + % [{tcp, [], Cases}]. + [{tcp, [], Cases}, {ssl, [], Cases}]. + +init_per_group(Group, Config) -> + % Our group name is the name of the driver + Driver = Group, %% Starting Distributed Erlang on local node {ok, _Pid} = gen_rpc_test_helper:start_distribution(?MASTER), - %% Setup application logging - ok = gen_rpc_test_helper:set_application_environment(), - %% Starting the application locally - {ok, _MasterApps} = application:ensure_all_started(?APP), - Config. + %% Setup the app locally + ok = gen_rpc_test_helper:start_master(Driver), + %% Save the driver in the state + gen_rpc_test_helper:store_driver_in_config(Driver, Config). -end_per_suite(_Config) -> +end_per_group(_Driver, _Config) -> ok. init_per_testcase(client_inactivity_timeout, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave + Driver = gen_rpc_test_helper:get_driver_from_config(Config), + ok = gen_rpc_test_helper:start_master(Driver), ok = application:set_env(?APP, client_inactivity_timeout, 500), - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + ok = gen_rpc_test_helper:start_slave(Driver), Config; init_per_testcase(server_inactivity_timeout, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + Driver = gen_rpc_test_helper:get_driver_from_config(Config), + ok = gen_rpc_test_helper:start_master(Driver), + ok = gen_rpc_test_helper:start_slave(Driver), ok = rpc:call(?SLAVE, application, set_env, [?APP, server_inactivity_timeout, 500]), Config; init_per_testcase(rpc_module_whitelist, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + Driver = gen_rpc_test_helper:get_driver_from_config(Config), + ok = gen_rpc_test_helper:start_master(Driver), + ok = gen_rpc_test_helper:start_slave(Driver), ok = rpc:call(?SLAVE, application, set_env, [?APP, rpc_module_list, [erlang, os]]), ok = rpc:call(?SLAVE, application, set_env, [?APP, rpc_module_control, whitelist]), Config; init_per_testcase(rpc_module_blacklist, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + Driver = gen_rpc_test_helper:get_driver_from_config(Config), + ok = gen_rpc_test_helper:start_master(Driver), + ok = gen_rpc_test_helper:start_slave(Driver), ok = rpc:call(?SLAVE, application, set_env, [?APP, rpc_module_list, [erlang, os]]), ok = rpc:call(?SLAVE, application, set_env, [?APP, rpc_module_control, blacklist]), Config; init_per_testcase(_OtherTest, Config) -> ok = gen_rpc_test_helper:restart_application(), - ok = gen_rpc_test_helper:set_application_environment(), - %% In order to connect to the slave - ok = gen_rpc_test_helper:start_slave(?SLAVE, 5370), + Driver = gen_rpc_test_helper:get_driver_from_config(Config), + ok = gen_rpc_test_helper:start_master(Driver), + ok = gen_rpc_test_helper:start_slave(Driver), Config. end_per_testcase(client_inactivity_timeout, Config) -> - ok = gen_rpc_test_helper:stop_slave(?SLAVE), + ok = gen_rpc_test_helper:stop_slave(), ok = application:set_env(?APP, client_inactivity_timeout, infinity), Config; end_per_testcase(server_inactivity_timeout, Config) -> - ok = gen_rpc_test_helper:stop_slave(?SLAVE), + ok = gen_rpc_test_helper:stop_slave(), ok = application:set_env(?APP, server_inactivity_timeout, infinity), Config; end_per_testcase(_OtherTest, Config) -> - ok = gen_rpc_test_helper:stop_slave(?SLAVE), + ok = gen_rpc_test_helper:stop_slave(), Config. %%% =================================================== @@ -106,6 +113,16 @@ call_with_receive_timeout(_Config) -> {badrpc, timeout} = gen_rpc:call(?SLAVE, timer, sleep, [500], 100), ok = timer:sleep(500). +call_module_version_check_success(_Config) -> + stub_function = gen_rpc:call(?SLAVE, {gen_rpc_test_helper, "1.0.0"}, stub_function, []). + +call_module_version_check_incompatible(_Config) -> + {badrpc, incompatible} = gen_rpc:call(?SLAVE, {gen_rpc_test_helper, "X.Y.Z"}, stub_function, []). + +call_module_version_check_invalid(_Config) -> + {badrpc, incompatible} = gen_rpc:call(?SLAVE, {gen_rpc_test_helper1, "X.Y.Z"}, stub_function, []), + {badrpc, incompatible} = gen_rpc:call(?SLAVE, {rpc, 1}, cast, []). + interleaved_call(_Config) -> %% Spawn 3 consecutive processes that execute gen_rpc:call %% to the remote node and wait an inversely proportionate time @@ -211,47 +228,51 @@ client_inactivity_timeout(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp), ok = timer:sleep(600), ClientName = gen_rpc_helper:make_process_name("client", ?SLAVE), - undefined = whereis(ClientName), + undefined = erlang:whereis(ClientName), [] = supervisor:which_children(gen_rpc_client_sup). server_inactivity_timeout(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp), ok = timer:sleep(600), ClientName = gen_rpc_helper:make_process_name("client", ?SLAVE), - undefined = whereis(ClientName), + undefined = erlang:whereis(ClientName), [] = supervisor:which_children(gen_rpc_client_sup). random_local_tcp_close(_Config) -> - {_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp), - ClientName = gen_rpc_helper:make_process_name("client", ?SLAVE), - {_, Socket} = sys:get_state(ClientName), - ok = gen_tcp:close(Socket), - ok = timer:sleep(100), % Give some time to the supervisor to kill the children - [] = gen_rpc:nodes(), - [] = supervisor:which_children(gen_rpc_client_sup), - [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_acceptor_sup]), - [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_server_sup]). + {_Mega, _Sec, _Micro} = rpc:call(?SLAVE, gen_rpc, call, [?MASTER, os, timestamp, []]), + [{_,AccPid,_,_}] = supervisor:which_children(gen_rpc_acceptor_sup), + true = erlang:exit(AccPid, kill), + ok = timer:sleep(600), % Give some time to the supervisor to kill the children + [] = rpc:call(?SLAVE, gen_rpc, nodes, []), + [] = supervisor:which_children(gen_rpc_acceptor_sup), + [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_client_sup]). random_remote_tcp_close(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp), [{_,AccPid,_,_}] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_acceptor_sup]), true = rpc:call(?SLAVE, erlang, exit, [AccPid,kill]), - ok = timer:sleep(100), + ok = timer:sleep(600), [] = gen_rpc:nodes(), [] = supervisor:which_children(gen_rpc_client_sup), - [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_acceptor_sup]), - [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_server_sup]). + [] = rpc:call(?SLAVE, supervisor, which_children, [gen_rpc_acceptor_sup]). rpc_module_whitelist(_Config) -> {_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp), ?SLAVE = gen_rpc:call(?SLAVE, erlang, node), - {badrpc,unauthorized} = gen_rpc:call(?SLAVE, application, which_applications). + {badrpc, unauthorized} = gen_rpc:call(?SLAVE, application, which_applications). rpc_module_blacklist(_Config) -> {badrpc, unauthorized} = gen_rpc:call(?SLAVE, os, timestamp), {badrpc, unauthorized} = gen_rpc:call(?SLAVE, erlang, node), 60000 = gen_rpc:call(?SLAVE, timer, seconds, [60]). +wrong_cookie(_Config) -> + OrigCookie = erlang:get_cookie(), + RandCookie = list_to_atom(atom_to_list(OrigCookie) ++ "123"), + true = erlang:set_cookie(node(), RandCookie), + {badrpc, invalid_cookie} = gen_rpc:call(?SLAVE, os, timestamp, []), + true = erlang:set_cookie(node(), OrigCookie). + %%% =================================================== %%% Auxiliary functions for test cases %%% =================================================== diff --git a/test/gen_rpc.config b/test/gen_rpc.config deleted file mode 100644 index 556a7e5..0000000 --- a/test/gen_rpc.config +++ /dev/null @@ -1,18 +0,0 @@ -%%% -*-mode:erlang;coding:utf-8;tabidth,:4;c-basic-offset:4;indent-tabs-mode:()-*- -%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: -%%% -[ - {sasl, [ - {errlog_type, error}, - {error_logger_mf_dir, false} - ]}, - {lager, [ - {colored, true}, - {handlers, [ - {lager_console_backend, [info, - {lager_default_formatter, ["[", date, " ", time, "] severity=", severity, " node=\"", {node, "undefined"}, "\" pid=\"", pid, - "\" module=", {module, "gen_rpc"}, " function=", {function, "undefined"}, " ", message, "\n"]} - ]} - ]} - ]} -]. diff --git a/test/gen_rpc.master.config b/test/gen_rpc.master.config new file mode 100644 index 0000000..70588f0 --- /dev/null +++ b/test/gen_rpc.master.config @@ -0,0 +1,39 @@ +%%% -*-mode:erlang;coding:utf-8;tabidth,:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +[ + {gen_rpc, [ + {tcp_server_port, false}, + {ssl_server_port, 5370}, + {ssl_server_options, [ + {certfile, "./priv/ssl/gen_rpc_master@127.0.0.1.cert.pem"}, + {keyfile, "./priv/ssl/gen_rpc_master@127.0.0.1.key.pem"}, + {cacertfile, "./priv/ssl/ca.cert.pem"} + ]}, + {ssl_client_options, [ + {certfile, "./priv/ssl/gen_rpc_master@127.0.0.1.cert.pem"}, + {keyfile, "./priv/ssl/gen_rpc_master@127.0.0.1.key.pem"}, + {cacertfile, "./priv/ssl/ca.cert.pem"} + ]}, + {default_client_driver, ssl}, + {client_config_per_node, #{ + 'gen_rpc_slave@127.0.0.1' => 5372 + }} + ]}, + {sasl, [ + {errlog_type, error}, + {error_logger_mf_dir, false} + ]}, + {lager, [ + {log_root, "./log"}, + {crash_log, "crash.log"}, + {crash_log_size, 0}, + {colored, true}, + {handlers, [ + {lager_console_backend, [debug, + {lager_default_formatter, ["[", date, " ", time, "] severity=", severity, " node=\"", {node, "undefined"}, "\" pid=\"", pid, + "\" module=", {module, "gen_rpc"}, " function=", {function, "undefined"}, " ", message, "\n"]} + ]} + ]} + ]} +]. diff --git a/test/gen_rpc.slave.config b/test/gen_rpc.slave.config new file mode 100644 index 0000000..57c1342 --- /dev/null +++ b/test/gen_rpc.slave.config @@ -0,0 +1,39 @@ +%%% -*-mode:erlang;coding:utf-8;tabidth,:4;c-basic-offset:4;indent-tabs-mode:()-*- +%%% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: +%%% +[ + {gen_rpc, [ + {tcp_server_port, false}, + {ssl_server_port, 5372}, + {ssl_server_options, [ + {certfile, "./priv/ssl/gen_rpc_slave@127.0.0.1.cert.pem"}, + {keyfile, "./priv/ssl/gen_rpc_slave@127.0.0.1.key.pem"}, + {cacertfile, "./priv/ssl/ca.cert.pem"} + ]}, + {ssl_client_options, [ + {certfile, "./priv/ssl/gen_rpc_slave@127.0.0.1.cert.pem"}, + {keyfile, "./priv/ssl/gen_rpc_slave@127.0.0.1.key.pem"}, + {cacertfile, "./priv/ssl/ca.cert.pem"} + ]}, + {default_client_driver, ssl}, + {client_config_per_node, #{ + 'gen_rpc_master@127.0.0.1' => 5370 + }} + ]}, + {sasl, [ + {errlog_type, error}, + {error_logger_mf_dir, false} + ]}, + {lager, [ + {log_root, "./log"}, + {crash_log, "crash.log"}, + {crash_log_size, 0}, + {colored, true}, + {handlers, [ + {lager_console_backend, [debug, + {lager_default_formatter, ["[", date, " ", time, "] severity=", severity, " node=\"", {node, "undefined"}, "\" pid=\"", pid, + "\" module=", {module, "gen_rpc"}, " function=", {function, "undefined"}, " ", message, "\n"]} + ]} + ]} + ]} +]. diff --git a/test/gen_rpc_test_helper.erl b/test/gen_rpc_test_helper.erl index 3eaad92..ca3dbf8 100644 --- a/test/gen_rpc_test_helper.erl +++ b/test/gen_rpc_test_helper.erl @@ -5,20 +5,26 @@ %%% -module(gen_rpc_test_helper). -author("Panagiotis Papadomitsos "). +-vsn("1.0.0"). %%% CT Macros -include_lib("test/include/ct.hrl"). %%% Public API -export([start_distribution/1, - start_slave/2, - set_application_environment/0, + start_master/1, + start_slave/1, + stop_slave/0, + set_driver_configuration/2, set_application_environment/1, + store_driver_in_config/2, + get_driver_from_config/1, get_test_functions/1, make_process_name/1, make_process_name/2, spawn_long_running/1, spawn_short_running/0, + stub_function/0, ping/1]). %%% =================================================== @@ -38,36 +44,97 @@ start_distribution(Node)-> {error, Reason} end. -start_slave(Slave, Port) -> +start_master(Driver) -> + ok = set_application_environment(?MASTER), + ok = set_driver_configuration(Driver, ?MASTER), + %% Start lager + {ok, _LApps} = application:ensure_all_started(lager), + %% Start the application remotely + {ok, _Apps} = application:ensure_all_started(?APP), + ok. + +start_slave(Driver) -> %% Starting a slave node with Distributed Erlang - SlaveStr = atom_to_list(Slave), + SlaveStr = atom_to_list(?SLAVE), [NameStr, IpStr] = string:tokens(SlaveStr, [$@]), Name = list_to_atom(NameStr), - {ok, _Slave} = slave:start(IpStr, Name, "+K true -gen_rpc tcp_server_port " ++ integer_to_list(Port)), - ok = rpc:call(Slave, code, add_pathsz, [code:get_path()]), - ok = set_application_environment(Slave), + {ok, _Slave} = slave:start(IpStr, Name), + ok = rpc:call(?SLAVE, code, add_pathsz, [code:get_path()]), + ok = set_application_environment(?SLAVE), + ok = set_driver_configuration(Driver, ?SLAVE), + %% Start lager + {ok, _SlaveLApps} = rpc:call(?SLAVE, application, ensure_all_started, [lager]), %% Start the application remotely - {ok, _SlaveApps} = rpc:call(Slave, application, ensure_all_started, [?APP]), + {ok, _SlaveApps} = rpc:call(?SLAVE, application, ensure_all_started, [?APP]), ok. -stop_slave(Slave) -> - ok = slave:stop(Slave), +stop_slave() -> + ok = slave:stop(?SLAVE), ok. -set_application_environment() -> - set_application_environment(node()). +set_application_environment(?MASTER) -> + ok = lists:foreach(fun({Application, Key, Value}) -> + ok = application:set_env(Application, Key, Value, [{persistent, true}]) + end, ?TEST_APPLICATION_ENV), + ok; -set_application_environment(Node) when is_atom(Node) -> +set_application_environment(?SLAVE) -> ok = lists:foreach(fun({Application, Key, Value}) -> - ok = rpc:call(Node, application, set_env, [Application, Key, Value, [{persistent, true}]]) + ok = rpc:call(?SLAVE, application, set_env, [Application, Key, Value, [{persistent, true}]]) end, ?TEST_APPLICATION_ENV), ok. +set_driver_configuration(ssl, ?MASTER) -> + Prefix = filename:join(["..", "..", ".."]), + CertFile = filename:join([Prefix, "priv", "ssl", atom_to_list(?MASTER)]), + CaFile = filename:join([Prefix, "priv", "ssl", "ca.cert.pem"]), + ok = application:set_env(?APP, default_client_driver, ssl, [{persistent, true}]), + ok = application:set_env(?APP, ssl_server_port, ?MASTER_PORT, [{persistent, true}]), + ok = application:set_env(?APP, ssl_server_options, [ + {certfile, CertFile ++ ".cert.pem"}, + {keyfile, CertFile ++ ".key.pem"}, + {cacertfile, CaFile}], [{persistent, true}]), + ok = application:set_env(?APP, ssl_client_options, [ + {certfile, CertFile ++ ".cert.pem"}, + {keyfile, CertFile ++ ".key.pem"}, + {cacertfile, CaFile}], [{persistent, true}]), + ok; + +set_driver_configuration(ssl, ?SLAVE) -> + Prefix = filename:join(["..", "..", ".."]), + CertFile = filename:join([Prefix, "priv", "ssl", atom_to_list(?SLAVE)]), + CaFile = filename:join([Prefix, "priv", "ssl", "ca.cert.pem"]), + ok = rpc:call(?SLAVE, application, set_env, [?APP, default_client_driver, ssl, [{persistent, true}]]), + ok = rpc:call(?SLAVE, application, set_env, [?APP, ssl_server_port, ?SLAVE_PORT, [{persistent, true}]]), + ok = rpc:call(?SLAVE, application, set_env, [?APP, ssl_server_options, [ + {certfile, CertFile ++ ".cert.pem"}, + {keyfile, CertFile ++ ".key.pem"}, + {cacertfile, CaFile}], [{persistent, true}]]), + ok = rpc:call(?SLAVE, application, set_env, [?APP, ssl_client_options, [ + {certfile, CertFile ++ ".cert.pem"}, + {keyfile, CertFile ++ ".key.pem"}, + {cacertfile, CaFile}], [{persistent, true}]]), + ok; + +set_driver_configuration(tcp, ?MASTER) -> + ok = application:set_env(?APP, default_client_driver, tcp, [{persistent, true}]), + ok = application:set_env(?APP, tcp_server_port, ?MASTER_PORT, [{persistent, true}]), + ok; + +set_driver_configuration(tcp, ?SLAVE) -> + ok = rpc:call(?SLAVE, application, set_env, [?APP, default_client_driver, tcp, [{persistent, true}]]), + ok = rpc:call(?SLAVE, application, set_env, [?APP, tcp_server_port, ?SLAVE_PORT, [{persistent, true}]]), + ok. + +store_driver_in_config(Driver, State) -> + lists:keystore(driver, 1, State, {driver,Driver}). + restart_application() -> _ = application:stop(?APP), _ = application:unload(?APP), ok = timer:sleep(100), - ok = application:start(?APP), + {ok, _LApps} = application:ensure_all_started(lager), + {ok, _Apps} = application:ensure_all_started(?APP), ok. get_test_functions(Module) -> @@ -93,17 +160,27 @@ get_test_functions(Module) -> ({_,_}) -> false end, Functions)]. +get_driver_from_config(Config) -> + case lists:keyfind(driver, 1, Config) of + false -> ?DEFAULT_DRIVER; + {driver, Driver} -> Driver + end. + make_process_name(Tag) -> make_process_name(node(), Tag). make_process_name(Node, Tag) when is_binary(Tag) -> NodeBin = atom_to_binary(Node, utf8), binary_to_atom(<>, utf8). + spawn_long_running(TimeSpan) -> spawn(fun() -> timer:sleep(TimeSpan) end). spawn_short_running() -> spawn(fun() -> exit(normal) end). +stub_function() -> + stub_function. + ping({Node, Process, Msg}) -> {Process, Node} ! {pong, {node(), Process, Msg}}. diff --git a/test/include/ct.hrl b/test/include/ct.hrl index 331f08d..9749615 100644 --- a/test/include/ct.hrl +++ b/test/include/ct.hrl @@ -11,20 +11,29 @@ %%% Node definitions -define(MASTER, 'gen_rpc_master@127.0.0.1'). +-define(MASTER_PORT, 5369). -define(SLAVE, 'gen_rpc_slave@127.0.0.1'). +-define(SLAVE_PORT, 5370). -define(FAKE_NODE, 'fake_node@1.2.3.4'). +-define(DEFAULT_DRIVER, tcp). + -define(TEST_APPLICATION_ENV, [{sasl, errlog_type, error}, {sasl, error_logger_mf_dir, false}, - {gen_rpc, connect_timeout, 500}, - {gen_rpc, send_timeout, 500}, - {gen_rpc, remote_tcp_server_ports, [ - {?MASTER, 5369}, - {?SLAVE, 5370} - ]}, - {lager, colored, true}, + {?APP, tcp_server_port, false}, + {?APP, ssl_server_port, false}, + {?APP, client_config_per_node, #{ + ?MASTER => ?MASTER_PORT, + ?SLAVE => ?SLAVE_PORT + }}, + {?APP, connect_timeout, 500}, + {?APP, send_timeout, 500}, + {lager, log_root, "./log"}, + {lager, crash_log, "crash.log"}, + {lager, crash_log_size, 0}, + {lager, colored, false}, {lager, handlers, [ - % Commented out to reduce test output polution, uncomment during development + %% Commented out to reduce test output polution, uncomment during development % {lager_common_test_backend, [debug, % {lager_default_formatter, ["[", date, " ", time, "] severity=", severity, " node=\"", {node, "undefined"}, "\" pid=\"", pid, % "\" module=", {module, "gen_rpc"}, " function=", {function, "undefined"}, " ", message, "\n"]}]}, @@ -32,4 +41,4 @@ {formatter_config, ["[", date, " ", time, "] severity=", severity, " node=\"", {node, "undefined"}, "\" pid=\"", pid, "\" module=", {module, "gen_rpc"}, " function=", {function, "undefined"}, " ", message, "\n"]}]} ]} -]). \ No newline at end of file +]). diff --git a/test/integration/.dockerignore b/test/integration/.dockerignore index 8b5fe2a..31760c8 100644 --- a/test/integration/.dockerignore +++ b/test/integration/.dockerignore @@ -23,3 +23,4 @@ _plt logs _build rebar3 +*.crashdump diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile index 269b147..ae49fdd 100644 --- a/test/integration/Dockerfile +++ b/test/integration/Dockerfile @@ -10,5 +10,5 @@ apt-get -qqy install wget && \ wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && \ dpkg -i erlang-solutions_1.0_all.deb && \ apt-get -qqy update && \ -apt-get -qqy install make gcc git esl-erlang elixir +apt-get -qqy install make gcc git esl-erlang ENTRYPOINT ["/bin/bash"] diff --git a/test/integration/integration-tests.sh b/test/integration/integration-tests.sh index 643a9f4..86d8ac6 100755 --- a/test/integration/integration-tests.sh +++ b/test/integration/integration-tests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -l # Copyright 2015 Panagiotis Papadomitsos. All Rights Reserved. # # Used to run automated integration tests using Docker @@ -10,17 +10,19 @@ NODES="" start_node() { export NAME=gen_rpc_${1} echo -n "Starting container ${NAME}: " - docker run -i -t -d --privileged --name ${NAME} -P gen_rpc:integration + docker run -tid --privileged --name ${NAME} -P gen_rpc:integration sleep 2 export IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${NAME}) export NODES="${IP}:${NODES}" - docker exec ${NAME} epmd -daemon - docker exec -t -i ${NAME} bash -c "echo gen_rpc > ~/.erlang.cookie" - docker exec -t -i ${NAME} bash -c "chmod 600 ~/.erlang.cookie" - docker exec -t -i ${NAME} bash -c "rm -fr /gen_rpc" + docker exec -d ${NAME} epmd -daemon + docker exec -ti ${NAME} bash -c 'mkdir -p /root/.cache' + docker exec -ti ${NAME} bash -c 'echo gen_rpc > ~/.erlang.cookie' + docker exec -ti ${NAME} bash -c 'chmod 600 ~/.erlang.cookie' + docker exec -ti ${NAME} bash -c 'rm -fr /gen_rpc/*' docker cp ../../ ${NAME}:/ - docker exec -t -i ${NAME} bash -c "cd /gen_rpc && make" - docker exec -t -i -d ${NAME} bash -c "cd /gen_rpc && ./rebar3 as dev do shell --name gen_rpc@${IP}" + docker cp ~/.cache/rebar3 ${NAME}:/root/.cache/rebar3 + docker exec -ti ${NAME} bash -c 'cd /gen_rpc && make' + docker exec -ti -d ${NAME} bash -c "cd /gen_rpc && ./rebar3 as dev do shell --name gen_rpc@${IP}" return $? } @@ -30,14 +32,16 @@ start_master() { docker run -i -t -d --privileged --name ${NAME} -P gen_rpc:integration sleep 2 export IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' gen_rpc_master) - docker exec ${NAME} epmd -daemon - docker exec -t -i ${NAME} bash -c "echo gen_rpc > ~/.erlang.cookie" - docker exec -t -i ${NAME} bash -c "chmod 600 ~/.erlang.cookie" - docker exec -t -i ${NAME} bash -c "rm -fr /gen_rpc/*" + docker exec -d ${NAME} epmd -daemon + docker exec -ti ${NAME} bash -c 'mkdir -p /root/.cache' + docker exec -ti ${NAME} bash -c "echo gen_rpc > ~/.erlang.cookie" + docker exec -ti ${NAME} bash -c "chmod 600 ~/.erlang.cookie" + docker exec -ti ${NAME} bash -c "rm -fr /gen_rpc/*" docker cp ../../ ${NAME}:/ - docker exec -t -i ${NAME} bash -c "cd /gen_rpc && make" + docker cp ~/.cache/rebar3 ${NAME}:/root/.cache/rebar3 + docker exec -ti ${NAME} bash -c "cd /gen_rpc && make" echo Starting integration tests on container ${NAME} - docker exec -t -i gen_rpc_master bash -c "export NODES=${NODES} NODE=gen_rpc@${IP} && cd /gen_rpc && make && ./rebar3 as test do ct --suite test/ct/integration_SUITE" + docker exec -ti gen_rpc_master bash -c "export NODES=${NODES} NODE=gen_rpc@${IP} && cd /gen_rpc && make && ./rebar3 as test do ct --suite test/ct/integration_SUITE" } destroy() { @@ -67,4 +71,4 @@ run() { exit ${RESULT} } -run \ No newline at end of file +run