Skip to content

knewter/bertgate

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BertGate

Kind-of-compatible BERT-rpc server and client for Elixir.

See http://bert-rpc.org/

Features

  1. it's secure: only functions in special modules can be called
  2. it's secure: authentication for accessing particular modules
  3. it isn't secure: you can still be DDOSed by excessive atom creation
  4. (kind of) BERT-rpc compatible
  5. Elixir exceptions are transparently transported to the client and raised here
  6. cast and call implemented
  7. info not implemented (-> no control signals, caching etc.)

Installation

For testing:

# git clone https://github.com/mprymek/bertgate
# cd bertgate
# mix do deps.get, compile

For mix.exs:

{ :bertgate, github: "mprymek/bertgate" }

Test

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

Usage

  1. define your functions in BertGate.Modules.YourModule, BertGate.Modules.OtherModule, ...
  2. 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}

Connection Manager

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

Python Interoperability

# 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'}]

Performance

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

About

Kind-of-compatible BERT-rpc server and client for Elixir

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published