Kind-of-compatible BERT-rpc server and client for Elixir.
- it's secure: only functions in special modules can be called
- it's secure: authentication for accessing particular modules
- it isn't secure: you can still be DDOSed by excessive atom creation
- (kind of) BERT-rpc compatible
- Elixir exceptions are transparently transported to the client and raised here
cast
andcall
implementedinfo
not implemented (-> no control signals, caching etc.)
For testing:
# git clone https://github.com/mprymek/bertgate
# cd bertgate
# mix do deps.get, compile
For mix.exs:
{ :bertgate, github: "mprymek/bertgate" }
Start server:
# mix server
Erlang/OTP 17 [erts-6.0] [source] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
[NOTIC] BertGate server listening on port 9484 with 20 acceptors
[NOTIC] Public modules: [:Bert]
Test connection to the server:
# iex -S mix client
Erlang/OTP 17 [erts-6.0] [source] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (0.14.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> conn=BertGate.Client.connect("localhost")
#Port<0.1268>
iex(2)> BertGate.Client.call(conn,:'Bert',:ping,[])
:pong
Or you can run unit tests:
# mix test
- define your functions in
BertGate.Modules.YourModule
,BertGate.Modules.OtherModule
, ... - if you want private modules, define custom authenticator. If not, list your modules in "public" option (see below)
Example user modules:
defmodule BertGate.Modules.CalcPublic do
def sum(_auth_data,x,y), do: x+y
end
defmodule BertGate.Modules.CalcPrivate do
def sum(_auth_data,x,y), do: {:really_confident_result,x+y}
end
Authenticator receives authentication token (arbitrary Erlang term) and returns nil
(failed authentication)
or new list of user-accessible modules and arbitrary auth_data. auth_data is then passed as a first argument
to every remotely invocated function. You can store any user-related data to it, BertGate doesn't use it, just passes
is from authenticator to your functions.
How to start server:
# authenticator(old_allowed,old_auth_data,token) -> {new_allowed,auth_data}
authenticator = fn
_,_,:calc_auth_token -> {[:'CalcPrivate'],:some_auth_data}
_,_,_ -> nil
end
{:ok, server} = BertGate.Server.start_link(%{
port: your_custom_port, # optional
authenticator: authenticator, # only needed if you want authenticated modules
public: [:'Bert',:'CalcPublic'], # public modules
})
You can call your functions from client like this:
iex(1)> conn=BertGate.Client.connect("localhost")
#Port<0.3540>
iex(2)> BertGate.Client.call(conn,:'CalcPublic',:sum,[5,6])
11
iex(3)> BertGate.Client.auth(conn,:calc_auth_token)
:ok
iex(4)> BertGate.Client.call(conn,:'CalcPrivate',:sum,[5,6])
{:really_confident_result,11}
Manages connections to :rpc
and BertGate servers. Whenever network error occur, the client is automatically
reconnected using stored options.
Server:
# iex --sname server -S mix server
[...]
iex(server@mydomain)1> BertGate.Server.start_link
[NOTIC] BertGate server listening on port 9484 with 20 acceptors
Client:
# iex --sname client -S mix client
[...]
iex(client@mydomain)1> Rpc.add_bert_server :local_bert, "localhost"
:ok
iex(client@mydomain)2> Rpc.call :local_bert, :'Bert', :ping
:pong
iex(client@mydomain)3> Rpc.add_rpc_node :local_rpc, :server@mydomain
:ok
iex(client@mydomain)4> Rpc.call :local_rpc, BertGate.Modules.Bert, :ping, [nil]
:pong
Authenticated connections:
iex(client@mydomain)2> Rpc.add_bert_server :calc_private, "localhost", %{auth: :calc_auth_token}
:ok
iex(client@mydomain)2> Rpc.call :calc_private, :'CalcPrivate', :sum, [5,6]
11
# easy_install bertrpc
# python
[...]
>>> import bertrpc
>>> service = bertrpc.Service('localhost', 9484)
>>> service.request('call').Bert.ping()
Atom('pong')
>>> service.request('call').Bert.some_integer()
1234
>>> service.request('call').Bert.some_float()
1.234
>>> service.request('call').Bert.some_atom()
Atom('this_is_atom')
>>> service.request('call').Bert.some_tuple()
(1, 2, 3, 4)
>>> service.request('call').Bert.some_bytelist()
[1, 2, 3, 4]
>>> service.request('call').Bert.some_list()
[1, 2, [3, 4]]
>>> service.request('call').Bert.some_binary()
'This is a binary'
>>> service.request('call').Bert.some_map()
{Atom('a'): 1, Atom('b'): 2}
>>> service.request('call').Bert.sum(1,6)
7
Async call:
>>> service.request('cast').Bert.ping()
Authentication:
>>> service.request('call').Auth.auth("secret")
Atom('ok')
Exceptions raised by user functions (server side) are also somehow supported on python side. You can distinguish them by code 601.
>>> service.request('call').Bert.exception1()
Traceback (most recent call last):
[...]
bertrpc.error.UserError: Class: Elixir.RuntimeError
Code: 601
UserError: [{Atom('__struct__'): Atom('Elixir.RuntimeError'), Atom('__exception__'): True, Atom('message'): 'Test exception'}]
BertGate's performance is similar to the erlang :rpc
module:
# elixir --sname server -S mix server
# elixir --sname client -S mix SpeedTest server@yourdomain
**** BertGate
Range: 125 - 25341 us
Median: 162 us
Average: 209 us
**** :rpc
Range: 137 - 35067 us
Median: 177 us
Average: 219 us