├── rebar.config ├── docs ├── images │ ├── socket.png │ ├── chumaki.jpg │ ├── entities.png │ ├── system_map.png │ └── contributing.png ├── architecture.md └── src │ ├── socket.xml │ ├── entities.xml │ └── system_map.xml ├── .gitignore ├── python-test ├── client.key ├── server.key ├── pull.py ├── publisher.py ├── push.py ├── pair_server.py ├── pair_client.py ├── subscriber.py ├── rep_server.py ├── req_server2.py ├── req_server.py ├── rep_client.py ├── req_client.py ├── dealer_client.py ├── stone_house_server.py ├── iron_house_server.py ├── stone_house_client.py └── iron_house_client.py ├── .github └── workflows │ └── rebar-publish.yml ├── CONTRIBUTING.md ├── src ├── chumak.app.src ├── chumak_lb.erl ├── chumak_xsub.erl ├── chumak_sup.erl ├── chumak_xpub.erl ├── chumak_bind.erl ├── chumak_pattern.erl ├── chumak_push.erl ├── chumak_cert.erl ├── chumak_subscriptions.erl ├── chumak_z85.erl ├── chumak_lbs.erl ├── chumak_curve_if.erl ├── chumak_resource.erl ├── chumak_router.erl ├── chumak_dealer.erl ├── chumak_pull.erl ├── chumak_rep.erl ├── chumak_pub.erl ├── chumak_pair.erl └── chumak_sub.erl ├── test ├── chumak_socket_test.erl ├── chumak_z85_test.erl ├── chumak_acceptance_version.erl ├── chumak_lb_test.erl ├── chumak_acceptance_error_handler.erl ├── chumak_stone_house_test.erl ├── chumak_acceptance_router_with_req.erl ├── chumak_subscriptions_test.erl ├── chumak_lbs_test.erl ├── chumak_acceptance_resource_with_req.erl ├── chumak_acceptance_router_with_dealer.erl ├── chumak_acceptance_error_handler_curve.erl ├── chumak_acceptance_push_with_push.erl ├── chumak_iron_house_test.erl ├── chumak_acceptance_pair.erl ├── chumak_acceptance_req_test.erl ├── chumak_protocol_curve_test.erl ├── chumak_command_test.erl ├── chumak_acceptance_rep_test.erl ├── chumak_acceptance_xpub_with_xsub.erl └── chumak_acceptance_pub_with_sub.erl ├── examples ├── pub_connect.erl ├── pull.erl ├── sub_bind.erl ├── rep_server.erl ├── pair_client.erl ├── push.erl ├── publisher.erl ├── pair_server.erl ├── subscriber.erl ├── resource_server.erl ├── Makefile ├── router_with_req.erl ├── req_server.erl ├── router_with_dealer.erl ├── req_client.erl └── resource_client.erl ├── include └── chumak.hrl ├── rebar.config.script └── README.md /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {project_plugins, [rebar3_hex]}. 3 | -------------------------------------------------------------------------------- /docs/images/socket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeromq/chumak/HEAD/docs/images/socket.png -------------------------------------------------------------------------------- /docs/images/chumaki.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeromq/chumak/HEAD/docs/images/chumaki.jpg -------------------------------------------------------------------------------- /docs/images/entities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeromq/chumak/HEAD/docs/images/entities.png -------------------------------------------------------------------------------- /docs/images/system_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeromq/chumak/HEAD/docs/images/system_map.png -------------------------------------------------------------------------------- /docs/images/contributing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeromq/chumak/HEAD/docs/images/contributing.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rebar.lock 2 | .DS_Store 3 | .rebar3 4 | _* 5 | .eunit 6 | *.o 7 | *.beam 8 | *.plt 9 | *.swp 10 | *.swo 11 | .erlang.cookie 12 | ebin 13 | log 14 | erl_crash.dump 15 | .rebar 16 | logs 17 | _build 18 | doc 19 | -------------------------------------------------------------------------------- /python-test/client.key: -------------------------------------------------------------------------------- 1 | # **** Generated on 2017-01-04 08:09:07.403000 by pyzmq **** 2 | # ZeroMQ CURVE **Secret** Certificate 3 | # DO NOT PROVIDE THIS FILE TO OTHER USERS nor change its permissions. 4 | 5 | metadata 6 | curve 7 | public-key = "T-jI=s%kd#Dm!5bD9AO-Gqu(:jruz2<[k]3Dzz2K" 8 | secret-key = "7u1$v?jp[jHd=wn!x?h@k-wNpBr!1!FM8LhOv+NR" 9 | -------------------------------------------------------------------------------- /python-test/server.key: -------------------------------------------------------------------------------- 1 | # **** Generated on 2017-01-04 08:09:07.380000 by pyzmq **** 2 | # ZeroMQ CURVE **Secret** Certificate 3 | # DO NOT PROVIDE THIS FILE TO OTHER USERS nor change its permissions. 4 | 5 | metadata 6 | curve 7 | public-key = "O#t%6HF5/T^CEh99=L$cnmF%:k2Iv#ncM=w01w@q" 8 | secret-key = "7U(aroF:XLH)>{^k&m#-F*p!Nyi4wSy2/PKq1TIB" 9 | -------------------------------------------------------------------------------- /.github/workflows/rebar-publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths: 4 | - src/chumak.app.src 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out 11 | uses: actions/checkout@v3 12 | 13 | - name: Publish to Hex.pm 14 | uses: erlangpack/github-action@v3 15 | env: 16 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thank you for your interest in contributing to this project. This project 5 | uses what is essentially the standard pull-request model. Fork, modify and 6 | create a pull request. 7 | 8 | More formal description of the process can be found in [Collective Code Construction Contract](http://rfc.zeromq.org/spec:42/C4/) (also known as _C4_). 9 | 10 | You get bonus points if you run dialyzer and fix-up all the warnings. We try to keep 11 | dialyzer happy! 12 | -------------------------------------------------------------------------------- /python-test/pull.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.PULL) 13 | socket.bind("tcp://*:5555") 14 | 15 | for request in range(100): 16 | time.sleep(2) 17 | message = socket.recv() 18 | print("Received reply %s [ %s ]" % (request, message)) 19 | -------------------------------------------------------------------------------- /python-test/publisher.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.PUB) 13 | socket.bind("tcp://*:5555") 14 | 15 | for request in range(100): 16 | message = socket.send("oi") 17 | print("Received reply %s [ %s ]" % (request, message)) 18 | time.sleep(1) 19 | -------------------------------------------------------------------------------- /python-test/push.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.PUSH) 13 | socket.connect("tcp://localhost:5555") 14 | 15 | for request in range(100): 16 | message = socket.send("freedom") 17 | print("send %s [ %s ]" % (request, message)) 18 | time.sleep(1) 19 | -------------------------------------------------------------------------------- /python-test/pair_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('PAIR server') 12 | socket = context.socket(zmq.PAIR) 13 | socket.bind("tcp://*:5555") 14 | 15 | for request in range(100): 16 | socket.send("I am a python man!") 17 | message = socket.recv() 18 | print("Recv %d, %s" % (request, message)) 19 | time.sleep(1) 20 | -------------------------------------------------------------------------------- /python-test/pair_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | 8 | import zmq 9 | import time 10 | context = zmq.Context() 11 | 12 | print('PAIR client') 13 | socket = context.socket(zmq.PAIR) 14 | socket.connect("tcp://localhost:5555") 15 | 16 | for request in range(100): 17 | message = socket.recv() 18 | print("Recv %d, %s" % (request, message)) 19 | time.sleep(1) 20 | socket.send("I am a python man!") 21 | -------------------------------------------------------------------------------- /python-test/subscriber.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.SUB) 13 | socket.setsockopt(zmq.SUBSCRIBE, b"") 14 | socket.connect("tcp://localhost:5555") 15 | 16 | for request in range(100): 17 | message = socket.recv() 18 | print("Received reply %s [ %s ]" % (request, message)) 19 | -------------------------------------------------------------------------------- /python-test/rep_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.REP) 13 | socket.bind("tcp://*:5555") 14 | 15 | 16 | for request in range(100): 17 | print("Sending request %s" % request) 18 | time.sleep(1) 19 | message = socket.recv() 20 | socket.send(b"Hello1") 21 | print("Received reply %s [ %s ]" % (request, message)) 22 | -------------------------------------------------------------------------------- /src/chumak.app.src: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | {application, chumak, 6 | [{description, "Erlang implementation of ZeroMQ Transport Protocol (ZMTP)"}, 7 | {vsn, "1.5.0"}, 8 | {registered, []}, 9 | {mod, { chumak, []}}, 10 | {applications, 11 | [kernel, 12 | stdlib 13 | ]}, 14 | {env,[]}, 15 | 16 | {modules, []}, 17 | {maintainers, ["Andriy Drozdyuk"]}, 18 | {licenses, ["MPLv2"]}, 19 | {links, [{"Github", "https://github.com/chovencorp/chumak"}]} 20 | ]}. 21 | -------------------------------------------------------------------------------- /python-test/req_server2.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.REP) 13 | 14 | socket.bind("tcp://*:5556") 15 | 16 | 17 | while True: 18 | message = socket.recv() 19 | print("Received reply [ %s ]" % message) 20 | 21 | if message == 'delay': 22 | time.sleep(0.2) 23 | 24 | print("Sending response") 25 | socket.send(message) 26 | 27 | -------------------------------------------------------------------------------- /test/chumak_socket_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_socket_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | init_with_invalid_pattern_test() -> 10 | {stop, Reason} = chumak_socket:init({foo, "my-identity"}), 11 | ?assertEqual(Reason, invalid_socket_type). 12 | 13 | already_started_socket_test() -> 14 | {ok, Pid1} = chumak:socket(dealer, "identity"), 15 | {ok, Pid2} = chumak:socket(dealer, "identity"), 16 | ?assertEqual(Pid1, Pid2), 17 | ok. 18 | -------------------------------------------------------------------------------- /python-test/req_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.REP) 13 | socket.setsockopt(zmq.IDENTITY, b"PEER") 14 | 15 | socket.bind("tcp://*:5555") 16 | 17 | 18 | while True: 19 | message = socket.recv() 20 | print("Received reply [ %s ]" % message) 21 | 22 | if message == 'delay': 23 | time.sleep(0.2) 24 | 25 | print("Sending response") 26 | socket.send(message) 27 | 28 | -------------------------------------------------------------------------------- /python-test/rep_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.REP) 13 | socket.connect("tcp://localhost:5555") 14 | 15 | 16 | for request in range(1): 17 | print("Sending request %s" % request) 18 | time.sleep(1) 19 | socket.send(b"Hello1") 20 | message = socket.recv() 21 | time.sleep(1) 22 | socket.send(b"Hello") 23 | message = socket.recv() 24 | print("Received reply %s [ %s ]" % (request, message)) 25 | -------------------------------------------------------------------------------- /test/chumak_z85_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_z85_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | encode_test() -> 10 | Binary = <<16#86, 16#4f, 16#d2, 16#6f, 16#b5, 16#59, 16#f7, 16#5B>>, 11 | String = "HelloWorld", 12 | RandomNrs = [rand:uniform(256) - 1 || _ <- lists:seq(1, 100)], 13 | RandomBytes = << <> || X <- RandomNrs >>, 14 | ?assertEqual(String, chumak_z85:encode(Binary)), 15 | ?assertEqual(Binary, chumak_z85:decode(String)), 16 | ?assertEqual(RandomBytes, 17 | chumak_z85:decode(chumak_z85:encode(RandomBytes))). 18 | -------------------------------------------------------------------------------- /python-test/req_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.REQ) 13 | socket.connect("tcp://localhost:5555") 14 | #socket.connect("tcp://localhost:5556") 15 | 16 | 17 | for request in range(100): 18 | print("Sending request %s" % request) 19 | time.sleep(1) 20 | socket.send(b"Hello1") 21 | message = socket.recv() 22 | time.sleep(1) 23 | socket.send(b"Hello") 24 | message = socket.recv() 25 | print("Received reply %s [ %s ]" % (request, message)) 26 | -------------------------------------------------------------------------------- /test/chumak_acceptance_version.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_version). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 3010). 9 | 10 | single_test_() -> 11 | [ 12 | { 13 | "Should return valid version when started", 14 | {setup, fun version_error/0, fun start/1, fun version_ok/1} 15 | } 16 | ]. 17 | 18 | version_error() -> 19 | ?_assertEqual({error, application_not_started}, chumak:version()). 20 | 21 | start(_) -> 22 | application:ensure_started(chumak). 23 | 24 | 25 | version_ok(_) -> 26 | ?_assertMatch({ok, _}, chumak:version()). 27 | 28 | -------------------------------------------------------------------------------- /examples/pub_connect.erl: -------------------------------------------------------------------------------- 1 | %% This examples demonstrates using a pub socket with connect. 2 | %% Make sure to start the sub socket on the other end with a bind. 3 | -module(pub_connect). 4 | -export([main/0]). 5 | 6 | main() -> 7 | application:start(chumak), 8 | {ok, Socket} = chumak:socket(pub), 9 | 10 | case chumak:connect(Socket, tcp, "localhost", 5555) of 11 | {ok, _BindPid} -> 12 | io:format("Binding OK with Pid: ~p\n", [Socket]); 13 | {error, Reason} -> 14 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 15 | X -> 16 | io:format("Unhandled reply for bind ~p \n", [X]) 17 | end, 18 | loop(Socket). 19 | 20 | loop(Socket) -> 21 | ok = chumak:send(Socket, <<" ", "Hello world">>), 22 | timer:sleep(1000), 23 | loop(Socket). -------------------------------------------------------------------------------- /examples/pull.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(pull). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(pull), 10 | 11 | case chumak:bind(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Binding OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket). 20 | 21 | loop(Socket) -> 22 | {ok, Data} = chumak:recv(Socket), 23 | io:format("Received ~p\n", [Data]), 24 | loop(Socket). 25 | -------------------------------------------------------------------------------- /examples/sub_bind.erl: -------------------------------------------------------------------------------- 1 | 2 | %% This examples demonstrates running a sub socket with a bind. 3 | %% Make sure to start the pub socket on the other end with connect. 4 | -module(sub_bind). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(sub), 10 | Topic = <<" ">>, 11 | chumak:subscribe(Socket, Topic), 12 | case chumak:bind(Socket, tcp, "localhost", 5555) of 13 | {ok, _BindPid} -> 14 | io:format("Binding OK with Pid: ~p\n", [Socket]); 15 | {error, Reason} -> 16 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 17 | X -> 18 | io:format("Unhandled reply for bind ~p \n", [X]) 19 | end, 20 | loop(Socket). 21 | 22 | loop(Socket) -> 23 | {ok, Data1} = chumak:recv(Socket), 24 | io:format("Received ~p\n", [Data1]), 25 | loop(Socket). -------------------------------------------------------------------------------- /examples/rep_server.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(rep_server). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(rep, "my-rep"), 10 | 11 | case chumak:bind(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Binding OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket). 20 | 21 | loop(Socket) -> 22 | Reply = chumak:recv(Socket), 23 | io:format("Question: ~p\n", [Reply]), 24 | chumak:send(Socket, <<"Hello reply">>), 25 | loop(Socket). 26 | -------------------------------------------------------------------------------- /examples/pair_client.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(pair_client). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(pair), 10 | 11 | case chumak:connect(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Connected OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket). 20 | 21 | loop(Socket) -> 22 | {ok, Data1} = chumak:recv_multipart(Socket), 23 | io:format("Received ~p\n", [Data1]), 24 | ok = chumak:send_multipart(Socket, [<<"Hey Jude">>]), 25 | loop(Socket). 26 | -------------------------------------------------------------------------------- /examples/push.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(push). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(push), 10 | 11 | case chumak:connect(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Binding OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket, 1). 20 | 21 | loop(Socket, Pos) -> 22 | io:format("Send..."), 23 | ok = chumak:send(Socket, <<"Hello A">>), 24 | ok = chumak:send(Socket, <<"Hello B">>), 25 | timer:sleep(1000), 26 | loop(Socket, Pos + 1). 27 | -------------------------------------------------------------------------------- /python-test/dealer_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | """ 7 | import zmq 8 | import time 9 | context = zmq.Context() 10 | 11 | print('Connection to hello world server') 12 | socket = context.socket(zmq.DEALER) 13 | socket.connect("tcp://localhost:5555") 14 | socket.connect("tcp://localhost:5556") 15 | 16 | 17 | for request in range(100): 18 | print("Sending request %s" % request) 19 | socket.send_multipart([b"", b"Hello1"]) 20 | socket.send_multipart([b"", b"Hello2"]) 21 | socket.send_multipart([b"", b"Hello2"]) 22 | message1 = socket.recv_multipart() 23 | message2 = socket.recv_multipart() 24 | message3 = socket.recv_multipart() 25 | time.sleep(1) 26 | print("Received reply %s [ %s ]" % (request, message1)) 27 | print("Received reply %s [ %s ]" % (request, message2)) 28 | -------------------------------------------------------------------------------- /examples/publisher.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(publisher). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(pub), 10 | 11 | case chumak:bind(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Binding OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket, 1). 20 | 21 | loop(Socket, Pos) -> 22 | ok = chumak:send(Socket, <<"A", Pos, "Hello A">>), 23 | ok = chumak:send(Socket, <<"B", Pos, "Hello B">>), 24 | io:format("."), 25 | timer:sleep(1000), 26 | loop(Socket, Pos + 1). 27 | -------------------------------------------------------------------------------- /examples/pair_server.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(pair_server). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(pair), 10 | 11 | case chumak:bind(Socket, tcp, "localhost", 5555) of 12 | {ok, _BindPid} -> 13 | io:format("Binding OK with Pid: ~p\n", [Socket]); 14 | {error, Reason} -> 15 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 16 | X -> 17 | io:format("Unhandled reply for bind ~p \n", [X]) 18 | end, 19 | loop(Socket). 20 | 21 | loop(Socket) -> 22 | ok = chumak:send_multipart(Socket, [<<"Hey Jude">>]), 23 | io:format("Sent\n"), 24 | {ok, Data2} = chumak:recv_multipart(Socket), 25 | io:format("Received ~p\n", [Data2]), 26 | loop(Socket). 27 | -------------------------------------------------------------------------------- /examples/subscriber.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(subscriber). 5 | -export([main/1]). 6 | 7 | main(Topic) -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(sub), 10 | chumak:subscribe(Socket, Topic), 11 | 12 | case chumak:connect(Socket, tcp, "localhost", 5555) of 13 | {ok, _BindPid} -> 14 | io:format("Binding OK with Pid: ~p\n", [Socket]); 15 | {error, Reason} -> 16 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 17 | X -> 18 | io:format("Unhandled reply for bind ~p \n", [X]) 19 | end, 20 | loop(Socket). 21 | 22 | loop(Socket) -> 23 | {ok, Data1} = chumak:recv_multipart(Socket), 24 | io:format("Received by multipart ~p\n", [Data1]), 25 | {ok, Data2} = chumak:recv(Socket), 26 | io:format("Received ~p\n", [Data2]), 27 | loop(Socket). 28 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | Architecture 2 | ============ 3 | 4 | This document aims to describe the architecture of the system to make it easier 5 | for contributors to understand its structure and behavior. 6 | 7 | System Overview 8 | --------------- 9 | 10 | The system map below presents an overview of the system. 11 | 12 | ![System Map](images/system_map.png) 13 | 14 | Sockets 15 | ------- 16 | 17 | Each socket creates a peer process for each remote peer it communicates with. 18 | 19 | ![Socket Composition Diagram](images/socket.png) 20 | 21 | Entity Relationship Diagrams 22 | ---------------------------- 23 | 24 | Here we can see different types of sockets and also which relationships 25 | the sockets participate in. 26 | 27 | ![Entity Relationship Diagram](images/entities.png) 28 | 29 | About Diagrams 30 | -------------- 31 | 32 | 1. The notation used in diagrams follows the [FMC](http://www.fmc-modeling.org/notation_reference) standard. 33 | 2. Diagrams can be modified with draw.io. The source can be found in [docs/src](src). 34 | -------------------------------------------------------------------------------- /examples/resource_server.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(resource_server). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Resource} = chumak:resource(), 10 | {ok, SocketA} = chumak:socket(rep, "A"), 11 | {ok, SocketB} = chumak:socket(rep, "B"), 12 | 13 | chumak:attach_resource(Resource, "service/a", SocketA), 14 | chumak:attach_resource(Resource, "service/b", SocketB), 15 | 16 | spawn_link(fun () -> 17 | loop(SocketA, <<"Hello A">>) 18 | end), 19 | spawn_link(fun () -> 20 | loop(SocketB, <<"Hello B">>) 21 | end), 22 | {ok, _BindPid} = chumak:bind(Resource, tcp, "localhost", 5555), 23 | 24 | receive 25 | _ -> ok 26 | end. 27 | 28 | loop(Socket, Msg) -> 29 | Data = chumak:recv(Socket), 30 | chumak:send(Socket, <<"Reply from: ", Msg/binary, " is ", Msg/binary>>), 31 | loop(Socket, Msg). 32 | -------------------------------------------------------------------------------- /test/chumak_lb_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_lb_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | new_test() -> 10 | ?assertEqual(chumak_lb:new(), []). 11 | 12 | put_test() -> 13 | Q1 = chumak_lb:new(), 14 | Q2 = chumak_lb:put(Q1, 1), 15 | ?assertEqual(Q2, [1]), 16 | Q3 = chumak_lb:put(Q2, 3), 17 | ?assertEqual(Q3, [3, 1]). 18 | 19 | get_test() -> 20 | Q1 = chumak_lb:put( 21 | chumak_lb:put(chumak_lb:new(), 1), 22 | 3), 23 | {Q2, 3} = chumak_lb:get(Q1), 24 | {Q3, 1} = chumak_lb:get(Q2), 25 | {Q4, 3} = chumak_lb:get(Q3), 26 | ?assertEqual(Q4, [1, 3]). 27 | 28 | get_empty_test() -> 29 | ?assertEqual(chumak_lb:get(chumak_lb:new()), none). 30 | 31 | delete_test() -> 32 | Q1 = chumak_lb:put( 33 | chumak_lb:put(chumak_lb:new(), 1), 34 | 3), 35 | Q2 = chumak_lb:delete(Q1, 1), 36 | Q3 = chumak_lb:delete(Q1, 3), 37 | 38 | ?assertEqual(Q2, [3]), 39 | ?assertEqual(Q3, [1]). 40 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: *.erl 2 | 3 | RUN_ARGS = erl -pa ../_build/default/lib/chumak/ebin -eval 4 | 5 | *.erl: 6 | erlc $@ 7 | 8 | req_client: req_client.erl 9 | ${RUN_ARGS} "req_client:main()" 10 | 11 | req_server: req_server.erl 12 | ${RUN_ARGS} "req_server:main()" 13 | 14 | rep_server: rep_server.erl 15 | ${RUN_ARGS} "rep_server:main()" 16 | 17 | router_with_dealer: router_with_dealer.erl 18 | ${RUN_ARGS} "router_with_dealer:main()" 19 | 20 | router_with_req: router_with_req.erl 21 | ${RUN_ARGS} "router_with_req:main()" 22 | 23 | publisher: publisher.erl 24 | ${RUN_ARGS} "publisher:main()" 25 | 26 | subscriber_a: subscriber.erl 27 | ${RUN_ARGS} 'subscriber:main(<<"A">>)' 28 | 29 | subscriber_b: subscriber.erl 30 | ${RUN_ARGS} 'subscriber:main(<<"B">>)' 31 | 32 | push: push.erl 33 | ${RUN_ARGS} "push:main()" 34 | 35 | pull: pull.erl 36 | ${RUN_ARGS} 'pull:main()' 37 | 38 | pair_server: pair_server.erl 39 | ${RUN_ARGS} "pair_server:main()" 40 | 41 | pair_client: pair_client.erl 42 | ${RUN_ARGS} 'pair_client:main()' 43 | 44 | resource_server: resource_server.erl 45 | ${RUN_ARGS} 'resource_server:main()' 46 | 47 | resource_client: resource_client.erl 48 | ${RUN_ARGS} 'resource_client:main()' 49 | -------------------------------------------------------------------------------- /src/chumak_lb.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Simple Round-robin load-balancer 6 | 7 | -module(chumak_lb). 8 | -export([new/0, put/2, get/1, delete/2, is_empty/1, to_list/1]). 9 | 10 | -type lb() :: list(). 11 | 12 | %% @doc returns an empty load-balancer 13 | -spec new() -> NewLB::lb(). 14 | new() -> 15 | []. 16 | 17 | %% @doc put a item to be balanced. 18 | -spec put(LB::lb(), Item::term()) -> NewLB::lb(). 19 | put(LB, Item) -> 20 | [Item|LB]. 21 | 22 | %% @doc get the next available item from load-balancer, return none if empty. 23 | -spec get(LB::lb()) -> none | {NewLB::lb(), Item::term()}. 24 | get([]) -> 25 | none; 26 | get([Head|Tail]) -> 27 | {Tail ++ [Head], Head}. 28 | 29 | %% @doc remove item from load-balancer 30 | -spec delete(LB::lb(), Item::term()) -> NewLB::lb(). 31 | delete(LB, Item)-> 32 | lists:delete(Item, LB). 33 | 34 | %% @doc return if true or false this LB is empty 35 | -spec is_empty(LB::lb()) -> true | false. 36 | is_empty([]) -> true; 37 | is_empty(_) -> false. 38 | 39 | to_list(LB) -> 40 | LB. 41 | -------------------------------------------------------------------------------- /include/chumak.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc Erlang common types for all modules 6 | 7 | -export_type([transport/0, socket_type/0]). 8 | -include_lib("kernel/include/logger.hrl"). 9 | 10 | -type transport() :: tcp. 11 | -type socket_type() :: req | rep | 12 | dealer | router | 13 | pub | xpub | 14 | sub | xsub | 15 | push | pull | 16 | pair. 17 | 18 | -type z85_key() :: string(). 19 | 20 | -type socket_option() :: curve_server | %% true | false 21 | curve_publickey | %% binary() 22 | curve_secretkey | %% binary() 23 | curve_serverkey | %% binary() 24 | curve_clientkeys. %% [binary() | z85_key()] 25 | 26 | -type security_mechanism() :: null | 27 | curve. 28 | 29 | -define(SOCKET_OPTS(Opts), lists:append([binary, {active, false}, {reuseaddr, true}], Opts)). 30 | -define(GREETINGS_TIMEOUT, 1000). 31 | -define(RECONNECT_TIMEOUT, 2000). 32 | -------------------------------------------------------------------------------- /examples/router_with_req.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(router_with_req). 5 | -export([main/0]). 6 | 7 | start_worker(Identity) -> 8 | Parent = self(), 9 | spawn_link( 10 | fun () -> 11 | {ok, Socket} = chumak:socket(req, Identity), 12 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", 5576), 13 | worker_loop(Socket, Identity, Parent) 14 | end 15 | ). 16 | 17 | worker_loop(Socket, Identity, Parent) -> 18 | chumak:send(Socket, <<"ready">>), 19 | {ok, Message} = chumak:recv(Socket), 20 | 21 | io:format("Message received from router ~p\n", [Message]). 22 | 23 | 24 | main() -> 25 | application:ensure_started(chumak), 26 | {ok, Socket} = chumak:socket(router), 27 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", 5576), 28 | 29 | start_worker("REQ-A"), 30 | start_worker("REQ-B"), 31 | loop(Socket). 32 | 33 | 34 | loop(Socket) -> 35 | {ok, [Identity, <<>>, <<"ready">>]} = chumak:recv_multipart(Socket), 36 | ok = chumak:send_multipart(Socket, [Identity, <<>>, <<"Reply">>]), 37 | loop(Socket). 38 | -------------------------------------------------------------------------------- /examples/req_server.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(req_server). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(req, "my-req"), 10 | 11 | case chumak:bind(Socket, tcp, "localhost", 5555) of 12 | {ok, Pid} -> 13 | send_messages(Socket, [ 14 | <<"Hello my dear friend">>, 15 | <<"Hello my old friend">>, 16 | <<"Hello all the things">> 17 | ]); 18 | {error, Reason} -> 19 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 20 | Reply -> 21 | io:format("Unhandled reply for connect ~p \n", [Reply]) 22 | end. 23 | 24 | send_messages(Socket, [Message|Messages]) -> 25 | case chumak:send(Socket, Message) of 26 | ok -> 27 | io:format("Send message: ~p\n", [Message]); 28 | {error, Reason} -> 29 | io:format("Failed to send message: ~p, reason: ~p\n", [Message, Reason]) 30 | end, 31 | case chumak:recv(Socket) of 32 | {ok, RecvMessage} -> 33 | io:format("Recv message: ~p\n", [RecvMessage]); 34 | {error, RecvReason} -> 35 | io:format("Failed to recv, reason: ~p\n", [RecvReason]) 36 | end, 37 | send_messages(Socket, Messages). 38 | -------------------------------------------------------------------------------- /test/chumak_acceptance_error_handler.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_error_handler). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 3011). 9 | 10 | single_test_() -> 11 | [ 12 | { 13 | "Should not start connection with invalid servers", 14 | {setup, fun start/0, fun stop/1, fun negotiate_messages/1} 15 | } 16 | ]. 17 | 18 | start() -> 19 | application:ensure_started(chumak), 20 | {ok, ServerPid} = chumak:socket(rep), 21 | {ok, _BindPid} = chumak:bind(ServerPid, tcp, "localhost", ?PORT), 22 | ServerPid. 23 | 24 | invalid_client() -> 25 | Parent = self(), 26 | spawn( 27 | fun () -> 28 | process_flag(trap_exit, true), 29 | {ok, Socket} = chumak:socket(push), 30 | {ok, PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT), 31 | link(PeerPid), %% to wait modifications 32 | client_loop(Parent, PeerPid) 33 | end 34 | ). 35 | 36 | client_loop(Parent, PeerPid) -> 37 | receive 38 | {'EXIT', PeerPid, {shutdown, Reason}} -> 39 | Parent ! {peer_finish, Reason} 40 | end. 41 | 42 | stop(Pid) -> 43 | gen_server:stop(Pid). 44 | 45 | 46 | negotiate_messages(_ServerPid) -> 47 | invalid_client(), 48 | 49 | Message = wait_for_msg(), 50 | 51 | [ 52 | ?_assertEqual({server_error, "Invalid socket-type push for rep server"}, Message) 53 | ]. 54 | 55 | wait_for_msg() -> 56 | receive 57 | {peer_finish, Msg} -> 58 | Msg 59 | end. 60 | -------------------------------------------------------------------------------- /docs/src/socket.xml: -------------------------------------------------------------------------------- 1 | 7Vpbc6s2EP41fiwDiOtjm5OcPLSdM82ZXh5VrAANRh4sx0l//VnBCiRuByfETWdiz9iwEtJqv29Xq7U35Gr39Lmi++wXvmXFxrW3TxvyaeO6LnE8+JKS50bi2HHQSNIq36KsE9zl/zLVEaXHfMsORkfBeSHyvSlMeFmyRBgyWlX8ZHa754U5656masZOcJfQYij9I9+KrJFGPqon5bcsTzM1s2Njy980eUgrfixxvo1L7utX07yjaizsf8jolp80EbkGw1acw8jyavd0xQppXGW25rmbidZW74qVqNv8A9AkH3ikxRGXjnqJZ2WLTOwKuHI25KeDqPgDu+IFr+o24lzJN7Tc50Whya8D+ZZyXoobussLyYSvNOM7ilIEHajS3GtP+658g5wWeVqCLIHVMGhEBRQkskttayZXIxXc0kPW3gxtgeZ5ZJVgSNZahLb5zPiOieoZumArUbxFIrcEPXWscOyokWU6IwLsSJGJaTt2hwZcICDj4JABOHsGZlgDICTlBQBaAQVPWR1RIMp7NBQiFOkgeCtggJFMw+DAkwcm3gEKdv26GApuiI/M+MIYCqu4gj+wN9tCxMZbXomMp7ykxXUn7YUGDR/2lIs/teu/4Nq2fHlXgl6y6Qfbsu1W0nRwPQlCLfjCqhzWII39SSLAyu2PctuB25KXcnKQ3ADY2P4PE+IZoaRHwUHU6fwz53vUxuABrXIKA+g0kBpNsWAS4gM/VglaLWxE0niL4l8UWPBhxz5+mgwgsRU4se2F+DmNM87wheegXOfYgW2Fjgcba/OJgVQbXm8NsVmNL2iVMoFD9qjULmURu1DxF7JLGn+EXTUvxtnlALlaLjXtThhOkesACxU9etUyjWBrEXAQulp6XYqaDapG8F3MVt+xiOdFbuhHgWvjgC2bIvBgh4QxcWzZq8eml0QlnGEue4IHIGmVgJyyXLC7Pa3Xe4LE2aTNsk0Dwf0/7NokxPylBQCDuJE7jewXK2wXiO37y5wuvGf3M6f23HGBzCle0TnSgh6kKtJyRo5vwtYGR92U9b5qYjge1L5C1D2Agr+yE3z+BngCQEtD3ADPe1jaLaosr3/H6zVQ9ULYTwxcAwxFGq7YQ4cVRa9BVRHqA9ZLwBoNQ+Ybwaq0/YD17WElcLK4EKwjKGqZLSKQHKvHuXOSmcmSUG5OS5LS+h7R6CWpiaRInhh5aj1lqaqBsnq0ctZ6y4pHJvLknH1arnt42sM83rbiuNG6zeNJJK0zmsdPcml5futa2nHIgfOoES0AmDMPYFDfmjuAhfHY6U8N3yTvg/PXcJKe2n6vpgazjOjwnVMeEIlK06hue9kB4sHkSqPI0lZCVGTt2W5yDYF3xuNw0ej30gOpg6nzvN/qrqpREnIBg5JBJ5g9WnY+qTty41+dI0u//N5p8915sRHhVvDDXiUsngnVU47nz/Ip9iC0dK+eTyz3vL6igaWFDzfoVWxW8zUytzi1/U1rHZ/x+Ot9DaPoyrVFKPEQY990fKMG1G4ken3oMlVESPbO2/AmPUYv1jhIppd6EfGg5qqhrn7OW+5Tc+UfImuVrh1EceD7TugCGEtc6mzqqyJot6h1yXrpQvh/SNNXVxQVI3WW4sa6TgGcEO8tC+ByeL1VHfLXL4ArS31UMscrmWaiTd6ukgm33Y//DZDdXyzI9Tc= -------------------------------------------------------------------------------- /examples/router_with_dealer.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(router_with_dealer). 5 | 6 | -export([main/0]). 7 | 8 | 9 | start_worker(Identity) -> 10 | Parent = self(), 11 | spawn_link( 12 | fun () -> 13 | {ok, Socket} = chumak:socket(dealer, Identity), 14 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", 5585), 15 | worker_loop(Socket, Identity, Parent) 16 | end 17 | ). 18 | 19 | worker_loop(Socket, Identity, Parent) -> 20 | {ok, Multipart} = chumak:recv_multipart(Socket), 21 | case Multipart of 22 | [<<"EXIT">>] -> 23 | ok; 24 | _ -> 25 | Parent ! {recv, Identity, Multipart} 26 | end. 27 | 28 | main() -> 29 | application:ensure_started(chumak), 30 | {ok, Socket} = chumak:socket(router), 31 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", 5585), 32 | 33 | start_worker("A"), 34 | start_worker("B"), 35 | 36 | timer:sleep(100), %% wait workers to be established 37 | 38 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"My message one">>]), 39 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"My message two">>]), 40 | 41 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"EXIT">>]), 42 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"EXIT">>]), 43 | 44 | MessageA = receive 45 | {recv, "A", MultipartA} -> 46 | MultipartA 47 | end, 48 | 49 | MessageB = receive 50 | {recv, "B", MultipartB} -> 51 | MultipartB 52 | end, 53 | io:format("Received: ~p...~p\n", [MessageA, MessageB]). 54 | -------------------------------------------------------------------------------- /examples/req_client.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(req_client). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | {ok, Socket} = chumak:socket(req, "my-req"), 10 | 11 | case chumak:connect(Socket, tcp, "localhost", 5555) of 12 | {ok, Pid} -> 13 | send_messages(Socket, [ 14 | <<"Hello my dear friend">>, 15 | <<"Hello my old friend">>, 16 | <<"Hello all the things">> 17 | ]); 18 | {error, Reason} -> 19 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 20 | Reply -> 21 | io:format("Unhandled reply for connect ~p \n", [Reply]) 22 | end. 23 | 24 | send_messages(Socket, []) -> 25 | send_messages(Socket, [ 26 | <<"Hello my dear friend">>, 27 | <<"Hello my old friend">>, 28 | <<"Hello all the things">> 29 | ]); 30 | 31 | send_messages(Socket, [Message|Messages]) -> 32 | case chumak:send(Socket, Message) of 33 | ok -> 34 | io:format("Send message: ~p\n", [Message]); 35 | {error, Reason} -> 36 | io:format("Failed to send message: ~p, reason: ~p\n", [Message, Reason]) 37 | end, 38 | case chumak:recv(Socket) of 39 | {ok, RecvMessage} -> 40 | io:format("Recv message: ~p\n", [RecvMessage]); 41 | {error, RecvReason} -> 42 | io:format("Failed to recv, reason: ~p\n", [RecvReason]) 43 | end, 44 | timer:sleep(1000), 45 | send_messages(Socket, Messages). 46 | -------------------------------------------------------------------------------- /src/chumak_xsub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ XSub Pattern for Erlang 6 | %% 7 | %% This pattern implement XSub especification 8 | %% from: http://rfc.zeromq.org/spec:29/PUBSUB#toc6 9 | 10 | -module(chumak_xsub). 11 | -define (SUB, chumak_sub). 12 | 13 | -export([valid_peer_type/1, init/1, peer_flags/1, accept_peer/2, peer_ready/3, 14 | send/3, recv/2, 15 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 16 | queue_ready/3, peer_disconected/2, subscribe/2, cancel/2, 17 | peer_reconnected/2, identity/1 18 | ]). 19 | 20 | init(Identity) -> 21 | ?SUB:init(Identity, [xsub]). 22 | 23 | identity(State) -> ?SUB:identity(State). 24 | valid_peer_type(SocketType) -> ?SUB:valid_peer_type(SocketType). 25 | peer_flags(State) -> ?SUB:peer_flags(State). 26 | accept_peer(State, PeerPid) -> ?SUB:accept_peer(State, PeerPid). 27 | peer_ready(State, PeerPid, Identity) -> ?SUB:peer_ready(State, PeerPid, Identity). 28 | send(State, Data, From) -> ?SUB:send(State, Data, From). 29 | recv(State, From) -> ?SUB:recv(State, From). 30 | send_multipart(State, Data, From) -> ?SUB:send_multipart(State, Data, From). 31 | recv_multipart(State, From) -> ?SUB:recv_multipart(State, From). 32 | peer_recv_message(State, Message, From) -> ?SUB:peer_recv_message(State, Message, From). 33 | queue_ready(State, Identity, From) -> ?SUB:queue_ready(State, Identity, From). 34 | peer_disconected(State, PeerPid) -> ?SUB:peer_disconected(State, PeerPid). 35 | 36 | %% Other sub-specific 37 | cancel(State, Topic) -> ?SUB:cancel(State, Topic). 38 | peer_reconnected(State, PeerPid) -> ?SUB:peer_reconnected(State, PeerPid). 39 | subscribe(State, Topic) -> ?SUB:subscribe(State, Topic). 40 | -------------------------------------------------------------------------------- /src/chumak_sup.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_sup). 6 | -behaviour(supervisor). 7 | 8 | -include("chumak.hrl"). 9 | 10 | -define(SUPERVISOR_FLAGS, #{strategy => one_for_one}). 11 | -define(CHILD_PROCESS_PREFIX, "chumak_socket_"). 12 | -define(SOCKET, chumak_socket). 13 | -define(RESOURCE, chumak_resource). 14 | 15 | 16 | -export([start_link/0, init/1]). 17 | -export([start_socket/1, start_socket/2, start_resource/0, get_child_id/1]). 18 | 19 | 20 | start_link() -> 21 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 22 | 23 | 24 | init(_Args) -> 25 | {ok, {?SUPERVISOR_FLAGS, []}}. 26 | 27 | -spec start_socket(Type::socket_type() | atom(), Identity::string()) -> {ok, SocketPid::pid()} | {error, Reason::atom()}. 28 | start_socket(Type, Identity) -> 29 | ProcessId = get_child_id(Identity), %% generate an atom ? 30 | case supervisor:start_child(?MODULE, #{ 31 | id=>ProcessId, 32 | restart=> transient, 33 | start=>{?SOCKET, start_link, [Type, Identity]} 34 | }) of 35 | {error, already_present} -> 36 | supervisor:restart_child(?MODULE, ProcessId); 37 | {error, {already_started, Pid}} -> 38 | {ok, Pid}; 39 | Res -> 40 | Res 41 | end. 42 | 43 | start_socket(Type) -> 44 | %% socket without identity not use supervisor because the identity 45 | %% is used to localize process inside supervisor. 46 | ?SOCKET:start_link(Type, ""). 47 | 48 | start_resource() -> 49 | %% Resource not use supervisor yet, because it's needed a identifier 50 | ?RESOURCE:start_link(). 51 | 52 | get_child_id(Identity) -> 53 | list_to_atom(string:concat(?CHILD_PROCESS_PREFIX, Identity)). 54 | -------------------------------------------------------------------------------- /examples/resource_client.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -module(resource_client). 5 | -export([main/0]). 6 | 7 | main() -> 8 | application:start(chumak), 9 | 10 | spawn_link(fun () -> 11 | service("service/a") 12 | end), 13 | spawn_link(fun () -> 14 | service("service/b") 15 | end), 16 | spawn_link(fun () -> 17 | service("service/c") 18 | end), 19 | 20 | receive 21 | _ -> ok 22 | end. 23 | 24 | service(Resource) -> 25 | {ok, Socket} = chumak:socket(req), 26 | case chumak:connect(Socket, tcp, "localhost", 5555, Resource) of 27 | {ok, Pid} -> 28 | send_messages(Socket, []); 29 | 30 | {error, Reason} -> 31 | io:format("Connection Failed for this reason: ~p\n", [Reason]); 32 | Reply -> 33 | io:format("Unhandled reply for connect ~p \n", [Reply]) 34 | end. 35 | 36 | send_messages(Socket, []) -> 37 | send_messages(Socket, [ 38 | <<"Hello my dear friend">>, 39 | <<"Hello my old friend">>, 40 | <<"Hello all the things">> 41 | ]); 42 | 43 | send_messages(Socket, [Message|Messages]) -> 44 | case chumak:send(Socket, Message) of 45 | ok -> 46 | io:format("Send message: ~p\n", [Message]); 47 | {error, Reason} -> 48 | io:format("Failed to send message: ~p, reason: ~p\n", [Message, Reason]) 49 | end, 50 | case chumak:recv(Socket) of 51 | {ok, RecvMessage} -> 52 | io:format("Recv message: ~p\n", [RecvMessage]); 53 | {error, RecvReason} -> 54 | io:format("Failed to recv, reason: ~p\n", [RecvReason]) 55 | end, 56 | timer:sleep(1000), 57 | send_messages(Socket, Messages). 58 | -------------------------------------------------------------------------------- /test/chumak_stone_house_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_stone_house_test). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(MESSAGE, <<"message from client">>). 9 | 10 | rep_single_test_() -> 11 | [ 12 | { 13 | "push - pull with curve security", 14 | {setup, fun start_valid/0, fun stop/1, fun push_and_pull/1} 15 | } 16 | ]. 17 | 18 | start_valid() -> 19 | #{public := PK, secret := SK} = chumak_curve_if:box_keypair(), 20 | application:ensure_started(chumak), 21 | {ok, Socket} = chumak:socket(pull), 22 | ok = chumak:set_socket_option(Socket, curve_server, true), 23 | ok = chumak:set_socket_option(Socket, curve_secretkey, SK), 24 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 5655), 25 | {Socket, PK}. 26 | 27 | 28 | stop({Pid, _}) -> 29 | gen_server:stop(Pid). 30 | 31 | push_and_pull({SocketPid, ServerKey})-> 32 | push(ServerKey), 33 | {ok, ReceivedData} = chumak:recv(SocketPid), 34 | [ 35 | ?_assertEqual(ReceivedData, ?MESSAGE) 36 | ]. 37 | 38 | push(ServerKey) -> 39 | #{public := PK, secret := SK} = chumak_curve_if:box_keypair(), 40 | spawn_link(fun () -> 41 | timer:sleep(100), %% wait socket to be acceptable 42 | {ok, ClientSocket} = chumak:socket(push), 43 | ok = chumak:set_socket_option(ClientSocket, curve_server, false), 44 | ok = chumak:set_socket_option(ClientSocket, curve_serverkey, ServerKey), 45 | ok = chumak:set_socket_option(ClientSocket, curve_secretkey, SK), 46 | ok = chumak:set_socket_option(ClientSocket, curve_publickey, PK), 47 | {ok, _ClientPid} = chumak:connect(ClientSocket, tcp, "127.0.0.1", 5655), 48 | ok = chumak:send(ClientSocket, ?MESSAGE) 49 | end). 50 | -------------------------------------------------------------------------------- /test/chumak_acceptance_router_with_req.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_router_with_req). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | single_test_() -> 9 | [ 10 | { 11 | "Should route message with two REQ", 12 | {setup, fun start/0, fun stop/1, fun negotiate_multiparts/1} 13 | } 14 | ]. 15 | 16 | start() -> 17 | application:ensure_started(chumak), 18 | {ok, Socket} = chumak:socket(router), 19 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", 5576), 20 | Socket. 21 | 22 | start_worker(Identity) -> 23 | Parent = self(), 24 | spawn_link( 25 | fun () -> 26 | {ok, Socket} = chumak:socket(req, Identity), 27 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", 5576), 28 | worker_loop(Socket, Identity, Parent) 29 | end 30 | ). 31 | 32 | worker_loop(Socket, Identity, Parent) -> 33 | chumak:send(Socket, <<"ready">>), 34 | {ok, Message} = chumak:recv(Socket), 35 | Parent ! {recv, Identity, Message}. 36 | 37 | 38 | stop(Pid) -> 39 | gen_server:stop(Pid). 40 | 41 | 42 | negotiate_multiparts(Socket) -> 43 | start_worker("REQ-A"), 44 | start_worker("REQ-B"), 45 | recv_and_send_message(Socket), 46 | timer:sleep(200), 47 | recv_and_send_message(Socket), 48 | 49 | MessageA = receive 50 | {recv, "REQ-A", MultipartA} -> 51 | MultipartA 52 | end, 53 | 54 | MessageB = receive 55 | {recv, "REQ-B", MultipartB} -> 56 | MultipartB 57 | end, 58 | 59 | [ 60 | ?_assertEqual(MessageA, <<"Reply: REQ-A">>), 61 | ?_assertEqual(MessageB, <<"Reply: REQ-B">>) 62 | ]. 63 | 64 | recv_and_send_message(Socket) -> 65 | {ok, [Identity, <<>>, <<"ready">>]} = chumak:recv_multipart(Socket), 66 | ok = chumak:send_multipart(Socket, [Identity, <<>>, <<"Reply: ", Identity/binary>>]). 67 | -------------------------------------------------------------------------------- /src/chumak_xpub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ XPub Pattern for Erlang 6 | %% 7 | %% This pattern implement XPub especification 8 | %% from: http://rfc.zeromq.org/spec:29/PUBSUB#toc4 9 | 10 | -module(chumak_xpub). 11 | -behaviour(chumak_pattern). 12 | -define(PUB, chumak_pub). 13 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 14 | send/3, recv/2, 15 | unblock/2, 16 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 17 | queue_ready/3, peer_disconected/2, peer_subscribe/3, peer_cancel_subscribe/3, 18 | identity/1 19 | ]). 20 | 21 | init(Identity) -> 22 | ?PUB:init(Identity, [xpub]). 23 | 24 | terminate(Reason, State) -> 25 | ?PUB:terminate(Reason, State). 26 | 27 | identity(State) -> ?PUB:identity(State). 28 | valid_peer_type(SocketType) -> ?PUB:valid_peer_type(SocketType). 29 | peer_flags(State) -> ?PUB:peer_flags(State). 30 | accept_peer(State, PeerPid) -> ?PUB:accept_peer(State, PeerPid). 31 | peer_ready(State, PeerPid, Identity) -> ?PUB:peer_ready(State, PeerPid, Identity). 32 | send(State, Data, From) -> ?PUB:send(State, Data, From). 33 | recv(State, From) -> ?PUB:recv(State, From). 34 | send_multipart(State, Data, From) -> ?PUB:send_multipart(State, Data, From). 35 | recv_multipart(State, From) -> ?PUB:recv_multipart(State, From). 36 | unblock(State, From) -> ?PUB:unblock(State, From). 37 | peer_recv_message(State, Message, From) -> ?PUB:peer_recv_message(State, Message, From). 38 | queue_ready(State, Identity, From) -> ?PUB:queue_ready(State, Identity, From). 39 | peer_disconected(State, PeerPid) -> ?PUB:peer_disconected(State, PeerPid). 40 | 41 | %% Pub specific 42 | peer_subscribe(State, PeerPid, Subscription) -> 43 | ?PUB:peer_subscribe(State, PeerPid, Subscription). 44 | peer_cancel_subscribe(State, PeerPid, Subscription) -> 45 | ?PUB:peer_cancel_subscribe(State, PeerPid, Subscription). 46 | -------------------------------------------------------------------------------- /docs/src/entities.xml: -------------------------------------------------------------------------------- 1 | 7Vvfc6MqG/5rcptRSUy93Hbb7cWemZ3pN+fHJVWinhLxU9Km569f0BcFMWnaVdOZNZlJzAsC8jzwPryQBbrZHb4VOE/+YBGhC8+JDgv0deF5HnJX4ktaXmuL6wR+bYmLNAJba3hI/yMqI1j3aURKIyNnjPI0N40hyzIScsOGi4K9mNm2jJq15jhWNbaGhxBT2/pXGvGktl6toXnSfk/SOFE1uw6kPOLwKS7YPoP6Fh7aVq86eYdVWZC/THDEXjQTuhUdWzAmSpZXu8MNobJzVbfV990dSW3aXZAM2nb6BlTf8IzpHh4d2sVfVV8kfEfFlbtA1yUv2BO5YZQVVRpyb+RbpGxTSpU9Y5m49XrLMn6HdymVJPgfTtgOgxXwFiypf2sFrj35FnZM0zgTNkq24kGun0nBU4HPFzBzlsv25DhMs/h7leerLK5uocJMFlSBQeTjyiew+we6TFZAgMCVCfrrG2E7wotXkUWlXkEfKXL7gOVLyxQvAFuis2S1AYYCO+Om7BYhcQEg9QMGI0sDrCD/P4lZgnNp3R1iOViX5Wu5o0sxcGS3VmjAbY74/ZKknDyIXpWGF5FfQyIUXUYETDYLnOrV2/s6L9qx0CHHPaHPRML7Nj+aqrY0ze+h1fL6zwEBDroAA24awP7axncFmP8KvFCsAW8+wzskvJ4Pt1wAXvCCGrwREU6nmBEeEmGEAKoLIAw16QOY7WW3zwgPiXAQXAzhKwvhfP84wzusB3aXgf4CdAFtzwUHPQHawDMN7cMM98geeUp81YpTA7ic8R3XH0+Kr2qtNoBngEd2x5MCDFUZ/rhMfmOA5SMOvyRe1vXAC+JGCu/1dPLLtUNW+V7mmPEe0SNPCbAd4spx+jsvoEYAGHmBqbAvB7cd8ipZ+ES4Bbh4NInn0Xi0ijvboWgL3m44eZdGkaymlxxM5N7SKkqfiHxE3NCJKjdRfIlaTHEp+0NeR7hMmlwh26UhJFD8SOh1s1kwCn0GoMnKgcGoiNGzzl5DFp0Yanvjl4hhB8tyMgdShp0HfKeztHag1/WRD6bBRz5UrWFJIrENBz9ZwRMWswzT29aqDTzZGxr25JDyv8Esr/+R18u1/JWJhlVJQsMgZYB01Bh+kCIVzyBhrwopOS74F7mZ2E4kle1O4KwqyqJODmHR0v8lnL8CxHjPmZxNmsf6zuQmVv/GWov9aLNAyfZFxf1K39U22f8naVMQinn6bG6T9pEAbv3BUlFtO59s0NJZbXw3UJ8m+7zNsk0Sn+AbVAWi92PCocwO0ZpGnsc9iOINyz113c+9KrWl3mrTZJipJ/b3K2zNqX84Op49J9nxPosofY5DI4PJkqPS4J1q4FxHVGlF0xHdVS9ZfYGjVGBxaqP+IzhbXnMAx7TuOiYFhOaYVFjJ2GQfwDEp9XKKBPbwO3VOYrgxZQ5yY6IIJbskp4y54qRQeHOA6Tqvp7eV7Rfdwlqh1voBs4h6wrAmfrsgZDmUsTxIQ+mWJGFBMBcjXxwDqvn2WVcvZ7FQ0fUYjZWv7Dlz9P7RrnBS+3lqx0eXoT38G2KdoQa3DmR9rCxl2bzaGHK1sQ5A0AHMqGdS7zs5NcRqQx11G3i1Uas8pflMxdfou1rwOc4s+Krg0mcSfOpA3Zi0aIQ/JDly/GrECK6aHDMxTGKoCeICxLDjkvNK4BIrAR+ZTsPzYe2uOQ2lHw1t0AkefIgEdgzy1EqgVeBTLQaO+JrzlwZHHNlSHNivf3ZnpM++lvCR2NnY+JvVyr3ynZXatlDkESeLHF9L75Dk3IXGG7X4wdJ1fW8TQF2jLULs44YhlQN73i0bdDu0cwANqb8ATBAlV+cNp9StfkegOCJOOwuUJt40dpRcnJdaOsFa0Kz+NPfikeMvmyT5OdrkArHQKaVxVxjPvNN4ZwhjmJGmF8aqNbMwvrAwRkrhqokB2UGzsYQxsqOfnyhEPr0qNieWj2tk/XjOUXmJhPjskZdn6ut3+KHTrfCHkdIfetbhvR2yw8AVK+Z4/kemppX5F0qk3NXw8Xzxs/27dI17+6d0dPsT -------------------------------------------------------------------------------- /test/chumak_subscriptions_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_subscriptions_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | new_test() -> 10 | ?assertEqual(chumak_subscriptions:new(), #{}). 11 | 12 | put_test() -> 13 | S1 = chumak_subscriptions:new(), 14 | S2 = chumak_subscriptions:put(S1, self(), <<"A">>), 15 | ?assertEqual(#{self() => [<<"A">>]}, S2), 16 | 17 | S3 = chumak_subscriptions:put(S2, self(), <<"B">>), 18 | ?assertEqual(#{self() => [<<"A">>, <<"B">>]}, S3). 19 | 20 | 21 | delete_by_peer_and_subscription_test() -> 22 | S1 = chumak_subscriptions:new(), 23 | S2 = chumak_subscriptions:put(S1, self(), <<"A">>), 24 | S3 = chumak_subscriptions:put(S2, self(), <<"B">>), 25 | S4 = chumak_subscriptions:delete(S3, self(), <<"A">>), 26 | S5 = chumak_subscriptions:delete(S3, self(), <<"C">>), 27 | ?assertEqual(#{self() => [<<"B">>]}, S4), 28 | ?assertEqual(#{self() => [<<"A">>, <<"B">>]}, S5). 29 | 30 | 31 | delete_by_peer_test() -> 32 | S1 = chumak_subscriptions:new(), 33 | S2 = chumak_subscriptions:put(S1, self(), <<"A">>), 34 | S3 = chumak_subscriptions:put(S2, self(), <<"B">>), 35 | S4 = chumak_subscriptions:delete(S3, self()), 36 | ?assertEqual(#{}, S4). 37 | 38 | match_test() -> 39 | OtherPid = spawn_link(fun () -> ok end), 40 | S1 = chumak_subscriptions:new(), 41 | S2 = chumak_subscriptions:put(S1, self(), <<"A">>), 42 | S3 = chumak_subscriptions:put(S2, self(), <<"B">>), 43 | S4 = chumak_subscriptions:put(S3, OtherPid, <<"D">>), 44 | S5 = chumak_subscriptions:put(S4, self(), <<"AB">>), 45 | S6 = chumak_subscriptions:put(S5, OtherPid, <<>>), 46 | 47 | M1 = chumak_subscriptions:match(S5, <<"A">>), 48 | M2 = chumak_subscriptions:match(S5, <<"B">>), 49 | M3 = chumak_subscriptions:match(S5, <<"C">>), 50 | M4 = chumak_subscriptions:match(S5, <<"D">>), 51 | M5 = chumak_subscriptions:match(S5, <<"AB">>), 52 | M6 = chumak_subscriptions:match(S6, <<"W">>), 53 | 54 | ?assertEqual([self()], M1), 55 | ?assertEqual([self()], M2), 56 | ?assertEqual([], M3), 57 | ?assertEqual([OtherPid], M4), 58 | ?assertEqual([self()], M5), 59 | ?assertEqual([OtherPid], M6). 60 | -------------------------------------------------------------------------------- /src/chumak_bind.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Listener for new connections. 6 | -module(chumak_bind). 7 | -include("chumak.hrl"). 8 | 9 | -export([start_link/2, listener/2]). 10 | 11 | -spec start_link(Host::string(), Port::number()) -> {ok, BindPid::pid()} | {error, Reason::term()}. 12 | start_link(Host, Port) -> 13 | ParentPid = self(), 14 | 15 | case getaddr(Host) of 16 | {ok, Addr} -> 17 | case gen_tcp:listen(Port, ?SOCKET_OPTS([{ip, Addr}])) of 18 | {ok, ListenSocket} -> 19 | Pid = spawn_link(?MODULE, listener, [ListenSocket, ParentPid]), 20 | {ok, Pid}; 21 | {error, Reason} -> 22 | ?LOG_ERROR("zmq listen error", #{error => listen_error, host => Host, addr => Addr, port => Port, reason => Reason}), 23 | {error, Reason} 24 | end; 25 | 26 | {error, IpReason} -> 27 | ?LOG_ERROR("zmq listen error", #{error => getaddr_error, host => Host, reason => IpReason}), 28 | {error, IpReason} 29 | end. 30 | 31 | 32 | listener(ListenSocket, ParentPid) -> 33 | try 34 | {ok, Socket} = gen_tcp:accept(ListenSocket), 35 | {ok, PeerPid} = gen_server:call(ParentPid, {accept, Socket}), %% get peer's pid of chumak_peer 36 | ok = gen_tcp:controlling_process(Socket, PeerPid), %% set controlling of new socket to chumak_peer 37 | %% Start to negotiate greetings after new process is owner 38 | gen_server:cast(PeerPid, negotiate_greetings), 39 | listener(ListenSocket, ParentPid) 40 | catch 41 | error:{badmatch, {error, closed}} -> 42 | ?LOG_INFO("zmq listener error", #{error => bind_closed}); 43 | error:{badmatch, {error, Reason}} -> 44 | ?LOG_ERROR("zmq listener error", #{error => accept_error, reason => Reason }), 45 | listener(ListenSocket, ParentPid); 46 | error:{badmatch, Error} -> 47 | ?LOG_ERROR("zmq listener error", #{error => accept_error, reason => Error }), 48 | listener(ListenSocket, ParentPid) 49 | end. 50 | 51 | -spec getaddr(Host::string()) -> {ok, inet:ip_address() | any} | {error, Reason::term()}. 52 | 53 | getaddr("*") -> 54 | {ok, any}; 55 | 56 | getaddr(Host) -> 57 | inet:getaddr(Host, inet). 58 | -------------------------------------------------------------------------------- /test/chumak_lbs_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_lbs_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | new_test() -> 10 | ?assertEqual(chumak_lbs:new(), {lbs, #{}, #{}}). 11 | 12 | put_test() -> 13 | Q1 = chumak_lbs:new(), 14 | Q2 = chumak_lbs:put(Q1, "A", 1), 15 | ?assertEqual(Q2, {lbs, 16 | #{"A" => [1]}, 17 | #{1 => "A"}} 18 | ), 19 | 20 | Q3 = chumak_lbs:put(Q2, "B", 3), 21 | ?assertEqual(Q3, {lbs, 22 | #{"A" => [1], "B" => [3]}, 23 | #{1 => "A", 3 => "B"}} 24 | ). 25 | 26 | get_test() -> 27 | Q1 = put_items(chumak_lbs:new(), [ 28 | {a, 1}, 29 | {b, 2}, 30 | {a, 3}, 31 | {b, 4}, 32 | {c, 6} 33 | ]), 34 | {Q2, 3} = chumak_lbs:get(Q1, a), 35 | {Q3, 1} = chumak_lbs:get(Q2, a), 36 | {Q4, 3} = chumak_lbs:get(Q3, a), 37 | {Q5, 4} = chumak_lbs:get(Q4, b), 38 | {Q6, 2} = chumak_lbs:get(Q5, b), 39 | {Q7, 6} = chumak_lbs:get(Q6, c), 40 | ?assertEqual(Q7, { 41 | lbs, 42 | #{a => [1,3], b => [4,2], c => [6]}, 43 | #{1 => a, 2 => b, 3 => a, 4 => b, 6 => c} 44 | }). 45 | 46 | get_empty_test() -> 47 | ?assertEqual(chumak_lbs:get(chumak_lbs:new(), a), none). 48 | 49 | delete_test() -> 50 | Q1 = put_items(chumak_lbs:new(), [ 51 | {a, 1}, 52 | {b, 2}, 53 | {b, 3} 54 | ]), 55 | Q2 = chumak_lbs:delete(Q1, 1), 56 | Q3 = chumak_lbs:delete(Q1, 3), 57 | Q4 = chumak_lbs:delete(Q3, 2), 58 | Q5 = chumak_lbs:delete(Q4, 1), 59 | 60 | ?assertEqual({lbs, #{b => [3,2]}, #{2 => b, 3 => b}}, Q2), 61 | ?assertEqual({lbs, #{a => [1],b => [2]}, #{1 => a, 2 => b}}, Q3), 62 | ?assertEqual({lbs, #{a => [1]}, #{1 => a}}, Q4), 63 | ?assertEqual({lbs, #{}, #{}}, Q5). 64 | 65 | put_items(LBs, []) -> 66 | LBs; 67 | put_items(LBs, [{Key, Value} | Tail]) -> 68 | NewLBs = chumak_lbs:put(LBs, Key, Value), 69 | put_items(NewLBs, Tail). 70 | -------------------------------------------------------------------------------- /src/chumak_pattern.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Pattern behaviour for Erlang 6 | %% 7 | %% This behaviour defines all methods that a pattern needs to implement. 8 | 9 | -module(chumak_pattern). 10 | -include("chumak.hrl"). 11 | 12 | -export([module/1, error_msg/1]). 13 | 14 | -type pattern_state() :: map(). 15 | 16 | -callback valid_peer_type(SocketType::socket_type()) -> valid | invalid. 17 | -callback init(Identity::string()) -> {ok, pattern_state()}. 18 | -callback terminate(Reason::term(), State::pattern_state()) -> ok. 19 | -callback peer_flags(State::pattern_state()) -> {SocketType::socket_type(), [PeerFlag::term()]}. 20 | -callback accept_peer(State::pattern_state(), PeerPid::pid()) -> Reply::term(). 21 | -callback peer_ready(State::pattern_state(), PeerPid::pid(), Identity::binary()) -> Reply::term(). 22 | -callback send(State::pattern_state(), Data::binary(), From::term()) -> Reply::term(). 23 | -callback recv(State::pattern_state(), From::term()) -> Reply::term(). 24 | -callback identity(State::pattern_state()) -> Identity::string(). 25 | 26 | %% Multipart support 27 | -callback send_multipart(State::pattern_state(), [Data::binary()], From::term()) -> Reply::term(). 28 | -callback recv_multipart(State::pattern_state(), From::term()) -> Reply::term(). 29 | 30 | -callback peer_recv_message(State::pattern_state(), Message::chumak_protocol:message(), From::pid()) -> Reply::term(). 31 | -callback queue_ready(State::pattern_state(), Identity::string(), From::pid()) -> Reply::term(). 32 | -callback peer_disconected(State::pattern_state(), PeerPid::pid()) -> Reply::term(). 33 | 34 | -callback unblock(State::pattern_state(), From::term()) -> Reply::term(). 35 | 36 | 37 | %% @doc find matching pattern for a socket type. 38 | -spec module(SocketType::socket_type()) -> module() | {error, invalid_socket_type}. 39 | module(req) -> chumak_req; 40 | module(rep) -> chumak_rep; 41 | module(dealer) -> chumak_dealer; 42 | module(router) -> chumak_router; 43 | module(pub) -> chumak_pub; 44 | module(sub) -> chumak_sub; 45 | module(xpub) -> chumak_xpub; 46 | module(xsub) -> chumak_xsub; 47 | module(push) -> chumak_push; 48 | module(pull) -> chumak_pull; 49 | module(pair) -> chumak_pair; 50 | module(_) -> {error, invalid_socket_type}. 51 | 52 | %% @doc helper to translate reason atom to human-readable 53 | error_msg(efsm) -> 54 | "operation cannot be performed on this socket at the moment". 55 | -------------------------------------------------------------------------------- /test/chumak_acceptance_resource_with_req.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_resource_with_req). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 2710). 9 | 10 | single_test_() -> 11 | [ 12 | { 13 | "Should route message with two distinct servers", 14 | {setup, fun start/0, fun stop/1, fun negotiate_messages/1} 15 | } 16 | ]. 17 | 18 | start() -> 19 | application:ensure_started(chumak), 20 | {ok, ResourceServerPid} = chumak:resource(), 21 | {ok, _BindPid} = chumak:bind(ResourceServerPid, tcp, "localhost", ?PORT), 22 | ResourceServerPid. 23 | 24 | rep_server(ResourceServer, Resource, Reply) -> 25 | spawn_link( 26 | fun () -> 27 | {ok, Socket} = chumak:socket(rep), 28 | chumak:attach_resource(ResourceServer, Resource, Socket), 29 | rep_server_loop(Socket, Reply) 30 | end 31 | ). 32 | 33 | rep_server_loop(Socket, Reply) -> 34 | {ok, Data} = chumak:recv(Socket), 35 | chumak:send(Socket, <>), 36 | rep_server_loop(Socket, Reply). 37 | 38 | 39 | start_req_client(Identity, Resource, SendMsg) -> 40 | Parent = self(), 41 | spawn_link( 42 | fun () -> 43 | {ok, Socket} = chumak:socket(req), 44 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT, Resource), 45 | chumak:send(Socket, SendMsg), 46 | {ok, Message} = chumak:recv(Socket), 47 | Parent ! {req_recv, Identity, Message} 48 | end 49 | ). 50 | 51 | 52 | stop(Pid) -> 53 | gen_server:stop(Pid). 54 | 55 | 56 | negotiate_messages(ResourceServerPid) -> 57 | rep_server(ResourceServerPid, "service/a", <<"Hello, I am the red ranger">>), 58 | rep_server(ResourceServerPid, "service/b", <<"Hello, I am the black ranger">>), 59 | 60 | timer:sleep(200), 61 | 62 | start_req_client("A", "service/a", <<"I am ready to rock">>), 63 | start_req_client("B", "service/b", <<"I am ready to beat">>), 64 | 65 | MessageA = wait_for_msg("A"), 66 | MessageB = wait_for_msg("B"), 67 | 68 | [ 69 | ?_assertEqual(MessageA, <<"Hello, I am the red ranger, I am ready to rock">>), 70 | ?_assertEqual(MessageB, <<"Hello, I am the black ranger, I am ready to beat">>) 71 | ]. 72 | 73 | wait_for_msg(Id) -> 74 | receive 75 | {req_recv, Id, Message} -> 76 | Message 77 | end. 78 | -------------------------------------------------------------------------------- /src/chumak_push.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Push Pattern for Erlang 6 | %% 7 | %% This pattern implement Push especification 8 | %% from: http://rfc.zeromq.org/spec:30/PIPELINE#toc3 9 | 10 | -module(chumak_push). 11 | -behaviour(chumak_pattern). 12 | 13 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 14 | send/3, recv/2, 15 | unblock/2, 16 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 17 | queue_ready/3, peer_disconected/2, identity/1 18 | ]). 19 | 20 | -record(chumak_push, { 21 | identity :: string(), 22 | lb :: list() 23 | }). 24 | 25 | valid_peer_type(pull) -> valid; 26 | valid_peer_type(_) -> invalid. 27 | 28 | init(Identity) -> 29 | State = #chumak_push{ 30 | identity=Identity, 31 | lb=chumak_lb:new() 32 | }, 33 | {ok, State}. 34 | 35 | terminate(_Reason, _State) -> 36 | ok. 37 | 38 | identity(#chumak_push{identity=Identity}) -> Identity. 39 | 40 | peer_flags(_State) -> 41 | {push, []}. 42 | 43 | accept_peer(State, PeerPid) -> 44 | NewLb = chumak_lb:put(State#chumak_push.lb, PeerPid), 45 | {reply, {ok, PeerPid}, State#chumak_push{lb=NewLb}}. 46 | 47 | peer_ready(State, _PeerPid, _Identity) -> 48 | {noreply, State}. 49 | 50 | send(State, Data, From) -> 51 | send_multipart(State, [Data], From). 52 | 53 | recv(State, From) -> 54 | recv_multipart(State, From). 55 | 56 | send_multipart(#chumak_push{lb=LB}=State, Multipart, From) -> 57 | case chumak_lb:get(LB) of 58 | none -> 59 | {reply, {error, no_connected_peers}, State}; 60 | {NewLB, PeerPid} -> 61 | chumak_peer:send(PeerPid, Multipart, From), 62 | {noreply, State#chumak_push{lb=NewLB}} 63 | end. 64 | 65 | recv_multipart(State, _From) -> 66 | {reply, {error, not_use}, State}. 67 | 68 | unblock(State, _From) -> 69 | {reply, {error, not_use}, State}. 70 | 71 | peer_recv_message(State, _Message, _From) -> 72 | %% This function will never called, because use PUSH not receive messages 73 | {noreply, State}. 74 | 75 | queue_ready(State, _Identity, _PeerPid) -> 76 | %% This function will never called, because use PUB not receive messages 77 | {noreply, State}. 78 | 79 | peer_disconected(#chumak_push{lb=LB}=State, PeerPid) -> 80 | NewLB = chumak_lb:delete(LB, PeerPid), 81 | {noreply, State#chumak_push{lb=NewLB}}. 82 | -------------------------------------------------------------------------------- /src/chumak_cert.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc Very simple function to read a certificate. 6 | 7 | -module(chumak_cert). 8 | 9 | %% @doc Very simple function to read a certificate. 10 | %% 11 | %% Certificates are in ZPL format, identical to what is used by 12 | %% pyzmq (the Python ZMQ implementation). 13 | %% 14 | %% Note that this implements the minimum to parse the Python certificates. 15 | 16 | -export([read/1]). 17 | 18 | -spec read(FileName::string()) -> [{public_key | private_key, binary()}] | {error, Reason::term()}. 19 | read(FileName) -> 20 | {ok, File} = file:open(FileName, [read]), 21 | find_curve_section(File). 22 | 23 | find_curve_section(File) -> 24 | case file:read_line(File) of 25 | {ok, String} -> 26 | case re:run(String, "^curve *\\n") of 27 | {match, _} -> 28 | find_keys(File, []); 29 | nomatch -> 30 | find_curve_section(File) 31 | end; 32 | eof -> 33 | {error, no_curve_section} 34 | end. 35 | 36 | find_keys(File, Acc) -> 37 | case file:read_line(File) of 38 | eof -> 39 | {ok, lists:reverse(Acc)}; 40 | {ok, String} -> 41 | case parse_key(String) of 42 | {ok, Key} -> 43 | find_keys(File, [Key | Acc]); 44 | continue -> 45 | find_keys(File, Acc); 46 | end_of_section -> 47 | {ok, lists:reverse(Acc)}; 48 | Error -> 49 | Error 50 | end 51 | end. 52 | 53 | -define(VALUE_SPEC, "^ *= *\"(.*)\" *$"). 54 | 55 | parse_key(" public-key" ++ Value) -> 56 | case re:run(Value, ?VALUE_SPEC) of 57 | {match, [_, {Start, Length}]} -> 58 | KeyEncoded = string:substr(Value, Start + 1, Length), 59 | {ok, {public_key, chumak_z85:decode(KeyEncoded)}}; 60 | _ -> 61 | {error, invalid_public_key_spec} 62 | end; 63 | parse_key(" secret-key" ++ Value) -> 64 | case re:run(Value, ?VALUE_SPEC) of 65 | {match, [_, {Start, Length}]} -> 66 | KeyEncoded = string:substr(Value, Start + 1, Length), 67 | {ok, {secret_key, chumak_z85:decode(KeyEncoded)}}; 68 | _ -> 69 | {error, invalid_secret_key_spec} 70 | end; 71 | parse_key(" " ++ _) -> 72 | continue; 73 | parse_key(Other) -> 74 | case re:run(Other, "^ *#") of 75 | {match, _} -> 76 | continue; 77 | _ -> 78 | end_of_section 79 | end. 80 | -------------------------------------------------------------------------------- /src/chumak_subscriptions.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Subscription manager 6 | 7 | -module(chumak_subscriptions). 8 | -include_lib("kernel/include/logger.hrl"). 9 | 10 | -export([new/0, put/3, delete/2, delete/3, match/2]). 11 | 12 | 13 | -type subscriptions() :: #{PeerPid::pid => [Subscription::binary()]}. 14 | 15 | %% @doc returns new subscriptions 16 | -spec new() -> NewSubscriptions::subscriptions(). 17 | new() -> 18 | #{}. 19 | 20 | %% @doc put a new subscription 21 | -spec put(Subscriptions1::subscriptions(), PeerPid::pid(), Subscription::binary()) -> Subscriptions2::subscriptions(). 22 | put(Subscriptions, PeerPid, Subscription) -> 23 | PeerSubscriptions = maps:get(PeerPid, Subscriptions, []) ++ [Subscription], 24 | maps:put(PeerPid, PeerSubscriptions, Subscriptions). 25 | 26 | %% @doc delete a new subscription by peer-pid and subscription 27 | -spec delete(Subscriptions1::subscriptions(), PeerPid::pid(), Subscription::binary()) -> Subscriptions2::subscriptions(). 28 | delete(Subscriptions, PeerPid, Subscription) -> 29 | PeerSubscriptions1 = maps:get(PeerPid, Subscriptions, []), 30 | PeerSubscriptions2 = lists:delete(Subscription, PeerSubscriptions1), 31 | 32 | case PeerSubscriptions2 of 33 | [] -> 34 | maps:remove(PeerPid, Subscriptions); 35 | _ -> 36 | maps:put(PeerPid, PeerSubscriptions2, Subscriptions) 37 | end. 38 | 39 | %% @doc delete a new subscription by peer-pid 40 | -spec delete(Subscriptions1::subscriptions(), PeerPid::pid()) -> Subscriptions2::subscriptions(). 41 | delete(Subscriptions, PeerPid) -> 42 | maps:remove(PeerPid, Subscriptions). 43 | 44 | %% @doc return the list of peers that matches with subscription 45 | -spec match(Subscriptions::subscriptions(), FirstPart::binary()) -> [PeerPid::pid()]. 46 | match(Subscriptions, FirstPart) -> 47 | PeerPids = maps:keys(Subscriptions), 48 | lists:filter(fun (PeerPid) -> 49 | peer_match(Subscriptions, PeerPid, FirstPart) 50 | end, PeerPids). 51 | 52 | %% Private API 53 | peer_match(Subscriptions, PeerPid, FirstPart) -> 54 | PeerSubscriptions = maps:get(PeerPid, Subscriptions, []), 55 | lists:any(fun (<<>>) -> 56 | true; 57 | 58 | (PeerSubscription) -> 59 | case binary:match(FirstPart, PeerSubscription) of 60 | {0, _} -> 61 | true; 62 | _ -> 63 | false 64 | end 65 | end, PeerSubscriptions). 66 | -------------------------------------------------------------------------------- /test/chumak_acceptance_router_with_dealer.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_router_with_dealer). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | single_test_() -> 9 | [ 10 | { 11 | "Should route message with two DEALERS", 12 | {setup, fun start/0, fun stop/1, fun negotiate_multiparts/1} 13 | } 14 | ]. 15 | 16 | start() -> 17 | application:ensure_started(chumak), 18 | {ok, Socket} = chumak:socket(router), 19 | #{public := ServerPK, secret := ServerSK} = chumak_curve_if:box_keypair(), 20 | #{public := ClientPK, secret := ClientSK} = chumak_curve_if:box_keypair(), 21 | ok = chumak:set_socket_option(Socket, curve_server, true), 22 | ok = chumak:set_socket_option(Socket, curve_secretkey, ServerSK), 23 | ok = chumak:set_socket_option(Socket, curve_clientkeys, [ClientPK]), 24 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", 5575), 25 | {Socket, #{curve_serverkey => ServerPK, 26 | curve_publickey => ClientPK, 27 | curve_secretkey => ClientSK}}. 28 | 29 | start_worker(Identity, CurveOptions) -> 30 | Parent = self(), 31 | spawn_link( 32 | fun () -> 33 | {ok, Socket} = chumak:socket(dealer, Identity), 34 | [chumak:set_socket_option(Socket, Option, Value) 35 | || {Option, Value} <- maps:to_list(CurveOptions)], 36 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", 5575), 37 | worker_loop(Socket, Identity, Parent) 38 | end 39 | ). 40 | 41 | worker_loop(Socket, Identity, Parent) -> 42 | {ok, Multipart} = chumak:recv_multipart(Socket), 43 | case Multipart of 44 | [<<"EXIT">>] -> 45 | ok; 46 | _ -> 47 | Parent ! {recv, Identity, Multipart} 48 | end. 49 | 50 | stop({Pid, _}) -> 51 | gen_server:stop(Pid). 52 | 53 | 54 | negotiate_multiparts({Socket, CurveOptions}) -> 55 | start_worker("A", CurveOptions), 56 | start_worker("B", CurveOptions), 57 | timer:sleep(200), %% wait client sockets to be estabilished 58 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"My message one">>]), 59 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"My message two">>]), 60 | 61 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"EXIT">>]), 62 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"EXIT">>]), 63 | 64 | MessageA = receive 65 | {recv, "A", MultipartA} -> 66 | MultipartA 67 | end, 68 | 69 | MessageB = receive 70 | {recv, "B", MultipartB} -> 71 | MultipartB 72 | end, 73 | 74 | [ 75 | ?_assertEqual(MessageA, [<<"My message one">>]), 76 | ?_assertEqual(MessageB, [<<"My message two">>]) 77 | ]. 78 | -------------------------------------------------------------------------------- /src/chumak_z85.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc Implements Z85 encoding and decoding, as specified in 6 | %% https://rfc.zeromq.org/spec:32/Z85/ 7 | 8 | -module(chumak_z85). 9 | 10 | -define(SPECIAL_CHARS, ".-:+=^!/*?&<>()[]{}@%$#"). 11 | 12 | -export([encode/1, decode/1]). 13 | 14 | -spec encode(binary()) -> string(). 15 | %% @doc Encode a binary to a Z85 string. 16 | %% 17 | %% As per the spec, the length of the binary SHALL be divisible by 4 18 | %% with no remainder. 19 | encode(Binary) when is_binary(Binary) -> 20 | case size(Binary) rem 4 of 21 | 0 -> 22 | lists:flatten([encode_octets(X) || 23 | <> <= Binary]) 24 | end. 25 | 26 | -spec decode(string()) -> binary(). 27 | %% @doc Decode a Z85 string to a binary. 28 | %% 29 | %% As per the spec, the length of the string SHALL be divisible by 5 30 | %% with no remainder. 31 | %% 32 | %% Note that not all strings are valid encodings. The result for a string that 33 | %% is not a valid encoding is unspecified. 34 | decode(String) when is_list(String) -> 35 | case length(String) rem 5 of 36 | 0 -> 37 | << <> || 38 | X <- to_nrs(String, [])>> 39 | end. 40 | 41 | %%% --------------------------------------------------------------------------- 42 | %%% Internal functions 43 | %%% --------------------------------------------------------------------------- 44 | to_nrs([], Acc) -> 45 | lists:reverse(Acc); 46 | to_nrs([C1, C2, C3, C4, C5 | T], Acc) -> 47 | Number = decode_char(C5) + 48 | decode_char(C4) * 85 + 49 | decode_char(C3) * 85 * 85 + 50 | decode_char(C2) * 85 * 85 * 85 + 51 | decode_char(C1) * 85 * 85 * 85 * 85, 52 | to_nrs(T, [Number | Acc]). 53 | 54 | decode_char(C) when $0 =< C, C =< $9 -> 55 | C - $0; 56 | decode_char(C) when $a =< C, C =< $z -> 57 | C - $a + 10; 58 | decode_char(C) when $A =< C, C =< $Z -> 59 | C - $A + 36; 60 | decode_char(C) -> 61 | find_pos(C, ?SPECIAL_CHARS, 62). 62 | 63 | find_pos(C, [C |_], N) -> 64 | N; 65 | find_pos(C, [_ |T], N) -> 66 | find_pos(C, T, N+1). 67 | 68 | encode_octets(Int) -> 69 | C1 = Int rem 85, 70 | Int2 = Int div 85, 71 | C2 = Int2 rem 85, 72 | Int3 = Int2 div 85, 73 | C3 = Int3 rem 85, 74 | Int4 = Int3 div 85, 75 | C4 = Int4 rem 85, 76 | C5 = Int4 div 85, 77 | [encode_85(C5), encode_85(C4), encode_85(C3), encode_85(C2), encode_85(C1)]. 78 | 79 | encode_85(X) when X =< 9 -> 80 | X + 48; % (0 => $0 = 48) 81 | encode_85(X) when X =< 35 -> 82 | X + 87; % (10 => $a = 97) 83 | encode_85(X) when X =< 61 -> 84 | X + 29; % (36 => $A = 36) 85 | encode_85(X) when X >= 61 -> 86 | lists:nth(X - 61, ?SPECIAL_CHARS). 87 | -------------------------------------------------------------------------------- /python-test/stone_house_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Stonehouse uses the "CURVE" security mechanism. 5 | This gives us strong encryption on data, and (as far as we know) unbreakable 6 | authentication. Stonehouse is the minimum you would use over public networks, 7 | and assures clients that they are speaking to an authentic server, while 8 | allowing any client to connect. 9 | Author: Chris Laws 10 | 11 | Modified by Willem de Jong - only start the Python server, the client is the 12 | Chumak Erlang implementation. 13 | 14 | To run from an erlang shell: 15 | cd("python-test"), 16 | {ok, ServerKeys} = chumak_cert:read("server.key"), 17 | SPK = proplists:get_value(public_key, ServerKeys), 18 | {ok, ClientKeys} = chumak_cert:read("client.key"), 19 | CSK = proplists:get_value(secret_key, ClientKeys), 20 | CPK = proplists:get_value(public_key, ClientKeys), 21 | application:start(chumak), 22 | {ok, Socket} = chumak:socket(pull), 23 | ok = chumak:set_socket_option(Socket, curve_secretkey, CSK), 24 | ok = chumak:set_socket_option(Socket, curve_publickey, CPK), 25 | ok = chumak:set_socket_option(Socket, curve_serverkey, SPK), 26 | {ok, _} = chumak:connect(Socket, tcp, "127.0.0.1", 9000), 27 | {ok, Message} = chumak:recv(Socket), 28 | io:format("received: ~p~n", [Message]), 29 | halt(). 30 | 31 | 32 | ''' 33 | 34 | import logging 35 | import os 36 | import sys 37 | import time 38 | 39 | import zmq 40 | import zmq.auth 41 | from zmq.auth.thread import ThreadAuthenticator 42 | 43 | 44 | def run(): 45 | ''' Run Stonehouse example ''' 46 | 47 | # These directories are generated by the generate_certificates script 48 | keys_dir = os.path.dirname(__file__) 49 | 50 | ctx = zmq.Context.instance() 51 | 52 | # Start an authenticator for this context. 53 | auth = ThreadAuthenticator(ctx) 54 | auth.start() 55 | auth.allow('127.0.0.1') 56 | # Tell the authenticator how to handle CURVE requests 57 | auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) 58 | 59 | server_key_file = os.path.join(keys_dir, "server.key") 60 | server_public, server_secret = zmq.auth.load_certificate(server_key_file) 61 | 62 | server = ctx.socket(zmq.PUSH) 63 | server.curve_secretkey = server_secret 64 | server.curve_publickey = server_public 65 | server.curve_server = True # must come before bind 66 | server.bind('tcp://*:9000') 67 | 68 | server.send(b"Hello") 69 | # Make sure that there is time to finish the handshake 70 | time.sleep(2) 71 | 72 | # stop auth thread 73 | auth.stop() 74 | 75 | if __name__ == '__main__': 76 | if zmq.zmq_version_info() < (4,0): 77 | raise RuntimeError("Security is not supported in libzmq version < 4.0. libzmq version {0}".format(zmq.zmq_version())) 78 | 79 | if '-v' in sys.argv: 80 | level = logging.DEBUG 81 | else: 82 | level = logging.INFO 83 | 84 | logging.basicConfig(level=level, format="[%(levelname)s] %(message)s") 85 | 86 | run() 87 | -------------------------------------------------------------------------------- /docs/src/system_map.xml: -------------------------------------------------------------------------------- 1 | 7Vxbk9o6Ev4180phCxt4TOZMkoezp1LJ1jm7jw5owBuDKeO57a9Py+6WJfmCDMIzqTFUcdENqb++qVviht3unj9n0WH7r3TNkxt/un6+YX/c+L7PvBm8iZKXssSbLsOyZJPFayyrCr7H/+fUEEsf4jU/ag3zNE3y+KAXrtL9nq9yrSzKsvRJb3afJvqvHqIN/WJV8H0VJfXSf+J1vi1LFwFOT5R/4fFmS7/sTbHmR7T6ucnShz3+3o3P7otHWb2LaCxsf9xG6/RJKWJ3QNgsTWFk8Wn3fMsTQVwiW9nvU0utnHfG9zi37g4sKHs8RskDrh0nlr8QMbb5LoFP3g37eMyz9Ce/TZM0K+qYdyueUHMfJ4lSfheKpyhP9/mnaBcnghX+HW3TXYSliDrwSvld6R344gnlURJv9lC2guVwqMQJECaiSUFsLpYjJriOjlv5pU4MpM8jz3KO3FoUIXE+83TH8+wFmmAt81jZhTh5gQR7qtjCn83Lsq3CEj5DhCNkxY0cu4IDPiAiLXCGiIZTePbpHrpeFxlX5GeoSZD8UkEo5CfVolLfWyBsF1F/QUJaEZuvQU/g1zTLt+km3UfJXVWq8OMUving8Oc4/48ongT47b/YCOiTvWCVz6igqJ3Mp4LaRcFXnsWwCEHtotsxj7L8g9B2FahF2ScAWw69NlpAiVL/P57nL4h39JCnUFQt6880PbRx1rR42LDRss5FsnMrixzTh2xFypikANa24dhugagLQDo5KeNJlMePumK/jC2QtxS2OAP0N4jeF5488jxeWegBOYDCux5QV+FdbzL1Fm28awd8sBwCeOz6NY1hLlL1+MyfTJnvB4tpOAu8EAcgOzAz1Hs5RRzDYCY5KTv+olW/V6UvbWyX0ke7oCt9LLxEusO6cPfR+YIEDeJPn0ud7he6QAqOrvDLOjdyr6EdZXEEA6hgi9/qrZpVYSSpuLowhuhiEUuw+YSpD6ymEUst4kIaQ+TFa/LDb80OmsKmbZDKIwFiMzSPsDCYhOrDcAlbeAQoHYnZULODaHC8kIvwpxWNfkxXP4FCLvZauME8U7lLhC32Wi48+pluSH0PUVOUO/ndqnI37e1Zun1EoQUFNkPaDICCR7PtcG+gB4R6hKZ72sY5/36IChXzBOEmXaFukugo5iJIp+39ddykf226KwaILbsacF1BA0z/4k/w+g0ABYRsFWcN0HtY2hf6efj8t8Pt8nwBBkMTrzkSVwEWW6i4YtElsJIJHmEdAtYl7hGuD+tihHUwWBmZqKvDGljEfUUc+2C/ThmNj37QCFVEu2n9NYeNjJKyejI4WmgVef+i9dd9gW81AsD64CdVBm7k04Ztdo2NBLUgsJJ8wIpdvF4XW4UmiUmh9X1S+PtbaMeJf08GakgW2mSEdi0NaQLJEKfZl0IgdmA58BhoB+FEB72dcEmzL9cDCJIjJDHZhnq0hAIoWrTEBTBoHFo2x0jE1uBozxDoSliPeKVtf8UwA0VBu4QLo38U5BTcF+831RJwDBEOxW1nc9Ni9di4eddN7HHt3TRDBUnCbgzQEu6sb8qRQ1qGcRensQmaXt+g6av1KXk4gEELcdzRoJGgEEOc1qMDmDAKlL4PE2ZPeqrFHqQm6ruba5kwyi2OJmw4E4boDm3C7DJ2J02YMYzDVAOmr8ZUg0WqgRSqmmlAazv8kYAQOe36yP0m6NCOTcsVvhY6dGJAs71hIjyhdfwIHzfi4/HhwLPH+AgLxkoYV6l/f0kCb6HHZyi9rlhlNkeZ08JTRjL2HMwovqlgtkpisZb3B8MSXZRXyJjJhO6IQw2HQXNmDfHKMQzvBlc4ZFpLmtVjm9cJw3tOQ5sjridwXaIrPwCuY9psOFwZQXZ9XBviOHQH5S0H2/S9hrx0IqBrZa5VuhORiqIiiX7w5KO823KWCT/rHHgPtiA7S/t0ipQPEN3z6MJTp7g3hYG6vKaLD2nLKJ6VBJ7cmKnS1UBIKrv0GJ+RzvICu3N7FgOhTr7CkW2vnk5/39H3M7xrIwRMntEg4uvSux5o93NZiL6/evX1YIC8lDNAjN6jyOiIT8dhotfEp76LWWU8ynkRFhRHbMsD1G61YcLvxQijc3SaO2Yz4xrrrOH8YJN2NUzmedxhdUpk9ZA9dkXDi2SFjIYzONdql30z81EW2bgq2yVuML+FS3awbvMGnXLxbrnULt4BdRatl0bPd/+IlXx/MoOr8HMPX40bul0GudlRm4XhRI4Gr+ZdH38+gWOo0yXcjRWvcKnwLH/w5MTnjbM44S22XxdpmUWwmFRrgVcjRGHe1ze7s1mP7vChnN/Zjm2IeqJbdlVxVdgSSKixZVgVdN5lruRSFWaZbldk81RC7M1JsqblHMiicZmG1Hwf4Qs6Gcoc0V7azKkB41ay58/tNmP9xYt1Lgc3k+2zXvbofrl4zS02Htc+tQZ6UY+mkSOg7rwCTTcSeKqrwFz848K8+wjE6Cq8sqtA/GqroDzD4s46/Mk2gW7kPORV0zMwLoDbKit9mozRMVX5K5pjQKc9HKsuuNLcoHpap2zqOdeaqfve82j4HRt+a8mSN+B7S5LmOc6mxlnlvnZe2nXdrNM6XAuH32WX0fa0yYrpcet7Yb2zA8mpB0PomnH90FFeBEl6nzfaRgdRunveiD9gmxxfjrtkAnZfdFQjs4LzmsIgLTGpJsatR6uswmTnyIiLQAcp544j+HQl1vkhDTJOI/JvAHlGO5chkF9YHO+/uh9vHNeTf1GkntdrOp/kIC9OR6NGv/1t+u3En7Z+O91I0q969kniav1NFrNP4XZNw5FrYawV/8egdUoGZdw6D8vuf58avW7HXndfuejKlrXs0/RDCBR07isJxg3BS1lfHpzq9O1Ld8aZYwwayzSS+Qt4MzBf+Ltc0RCAifPS8IwnqV4tWcjMG09Bw3FYN8lC+Fr913HJU9U/SrO7Xw== -------------------------------------------------------------------------------- /python-test/iron_house_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Ironhouse extends Stonehouse with client public key authentication. 5 | 6 | This is the strongest security model we have today, protecting against every 7 | attack we know about, except end-point attacks (where an attacker plants 8 | spyware on a machine to capture data before it's encrypted, or after it's 9 | decrypted). 10 | 11 | Author: Chris Laws 12 | 13 | Modified by Willem de Jong - only start the Python server, the client is the 14 | Chumak Erlang implementation. 15 | 16 | To run from an erlang shell: 17 | cd("python-test"), 18 | {ok, ServerKeys} = chumak_cert:read("server.key"), 19 | SPK = proplists:get_value(public_key, ServerKeys), 20 | {ok, ClientKeys} = chumak_cert:read("client.key"), 21 | CSK = proplists:get_value(secret_key, ClientKeys), 22 | CPK = proplists:get_value(public_key, ClientKeys), 23 | application:start(chumak), 24 | {ok, Socket} = chumak:socket(pull), 25 | ok = chumak:set_socket_option(Socket, curve_secretkey, CSK), 26 | ok = chumak:set_socket_option(Socket, curve_publickey, CPK), 27 | ok = chumak:set_socket_option(Socket, curve_serverkey, SPK), 28 | {ok, _} = chumak:connect(Socket, tcp, "127.0.0.1", 9000), 29 | {ok, Message} = chumak:recv(Socket), 30 | io:format("received: ~p~n", [Message]), 31 | halt(). 32 | 33 | ''' 34 | 35 | import logging 36 | import os 37 | import sys 38 | import time 39 | 40 | import zmq 41 | import zmq.auth 42 | from zmq.auth.thread import ThreadAuthenticator 43 | 44 | 45 | def run(): 46 | ''' Run Ironhouse example ''' 47 | 48 | # These directories are generated by the generate_certificates script 49 | keys_dir = os.path.dirname(__file__) 50 | 51 | ctx = zmq.Context.instance() 52 | 53 | # Start an authenticator for this context. 54 | auth = ThreadAuthenticator(ctx) 55 | auth.start() 56 | auth.allow('127.0.0.1') 57 | # Tell authenticator to use the certificate in a directory 58 | print(keys_dir) 59 | #auth.configure_curve(domain='*', location=keys_dir) 60 | auth.configure_curve(domain='*', location=".") 61 | 62 | server_key_file = os.path.join(keys_dir, "server.key") 63 | server_public, server_secret = zmq.auth.load_certificate(server_key_file) 64 | 65 | server = ctx.socket(zmq.PUSH) 66 | server.curve_secretkey = server_secret 67 | server.curve_publickey = server_public 68 | server.curve_server = True # must come before bind 69 | server.bind('tcp://*:9000') 70 | 71 | server.send(b"Hello") 72 | # Make sure that there is time to finish the handshake 73 | time.sleep(2) 74 | 75 | # stop auth thread 76 | auth.stop() 77 | 78 | if __name__ == '__main__': 79 | if zmq.zmq_version_info() < (4,0): 80 | raise RuntimeError("Security is not supported in libzmq version < 4.0. libzmq version {0}".format(zmq.zmq_version())) 81 | 82 | if '-v' in sys.argv: 83 | level = logging.DEBUG 84 | else: 85 | level = logging.INFO 86 | 87 | logging.basicConfig(level=level, format="[%(levelname)s] %(message)s") 88 | 89 | run() 90 | -------------------------------------------------------------------------------- /src/chumak_lbs.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Round-robin load-balancer based on an identifier 6 | 7 | -module(chumak_lbs). 8 | -export([new/0, put/3, get/2, delete/2, iterator/1, next/1]). 9 | 10 | -record(lbs, { 11 | map :: map(), %% map of load balancers 12 | xref %% reverse reference useful to locate identity at deletion 13 | }). 14 | -type lbs() :: #lbs{}. 15 | -type lbs_iterator() :: maps:ierator(). 16 | 17 | 18 | %% @doc returns an empty load-balancer by identifier 19 | -spec new() -> NewLBs::lbs(). 20 | new() -> 21 | #lbs{map=#{}, xref=#{}}. 22 | 23 | %% @doc put a item to be balanced. 24 | -spec put(LBs::lbs(), Identifier::term(), Item::term()) -> NewLB::lbs(). 25 | put(#lbs{map=LBMap, xref=XRef}=LBs, Identifier, Item) -> 26 | LB = get_sub_lb(LBMap, Identifier), 27 | NewLB = chumak_lb:put(LB, Item), 28 | 29 | NewLBMap = LBMap#{Identifier => NewLB}, 30 | NewXRef = XRef#{Item => Identifier}, 31 | 32 | LBs#lbs{map=NewLBMap, xref=NewXRef}. 33 | 34 | %% @doc get the next available item from load-balancer, return none if empty. 35 | -spec get(LBs::lbs(), Identifier::term()) -> none | {NewLBs::lbs(), Item::term()}. 36 | get(#lbs{map=LBMap}=LBs, Identifier) -> 37 | LB = get_sub_lb(LBMap, Identifier), 38 | 39 | case chumak_lb:get(LB) of 40 | {NewLB, Item} -> 41 | NewLBMap = LBMap#{Identifier => NewLB}, 42 | NewLBs = LBs#lbs{map=NewLBMap}, 43 | {NewLBs, Item}; 44 | 45 | none -> 46 | none 47 | end. 48 | 49 | %% @doc remove item from load-balancer 50 | -spec delete(LBs::lbs(), Item::term()) -> NewLBs::lbs(). 51 | delete(#lbs{xref=LBXRef}=LBs, Item)-> 52 | case maps:find(Item, LBXRef) of 53 | {ok, Identifier} -> 54 | delete_by_identifier(LBs, Identifier, Item); 55 | error -> 56 | LBs 57 | end. 58 | 59 | %% @doc create iterator over load-balancer identifiers 60 | -spec iterator(LBs::lbs()) -> lbs_iterator(). 61 | iterator(#lbs{map=Map}) -> 62 | maps:iterator(Map). 63 | 64 | %% @doc retrieve next key-value association in load-balancer identifiers 65 | -spec next(Iter::lbs_iterator()) -> {Key::term(), Val::term(), NextIter::lbs_iterator()} | none. 66 | next(Iter) -> 67 | maps:next(Iter). 68 | 69 | %% Private API 70 | delete_by_identifier(#lbs{map=LBMap, xref=LBXRef}=LBs, Identifier, Item) -> 71 | LB = get_sub_lb(LBMap, Identifier), 72 | NewLB = chumak_lb:delete(LB, Item), 73 | 74 | NewLBMap = case chumak_lb:is_empty(NewLB) of 75 | true -> 76 | maps:remove(Identifier, LBMap); 77 | false -> 78 | LBMap#{Identifier => NewLB} 79 | end, 80 | 81 | NewXRef = maps:remove(Item, LBXRef), 82 | LBs#lbs{map=NewLBMap, xref=NewXRef}. 83 | 84 | get_sub_lb(LbsMap, Identity) -> 85 | case maps:find(Identity, LbsMap) of 86 | {ok, X} -> 87 | X; 88 | error -> 89 | chumak_lb:new() 90 | end. 91 | -------------------------------------------------------------------------------- /src/chumak_curve_if.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc This module interfaces to the encryption library. 6 | %% 7 | %% Three variants are supported: nacl, enacl and nacerl. Which variant 8 | %% is used is determined at the time of compilation. 9 | %% 10 | %% If no encryption library is available, an error is thrown. 11 | 12 | -module(chumak_curve_if). 13 | 14 | -export([randombytes/1, 15 | box_keypair/0, 16 | box/4, 17 | box_open/4]). 18 | 19 | -ifdef(CHUMAK_CURVE_LIB_NACERL). 20 | -define(CURVE_MOD, nacerl). 21 | -endif. 22 | 23 | -ifdef(CHUMAK_CURVE_LIB_NACL). 24 | -define(CURVE_MOD, nacl). 25 | -endif. 26 | 27 | -ifdef(CHUMAK_CURVE_LIB_ENACL). 28 | -define(CURVE_MOD, enacl). 29 | -endif. 30 | 31 | -ifdef(CHUMAK_CURVE_LIB_NONE). 32 | -define(CURVE_MOD, none). 33 | -endif. 34 | 35 | -ifndef(CURVE_MOD). 36 | -define(CURVE_MOD, none). 37 | -endif. 38 | 39 | -spec randombytes(Size::integer()) -> binary(). 40 | randombytes(Size) -> 41 | case ?CURVE_MOD of 42 | none -> 43 | throw(not_supported); 44 | _ -> 45 | ?CURVE_MOD:randombytes(Size) 46 | end. 47 | 48 | %% @doc Generate a key pair. 49 | -spec box_keypair() -> #{secret => binary(), public => binary()}. 50 | box_keypair() -> 51 | case ?CURVE_MOD of 52 | none -> 53 | throw(not_supported); 54 | nacl -> 55 | {nacl_box_keypair, Pk, Sk} = nacl:box_keypair(), 56 | #{secret => Sk, public => Pk}; 57 | _ -> 58 | ?CURVE_MOD:box_keypair() 59 | end. 60 | 61 | %% @doc Encrypts+authenticates a message to another party. 62 | %% 63 | %% Encrypt a Message to the party identified by that party's public key using your own 64 | %% secret key to authenticate yourself. Requires a `Nonce' in addition. Returns the 65 | %% encrypted message. 66 | %% @end 67 | -spec box(Message::binary(), 68 | Nonce::binary(), 69 | PublicKey::binary(), 70 | SecretKey::binary()) -> binary(). 71 | box(Message, Nonce, PublicKey, SecretKey) -> 72 | case ?CURVE_MOD of 73 | none -> 74 | throw(not_supported); 75 | nacl -> 76 | {nacl_envelope, _, Binary} = nacl:box(Message, Nonce, PublicKey, SecretKey), 77 | Binary; 78 | _ -> 79 | ?CURVE_MOD:box(Message, Nonce, PublicKey, SecretKey) 80 | end. 81 | 82 | %% @doc Decrypts+verifies a message from another party. 83 | %% 84 | %% Decrypt a message (`Box') into a message given the other party's public key and your secret 85 | %% key. Also requires the same nonce as was used by the other party. Returns the plaintext 86 | %% message. 87 | %% @end 88 | -spec box_open(Box::binary(), 89 | Nonce::binary(), 90 | PublicKey::binary(), 91 | SecretKey::binary()) -> {ok, binary()}. 92 | box_open(Box, Nonce, PublicKey, SecretKey) -> 93 | case ?CURVE_MOD of 94 | none -> 95 | throw(not_supported); 96 | _ -> 97 | ?CURVE_MOD:box_open(Box, Nonce, PublicKey, SecretKey) 98 | end. 99 | -------------------------------------------------------------------------------- /python-test/stone_house_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Stonehouse uses the "CURVE" security mechanism. 5 | This gives us strong encryption on data, and (as far as we know) unbreakable 6 | authentication. Stonehouse is the minimum you would use over public networks, 7 | and assures clients that they are speaking to an authentic server, while 8 | allowing any client to connect. 9 | Author: Chris Laws 10 | 11 | Modified by Willem de Jong - only start the Python client, the server is the 12 | Chumak Erlang implementation. 13 | 14 | To run, start an Erlang shell and issue the following commands: 15 | cd("python-test"), 16 | {ok, ServerKeys} = chumak_cert:read("server.key"), 17 | SK = proplists:get_value(secret_key, ServerKeys), 18 | application:start(chumak), 19 | {ok, Socket} = chumak:socket(push), 20 | ok = chumak:set_socket_option(Socket, curve_server, true), 21 | ok = chumak:set_socket_option(Socket, curve_secretkey, SK), 22 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 9000). 23 | timer:sleep(1000), 24 | chumak:send(Socket, <<"Hello">>), 25 | halt(). 26 | 27 | ''' 28 | 29 | import logging 30 | import os 31 | import sys 32 | import time 33 | 34 | import zmq 35 | import zmq.auth 36 | from zmq.auth.thread import ThreadAuthenticator 37 | 38 | 39 | def run(): 40 | ''' Run Stonehouse example ''' 41 | 42 | # These directories are generated by the generate_certificates script 43 | keys_dir = os.path.dirname(__file__) 44 | 45 | ctx = zmq.Context.instance() 46 | 47 | # Start an authenticator for this context. 48 | auth = ThreadAuthenticator(ctx) 49 | auth.start() 50 | auth.allow('127.0.0.1') 51 | # Tell the authenticator how to handle CURVE requests 52 | auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) 53 | 54 | client = ctx.socket(zmq.PULL) 55 | # We need two certificates, one for the client and one for 56 | # the server. The client must know the server's public key 57 | # to make a CURVE connection. 58 | client_secret_file = os.path.join(keys_dir, "client.key") 59 | client_public, client_secret = zmq.auth.load_certificate(client_secret_file) 60 | client.curve_secretkey = client_secret 61 | client.curve_publickey = client_public 62 | 63 | # The client must know the server's public key to make a CURVE connection. 64 | server_public_file = os.path.join(keys_dir, "server.key") 65 | server_public, _ = zmq.auth.load_certificate(server_public_file) 66 | client.curve_serverkey = server_public 67 | 68 | client.connect('tcp://127.0.0.1:9000') 69 | 70 | if client.poll(100000): 71 | msg = client.recv() 72 | if msg == b"Hello": 73 | logging.info("Stonehouse test OK") 74 | else: 75 | logging.error("Stonehouse test FAIL") 76 | 77 | # stop auth thread 78 | auth.stop() 79 | 80 | if __name__ == '__main__': 81 | if zmq.zmq_version_info() < (4,0): 82 | raise RuntimeError("Security is not supported in libzmq version < 4.0. libzmq version {0}".format(zmq.zmq_version())) 83 | 84 | if '-v' in sys.argv: 85 | level = logging.DEBUG 86 | else: 87 | level = logging.INFO 88 | 89 | logging.basicConfig(level=level, format="[%(levelname)s] %(message)s") 90 | 91 | run() 92 | -------------------------------------------------------------------------------- /python-test/iron_house_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Stonehouse uses the "CURVE" security mechanism. 5 | This gives us strong encryption on data, and (as far as we know) unbreakable 6 | authentication. Stonehouse is the minimum you would use over public networks, 7 | and assures clients that they are speaking to an authentic server, while 8 | allowing any client to connect. 9 | Author: Chris Laws 10 | 11 | Modified by Willem de Jong - only start the Python client, the server is the 12 | Chumak Erlang implementation. 13 | 14 | To run, start an Erlang shell and issue the following commands: 15 | cd("python-test"), 16 | {ok, ServerKeys} = chumak_cert:read("server.key"), 17 | SK = proplists:get_value(secret_key, ServerKeys), 18 | {ok, ClientKeys} = chumak_cert:read("client.key"), 19 | CK = proplists:get_value(public_key, ClientKeys), 20 | application:start(chumak), 21 | {ok, Socket} = chumak:socket(push), 22 | ok = chumak:set_socket_option(Socket, curve_server, true), 23 | ok = chumak:set_socket_option(Socket, curve_secretkey, SK), 24 | ok = chumak:set_socket_option(Socket, curve_clientkeys, [CK]), 25 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 9000). 26 | timer:sleep(1000), 27 | chumak:send(Socket, <<"Hello">>), 28 | halt(). 29 | 30 | ''' 31 | 32 | import logging 33 | import os 34 | import sys 35 | import time 36 | 37 | import zmq 38 | import zmq.auth 39 | from zmq.auth.thread import ThreadAuthenticator 40 | 41 | 42 | def run(): 43 | ''' Run Ironhouse example ''' 44 | 45 | # These directories are generated by the generate_certificates script 46 | keys_dir = os.path.dirname(__file__) 47 | 48 | ctx = zmq.Context.instance() 49 | 50 | # # Start an authenticator for this context. 51 | # auth = ThreadAuthenticator(ctx) 52 | # auth.start() 53 | # auth.allow('127.0.0.1') 54 | # # Tell the authenticator how to handle CURVE requests 55 | # auth.configure_curve(domain='*', location=zmq.auth.CURVE_ALLOW_ANY) 56 | 57 | client = ctx.socket(zmq.PULL) 58 | # We need two certificates, one for the client and one for 59 | # the server. The client must know the server's public key 60 | # to make a CURVE connection. 61 | client_secret_file = os.path.join(keys_dir, "client.key") 62 | client_public, client_secret = zmq.auth.load_certificate(client_secret_file) 63 | client.curve_secretkey = client_secret 64 | client.curve_publickey = client_public 65 | 66 | # The client must know the server's public key to make a CURVE connection. 67 | server_public_file = os.path.join(keys_dir, "server.key") 68 | server_public, _ = zmq.auth.load_certificate(server_public_file) 69 | client.curve_serverkey = server_public 70 | 71 | client.connect('tcp://127.0.0.1:9000') 72 | 73 | if client.poll(100000): 74 | msg = client.recv() 75 | if msg == b"Hello": 76 | logging.info("Ironhouse test OK") 77 | else: 78 | logging.error("Ironhouse test FAIL") 79 | 80 | # stop auth thread 81 | # auth.stop() 82 | 83 | if __name__ == '__main__': 84 | if zmq.zmq_version_info() < (4,0): 85 | raise RuntimeError("Security is not supported in libzmq version < 4.0. libzmq version {0}".format(zmq.zmq_version())) 86 | 87 | if '-v' in sys.argv: 88 | level = logging.DEBUG 89 | else: 90 | level = logging.INFO 91 | 92 | logging.basicConfig(level=level, format="[%(levelname)s] %(message)s") 93 | 94 | run() 95 | -------------------------------------------------------------------------------- /test/chumak_acceptance_error_handler_curve.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_error_handler_curve). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 3010). 9 | 10 | %% Matching private and secret key, generated by nacl:box_keypair(). 11 | -define(PK, <<88,81,231,252,129,41,117,194,22,78,132,89,111,97,130,200, 12 | 56,248,239,34,254, 93,19,12,241,238,126,248,251,254,214,10>>). 13 | -define(SK, <<68,113,186,69,207,180,223,213,66,133,138,124,102,34,204,226, 14 | 174,85,89,65,233, 202,180,173,253,181,73,78,121,87,220,133>>). 15 | -define(WRONG_KEY, <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 16 | 174,85,89,65,233, 202,180,173,253,181,73,78,121,87,220,133>>). 17 | 18 | single_test_() -> 19 | [ 20 | { 21 | "Should not start connection if security mechanism does not match", 22 | {setup, fun start/0, fun stop/1, fun mechanism_mismatch/1} 23 | }, 24 | { 25 | "Should not start connection if server key is not correct", 26 | {setup, fun start/0, fun stop/1, fun wrong_key/1} 27 | } 28 | ]. 29 | 30 | start() -> 31 | application:ensure_started(chumak), 32 | {ok, ServerPid} = chumak:socket(pull), 33 | ok = chumak:set_socket_option(ServerPid, curve_server, true), 34 | ok = chumak:set_socket_option(ServerPid, curve_secretkey, ?SK), 35 | {ok, _BindPid} = chumak:bind(ServerPid, tcp, "localhost", ?PORT), 36 | ServerPid. 37 | 38 | stop(Pid) -> 39 | gen_server:stop(Pid), 40 | application:stop(chumak). 41 | 42 | mechanism_mismatch(_ServerPid) -> 43 | null_client(), 44 | Message = receive 45 | {peer_finish, Msg} -> 46 | Msg; 47 | Other -> 48 | Other 49 | end, 50 | [ 51 | ?_assertMatch({server_error, "Security mechanism mismatch"}, Message) 52 | ]. 53 | 54 | null_client() -> 55 | Parent = self(), 56 | spawn( 57 | fun () -> 58 | process_flag(trap_exit, true), 59 | {ok, Socket} = chumak:socket(push), 60 | {ok, PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT), 61 | link(PeerPid), %% to wait modifications 62 | receive 63 | {'EXIT', PeerPid, {shutdown, Reason}} -> 64 | Parent ! {peer_finish, Reason}; 65 | Other -> 66 | Parent ! {other, Other} 67 | end 68 | end 69 | ). 70 | 71 | wrong_key(_ServerPid) -> 72 | client_with_wrong_key(), 73 | Message = receive 74 | {peer_finish, Msg} -> 75 | Msg; 76 | Other -> 77 | Other 78 | end, 79 | [ 80 | ?_assertMatch({server_error, {error, closed}}, Message) 81 | ]. 82 | 83 | 84 | client_with_wrong_key() -> 85 | Parent = self(), 86 | spawn( 87 | fun () -> 88 | process_flag(trap_exit, true), 89 | {ok, ClientSocket} = chumak:socket(push), 90 | ok = chumak:set_socket_option(ClientSocket, curve_server, false), 91 | ok = chumak:set_socket_option(ClientSocket, curve_serverkey, ?WRONG_KEY), 92 | ok = chumak:set_socket_option(ClientSocket, curve_secretkey, ?SK), 93 | ok = chumak:set_socket_option(ClientSocket, curve_publickey, ?PK), 94 | {ok, PeerPid} = chumak:connect(ClientSocket, tcp, "localhost", ?PORT), 95 | link(PeerPid), %% to wait modifications 96 | receive 97 | {'EXIT', PeerPid, {shutdown, Reason}} -> 98 | Parent ! {peer_finish, Reason}; 99 | Other -> 100 | Parent ! {other, Other} 101 | end 102 | end 103 | ). 104 | -------------------------------------------------------------------------------- /src/chumak_resource.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Resource Router implementation for Erlang 6 | %% @hidden 7 | 8 | -module(chumak_resource). 9 | -behaviour(gen_server). 10 | -include_lib("kernel/include/logger.hrl"). 11 | 12 | %% api behaviour 13 | -export([start_link/0]). 14 | 15 | %% gen_server behaviors 16 | -export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, init/1, terminate/2]). 17 | 18 | %% public API implementation 19 | -spec start_link() -> {ok, Pid::pid()} | {error, Reason::term()}. 20 | start_link() -> 21 | gen_server:start_link(?MODULE, {}, []). 22 | 23 | 24 | -record(state, { 25 | resources :: map(), 26 | monitors :: map() 27 | }). 28 | 29 | %% gen_server implementation 30 | init(_Args) -> 31 | process_flag(trap_exit, true), 32 | State = #state{ 33 | resources=#{}, 34 | monitors=#{} 35 | }, 36 | {ok, State}. 37 | 38 | code_change(_OldVsn, State, _Extra) -> 39 | {ok, State}. 40 | 41 | handle_call({accept, SocketPid}, _From, State) -> 42 | case chumak_peer:accept(none, SocketPid, [multi_socket_type]) of 43 | {ok, Pid} -> 44 | {reply, {ok, Pid}, State}; 45 | {error, Reason} -> 46 | {reply, {error, Reason}, State} 47 | end; 48 | 49 | handle_call({bind, tcp, Host, Port}, _From, State) -> 50 | Reply = chumak_bind:start_link(Host, Port), 51 | {reply, Reply, State}; 52 | 53 | handle_call({bind, Protocol, _Host, _Port}, _From, State) -> 54 | {reply, {error, {unsupported_protocol, Protocol}}, State}; 55 | 56 | handle_call({route_resource, Resource}, _From, #state{resources=Resources}=State) -> 57 | case maps:find(Resource, Resources) of 58 | {ok, NewSocket} -> 59 | Flags = gen_server:call(NewSocket, get_flags), 60 | {reply, {change_socket, NewSocket, Flags}, State}; 61 | error -> 62 | {reply, close, State} 63 | end. 64 | 65 | handle_cast({attach, Resource, SocketPid}, #state{resources=Resources, monitors=Monitors}=State) -> 66 | NewResources = Resources#{Resource => SocketPid}, 67 | 68 | MonRef = erlang:monitor(process, SocketPid), 69 | NewMonitors = Monitors#{SocketPid => {Resource, MonRef}}, 70 | {noreply, State#state{resources=NewResources, monitors=NewMonitors}}; 71 | 72 | handle_cast({detach, Resource}, #state{resources=Resources, monitors=Monitors}=State) -> 73 | case maps:take(Resource, Resources) of 74 | {SocketPid, NewResources} -> 75 | case maps:take(SocketPid, Monitors) of 76 | {{Resource, MonRef}, NewMonitors} -> 77 | erlang:demonitor(MonRef), 78 | {noreply, State#state{resources=NewResources, monitors=NewMonitors}}; 79 | _ -> 80 | {noreply, State#state{resources=NewResources}} 81 | end; 82 | _ -> 83 | {noreply, State} 84 | end; 85 | 86 | handle_cast(CastMsg, State) -> 87 | ?LOG_INFO("zmq system error", #{error => unhandled_handle_cast, args => CastMsg }), 88 | {noreply, State}. 89 | 90 | handle_info({'DOWN', MonRef, process, SocketPid, _}, #state{resources=Resources, monitors=Monitors}=State) -> 91 | case maps:take(SocketPid, Monitors) of 92 | {{Resource, MonRef}, NewMonitors} -> 93 | NewResources = maps:remove(Resource, Resources), 94 | {noreply, State#state{resources=NewResources, monitors=NewMonitors}}; 95 | _ -> 96 | {noreply, State} 97 | end; 98 | 99 | handle_info({'EXIT', _Pid, {shutdown, invalid_resource}}, State) -> 100 | {noreply, State}; 101 | 102 | handle_info(InfoMsg, State) -> 103 | ?LOG_INFO("zmq system error", #{error => unhandled_handle_info, args => InfoMsg}), 104 | {noreply, State}. 105 | 106 | terminate(_Reason, _State) -> 107 | %% TODO: close all resources 108 | ok. 109 | -------------------------------------------------------------------------------- /test/chumak_acceptance_push_with_push.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_push_with_push). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 5595). 9 | 10 | push_test_() -> 11 | [ 12 | { 13 | "Should send with round robin strategy", 14 | {setup, fun start/0, fun stop/1, fun send_round_robin/1} 15 | }, 16 | { 17 | "Should send and recv multi_part", 18 | {setup, fun start/0, fun stop/1, fun send_multi_part/1} 19 | }, 20 | { 21 | "When disconnected, should return no connected peers", 22 | {setup, fun start/0, fun stop/1, 23 | fun send_message_without_connect/1} 24 | }, 25 | { 26 | "When call recv(), should deny recv message", 27 | {setup, fun start/0, fun stop/1, 28 | fun send_and_recv/1} 29 | } 30 | ]. 31 | 32 | 33 | start() -> 34 | application:ensure_started(chumak), 35 | {ok, Socket} = chumak:socket(push), 36 | {ok, _Client} = chumak:bind(Socket, tcp, "localhost", ?PORT), 37 | Socket. 38 | 39 | stop(Pid) -> 40 | gen_server:stop(Pid). 41 | 42 | send_round_robin(SocketPid) -> 43 | Parent = self(), 44 | 45 | spawn_link( 46 | fun() -> 47 | {ok, Client} = chumak:socket(pull), 48 | {ok, _PeerPid} = chumak:connect(Client, tcp, "localhost", ?PORT), 49 | {ok, Data} = chumak:recv(Client), 50 | Parent ! {recv, 1, Data} 51 | end 52 | ), 53 | timer:sleep(100), 54 | spawn_link( 55 | fun() -> 56 | {ok, Client} = chumak:socket(pull), 57 | {ok, _PeerPid} = chumak:connect(Client, tcp, "localhost", ?PORT), 58 | {ok, Data} = chumak:recv(Client), 59 | Parent ! {recv, 2, Data} 60 | end 61 | ), 62 | timer:sleep(200), 63 | 64 | ok = chumak:send(SocketPid, <<"first message">>), 65 | ok = chumak:send(SocketPid, <<"second message">>), 66 | 67 | Message1 = receive 68 | {recv, 1, Data1} -> 69 | Data1 70 | end, 71 | Message2 = receive 72 | {recv, 2, Data2} -> 73 | Data2 74 | end, 75 | 76 | [ 77 | ?_assertEqual(<<"first message">>, Message2), 78 | ?_assertEqual(<<"second message">>, Message1) 79 | ]. 80 | 81 | send_multi_part(SocketPid) -> 82 | Parent = self(), 83 | 84 | spawn_link( 85 | fun() -> 86 | {ok, Client} = chumak:socket(pull), 87 | {ok, _PeerPid} = chumak:connect(Client, tcp, "localhost", ?PORT), 88 | {ok, Data} = chumak:recv_multipart(Client), 89 | Parent ! {recv, 3, Data} 90 | end 91 | ), 92 | 93 | timer:sleep(100), 94 | spawn_link( 95 | fun() -> 96 | {ok, Client} = chumak:socket(pull), 97 | {ok, _PeerPid} = chumak:connect(Client, tcp, "localhost", ?PORT), 98 | {ok, Data} = chumak:recv_multipart(Client), 99 | Parent ! {recv, 4, Data} 100 | end 101 | ), 102 | 103 | timer:sleep(200), 104 | 105 | ok = chumak:send_multipart(SocketPid, [<<"Hey">>, <<"Joe">>]), 106 | ok = chumak:send_multipart(SocketPid, [<<"Lucy">>, <<"Sky">>]), 107 | 108 | Message1 = receive 109 | {recv, 3, Data1} -> 110 | Data1 111 | end, 112 | Message2 = receive 113 | {recv, 4, Data2} -> 114 | Data2 115 | end, 116 | 117 | [ 118 | ?_assertEqual([<<"Lucy">>, <<"Sky">>], Message1), 119 | ?_assertEqual([<<"Hey">>, <<"Joe">>], Message2) 120 | ]. 121 | 122 | 123 | send_message_without_connect(SocketPid) -> 124 | [ 125 | ?_assertEqual({error,no_connected_peers}, chumak:send(SocketPid, <<"first message">>)) 126 | ]. 127 | 128 | send_and_recv(SocketPid) -> 129 | [ 130 | ?_assertEqual({error,not_use}, chumak:recv(SocketPid)) 131 | ]. 132 | -------------------------------------------------------------------------------- /test/chumak_iron_house_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_iron_house_test). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(MESSAGE, <<"message from client">>). 9 | 10 | 11 | authentication_test_() -> 12 | [ 13 | { 14 | "Authenticated", 15 | {setup, fun start_authenticated/0, fun stop/1, fun push_and_pull/1} 16 | } 17 | , { 18 | "No authentication", 19 | {setup, fun start_no_authentication/0, fun stop/1, fun push_and_pull/1} 20 | } 21 | , { 22 | "Not authenticated (should fail)", 23 | {setup, fun start_not_authenticated/0, fun stop/1, fun negotiate_messages/1} 24 | } 25 | ]. 26 | 27 | start_authenticated() -> 28 | start(true). 29 | 30 | start_no_authentication() -> 31 | start(any). 32 | 33 | start_not_authenticated() -> 34 | start(false). 35 | 36 | start(Authenticated) -> 37 | #{public := ServerPK, secret := ServerSK} = chumak_curve_if:box_keypair(), 38 | #{public := ClientPK, secret := ClientSK} = chumak_curve_if:box_keypair(), 39 | application:ensure_started(chumak), 40 | {ok, Socket} = chumak:socket(pull), 41 | ok = chumak:set_socket_option(Socket, curve_server, true), 42 | ok = chumak:set_socket_option(Socket, curve_secretkey, ServerSK), 43 | case Authenticated of 44 | true -> 45 | ok = chumak:set_socket_option(Socket, curve_clientkeys, [ClientPK]); 46 | false -> 47 | RandomKey = chumak_curve_if:randombytes(32), 48 | ok = chumak:set_socket_option(Socket, curve_clientkeys, [RandomKey]); 49 | any-> 50 | ok = chumak:set_socket_option(Socket, curve_clientkeys, any) 51 | end, 52 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 5655), 53 | {Socket, ServerPK, ClientPK, ClientSK}. 54 | 55 | 56 | stop({Pid, _, _, _}) -> 57 | gen_server:stop(Pid). 58 | 59 | push_and_pull({SocketPid, ServerKey, PublicKey, SecretKey})-> 60 | push(ServerKey, PublicKey, SecretKey), 61 | {ok, ReceivedData} = chumak:recv(SocketPid), 62 | [ 63 | ?_assertEqual(ReceivedData, ?MESSAGE) 64 | ]. 65 | 66 | 67 | push(ServerKey, PK, SK) -> 68 | spawn_link(fun () -> 69 | {ok, ClientSocket} = chumak:socket(push), 70 | ok = chumak:set_socket_option(ClientSocket, curve_server, false), 71 | ok = chumak:set_socket_option(ClientSocket, curve_serverkey, ServerKey), 72 | ok = chumak:set_socket_option(ClientSocket, curve_secretkey, SK), 73 | ok = chumak:set_socket_option(ClientSocket, curve_publickey, PK), 74 | {ok, _ClientPid} = chumak:connect(ClientSocket, tcp, "127.0.0.1", 5655), 75 | ok = chumak:send(ClientSocket, ?MESSAGE) 76 | end). 77 | 78 | 79 | negotiate_messages(Values) -> 80 | invalid_client(Values), 81 | Message = wait_for_msg(), 82 | [ 83 | ?_assertEqual({server_error, {error, closed}}, Message) 84 | ]. 85 | 86 | wait_for_msg() -> 87 | receive 88 | {peer_finish, Msg} -> 89 | Msg 90 | end. 91 | 92 | invalid_client({_SocketPid, ServerKey, PK, SK}) -> 93 | Parent = self(), 94 | spawn( 95 | fun () -> 96 | process_flag(trap_exit, true), 97 | {ok, Socket} = chumak:socket(push), 98 | ok = chumak:set_socket_option(Socket, curve_server, false), 99 | ok = chumak:set_socket_option(Socket, curve_serverkey, ServerKey), 100 | ok = chumak:set_socket_option(Socket, curve_secretkey, SK), 101 | ok = chumak:set_socket_option(Socket, curve_publickey, PK), 102 | {ok, PeerPid} = chumak:connect(Socket, tcp, "127.0.0.1", 5655), 103 | link(PeerPid), %% to wait modifications 104 | client_loop(Parent, PeerPid) 105 | end 106 | ). 107 | 108 | client_loop(Parent, PeerPid) -> 109 | receive 110 | {'EXIT', PeerPid, {shutdown, Reason}} -> 111 | Parent ! {peer_finish, Reason} 112 | end. 113 | -------------------------------------------------------------------------------- /src/chumak_router.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Router Pattern for Erlang 6 | %% 7 | %% This pattern implement Router especification 8 | %% from: http://rfc.zeromq.org/spec:28/REQREP#toc6 9 | 10 | -module(chumak_router). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, identity/1]). 19 | 20 | -record(chumak_router, { 21 | identity :: string(), 22 | lbs, %% loadbalancers based on identity 23 | pending_recv :: nil | {from, From::term()}, 24 | recv_queue :: queue:queue() 25 | }). 26 | 27 | valid_peer_type(req) -> valid; 28 | valid_peer_type(router) -> valid; 29 | valid_peer_type(dealer) -> valid; 30 | valid_peer_type(_) -> invalid. 31 | 32 | init(Identity) -> 33 | State = #chumak_router{ 34 | identity=Identity, 35 | lbs=chumak_lbs:new(), 36 | recv_queue=queue:new(), 37 | pending_recv=nil 38 | }, 39 | {ok, State}. 40 | 41 | terminate(_Reason, #chumak_router{pending_recv=Recv}) -> 42 | case Recv of 43 | {from, From} -> gen_server:reply(From, {error, closed}); 44 | _ -> ok 45 | end, 46 | ok. 47 | 48 | identity(#chumak_router{identity=I}) -> I. 49 | 50 | peer_flags(_State) -> 51 | {router, [incoming_queue]}. 52 | 53 | accept_peer(State, PeerPid) -> 54 | {reply, {ok, PeerPid}, State}. 55 | 56 | peer_ready(#chumak_router{lbs=LBs}=State, PeerPid, Identity) -> 57 | NewLBs = chumak_lbs:put(LBs, Identity, PeerPid), 58 | {noreply, State#chumak_router{lbs=NewLBs}}. 59 | 60 | send(State, _Data, _From) -> 61 | {reply, {error, send_implemented_yet}, State}. 62 | 63 | recv(State, _From) -> 64 | {reply, {error, recv_implemented_yet}, State}. 65 | 66 | send_multipart(#chumak_router{lbs=LBs}=State, Multipart, _From) when length(Multipart) >= 2 -> 67 | [Identity|RemmaingMultipart] = Multipart, 68 | case chumak_lbs:get(LBs, binary_to_list(Identity)) of 69 | {NewLBs, PeerPid} -> 70 | chumak_peer:send(PeerPid, RemmaingMultipart), 71 | {reply, ok, State#chumak_router{lbs=NewLBs}}; 72 | 73 | none -> 74 | {reply, {error, no_peers}, State} 75 | end; 76 | 77 | send_multipart(State, _Multipart, _From) -> 78 | {reply, {error, identity_missing}, State}. 79 | 80 | recv_multipart(#chumak_router{recv_queue=RecvQueue, pending_recv=nil}=State, From) -> 81 | case queue:out(RecvQueue) of 82 | {{value, Multipart}, NewRecvQueue} -> 83 | {reply, {ok, Multipart}, State#chumak_router{recv_queue=NewRecvQueue}}; 84 | {empty, _RecvQueue} -> 85 | {noreply, State#chumak_router{pending_recv={from, From}}} 86 | end; 87 | recv_multipart(State, _From) -> 88 | {reply, {error, efsm}, State}. 89 | 90 | unblock(#chumak_router{pending_recv={from, PendingRecv}}=State, _From) -> 91 | NewState = State#chumak_router{pending_recv=nil}, 92 | gen_server:reply(PendingRecv, {error, again}), 93 | {reply, ok, NewState}; 94 | 95 | unblock(#chumak_router{pending_recv=nil}=State, _From) -> 96 | {reply, ok, State}. 97 | 98 | peer_recv_message(State, _Message, _From) -> 99 | %% This function will never called, because use incoming_queue property 100 | {noreply, State}. 101 | 102 | queue_ready(State, Identity, PeerPid) -> 103 | case chumak_peer:incoming_queue_out(PeerPid) of 104 | {out, Multipart} -> 105 | IdentityBin = list_to_binary(Identity), 106 | {noreply,handle_queue_ready(State,[IdentityBin | Multipart])}; 107 | empty -> 108 | {noreply,State}; 109 | {error,Info}-> 110 | ?LOG_WARNING("zmq queue error", #{error => send_error, type => router, reason => Info}), 111 | {noreply,State} 112 | end. 113 | 114 | peer_disconected(#chumak_router{lbs=LBs}=State, PeerPid) -> 115 | NewLBs = chumak_lbs:delete(LBs, PeerPid), 116 | {noreply, State#chumak_router{lbs=NewLBs}}. 117 | 118 | handle_queue_ready(#chumak_router{recv_queue=RecvQueue, pending_recv=nil}=State,Data)-> 119 | NewRecvQueue = queue:in(Data, RecvQueue), 120 | State#chumak_router{recv_queue=NewRecvQueue}; 121 | 122 | handle_queue_ready(#chumak_router{pending_recv={from, PendingRecv}}=State, Data)-> 123 | gen_server:reply(PendingRecv, {ok, Data}), %% if there is a waiter reply directly 124 | State#chumak_router{pending_recv=nil}. 125 | -------------------------------------------------------------------------------- /src/chumak_dealer.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | %% @doc ZeroMQ Dealer Pattern for Erlang 7 | %% 8 | %% This pattern implement Dealer especification 9 | %% from: http://rfc.zeromq.org/spec:28/REQREP#toc5 10 | 11 | -module(chumak_dealer). 12 | -behaviour(chumak_pattern). 13 | -include_lib("kernel/include/logger.hrl"). 14 | 15 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 16 | send/3, recv/2, 17 | unblock/2, 18 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 19 | queue_ready/3, peer_disconected/2, identity/1]). 20 | 21 | -record(chumak_dealer, { 22 | identity :: string(), 23 | lb :: list(), 24 | pending_recv=none :: none | {from, From::term()}, 25 | state=idle :: idle | wait_req 26 | }). 27 | 28 | % -define(RETRIES, 20). 29 | 30 | valid_peer_type(rep) -> valid; 31 | valid_peer_type(router) -> valid; 32 | valid_peer_type(dealer) -> valid; 33 | valid_peer_type(_) -> invalid. 34 | 35 | 36 | init(Identity) -> 37 | State = #chumak_dealer{ 38 | identity=Identity, 39 | lb=chumak_lb:new() 40 | }, 41 | {ok, State}. 42 | 43 | identity(#chumak_dealer{identity=I}) -> I. 44 | 45 | terminate(_Reason, _State) -> 46 | ok. 47 | 48 | peer_flags(_State) -> 49 | {dealer, [incoming_queue]}. 50 | 51 | accept_peer(State, PeerPid) -> 52 | NewLb = chumak_lb:put(State#chumak_dealer.lb, PeerPid), 53 | {reply, {ok, PeerPid}, State#chumak_dealer{lb=NewLb}}. 54 | 55 | peer_ready(State, _PeerPid, _Identity) -> 56 | {noreply, State}. 57 | 58 | send(State, _Data, _From) -> 59 | {reply, {error, not_implemented_yet}, State}. 60 | 61 | recv(State, _From) -> 62 | {reply, {error, not_implemented_yet}, State}. 63 | 64 | send_multipart(#chumak_dealer{lb=LB}=State, Multipart, From) -> 65 | case chumak_lb:get(LB) of 66 | none -> 67 | {reply, {error, no_connected_peers}, State}; 68 | {NewLB, PeerPid} -> 69 | chumak_peer:send(PeerPid, Multipart, From), 70 | {noreply, State#chumak_dealer{lb=NewLB}} 71 | end. 72 | 73 | recv_multipart(#chumak_dealer{state=idle, lb=LB}=State, From) -> 74 | case chumak_lb:get(LB) of 75 | none -> 76 | {noreply, State#chumak_dealer{state=wait_req, pending_recv={from, From}}}; 77 | {NewLB, PeerPid} -> 78 | direct_recv_multipart(State#chumak_dealer{lb=NewLB}, PeerPid, PeerPid, From) 79 | end; 80 | 81 | recv_multipart(State, _From) -> 82 | {reply, {error, efsm}, State}. 83 | 84 | peer_recv_message(State, _Message, _From) -> 85 | %% This function will never called, because use incoming_queue property 86 | {noreply, State}. 87 | 88 | unblock(#chumak_dealer{pending_recv={from, PendingRecv}}=State, _From) -> 89 | NewState = State#chumak_dealer{pending_recv=none, state=idle}, 90 | gen_server:reply(PendingRecv, {error, again}), 91 | {reply, ok, NewState}; 92 | 93 | unblock(#chumak_dealer{state=idle}=State, _From) -> 94 | {reply, ok, State}. 95 | 96 | queue_ready(#chumak_dealer{state=wait_req, pending_recv={from, PendingRecv}}=State, _Identity, PeerPid) -> 97 | FutureState = 98 | case chumak_peer:incoming_queue_out(PeerPid) of 99 | {out, Messages} -> 100 | gen_server:reply(PendingRecv, {ok, Messages}), 101 | State#chumak_dealer{state=idle, pending_recv=none}; 102 | empty -> 103 | gen_server:reply(PendingRecv, {error, queue_empty}), 104 | State#chumak_dealer{state=idle, pending_recv=none}; 105 | {error,Info}-> 106 | ?LOG_WARNING("zmq queue error", #{error => cannot_process, reason => Info}), 107 | State 108 | end, 109 | {noreply, FutureState}; 110 | 111 | queue_ready(State, _Identity, _PeerPid) -> 112 | {noreply, State}. 113 | 114 | peer_disconected(#chumak_dealer{lb=LB}=State, PeerPid) -> 115 | NewLB = chumak_lb:delete(LB, PeerPid), 116 | {noreply, State#chumak_dealer{lb=NewLB}}. 117 | 118 | %% implement direct recv from peer queues 119 | direct_recv_multipart(#chumak_dealer{lb=LB}=State, FirstPeerPid, PeerPid, From) -> 120 | case chumak_peer:incoming_queue_out(PeerPid) of 121 | {out, Messages} -> 122 | {reply, {ok, Messages}, State}; 123 | 124 | {error, {timeout, _}} -> 125 | {reply, {error, timeout}, State}; 126 | 127 | empty -> 128 | case chumak_lb:get(LB) of 129 | {NewLB, FirstPeerPid} -> 130 | {noreply, State#chumak_dealer{state=wait_req, pending_recv={from, From}, lb=NewLB}}; 131 | {NewLB, OtherPeerPid} -> 132 | direct_recv_multipart(State#chumak_dealer{lb=NewLB}, FirstPeerPid, OtherPeerPid, From) 133 | end 134 | end. 135 | -------------------------------------------------------------------------------- /src/chumak_pull.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Pull Pattern for Erlang 6 | %% 7 | %% This pattern implement Pull especification 8 | %% from: http://rfc.zeromq.org/spec:30/PIPELINE#toc4 9 | 10 | -module(chumak_pull). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, identity/1 19 | ]). 20 | 21 | -record(chumak_pull, { 22 | identity :: string(), 23 | pending_recv :: nil | {from, From::term()}, 24 | pending_recv_multipart :: nil | {from, From::term()}, 25 | recv_queue :: queue:queue() 26 | }). 27 | 28 | valid_peer_type(push) -> valid; 29 | valid_peer_type(_) -> invalid. 30 | 31 | init(Identity) -> 32 | State = #chumak_pull{ 33 | identity=Identity, 34 | recv_queue=queue:new(), 35 | pending_recv=nil, 36 | pending_recv_multipart=nil 37 | }, 38 | {ok, State}. 39 | 40 | terminate(_Reason, #chumak_pull{pending_recv=Recv, pending_recv_multipart=RecvM}) -> 41 | case Recv of 42 | {from, From} -> gen_server:reply(From, {error, closed}); 43 | _ -> ok 44 | end, 45 | 46 | case RecvM of 47 | {from, FromM} -> gen_server:reply(FromM, {error, closed}); 48 | _ -> ok 49 | end, 50 | 51 | ok. 52 | 53 | identity(#chumak_pull{identity=Identity}) -> Identity. 54 | 55 | peer_flags(_State) -> 56 | {pull, [incoming_queue]}. 57 | 58 | accept_peer(State, PeerPid) -> 59 | {reply, {ok, PeerPid}, State}. 60 | 61 | peer_ready(State, _PeerPid, _Identity) -> 62 | {noreply, State}. 63 | 64 | send(State, Data, From) -> 65 | send_multipart(State, [Data], From). 66 | 67 | recv(#chumak_pull{pending_recv=nil, pending_recv_multipart=nil}=State, From) -> 68 | case queue:out(State#chumak_pull.recv_queue) of 69 | {{value, Multipart}, NewRecvQueue} -> 70 | Msg = binary:list_to_bin(Multipart), 71 | {reply, {ok, Msg}, State#chumak_pull{recv_queue=NewRecvQueue}}; 72 | {empty, _RecvQueue} -> 73 | {noreply, State#chumak_pull{pending_recv={from, From}}} 74 | end; 75 | 76 | recv(State, _From) -> 77 | {reply, {error, already_pending_recv}, State}. 78 | 79 | send_multipart(State, _Multipart, _From) -> 80 | {reply, {error, not_use}, State}. 81 | 82 | recv_multipart(#chumak_pull{pending_recv=nil, pending_recv_multipart=nil}=State, From) -> 83 | case queue:out(State#chumak_pull.recv_queue) of 84 | {{value, Multipart}, NewRecvQueue} -> 85 | {reply, {ok, Multipart}, State#chumak_pull{recv_queue=NewRecvQueue}}; 86 | 87 | {empty, _RecvQueue} -> 88 | {noreply, State#chumak_pull{pending_recv_multipart={from, From}}} 89 | end; 90 | 91 | recv_multipart(State, _From) -> 92 | {reply, {error, already_pending_recv}, State}. 93 | 94 | peer_recv_message(State, _Message, _From) -> 95 | %% This function will never called, because use incoming_queue property 96 | {noreply, State}. 97 | 98 | unblock(#chumak_pull{pending_recv=Recv,pending_recv_multipart=MultiRecv}=State, _From) -> 99 | NewState = 100 | case Recv of 101 | {from, From} -> 102 | gen_server:reply(From, {error, again}), 103 | State#chumak_pull{pending_recv=nil}; 104 | nil -> State 105 | end, 106 | MultiNewState = 107 | case MultiRecv of 108 | {from, MultiFrom} -> 109 | gen_server:reply(MultiFrom, {error, again}), 110 | NewState#chumak_pull{pending_recv_multipart=nil}; 111 | nil -> NewState 112 | end, 113 | {reply, ok, MultiNewState}. 114 | 115 | queue_ready(State, _Identity, PeerPid) -> 116 | case chumak_peer:incoming_queue_out(PeerPid) of 117 | {out, Multipart} -> 118 | {noreply,handle_queue_ready(State,Multipart)}; 119 | empty -> 120 | {noreply,State}; 121 | {error,Info}-> 122 | ?LOG_WARNING("zmq queue error", #{error => send_error, reason => Info}), 123 | {noreply,State} 124 | end. 125 | 126 | peer_disconected(State, _PeerPid) -> 127 | {noreply, State}. 128 | 129 | handle_queue_ready(#chumak_pull{pending_recv=nil, pending_recv_multipart=nil}=State,Data)-> 130 | NewRecvQueue = queue:in(Data, State#chumak_pull.recv_queue), 131 | State#chumak_pull{recv_queue=NewRecvQueue}; 132 | 133 | %% when pending recv 134 | handle_queue_ready(#chumak_pull{pending_recv={from, PendingRecv}, pending_recv_multipart=nil}=State, Data)-> 135 | Msg = binary:list_to_bin(Data), 136 | gen_server:reply(PendingRecv, {ok, Msg}), 137 | State#chumak_pull{pending_recv=nil}; 138 | 139 | %% when pending recv_multipart 140 | handle_queue_ready(#chumak_pull{pending_recv=nil, pending_recv_multipart={from, PendingRecv}}=State, Data)-> 141 | gen_server:reply(PendingRecv, {ok, Data}), 142 | State#chumak_pull{pending_recv_multipart=nil}. 143 | -------------------------------------------------------------------------------- /test/chumak_acceptance_pair.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_pair). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 5587). 9 | 10 | normal_test_() -> 11 | [ 12 | { 13 | "Should deliver message for the paired peer", 14 | {setup, fun start/0, fun stop/1, fun negotiate_without_multipart/1} 15 | } 16 | , { 17 | "Should deliver message for the paired peer, encrypted", 18 | {setup, fun start_curve/0, fun stop/1, fun negotiate_without_multipart_curve/1} 19 | } 20 | , { 21 | "Should deliver message for the paired peer using multipart", 22 | {setup, fun start/0, fun stop/1, fun negotiate_with_multipart/1} 23 | } 24 | , { 25 | "Should deliver message for the paired peer without timeout", 26 | {setup, fun start/0, fun stop/1, fun negotiate_without_timeout/1} 27 | } 28 | , { 29 | "Should deliver message for the paired peer with delayed message", 30 | {setup, fun start/0, fun stop/1, fun negotiate_with_delay/1} 31 | } 32 | ]. 33 | 34 | start() -> 35 | application:ensure_started(chumak), 36 | {ok, Socket} = chumak:socket(pair), 37 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", ?PORT), 38 | {Socket, #{}}. 39 | 40 | start_curve() -> 41 | application:ensure_started(chumak), 42 | {ok, Socket} = chumak:socket(pair), 43 | #{public := ServerPK, secret := ServerSK} = chumak_curve_if:box_keypair(), 44 | #{public := ClientPK, secret := ClientSK} = chumak_curve_if:box_keypair(), 45 | ok = chumak:set_socket_option(Socket, curve_server, true), 46 | ok = chumak:set_socket_option(Socket, curve_secretkey, ServerSK), 47 | ok = chumak:set_socket_option(Socket, curve_clientkeys, any), 48 | {ok, _BindProc} = chumak:bind(Socket, tcp, "localhost", ?PORT), 49 | {Socket, #{curve_serverkey => ServerPK, 50 | curve_publickey => ClientPK, 51 | curve_secretkey => ClientSK}}. 52 | 53 | start_worker(Identity, Func, CurveOptions) -> 54 | Parent = self(), 55 | spawn_link( 56 | fun () -> 57 | {ok, Socket} = chumak:socket(pair, Identity), 58 | [chumak:set_socket_option(Socket, Option, Value) 59 | || {Option, Value} <- maps:to_list(CurveOptions)], 60 | {ok, _PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT), 61 | Func(Socket, Identity, Parent) 62 | end 63 | ). 64 | 65 | stop({Pid, _}) -> 66 | gen_server:stop(Pid). 67 | 68 | negotiate_without_multipart({Socket, CurveOptions}) -> 69 | do_negotiate(Socket, CurveOptions, "PAIR-A"). 70 | 71 | negotiate_without_multipart_curve({Socket, CurveOptions}) -> 72 | do_negotiate(Socket, CurveOptions, "PAIR-AC"). 73 | 74 | do_negotiate(Socket, CurveOptions, Id) -> 75 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 76 | {ok, Message} = chumak:recv(ClientSocket), 77 | Parent ! {recv, Identity, Message} 78 | end, 79 | start_worker(Id, NegociateFunc, CurveOptions), 80 | timer:sleep(200), 81 | ok = chumak:send(Socket, <<"Hey brother">>), 82 | 83 | Message = receive 84 | {recv, Id, MultipartA} -> 85 | MultipartA 86 | end, 87 | [ 88 | ?_assertEqual(Message, <<"Hey brother">>) 89 | ]. 90 | 91 | negotiate_with_multipart({Socket, CurveOptions}) -> 92 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 93 | {ok, Message} = chumak:recv_multipart(ClientSocket), 94 | Parent ! {recv, Identity, Message} 95 | end, 96 | start_worker("PAIR-B", NegociateFunc, CurveOptions), 97 | timer:sleep(200), 98 | ok = chumak:send_multipart(Socket, [<<"Hey">>, <<"Jude">>]), 99 | 100 | Message = receive 101 | {recv, "PAIR-B", MultipartA} -> 102 | MultipartA 103 | end, 104 | [ 105 | ?_assertEqual(Message, [<<"Hey">>, <<"Jude">>]) 106 | ]. 107 | 108 | negotiate_without_timeout({Socket, CurveOptions}) -> 109 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 110 | {ok, Message} = chumak:recv(ClientSocket), 111 | Parent ! {recv, Identity, Message} 112 | end, 113 | start_worker("PAIR-C", NegociateFunc, CurveOptions), 114 | ok = chumak:send(Socket, <<"Hey mother">>), 115 | 116 | Message = receive 117 | {recv, "PAIR-C", MultipartA} -> 118 | MultipartA 119 | end, 120 | [ 121 | ?_assertEqual(Message, <<"Hey mother">>) 122 | ]. 123 | 124 | negotiate_with_delay({Socket, CurveOptions}) -> 125 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 126 | timer:sleep(200), 127 | {ok, Message} = chumak:recv(ClientSocket), 128 | Parent ! {recv, Identity, Message} 129 | end, 130 | start_worker("PAIR-D", NegociateFunc, CurveOptions), 131 | ok = chumak:send(Socket, <<"Hey father">>), 132 | 133 | Message = receive 134 | {recv, "PAIR-D", MultipartA} -> 135 | MultipartA 136 | end, 137 | [ 138 | ?_assertEqual(Message, <<"Hey father">>) 139 | ]. 140 | -------------------------------------------------------------------------------- /test/chumak_acceptance_req_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_req_test). 6 | -export([echo_rep_server/1]). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | req_single_test_() -> 10 | [ 11 | { 12 | "Should send one message at time", 13 | {setup, fun start/0, fun stop/1, fun send_two_message_two_times/1} 14 | }, 15 | { 16 | "When disconnected, should return no connected peers", 17 | {setup, fun start_without_connect/0, fun stop/1, 18 | fun send_message_without_connect/1} 19 | }, 20 | { 21 | "When call send() and recv(), should recv message", 22 | {setup, fun start/0, fun stop/1, 23 | fun send_and_recv/1} 24 | }, 25 | { 26 | "When call send() and more later call recv(), should recv message", 27 | {setup, fun start/0, fun stop/1, 28 | fun send_and_recv_with_delay/1} 29 | }, 30 | { 31 | "When call send() and twice recv(), should deny the second recv", 32 | {setup, fun start/0, fun stop/1, 33 | fun send_and_twice_recv/1} 34 | }, 35 | { 36 | "When call send() and twice recv() by distinct processes, should deny the second recv", 37 | {setup, fun start/0, fun stop/1, 38 | fun send_and_twice_recv_by_two_process/1} 39 | } 40 | ]. 41 | 42 | req_with_load_balancer_test_() -> 43 | [ 44 | { 45 | "Should send and receive more than one message", 46 | {setup, fun start_with_lb/0, fun stop/1, fun send_and_receive_10_messages/1} 47 | } 48 | ]. 49 | 50 | start() -> 51 | application:ensure_started(chumak), 52 | {ok, Socket} = chumak:socket(req), 53 | ensure_echo_rep_server(rep_server1, 5555), 54 | ensure_echo_rep_server(rep_server2, 5556), 55 | 56 | {ok, _Client} = chumak:connect(Socket, tcp, "localhost", 5555), 57 | Socket. 58 | 59 | ensure_echo_rep_server(Alias, Port) -> 60 | case whereis(Alias) of 61 | undefined -> 62 | {ok, Socket} = chumak:socket(rep, atom_to_list(Alias)), 63 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", Port), 64 | spawn(?MODULE, echo_rep_server, [Socket]), 65 | register(Alias, Socket), 66 | Socket; 67 | Pid -> 68 | Pid 69 | end. 70 | 71 | echo_rep_server(Socket) -> 72 | {ok, Data} = chumak:recv(Socket), 73 | case Data of 74 | <<"delay">> -> 75 | timer:sleep(200); 76 | _ -> 77 | pass 78 | end, 79 | 80 | chumak:send(Socket, Data), 81 | echo_rep_server(Socket). 82 | 83 | start_with_lb() -> 84 | application:ensure_started(chumak), 85 | {ok, Socket} = chumak:socket(req), 86 | 87 | ensure_echo_rep_server(rep_server1, 5555), 88 | ensure_echo_rep_server(rep_server2, 5556), 89 | 90 | {ok, _Client1} = chumak:connect(Socket, tcp, "localhost", 5555), 91 | {ok, _Client2} = chumak:connect(Socket, tcp, "localhost", 5556), 92 | Socket. 93 | 94 | 95 | start_without_connect() -> 96 | application:ensure_started(chumak), 97 | {ok, Socket} = chumak:socket(req), 98 | Socket. 99 | 100 | stop(Pid) -> 101 | gen_server:stop(Pid). 102 | 103 | send_two_message_two_times(SocketPid)-> 104 | [ 105 | ?_assertEqual(chumak:send(SocketPid, <<"first message">>), ok), 106 | ?_assertEqual(chumak:send(SocketPid, <<"second message">>), {error, efsm}) 107 | ]. 108 | 109 | send_message_without_connect(SocketPid) -> 110 | [ 111 | ?_assertEqual(chumak:send(SocketPid, <<"first message">>), {error,no_connected_peers}) 112 | ]. 113 | 114 | send_and_recv(SocketPid) -> 115 | [ 116 | ?_assertEqual(chumak:send(SocketPid, "message"), ok), 117 | ?_assertEqual(chumak:recv(SocketPid), {ok, <<"message">>}) 118 | ]. 119 | 120 | send_and_recv_with_delay(SocketPid) -> 121 | ok = chumak:send(SocketPid, <<"delayed message">>), 122 | ok = timer:sleep(200), 123 | [ 124 | ?_assertEqual(chumak:recv(SocketPid), {ok, <<"delayed message">>}) 125 | ]. 126 | 127 | send_and_twice_recv(SocketPid) -> 128 | [ 129 | ?_assertEqual(chumak:send(SocketPid, <<"twice recv">>), ok), 130 | ?_assertEqual(chumak:recv(SocketPid), {ok, <<"twice recv">>}), 131 | ?_assertEqual(chumak:recv(SocketPid), {error, efsm}) 132 | ]. 133 | 134 | send_and_twice_recv_by_two_process(SocketPid) -> 135 | ok = chumak:send(SocketPid, <<"delay">>), 136 | Parent = self(), 137 | 138 | spawn_link(fun () -> 139 | Reply = chumak:recv(SocketPid), 140 | timer:sleep(400), 141 | Parent ! {recv_reply, Reply} 142 | end), 143 | {ok, <<"delay">>} = chumak:recv(SocketPid), 144 | AsyncReply = receive 145 | {recv_reply, X} -> 146 | X 147 | end, 148 | [ 149 | ?_assertEqual(AsyncReply, {error, efsm}) 150 | ]. 151 | 152 | send_and_receive_10_messages(SocketPid) -> 153 | lists:map( 154 | fun (I) -> 155 | Message = io_lib:format("Message number ~p", [I]), 156 | MessageBin = list_to_binary(Message), 157 | ok = chumak:send(SocketPid, MessageBin), 158 | Reply = chumak:recv(SocketPid), 159 | ?_assertEqual(Reply, {ok, MessageBin}) 160 | end, lists:seq(1, 10)). 161 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %% This script tries to discover which library can be used 2 | %% to support curve encryption (if any). Curve encryption is 3 | %% used by the CURVE security model. 4 | %% 5 | %% If the CHUMAK_CURVE_LIB environment variable is set, 6 | %% that determines what is used. Supported values are: 7 | %% - nacerl - this is the minimal variant using tweetnacl. 8 | %% https://github.com/willemdj/NaCerl 9 | %% - nacl - this is similar to nacerl, but it depends on libsodium. 10 | %% https://github.com/tonyg/erlang-nacl 11 | %% - enacl - this also depends on libsodium, but it also requires 12 | %% an Erlang VM that supports dirty schedulers. 13 | %% https://github.com/jlouis/enacl 14 | %% - none - no support for the CURVE security model. 15 | %% 16 | %% If the CHUMAK_CURVE_LIB environment variable is _not_ set, 17 | %% nacerl will be used. On Windows an additional check will be 18 | %% performed first: if gcc and make are not available, `none` will 19 | %% be assumed. 20 | %% 21 | %% Since it is not possible to reliably and completely build any of 22 | %% the options that depend on libsodium from rebar3, only in the `nacerl` 23 | %% case the library will be fetched and built automatically as a 24 | %% dependency. In the other cases you will have to make the library available 25 | %% via other means. 26 | %% 27 | %% The selected option ("nacl", "enacl", "nacerl" or "none") will be 28 | %% passed as a macro to the compiler. Based on this macro it is 29 | %% determined which library is used by the code. 30 | 31 | %% Some of the ideas used here were found in 32 | %% https://github.com/klacke/yaws/blob/master/rebar.config.script 33 | %% and 34 | %% https://github.com/dgud/esdl/blob/master/rebar.config.script 35 | 36 | UpdateCfg = fun(Config, _Key, undefined) -> 37 | Config; 38 | (Config, Key, NewVal) -> 39 | case lists:keyfind(Key, 1, Config) of 40 | {Key, Vals} -> 41 | NVals = [NewVal | Vals], 42 | lists:keyreplace(Key, 1, Config, {Key, NVals}); 43 | false -> 44 | Config ++ [{Key, [NewVal]}] 45 | end 46 | end, 47 | 48 | NaCerlDep = {nacerl, ".*", {git, "https://github.com/willemdj/NaCerl.git", {branch, "master"}}}, 49 | 50 | IsAvailable = 51 | fun(Executable) -> 52 | case os:find_executable(Executable) of 53 | false -> 54 | io:format("Building Chumak with curve support on windows" 55 | " requires ~p~n", [Executable]), 56 | false; 57 | _ -> 58 | true 59 | end 60 | end, 61 | 62 | AllAvailable = 63 | fun(Executables) -> 64 | lists:all(fun(Bool) -> Bool end, [IsAvailable(E) || E <- Executables]) 65 | end, 66 | 67 | %% The only dependency where it makes sense to fecth and build it 68 | %% from here, is NaCerl. The other ones need libsodium and must be installed 69 | %% as a separate step. 70 | DetermineCurveDep = 71 | fun() -> 72 | case os:type() of 73 | {win32, _} -> 74 | case AllAvailable(["gcc", "make"]) of 75 | true -> 76 | NaCerlDep; 77 | false -> 78 | io:format("not all required tools are available to build " 79 | "curve security mechanism.~n"), 80 | undefined 81 | end; 82 | _ -> 83 | undefined 84 | end 85 | end, 86 | 87 | %% If the CHUMAK_CURVE_LIB environment variable is set, that determines 88 | %% what is used. 89 | CurveLibEnv = os:getenv("CHUMAK_CURVE_LIB"), 90 | 91 | CurveDep = case CurveLibEnv of 92 | false -> 93 | undefined; 94 | Val -> 95 | case string:to_lower(Val) of 96 | "nacerl" -> 97 | NaCerlDep; 98 | _ -> 99 | DetermineCurveDep() 100 | end 101 | end, 102 | 103 | CurveCompilerOption = case CurveDep of 104 | NaCerlDep -> 105 | {d, 'CHUMAK_CURVE_LIB_NACERL', true}; 106 | _ -> 107 | case CurveLibEnv of 108 | false -> 109 | undefined; 110 | Value -> 111 | case string:to_lower(Value) of 112 | "nacerl" -> 113 | {d, 'CHUMAK_CURVE_LIB_NACERL', true}; 114 | "nacl" -> 115 | {d, 'CHUMAK_CURVE_LIB_NACL', true}; 116 | "enacl" -> 117 | {d, 'CHUMAK_CURVE_LIB_ENACL', true}; 118 | "none" -> 119 | undefined 120 | end 121 | end 122 | end, 123 | 124 | Config2 = UpdateCfg(CONFIG, erl_opts, CurveCompilerOption), 125 | UpdateCfg(Config2, deps, CurveDep). 126 | -------------------------------------------------------------------------------- /src/chumak_rep.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Rep Pattern for Erlang 6 | %% 7 | %% This pattern implement REP especification 8 | %% from: http://rfc.zeromq.org/spec:28/REQREP#toc4 9 | 10 | -module(chumak_rep). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, identity/1]). 19 | 20 | %% state for a pattern always to be module name. 21 | -record(chumak_rep, { 22 | identity :: string(), 23 | pending_recv=nil :: nil | {from, From::term()}, 24 | state=idle :: idle | wait_req, 25 | lb :: list(), 26 | last_recv_peer=nil :: nil | pid() 27 | }). 28 | 29 | valid_peer_type(req) -> valid; 30 | valid_peer_type(dealer) -> valid; 31 | valid_peer_type(_) -> invalid. 32 | 33 | init(Identity) -> 34 | State = #chumak_rep{ 35 | identity=Identity, 36 | lb=chumak_lb:new() 37 | }, 38 | {ok, State}. 39 | 40 | terminate(_Reason, _State) -> 41 | ok. 42 | 43 | identity(#chumak_rep{identity=I}) -> I. 44 | 45 | peer_flags(_State) -> 46 | {rep, [incoming_queue]}. 47 | 48 | accept_peer(State, PeerPid) -> 49 | NewLb = chumak_lb:put(State#chumak_rep.lb, PeerPid), 50 | {reply, {ok, PeerPid}, State#chumak_rep{lb=NewLb}}. 51 | 52 | peer_ready(State, _PeerPid, _Identity) -> 53 | {noreply, State}. 54 | 55 | send(#chumak_rep{last_recv_peer=nil}=State, _Data, _From) -> 56 | {reply, {error, efsm}, State}; 57 | 58 | send(#chumak_rep{last_recv_peer=LastRecvPeer}=State, Data, _From) 59 | when is_pid(LastRecvPeer) -> 60 | chumak_peer:send(LastRecvPeer, [<<>>, Data]), 61 | {reply, ok, State#chumak_rep{last_recv_peer=nil}}. 62 | 63 | 64 | recv(#chumak_rep{state=idle, lb=LB}=State, From) -> 65 | case chumak_lb:get(LB) of 66 | none -> 67 | {noreply, State#chumak_rep{state=wait_req, pending_recv={from, From}}}; 68 | {NewLB, PeerPid} -> 69 | direct_recv(State#chumak_rep{lb=NewLB}, PeerPid, PeerPid, From) 70 | end; 71 | 72 | recv(State, _From) -> 73 | {reply, {error, efsm}, State}. 74 | 75 | unblock(#chumak_rep{pending_recv={from, PendingRecv}}=State, _From) -> 76 | NewState = State#chumak_rep{pending_recv=nil}, 77 | gen_server:reply(PendingRecv, {error, again}), 78 | {reply, ok, NewState}; 79 | 80 | unblock(#chumak_rep{pending_recv=nil}=State, _From) -> 81 | {reply, ok, State}. 82 | 83 | 84 | send_multipart(State, _Multipart, _From) -> 85 | {reply, {error, not_implemented_yet}, State}. 86 | 87 | recv_multipart(State, _From) -> 88 | {reply, {error, not_implemented_yet}, State}. 89 | 90 | peer_recv_message(State, _Message, _From) -> 91 | %% This function will never called, because use incoming_queue property 92 | {noreply, State}. 93 | 94 | queue_ready(#chumak_rep{state=wait_req, pending_recv={from, PendingRecv}}=State, _Identity, PeerPid) -> 95 | FutureState = State#chumak_rep{state=idle, pending_recv=nil}, 96 | case recv_from_peer(PeerPid) of 97 | {ok, Message} -> 98 | gen_server:reply(PendingRecv, {ok, Message}), 99 | {noreply, FutureState#chumak_rep{last_recv_peer=PeerPid}}; 100 | 101 | {error, Reason} -> 102 | gen_server:reply(PendingRecv, {error, Reason}), 103 | {noreply, FutureState}; 104 | 105 | empty -> 106 | gen_server:reply(PendingRecv, {error, queue_empty}), 107 | {noreply, FutureState} 108 | end; 109 | 110 | queue_ready(State, _Identity, _PeerPid) -> 111 | %% Not used in iddle state 112 | {noreply, State}. 113 | 114 | peer_disconected(#chumak_rep{lb=LB}=State, PeerPid) -> 115 | NewLB = chumak_lb:delete(LB, PeerPid), 116 | {noreply, State#chumak_rep{lb=NewLB}}. 117 | 118 | %% implement direct recv from peer queues 119 | direct_recv(#chumak_rep{lb=LB}=State, FirstPeerPid, PeerPid, From) -> 120 | case recv_from_peer(PeerPid) of 121 | {ok, Message} -> 122 | {reply, {ok, Message}, State#chumak_rep{last_recv_peer=PeerPid}}; 123 | 124 | {error, Reason} -> 125 | {reply, {error, Reason}, State}; 126 | 127 | empty -> 128 | case chumak_lb:get(LB) of 129 | {NewLB, FirstPeerPid} -> 130 | {noreply, State#chumak_rep{state=wait_req, pending_recv={from, From}, lb=NewLB}}; 131 | {NewLB, OtherPeerPid} -> 132 | direct_recv(State#chumak_rep{lb=NewLB}, FirstPeerPid, OtherPeerPid, From) 133 | end 134 | end. 135 | 136 | recv_from_peer(PeerPid) -> 137 | case chumak_peer:incoming_queue_out(PeerPid) of 138 | {out, Messages} -> 139 | decode_messages(Messages); 140 | empty -> 141 | empty; 142 | {error,Info}-> 143 | ?LOG_WARNING("zmq send error", #{error => send_error, reason => Info}), 144 | empty 145 | end. 146 | 147 | decode_messages([<<>>|Tail])-> 148 | {ok, binary:list_to_bin(Tail)}; 149 | decode_messages([Delimiter|_Tail]) -> 150 | ?LOG_WARNING("zmq decode error", #{error => invalid_delimiter_frame, pattern => rep, obtained_frame => Delimiter, expected_frame => <<>> }), 151 | {error, invalid_delimiter_frame}. 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chumak 2 | 3 | ![Chumaki](docs/images/chumaki.jpg) 4 | 5 | ## What is chumak? 6 | 7 | `chumak` is a library written in [Erlang](https://www.erlang.org/). It implements the ZeroMQ Message Transport Protocol (ZMTP). `chumak` supports ZMTP version [3.1](http://rfc.zeromq.org/spec:37/ZMTP/). 8 | 9 | ## Goal 10 | 11 | The goal of `chumak` application is to provide up-to-date native Erlang implementation of ZMTP. 12 | 13 | ## Features 14 | 15 | 1. Resource Property *(NEW in 3.1!)* 16 | 2. Request-Reply pattern 17 | 3. Publish-Subscribe pattern 18 | 4. Pipeline Pattern 19 | 5. Exclusive Pair Pattern 20 | 6. Version Negotiation 21 | 7. NULL Security Mechanism 22 | 8. CURVE Security Mechanism 23 | 9. Error Handling 24 | 10. Framing 25 | 11. Socket-Type Property & Identity Property 26 | 12. Backwards Interoperability with ZMTP 3.0 27 | 28 | 29 | ## Install 30 | 31 | You can install `chumak` from [hex.pm](https://hex.pm/packages/chumak) by including the following in your `rebar.config`: 32 | 33 | ```erlang 34 | {deps,[ 35 | {chumak, "X.Y.Z"} 36 | ]}. 37 | ``` 38 | where _X.Y.Z_ is one of the [release versions](https://github.com/chovencorp/chumak/releases). 39 | 40 | For more info on rebar3 dependencies see the [rebar3 docs](http://www.rebar3.org/docs/dependencies). 41 | 42 | ## Usage 43 | 44 | See [examples](examples). Otherwise use just like a regular Erlang/OTP application. 45 | 46 | If you would like to use [python tests](python-test) to try language interop, you need to have [pyzmq](https://github.com/zeromq/pyzmq) installed. 47 | 48 | ## Build 49 | 50 | ``` 51 | $ rebar3 compile 52 | ``` 53 | 54 | By default, this will try to build a version of the application that 55 | does not include support for the CURVE security model. 56 | 57 | The environment variable `CHUMAK_CURVE_LIB` can be used to specify a 58 | NIF that implements the encryption functions that are required to support 59 | the CURVE security model. 60 | 61 | The following values for `CHUMAK_CURVE_LIB` are supported: 62 | 63 | - nacerl - this is the minimal variant using the tweetnacl C library. By 64 | default it is fetched and built from https://github.com/willemdj/NaCerl. 65 | 66 | Compilation of nacerl requires gcc and make. Since these tools 67 | may not be available on windows systems, a check on the 68 | availability of these tools will be done. If they are not 69 | available the dependency will not be fetched and there will be 70 | no support for the CURVE security model. 71 | 72 | - nacl - this is similar to nacerl, but it depends on libsodium. The 73 | repository for this is https://github.com/tonyg/erlang-nacl. The 74 | the build process for Chumak will not automatically fetch and 75 | build it, but if `CHUMAK_CURVE_LIB` is set to "nacl", it will be 76 | assumed that this library is available and it will be used. 77 | 78 | - enacl - this also depends on libsodium, but it also requires 79 | an Erlang VM that supports dirty schedulers. The repository is 80 | https://github.com/jlouis/enacl. The build process for 81 | Chumak will not automatically fetch and build it, but if 82 | `CHUMAK_CURVE_LIB` is set to "enacl", it will be assumed that 83 | this library is available and it will be used. 84 | 85 | ## Test 86 | 87 | ``` 88 | $ rebar3 eunit -c 89 | ``` 90 | The `-c` will allow you to see the test coverage by running the command below. 91 | 92 | ## Coverage 93 | 94 | ``` 95 | $ rebar3 cover 96 | ``` 97 | 98 | ## Generate Docs 99 | 100 | ``` 101 | $ rebar3 edoc 102 | ``` 103 | 104 | ## Architecture 105 | 106 | [Architecture](docs/architecture.md) describes the system structure. 107 | 108 | ## Help Wanted 109 | 110 | Would you like to help with the project? Pick any of the issues tagged [help wanted](https://github.com/zeromq/chumak/labels/help%20wanted) and contribute! 111 | 112 | ## Contributing 113 | 114 | See [Contributing](CONTRIBUTING.md). 115 | 116 | 117 | ## FAQ 118 | 119 | 1. Why another Erlang implementation? 120 | 121 | Because the existing Erlang implementations and bindings are out of date. 122 | 123 | 2. Can I use `chumak` for free? 124 | 125 | Yes, as long as you abide by the terms of the [MPLv2 license](LICENSE). In short, you can include this code as a part of a larger work, even commercial. It is only when you modify `chumak` source code itself that you have to make that change available. Please read the license, as this description is not complete by any means. 126 | 127 | 3. Do I have to sign over my copyright when contributing? 128 | 129 | No. Everyone owns the piece of code they contribute. 130 | Please see [Contributing](CONTRIBUTING.md) for details. 131 | 132 | 133 | ## License 134 | 135 | This project is licensed under Mozilla Public License Version 2.0. 136 | See [license](LICENSE) for complete license terms. 137 | 138 | ## Etymology 139 | 140 | From [Wikipedia](https://en.wikipedia.org/wiki/Chumak): 141 | 142 | >Chumak (Ukrainian: чумак) is a historic occupation on the territory of the modern Ukraine 143 | >as merchants or traders, primarily known for the trade in salt. 144 | 145 | ## How to publish new Hex.pm version 146 | 147 | To update a hex.pm version, you simply need to bump the version of erlang package and the github action will publish a new version on Hex.pm: 148 | 149 | 1. Adjust the version of the package in `src/chumak.app.src` 150 | 2. Commit & Push 151 | 3. Done 152 | 153 | 154 | 155 | ## How to publish to Hex.pm manually (Note: this is now superceeded by automatic github action above) 156 | 157 | This info is here for maintainers - since I keep forgetting how to do this. 158 | 159 | 1. Adjust the version of the package in `src/chumak.app.src` 160 | 2. Login to hex.pm: `rebar3 hex user auth` 161 | 3. Put in your hex.pm username and your password (ignore the warning) - enter it 2 more times! (weird) 162 | 3. Publish: `rebar3 hex publish` 163 | -------------------------------------------------------------------------------- /src/chumak_pub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Pub Pattern for Erlang 6 | %% 7 | %% This pattern implement Pub especification 8 | %% from: http://rfc.zeromq.org/spec:29/PUBSUB#toc3 9 | 10 | -module(chumak_pub). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, init/2, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, peer_subscribe/3, peer_cancel_subscribe/3, 19 | identity/1 20 | ]). 21 | 22 | -record(chumak_pub, { 23 | identity :: string(), 24 | subscriptions :: #{PeerPid::pid => [Subscription::binary()]}, 25 | xpub=false :: false | true, 26 | recv_queue=nil :: nil | {some, queue:queue()}, %% only for xpub 27 | pending_recv=nil :: nil | {from, From::term()} 28 | }). 29 | 30 | valid_peer_type(sub) -> valid; 31 | valid_peer_type(xsub) -> valid; 32 | valid_peer_type(_) -> invalid. 33 | 34 | init(Identity) -> 35 | init(Identity, []). 36 | 37 | init(Identity, Opts) -> 38 | State = #chumak_pub{ 39 | identity=Identity, 40 | subscriptions=chumak_subscriptions:new() 41 | }, 42 | {ok, apply_opts(State, Opts)}. 43 | 44 | terminate(_Reason, #chumak_pub{pending_recv=Recv}) -> 45 | case Recv of 46 | {from, From} -> gen_server:reply(From, {error, closed}); 47 | _ -> ok 48 | end, 49 | 50 | ok. 51 | 52 | identity(#chumak_pub{identity=I}) -> I. 53 | 54 | apply_opts(State, []) -> 55 | State; 56 | 57 | apply_opts(State, [xpub | Opts]) -> 58 | apply_opts(State#chumak_pub{ 59 | xpub=true, 60 | recv_queue={some, queue:new()} 61 | }, Opts). 62 | 63 | peer_flags(#chumak_pub{xpub=true}) -> 64 | {xpub, [incoming_queue]}; 65 | 66 | peer_flags(_State) -> 67 | %% pub_compatible_layer is used to allow decoder to understand old style of subscription 68 | %% in the 3.0 version of ZeroMQ. 69 | {pub, [pub_compatible_layer]}. 70 | 71 | accept_peer(State, PeerPid) -> 72 | {reply, {ok, PeerPid}, State}. 73 | 74 | peer_ready(State, _PeerPid, _Identity) -> 75 | {noreply, State}. 76 | 77 | send(State, Data, From) -> 78 | send_multipart(State, [Data], From). 79 | 80 | recv(#chumak_pub{xpub=true}=State, _From) -> 81 | {reply, {error, not_implemented_yet}, State}; 82 | 83 | recv(State, _From) -> 84 | {reply, {error, not_use}, State}. 85 | 86 | send_multipart(#chumak_pub{subscriptions=Subscriptions}=State, Multipart, _From) -> 87 | [FirstPart | _] = Multipart, 88 | PeersPids = chumak_subscriptions:match(Subscriptions, FirstPart), 89 | 90 | lists:foreach(fun (PeerPid) -> 91 | chumak_peer:send(PeerPid, Multipart) 92 | end, PeersPids), 93 | 94 | {reply, ok, State}. 95 | 96 | recv_multipart(#chumak_pub{pending_recv=nil, xpub=true, recv_queue={some, RecvQueue}}=State, From) -> 97 | case queue:out(RecvQueue) of 98 | {{value, Multipart}, NewRecvQueue} -> 99 | {reply, {ok, Multipart}, State#chumak_pub{recv_queue={some, NewRecvQueue}}}; 100 | 101 | {empty, _RecvQueue} -> 102 | {noreply, State#chumak_pub{pending_recv={from, From}}} 103 | end; 104 | 105 | recv_multipart(#chumak_pub{xpub=true}=State, _From) -> 106 | {reply, {error, efsm}, State}; 107 | 108 | recv_multipart(State, _From) -> 109 | {reply, {error, not_use}, State}. 110 | 111 | unblock(#chumak_pub{pending_recv={from, PendingRecv}}=State, _From) -> 112 | NewState = State#chumak_pub{pending_recv=nil}, 113 | gen_server:reply(PendingRecv, {error, again}), 114 | {reply, ok, NewState}; 115 | 116 | unblock(#chumak_pub{pending_recv=nil}=State, _From) -> 117 | {reply, ok, State}. 118 | 119 | peer_recv_message(State, _Message, _From) -> 120 | %% This function will never called, because use PUB not receive messages 121 | {noreply, State}. 122 | 123 | queue_ready(#chumak_pub{xpub=true}=State, _Identity, PeerPid) -> 124 | case chumak_peer:incoming_queue_out(PeerPid) of 125 | {out, Multipart} -> 126 | {noreply,handle_queue_ready(State,Multipart)}; 127 | empty -> 128 | {noreply,State}; 129 | {error,Info}-> 130 | ?LOG_WARNING("zmq queue error", #{error => send_error, reason => Info}), 131 | {noreply,State} 132 | end; 133 | 134 | queue_ready(State, _Identity, _PeerPid) -> 135 | %% This function will never called, because use PUB not receive messages 136 | {noreply, State}. 137 | 138 | peer_disconected(#chumak_pub{subscriptions=Subscriptions}=State, PeerPid) -> 139 | NewSubscriptions = chumak_subscriptions:delete(Subscriptions, PeerPid), 140 | {noreply, State#chumak_pub{subscriptions=NewSubscriptions}}. 141 | 142 | peer_subscribe(#chumak_pub{subscriptions=Subscriptions}=State, PeerPid, Subscription) -> 143 | NewSubscriptions = chumak_subscriptions:put(Subscriptions, PeerPid, Subscription), 144 | {noreply, State#chumak_pub{subscriptions=NewSubscriptions}}. 145 | 146 | peer_cancel_subscribe(#chumak_pub{subscriptions=Subscriptions}=State, PeerPid, Subscription) -> 147 | NewSubscriptions = chumak_subscriptions:delete(Subscriptions, PeerPid, Subscription), 148 | {noreply, State#chumak_pub{subscriptions=NewSubscriptions}}. 149 | 150 | handle_queue_ready(#chumak_pub{xpub=true, pending_recv=nil, recv_queue={some, RecvQueue}}=State,Data)-> 151 | %% queue ready for XPUB pattern 152 | NewRecvQueue = queue:in(Data, RecvQueue), 153 | State#chumak_pub{recv_queue={some, NewRecvQueue}}; 154 | 155 | handle_queue_ready(#chumak_pub{xpub=true, pending_recv={from, PendingRecv}}=State, Data)-> 156 | gen_server:reply(PendingRecv, {ok, Data}), 157 | State#chumak_pub{pending_recv=nil}. 158 | -------------------------------------------------------------------------------- /test/chumak_protocol_curve_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_protocol_curve_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | messages_test() -> 10 | #{public := ClientPublicKey, 11 | secret := ClientSecretKey} = chumak_curve_if:box_keypair(), 12 | #{public := ServerPublicKey, 13 | secret := ServerSecretKey} = chumak_curve_if:box_keypair(), 14 | #{public := ClientPublicTransientKey, 15 | secret := ClientSecretTransientKey} = chumak_curve_if:box_keypair(), 16 | #{public := ServerPublicTransientKey, 17 | secret := ServerSecretTransientKey} = chumak_curve_if:box_keypair(), 18 | #{public := CookiePublicKey, 19 | secret := CookieSecretKey} = chumak_curve_if:box_keypair(), 20 | ClientShortNonce = 1, 21 | ServerShortNonce = 1, 22 | ServerSecData1 = #{mechanism => curve, 23 | role => server, 24 | server_public_transient_key => 25 | ServerPublicTransientKey, 26 | server_secret_transient_key => 27 | ServerSecretTransientKey, 28 | curve_secretkey => ServerSecretKey, 29 | cookie_public_key => CookiePublicKey, 30 | cookie_secret_key => CookieSecretKey, 31 | server_nonce => ServerShortNonce}, 32 | ServerDec1 = chumak_protocol:new_decoder(ServerSecData1), 33 | ClientSecData1 = #{mechanism => curve, 34 | role => client, 35 | client_public_transient_key => 36 | ClientPublicTransientKey, 37 | client_secret_transient_key => 38 | ClientSecretTransientKey, 39 | curve_publickey => ClientPublicKey, 40 | curve_secretkey => ClientSecretKey, 41 | curve_serverkey => ServerPublicKey, 42 | client_nonce => ClientShortNonce}, 43 | ClientDec1 = chumak_protocol:new_decoder(ClientSecData1), 44 | %% Client receives greeting 45 | ServerGreeting = chumak_protocol:build_greeting_frame(true, curve), 46 | {ready, ClientDec2} = chumak_protocol:decode(ClientDec1, ServerGreeting), 47 | %% Server receives greeting 48 | ClientGreeting = chumak_protocol:build_greeting_frame(false, curve), 49 | {ready, ServerDec2} = chumak_protocol:decode(ServerDec1, ClientGreeting), 50 | 51 | %% Client sends Hello 52 | {Hello, ClientSecData2} = chumak_protocol:build_hello_frame(ClientSecData1), 53 | ClientDec3 = chumak_protocol:set_decoder_security_data(ClientDec2, 54 | ClientSecData2), 55 | {ok, ServerDec3, [{hello}]} = chumak_protocol:decode(ServerDec2, Hello), 56 | ServerSecData2 = chumak_protocol:decoder_security_data(ServerDec3), 57 | #{client_nonce := ClientNonceDecoded, 58 | client_public_transient_key := CPTKDecoded} = ServerSecData2, 59 | ?assertEqual(ClientShortNonce, ClientNonceDecoded), 60 | ?assertEqual(ClientPublicTransientKey, CPTKDecoded), 61 | 62 | %% Server sends Welcome 63 | {Welcome, ServerSecData3} = chumak_protocol:build_welcome_frame(ServerSecData2), 64 | 65 | %% Server discards transient keys 66 | ?assertMatch(#{client_public_transient_key := <<>>, 67 | server_public_transient_key := <<>>, 68 | server_secret_transient_key := <<>>}, ServerSecData3), 69 | 70 | ServerDec4 = chumak_protocol:set_decoder_security_data(ServerDec3, 71 | ServerSecData3), 72 | 73 | %% Client decodes Welcome 74 | {ok, ClientDec4, [_WelcomeRec]} = chumak_protocol:decode(ClientDec3, 75 | Welcome), 76 | ClientSecData3 = chumak_protocol:decoder_security_data(ClientDec4), 77 | #{server_public_transient_key := SPTKDecoded} = ClientSecData3, 78 | ?assertEqual(ServerPublicTransientKey, SPTKDecoded), 79 | %% Client sends Initiate 80 | {Initiate, ClientSecData4} = 81 | chumak_protocol:build_initiate_frame([], ClientSecData3), 82 | ClientDec5 = chumak_protocol:set_decoder_security_data(ClientDec4, 83 | ClientSecData4), 84 | %% Server decodes Initiate 85 | {ok, ServerDec5, [_InitiateRec]} = chumak_protocol:decode(ServerDec4, 86 | Initiate), 87 | ServerSecData4 = chumak_protocol:decoder_security_data(ServerDec5), 88 | #{client_public_permanent_key := CPPKDecoded, 89 | server_nonce := ServerNonce} = ServerSecData4, 90 | ?assertEqual(ClientPublicKey, CPPKDecoded), 91 | 92 | %% Server sends Ready 93 | {Ready, ServerSecData5} = 94 | chumak_protocol:build_ready_frame( 95 | [], ServerSecData4#{server_nonce => ServerNonce + 1}), 96 | ServerDec6 = chumak_protocol:set_decoder_security_data(ServerDec5, 97 | ServerSecData5), 98 | %% Client decodes Ready 99 | {ok, ClientDec6, [ReadyRec]} = chumak_protocol:decode(ClientDec5, 100 | Ready), 101 | ?assertEqual(element(1, ReadyRec), ready), 102 | ClientSecData5 = chumak_protocol:decoder_security_data(ClientDec6), 103 | #{server_nonce := ServerNonceDecoded} = ClientSecData5, 104 | ?assertEqual(2, ServerNonceDecoded), 105 | 106 | %% Client sends message 107 | MessageText = <<"this is a test">>, 108 | {Message, _ClientSecData6} = 109 | chumak_protocol:encode_message_multipart([MessageText], curve, 110 | ClientSecData5), 111 | %% Server receives message 112 | {ok, _, [{message, DecodedMessage, _}]} = chumak_protocol:decode(ServerDec6, Message), 113 | ?assertEqual(MessageText, DecodedMessage). 114 | -------------------------------------------------------------------------------- /src/chumak_pair.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Pair Pattern for Erlang 6 | %% 7 | %% This pattern implement Pair especification 8 | %% from: http://rfc.zeromq.org/spec:31/EXPAIR#toc3 9 | 10 | -module(chumak_pair). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, identity/1 19 | ]). 20 | 21 | -record(chumak_pair, { 22 | identity :: string(), 23 | pair_pid :: nil | pid(), 24 | pending_send :: nil | {term(), [binary()]}, 25 | pending_recv :: nil | term(), 26 | pending_recv_multipart :: nil | term(), 27 | recv_queue :: queue:queue() 28 | }). 29 | 30 | valid_peer_type(pair) -> valid; 31 | valid_peer_type(_) -> invalid. 32 | 33 | init(Identity) -> 34 | State = #chumak_pair{ 35 | identity=Identity, 36 | pair_pid=nil, 37 | pending_recv=nil, 38 | pending_recv_multipart=nil, 39 | pending_send=nil, 40 | recv_queue=queue:new() 41 | }, 42 | {ok, State}. 43 | 44 | terminate(_Reason, #chumak_pair{ 45 | pending_recv=Recv, 46 | pending_recv_multipart=RecvM}) -> 47 | 48 | case Recv of 49 | {from, From} -> gen_server:reply(From, {error, closed}); 50 | _ -> ok 51 | end, 52 | 53 | case RecvM of 54 | nil -> ok; 55 | FromM -> gen_server:reply(FromM, {error, closed}) 56 | end, 57 | 58 | ok. 59 | 60 | identity(#chumak_pair{identity=Identity}) -> Identity. 61 | 62 | peer_flags(_State) -> 63 | {pair, [incoming_queue]}. 64 | 65 | accept_peer(#chumak_pair{pair_pid=nil}=State, PeerPid) -> 66 | {reply, {ok, PeerPid}, State#chumak_pair{pair_pid=PeerPid}}; 67 | 68 | accept_peer(State, PeerPid) -> 69 | ?LOG_WARNING("zmq connect deny", #{error => already_paired}), 70 | chumak_peer:send_error(PeerPid, "This peer is already paired"), 71 | chumak_peer:close(PeerPid), 72 | {reply, {error, peer_already_paired}, State}. 73 | 74 | peer_ready(#chumak_pair{pending_send=PendingSend, pair_pid=PeerPid}=State, PeerPid, _Identity) -> 75 | case PendingSend of 76 | {From, Multipart} -> 77 | chumak_peer:send(PeerPid, Multipart, From); 78 | nil -> 79 | pass 80 | end, 81 | {noreply, State#chumak_pair{pending_send=nil}}; 82 | 83 | peer_ready(State, _PeerPid, _Identity) -> 84 | {noreply, State}. 85 | 86 | send(State, Data, From) -> 87 | send_multipart(State, [Data], From). 88 | 89 | recv(#chumak_pair{pending_recv=nil, pending_recv_multipart=nil}=State, From) -> 90 | case queue:out(State#chumak_pair.recv_queue) of 91 | {{value, Multipart}, NewRecvQueue} -> 92 | Msg = binary:list_to_bin(Multipart), 93 | {reply, {ok, Msg}, State#chumak_pair{recv_queue=NewRecvQueue}}; 94 | 95 | {empty, _RecvQueue} -> 96 | {noreply, State#chumak_pair{pending_recv=From}} 97 | end; 98 | 99 | recv(State, _From) -> 100 | {reply, {error, already_pending_recv}, State}. 101 | 102 | unblock(#chumak_pair{pending_recv=PendingRecv}=State, _From) when PendingRecv /= nil -> 103 | NewState = State#chumak_pair{pending_recv=nil}, 104 | gen_server:reply(PendingRecv, {error, again}), 105 | {reply, ok, NewState}; 106 | 107 | unblock(#chumak_pair{pending_recv=nil}=State, _From) -> 108 | {reply, ok, State}. 109 | 110 | 111 | send_multipart(#chumak_pair{pending_send=nil, pair_pid=nil}=State, Multipart, From) -> 112 | %% set send await 113 | {noreply, State#chumak_pair{pending_send={From, Multipart}}}; 114 | 115 | send_multipart(#chumak_pair{pending_send=nil, pair_pid=PeerPid}=State, Multipart, From) -> 116 | %% send message now 117 | chumak_peer:send(PeerPid, Multipart, From), 118 | {noreply, State}; 119 | 120 | send_multipart(State, _Multipart, _From) -> 121 | {reply, {error, pendind_send_already_called}, State}. 122 | 123 | recv_multipart(#chumak_pair{pending_recv=nil, pending_recv_multipart=nil}=State, From) -> 124 | case queue:out(State#chumak_pair.recv_queue) of 125 | {{value, Multipart}, NewRecvQueue} -> 126 | {reply, {ok, Multipart}, State#chumak_pair{recv_queue=NewRecvQueue}}; 127 | 128 | {empty, _RecvQueue} -> 129 | {noreply, State#chumak_pair{pending_recv_multipart=From}} 130 | end; 131 | 132 | recv_multipart(State, _From) -> 133 | {reply, {error, already_pending_recv}, State}. 134 | 135 | peer_recv_message(State, _Message, _From) -> 136 | %% This function will never called, because use PAIR use the incoming_queue parameter 137 | {noreply, State}. 138 | 139 | queue_ready( 140 | #chumak_pair{pair_pid=PeerPid, 141 | pending_recv=PendingRecv, 142 | pending_recv_multipart=PendingRecvMultiPart, 143 | recv_queue=RecvQueue}=State, _Identity, PeerPid) -> 144 | 145 | {out, Multipart} = chumak_peer:incoming_queue_out(PeerPid), 146 | 147 | NewRecvQueue = case {PendingRecv, PendingRecvMultiPart} of 148 | {nil, nil} -> 149 | queue:in(Multipart, RecvQueue); 150 | 151 | {_, nil} -> 152 | Msg = binary:list_to_bin(Multipart), 153 | gen_server:reply(PendingRecv, {ok, Msg}), 154 | RecvQueue; 155 | 156 | {nil, _}-> 157 | gen_server:reply(PendingRecvMultiPart, {ok, Multipart}), 158 | RecvQueue 159 | end, 160 | 161 | {noreply, State#chumak_pair{pending_recv=nil, pending_recv_multipart=nil, recv_queue=NewRecvQueue}}; 162 | 163 | queue_ready(State, _Identity, _PeerPid) -> 164 | {noreply, State}. 165 | 166 | peer_disconected(State, _PeerPid) -> 167 | {noreply, State#chumak_pair{pair_pid=nil}}. 168 | -------------------------------------------------------------------------------- /test/chumak_command_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_command_test). 6 | 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | decode_ping_test() -> 10 | Frame = <<4, "PING", 1, 0>>, 11 | {ok, Command, #{}} = chumak_command:decode(Frame, #{}), 12 | ?assertEqual(chumak_command:command_name(Command), ping). 13 | 14 | 15 | decode_ready_test() -> 16 | BigProp = binary:copy(<<"luke">>, 100), 17 | Frame = << 18 | 5, "READY", %% Command name "READY" 19 | 11, "Socket-Type", %% Property name "Socket-Type" 20 | 0, 0, 0, 6, "DEALER", %% Property value "DEALER" 21 | 8, "Identity", %% Property name "Identity" 22 | 0, 0, 0, 5, "HELLO", %% Property value "HELLO" 23 | 8, "resource", %% Property name "Resource" 24 | 0, 0, 0, 11, "My-Resource",%% Property value "My-Resource" 25 | 7, "X-Debug", %% Property name for application use 26 | 0, 0, 0, 8, "disabled", %% Property value for application use 27 | 5, "X-Big", %% Property name for big property test 28 | 0, 0, 1, 144, BigProp/binary %% Property value for big property test 29 | >>, 30 | {ok, Command, #{}} = chumak_command:decode(Frame, #{}), 31 | ?assertEqual(chumak_command:command_name(Command), ready), 32 | ?assertEqual(chumak_command:ready_socket_type(Command), "dealer"), 33 | ?assertEqual(chumak_command:ready_identity(Command), "HELLO"), 34 | ?assertEqual(chumak_command:ready_resource(Command), "My-Resource"), 35 | ?assertEqual(chumak_command:ready_metadata(Command), #{ 36 | "x-debug" => "disabled", 37 | "x-big" => binary_to_list(BigProp) 38 | }). 39 | 40 | decode_wrong_ready_test() -> 41 | Frame = << 42 | 5, "READY", %% Command name "READY" 43 | 110, "Wrong or corrupted" %% Wrong bytes 44 | >>, 45 | {error, wrong_ready_message} = chumak_command:decode(Frame, #{}). 46 | 47 | 48 | encode_ready_command_test() -> 49 | Frame1 = chumak_command:encode_ready(req, "", "", #{}), 50 | ?assertMatch(<< 51 | 5, "READY", 52 | 11, "Socket-Type", 53 | 0, 0, 0, 3, "REQ", 54 | _/binary 55 | >>, Frame1), 56 | 57 | Frame2 = chumak_command:encode_ready(rep, "my-name", "my-resource", #{}), 58 | ?assertEqual(Frame2, << 59 | 5, "READY", 60 | 11, "Socket-Type", 61 | 0, 0, 0, 3, "REP", 62 | 8, "Identity", 63 | 0, 0, 0, 7, "my-name", 64 | 8, "Resource", 65 | 0, 0, 0, 11, "my-resource" 66 | >>), 67 | Frame3 = chumak_command:encode_ready(rep, "my-name", "my-resource", #{"X-Host"=>"Host1"}), 68 | ?assertEqual(Frame3, << 69 | 5, "READY", 70 | 11, "Socket-Type", 71 | 0, 0, 0, 3, "REP", 72 | 8, "Identity", 73 | 0, 0, 0, 7, "my-name", 74 | 8, "Resource", 75 | 0, 0, 0, 11, "my-resource", 76 | 6, "X-Host", 77 | 0, 0, 0, 5, "Host1" 78 | >>), 79 | LargeProp = string:copies("large", 100), 80 | LargePropBin = list_to_binary(LargeProp), 81 | Frame4 = chumak_command:encode_ready(rep, "my-name", "my-resource", #{"X-Host"=>"Host1", "X-Large"=>LargeProp}), 82 | ?assertEqual(Frame4, << 83 | 5, "READY", 84 | 11, "Socket-Type", 85 | 0, 0, 0, 3, "REP", 86 | 8, "Identity", 87 | 0, 0, 0, 7, "my-name", 88 | 8, "Resource", 89 | 0, 0, 0, 11, "my-resource", 90 | 6, "X-Host", 91 | 0, 0, 0, 5, "Host1", 92 | 7, "X-Large", 93 | 0, 0, 1, 244, 94 | LargePropBin/binary>>). 95 | 96 | decode_error_test() -> 97 | Frame = <<5, "ERROR", 19 ,"Invalid socket type">>, 98 | {ok, Command, #{}} = chumak_command:decode(Frame, #{}), 99 | ?assertEqual(chumak_command:command_name(Command), error), 100 | ?assertEqual(chumak_command:error_reason(Command), "Invalid socket type"). 101 | 102 | decode_invalid_error_test() -> 103 | Frame = <<5, "ERROR", 10, "Broken">>, 104 | {error, wrong_error_message} = chumak_command:decode(Frame, #{}). 105 | 106 | encode_error_command_test() -> 107 | Frame = chumak_command:encode_error("Broken light-saber"), 108 | ?assertEqual(Frame, << 109 | 5, "ERROR", 110 | 18, "Broken light-saber" 111 | >>). 112 | 113 | encode_subscribe_command_test() -> 114 | Frame = chumak_command:encode_subscribe(<<"debug">>), 115 | ?assertEqual(Frame, << 116 | 9, "SUBSCRIBE", 117 | "debug" 118 | >>). 119 | 120 | 121 | decode_subscribe_command_test() -> 122 | Frame = <<9, "SUBSCRIBE", "warn">>, 123 | {ok, Command, #{}} = chumak_command:decode(Frame, #{}), 124 | ?assertEqual( 125 | chumak_command:subscribe_subscription(Command), 126 | <<"warn">> 127 | ). 128 | 129 | encode_cancel_command_test() -> 130 | Frame = chumak_command:encode_cancel(<<"error">>), 131 | ?assertEqual(Frame, << 132 | 6, "CANCEL", 133 | "error" 134 | >>). 135 | 136 | 137 | decode_subscribe_cancel_test() -> 138 | Frame = <<6, "CANCEL", "fatal">>, 139 | {ok, Command, #{}} = chumak_command:decode(Frame, #{}), 140 | ?assertEqual( 141 | chumak_command:cancel_subscription(Command), 142 | <<"fatal">> 143 | ). 144 | -------------------------------------------------------------------------------- /src/chumak_sub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | %% @doc ZeroMQ Sub Pattern for Erlang 6 | %% 7 | %% This pattern implement Sub especification 8 | %% from: http://rfc.zeromq.org/spec:29/PUBSUB#toc5 9 | 10 | -module(chumak_sub). 11 | -behaviour(chumak_pattern). 12 | -include_lib("kernel/include/logger.hrl"). 13 | 14 | -export([valid_peer_type/1, init/1, init/2, terminate/2, peer_flags/1, accept_peer/2, peer_ready/3, 15 | send/3, recv/2, 16 | unblock/2, 17 | send_multipart/3, recv_multipart/2, peer_recv_message/3, 18 | queue_ready/3, peer_disconected/2, subscribe/2, cancel/2, 19 | peer_reconnected/2, identity/1 20 | ]). 21 | 22 | -record(chumak_sub, { 23 | identity :: string(), 24 | topics :: list(), 25 | peers :: list(), 26 | pending_recv=nil :: nil | {from, From::term()}, 27 | pending_recv_multipart :: nil | false | true, 28 | recv_queue :: queue:queue(), 29 | xsub=false :: true | false 30 | }). 31 | 32 | valid_peer_type(pub) -> valid; 33 | valid_peer_type(xpub) -> valid; 34 | valid_peer_type(_) -> invalid. 35 | 36 | init(Identity) -> 37 | init(Identity, []). 38 | 39 | init(Identity, Opts) -> 40 | State = #chumak_sub{ 41 | identity=Identity, 42 | topics=[], 43 | peers=[], 44 | recv_queue=queue:new(), 45 | pending_recv=nil, 46 | pending_recv_multipart=nil 47 | }, 48 | {ok, apply_opts(State, Opts)}. 49 | 50 | terminate(_Reason, #chumak_sub{pending_recv=Recv}) -> 51 | case Recv of 52 | {from, From} -> gen_server:reply(From, {error, closed}); 53 | _ -> ok 54 | end, 55 | ok. 56 | 57 | apply_opts(State, []) -> 58 | State; 59 | apply_opts(State, [xsub | Opts]) -> 60 | apply_opts(State#chumak_sub{xsub=true}, Opts). 61 | 62 | identity(#chumak_sub{identity=Identity}) -> Identity. 63 | 64 | peer_flags(#chumak_sub{xsub=true}) -> 65 | {xsub, [incoming_queue]}; 66 | peer_flags(_State) -> 67 | {sub, [incoming_queue]}. 68 | 69 | accept_peer(#chumak_sub{peers=Peers}=State, PeerPid) -> 70 | NewPeers = [PeerPid | Peers], 71 | {reply, {ok, PeerPid}, State#chumak_sub{peers=NewPeers}}. 72 | 73 | peer_ready(#chumak_sub{topics=Topics}=State, PeerPid, _Identity) -> 74 | send_subscriptions(Topics, PeerPid), 75 | {noreply, State}. 76 | 77 | send(State, Data, From) -> 78 | send_multipart(State, [Data], From). 79 | 80 | recv(#chumak_sub{pending_recv=nil, recv_queue=RecvQueue}=State, From) -> 81 | case queue:out(RecvQueue) of 82 | {{value, Multipart}, NewRecvQueue} -> 83 | FullMsg = binary:list_to_bin(Multipart), 84 | {reply, {ok, FullMsg}, State#chumak_sub{recv_queue=NewRecvQueue}}; 85 | 86 | {empty, _RecvQueue} -> 87 | {noreply, State#chumak_sub{pending_recv={from, From}, pending_recv_multipart=false}} 88 | end; 89 | 90 | recv(State, _From) -> 91 | {reply, {error, efsm}, State}. 92 | 93 | unblock(#chumak_sub{pending_recv={from, PendingRecv}}=State, _From) -> 94 | NewState = State#chumak_sub{pending_recv=nil, pending_recv_multipart=nil}, 95 | gen_server:reply(PendingRecv, {error, again}), 96 | {reply, ok, NewState}; 97 | 98 | unblock(#chumak_sub{pending_recv=nil}=State, _From) -> 99 | {reply, ok, State}. 100 | 101 | send_multipart(#chumak_sub{xsub=true, peers=Peers}=State, Multipart, _From) -> 102 | lists:foreach(fun (PeerPid) -> 103 | chumak_peer:send(PeerPid, Multipart) 104 | end, Peers), 105 | {reply, ok, State}; 106 | 107 | send_multipart(State, _Multipart, _From) -> 108 | {reply, {error, not_use}, State}. 109 | 110 | recv_multipart(#chumak_sub{pending_recv=nil, recv_queue=RecvQueue}=State, From) -> 111 | case queue:out(RecvQueue) of 112 | {{value, Multipart}, NewRecvQueue} -> 113 | {reply, {ok, Multipart}, State#chumak_sub{recv_queue=NewRecvQueue}}; 114 | {empty, _RecvQueue} -> 115 | {noreply, State#chumak_sub{pending_recv={from, From}, pending_recv_multipart=true}} 116 | end; 117 | 118 | recv_multipart(State, _From) -> 119 | {reply, {error, efsm}, State}. 120 | 121 | peer_recv_message(State, _Message, _From) -> 122 | %% This function will never called, because use PUB not receive messages 123 | {noreply, State}. 124 | 125 | queue_ready(State, _Identity, PeerPid) -> 126 | case chumak_peer:incoming_queue_out(PeerPid) of 127 | {out, Multipart} -> 128 | {noreply,handle_queue_ready(State,Multipart)}; 129 | empty -> 130 | {noreply,State}; 131 | {error,Info}-> 132 | ?LOG_ERROR("zmq queue error", #{error => process, type => sub, reason => Info}), 133 | {noreply,State} 134 | end. 135 | 136 | peer_disconected(#chumak_sub{peers=Peers}=State, PeerPid) -> 137 | NewPeers = lists:delete(PeerPid, Peers), 138 | {noreply, State#chumak_sub{peers=NewPeers}}. 139 | 140 | subscribe(#chumak_sub{topics=Topics, peers=Peers}=State, Topic) -> 141 | send_subscription_to_peers(Topic, Peers), 142 | {noreply, State#chumak_sub{topics=[Topic |Topics]}}. 143 | 144 | cancel(#chumak_sub{topics=Topics, peers=Peers}=State, Topic) -> 145 | send_cancel_subscription_to_peers(Topic, Peers), 146 | NewTopics = lists:delete(Topic, Topics), 147 | {noreply, State#chumak_sub{topics=NewTopics}}. 148 | 149 | peer_reconnected(#chumak_sub{topics=Topics}=State, PeerPid) -> 150 | send_subscriptions(Topics, PeerPid), 151 | {noreply, State}. 152 | 153 | %% PRIVATE API 154 | send_subscriptions(Topics, PeerPid) -> 155 | lists:foreach(fun (Topic) -> 156 | chumak_peer:send_subscription(PeerPid, Topic) 157 | end, Topics). 158 | 159 | send_subscription_to_peers(Topic, Peers) -> 160 | lists:foreach(fun (PeerPid) -> 161 | chumak_peer:send_subscription(PeerPid, Topic) 162 | end, Peers). 163 | 164 | send_cancel_subscription_to_peers(Topic, Peers) -> 165 | lists:foreach(fun (PeerPid) -> 166 | chumak_peer:send_cancel_subscription(PeerPid, Topic) 167 | end, Peers). 168 | 169 | handle_queue_ready(#chumak_sub{recv_queue=RecvQueue, pending_recv=nil}=State,Data)-> 170 | NewRecvQueue = queue:in(Data, RecvQueue), 171 | State#chumak_sub{recv_queue=NewRecvQueue}; 172 | 173 | handle_queue_ready(#chumak_sub{pending_recv={from, PendingRecv}, 174 | pending_recv_multipart=IsPendingMultipart} = State, Data)-> 175 | case IsPendingMultipart of 176 | true -> 177 | gen_server:reply(PendingRecv, {ok, Data}); 178 | false -> 179 | FullMsg = binary:list_to_bin(Data), 180 | gen_server:reply(PendingRecv, {ok, FullMsg}) 181 | end, 182 | State#chumak_sub{pending_recv=nil, pending_recv_multipart=nil}. 183 | -------------------------------------------------------------------------------- /test/chumak_acceptance_rep_test.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_rep_test). 6 | -export([echo_req_server/1]). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | rep_single_test_() -> 10 | [ 11 | { 12 | "Should block recv until one peer send message", 13 | {setup, fun start_req/0, fun stop_req/1, fun rep_recv_and_send/1} 14 | }, 15 | { 16 | "recv and send with curve security", 17 | {setup, fun start_req_curve/0, fun stop_req/1, fun rep_recv_and_send/1} 18 | }, 19 | { 20 | "Should deny send without received a message", 21 | {setup, fun start_req/0, fun stop_req/1, fun rep_send_without_recv/1} 22 | }, 23 | { 24 | "Should deny twice recvs", 25 | {setup, fun start_req/0, fun stop_req/1, fun rep_twice_recv/1} 26 | }, 27 | { 28 | "Should bufferize while sent is not called yet", 29 | {setup, fun start_req/0, fun stop_req/1, fun rep_bufferize/1} 30 | } 31 | ]. 32 | 33 | rep_reverse_test_() -> 34 | [ 35 | { 36 | "Should connect to req peer", 37 | {setup, fun start_req_reverse/0, fun stop_req/1, fun recv_reverse/1} 38 | } 39 | ]. 40 | 41 | rep_with_dealer_test_() -> 42 | [ 43 | { 44 | "Should connect to dealer peer", 45 | {setup, fun start_req/0, fun stop_req/1, fun req_with_dealers/1} 46 | } 47 | ]. 48 | 49 | start_req() -> 50 | application:ensure_started(chumak), 51 | {ok, Socket} = chumak:socket(rep), 52 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 5655), 53 | {Socket, #{}}. 54 | 55 | start_req_curve() -> 56 | application:ensure_started(chumak), 57 | {ok, Socket} = chumak:socket(rep), 58 | #{public := ServerPK, secret := ServerSK} = chumak_curve_if:box_keypair(), 59 | #{public := ClientPK, secret := ClientSK} = chumak_curve_if:box_keypair(), 60 | ok = chumak:set_socket_option(Socket, curve_server, true), 61 | ok = chumak:set_socket_option(Socket, curve_secretkey, ServerSK), 62 | ok = chumak:set_socket_option(Socket, curve_clientkeys, any), 63 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", 5655), 64 | {Socket, #{curve_serverkey => ServerPK, 65 | curve_publickey => ClientPK, 66 | curve_secretkey => ClientSK}}. 67 | 68 | start_req_reverse() -> 69 | application:ensure_started(chumak), 70 | {ok, Socket} = chumak:socket(rep), 71 | req_server(5755), 72 | timer:sleep(50), %% wait the connection 73 | {ok, _BindProc} = chumak:connect(Socket, tcp, "127.0.0.1", 5755), 74 | {Socket, #{}}. 75 | 76 | req_server(Port) -> 77 | {ok, Socket} = chumak:socket(req), 78 | {ok, _BindProc} = chumak:bind(Socket, tcp, "127.0.0.1", Port), 79 | spawn(?MODULE, echo_req_server, [Socket]). 80 | 81 | echo_req_server(Socket) -> 82 | timer:sleep(100), %% wait for a peer connect 83 | ok = chumak:send(Socket, <<"Reverse Hello">>), 84 | {ok, Message} = chumak:recv(Socket), 85 | 86 | case Message of 87 | <<"quit">> -> 88 | quit; 89 | _ -> 90 | echo_req_server(Socket) 91 | end. 92 | 93 | stop_req({Pid, _}) -> 94 | gen_server:stop(Pid). 95 | 96 | rep_recv_and_send({SocketPid, CurveOptions})-> 97 | spy_client(CurveOptions), 98 | {ok, <<"message from client 1">>} = chumak:recv(SocketPid), 99 | ok = chumak:send(SocketPid, <<"reply from client 1">>), 100 | ReceivedData = receive 101 | {peer_recv, Data} -> Data 102 | end, 103 | [ 104 | ?_assertEqual(ReceivedData, <<"reply from client 1">>) 105 | ]. 106 | 107 | recv_reverse({SocketPid, _}) -> 108 | {ok, Message1} = chumak:recv(SocketPid), 109 | ok = chumak:send(SocketPid, <<"continue">>), 110 | {ok, Message2} = chumak:recv(SocketPid), 111 | ok = chumak:send(SocketPid, <<"quit">>), 112 | [ 113 | ?_assertEqual(Message1, <<"Reverse Hello">>), 114 | ?_assertEqual(Message2, <<"Reverse Hello">>) 115 | ]. 116 | 117 | rep_send_without_recv({SocketPid, _}) -> 118 | [ 119 | ?_assertEqual(chumak:send(SocketPid, <<"ok">>), {error, efsm}) 120 | ]. 121 | 122 | rep_twice_recv({SocketPid, CurveOptions}) -> 123 | spy_client(CurveOptions), 124 | Parent = self(), 125 | spawn_link(fun () -> 126 | timer:sleep(100), 127 | Reply = chumak:recv(SocketPid), 128 | Parent ! {recv_reply, Reply} 129 | end), 130 | {ok, ReceivedData} = chumak:recv(SocketPid), 131 | AsyncReply = receive 132 | {recv_reply, X} -> 133 | X 134 | end, 135 | [ 136 | ?_assertEqual(ReceivedData, <<"message from client 1">>), 137 | ?_assertEqual(AsyncReply, {error, efsm}) 138 | ]. 139 | 140 | rep_bufferize({SocketPid, CurveOptions}) -> 141 | spy_client(CurveOptions, <<"message 1">>), 142 | spy_client(CurveOptions, <<"message 2">>), 143 | timer:sleep(250), 144 | {ok, ReceivedData1} = chumak:recv(SocketPid), 145 | chumak:send(SocketPid, <<"ok">>), 146 | 147 | {ok, ReceivedData2} = chumak:recv(SocketPid), 148 | chumak:send(SocketPid, <<"ok">>), 149 | [ 150 | ?_assertEqual([<<"message 1">>, <<"message 2">>], lists:sort([ReceivedData1, ReceivedData2])) 151 | ]. 152 | 153 | req_with_dealers({SocketPid, _}) -> 154 | dealer_client(), 155 | 156 | {ok, ReceivedData1} = chumak:recv(SocketPid), 157 | chumak:send(SocketPid, <<"ok 1">>), 158 | 159 | {ok, ReceivedData2} = chumak:recv(SocketPid), 160 | chumak:send(SocketPid, <<"ok 2">>), 161 | 162 | RecvMsgs = receive 163 | {recv_msgs, Msgs} -> 164 | Msgs 165 | end, 166 | 167 | [ 168 | ?_assertEqual(<<"packet 1">>, ReceivedData1), 169 | ?_assertEqual(<<"packet 2">>, ReceivedData2), 170 | ?_assertEqual([[<<>>, <<"ok 1">>], [<<>>, <<"ok 2">>]], RecvMsgs) 171 | ]. 172 | 173 | spy_client(CurveOptions) -> 174 | spy_client(CurveOptions, <<"message from client 1">>). 175 | 176 | 177 | spy_client(CurveOptions, Msg) -> 178 | Parent = self(), 179 | spawn_link(fun () -> 180 | timer:sleep(100), %% wait socket to be acceptable 181 | {ok, ClientSocket} = chumak:socket(req), 182 | [chumak:set_socket_option(ClientSocket, Option, Value) 183 | || {Option, Value} <- maps:to_list(CurveOptions)], 184 | {ok, _ClientPid} = chumak:connect(ClientSocket, tcp, "127.0.0.1", 5655), 185 | ok = chumak:send(ClientSocket, Msg), 186 | {ok, Data} = chumak:recv(ClientSocket), 187 | Parent ! {peer_recv, Data} 188 | end). 189 | 190 | dealer_client() -> 191 | Parent = self(), 192 | spawn_link(fun () -> 193 | timer:sleep(100), %% wait socket to be acceptable 194 | {ok, ClientSocket} = chumak:socket(dealer), 195 | {ok, _ClientPid} = chumak:connect(ClientSocket, tcp, "127.0.0.1", 5655), 196 | ok = chumak:send_multipart(ClientSocket, [<<>>, <<"packet 1">>]), 197 | ok = chumak:send_multipart(ClientSocket, [<<>>, <<"packet 2">>]), 198 | 199 | {ok, Message1} = chumak:recv_multipart(ClientSocket), 200 | {ok, Message2} = chumak:recv_multipart(ClientSocket), 201 | 202 | Parent ! {recv_msgs, [Message1, Message2]} 203 | 204 | 205 | end). 206 | -------------------------------------------------------------------------------- /test/chumak_acceptance_xpub_with_xsub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_xpub_with_xsub). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 5587). 9 | 10 | normal_test_() -> 11 | [ 12 | { 13 | "Should deliver message for all subscribers", 14 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_without_multipart/1} 15 | } 16 | , { 17 | "Should deliver message for all subscribers using multipart", 18 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_multipart/1} 19 | } 20 | , { 21 | "Should deliver message for all subscribers that matching with pattern", 22 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_matching/1} 23 | } 24 | , { 25 | "Should resend subscriptions when reconnection occurred", 26 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_reconnect/1} 27 | } 28 | , { 29 | "Should allow to cancel and make other subscriptions", 30 | {setup, fun start/0, fun stop/1, fun cancel_and_remake_subscriptions/1} 31 | } 32 | , { 33 | "Should allow to XPUB recv messages and XSUB send messages", 34 | {setup, fun start/0, fun stop/1, fun negociate_reverse_messages/1} 35 | } 36 | ]. 37 | 38 | start() -> 39 | application:ensure_started(chumak), 40 | {ok, Socket} = chumak:socket(xpub), 41 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", ?PORT), 42 | Socket. 43 | 44 | start_worker(Identity, SubscribeTopic, Func) -> 45 | Parent = self(), 46 | spawn_link( 47 | fun () -> 48 | {ok, Socket} = chumak:socket(xsub, Identity), 49 | chumak:subscribe(Socket, SubscribeTopic), 50 | {ok, PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT), 51 | 52 | case erlang:fun_info(Func, arity) of 53 | {arity, 3} -> 54 | Func(Socket, Identity, Parent); 55 | {arity, 4} -> 56 | Func(Socket, PeerPid, Identity, Parent) 57 | end 58 | end 59 | ). 60 | 61 | stop(Pid) -> 62 | gen_server:stop(Pid). 63 | 64 | negotiate_subcriptions_without_multipart(Socket) -> 65 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 66 | {ok, Message} = chumak:recv(ClientSocket), 67 | Parent ! {recv, Identity, Message} 68 | end, 69 | start_worker("XSUB-A", <<>>, NegociateFunc), 70 | start_worker("XSUB-B", <<>>, NegociateFunc), 71 | timer:sleep(200), 72 | ok = chumak:send(Socket, <<"Ready">>), 73 | 74 | MessageA = receive 75 | {recv, "XSUB-A", MultipartA} -> 76 | MultipartA 77 | end, 78 | 79 | MessageB = receive 80 | {recv, "XSUB-B", MultipartB} -> 81 | MultipartB 82 | end, 83 | 84 | [ 85 | ?_assertEqual(MessageA, <<"Ready">>), 86 | ?_assertEqual(MessageB, <<"Ready">>) 87 | ]. 88 | 89 | negotiate_subcriptions_with_multipart(Socket) -> 90 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 91 | {ok, Message} = chumak:recv_multipart(ClientSocket), 92 | Parent ! {recv, Identity, Message} 93 | end, 94 | start_worker("XSUB-C", <<>>, NegociateFunc), 95 | start_worker("XSUB-D", <<>>, NegociateFunc), 96 | timer:sleep(200), 97 | ok = chumak:send_multipart(Socket, [<<"Ready">>, <<"OK">>]), 98 | 99 | MessageA = receive 100 | {recv, "XSUB-C", MultipartA} -> 101 | MultipartA 102 | end, 103 | 104 | MessageB = receive 105 | {recv, "XSUB-D", MultipartB} -> 106 | MultipartB 107 | end, 108 | 109 | [ 110 | ?_assertEqual(MessageA, [<<"Ready">>, <<"OK">>]), 111 | ?_assertEqual(MessageB, [<<"Ready">>, <<"OK">>]) 112 | ]. 113 | 114 | negotiate_subcriptions_with_matching(Socket) -> 115 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 116 | {ok, Message} = chumak:recv_multipart(ClientSocket), 117 | Parent ! {recv, Identity, Message} 118 | end, 119 | start_worker("XSUB-E", <<"debug">>, NegociateFunc), 120 | start_worker("XSUB-F", <<"info">>, NegociateFunc), 121 | timer:sleep(200), 122 | ok = chumak:send_multipart(Socket, [<<"debug">>,<<"DebugReady">>]), 123 | ok = chumak:send_multipart(Socket, [<<"info">>,<<"InfoReady">>]), 124 | 125 | MessageA = receive 126 | {recv, "XSUB-E", MultipartA} -> 127 | MultipartA 128 | end, 129 | 130 | MessageB = receive 131 | {recv, "XSUB-F", MultipartB} -> 132 | MultipartB 133 | end, 134 | 135 | [ 136 | ?_assertEqual([<<"debug">>, <<"DebugReady">>], MessageA), 137 | ?_assertEqual([<<"info">>, <<"InfoReady">>], MessageB) 138 | ]. 139 | 140 | negotiate_subcriptions_with_reconnect(Socket) -> 141 | NegociateFunc = fun (ClientSocket, PeerPid, Identity, Parent) -> 142 | {ok, Message} = chumak:recv_multipart(ClientSocket), 143 | Parent ! {recv, Identity, Message}, 144 | chumak_peer:reconnect(PeerPid), 145 | {ok, Message} = chumak:recv_multipart(ClientSocket), 146 | Parent ! {recv, Identity, Message} 147 | end, 148 | start_worker("XSUB-G", <<"A">>, NegociateFunc), 149 | timer:sleep(200), 150 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"Message A">>]), 151 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"Message B">>]), 152 | 153 | Message1 = receive 154 | {recv, "XSUB-G", MultipartA} -> 155 | MultipartA 156 | end, 157 | timer:sleep(300), %% waits for reconnection 158 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"Message A">>]), 159 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"Message B">>]), 160 | 161 | Message2 = receive 162 | {recv, "XSUB-G", MultipartB} -> 163 | MultipartB 164 | end, 165 | 166 | [ 167 | ?_assertEqual([<<"A">>, <<"Message A">>], Message1), 168 | ?_assertEqual([<<"A">>, <<"Message A">>], Message2) 169 | ]. 170 | 171 | cancel_and_remake_subscriptions(Socket) -> 172 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 173 | chumak:cancel(ClientSocket, <<"Z">>), 174 | chumak:subscribe(ClientSocket, <<"W">>), 175 | {ok, Message} = chumak:recv_multipart(ClientSocket), 176 | Parent ! {recv, Identity, Message} 177 | end, 178 | start_worker("XSUB-H", <<"Z">>, NegociateFunc), 179 | 180 | %% waits the negotiation 181 | timer:sleep(400), 182 | ok = chumak:send_multipart(Socket, [<<"Z">>, <<"Message Z">>]), 183 | ok = chumak:send_multipart(Socket, [<<"W">>, <<"Message W">>]), 184 | 185 | Message = receive 186 | {recv, "XSUB-H", MultipartA} -> 187 | MultipartA 188 | end, 189 | [ 190 | ?_assertEqual([<<"W">>, <<"Message W">>], Message) 191 | ]. 192 | 193 | negociate_reverse_messages(Socket) -> 194 | NegociateFunc = fun (ClientSocket, _Identity, _Parent) -> 195 | ok = chumak:send_multipart(ClientSocket, [<<"hey girl <3">>]) 196 | end, 197 | start_worker("XSUB-I", <<"A">>, NegociateFunc), 198 | {ok, [Data]} = chumak:recv_multipart(Socket), 199 | 200 | [ 201 | ?_assertEqual(<<"hey girl <3">>, Data) 202 | ]. 203 | -------------------------------------------------------------------------------- /test/chumak_acceptance_pub_with_sub.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -module(chumak_acceptance_pub_with_sub). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | -define(PORT, 5586). 9 | 10 | normal_test_() -> 11 | [ 12 | { 13 | "Should deny PUB to recv message", 14 | {setup, fun start/0, fun stop/1, fun deny_pub_to_receive/1} 15 | } 16 | , { 17 | "Should deny SUB to send message", 18 | {setup, fun start/0, fun stop/1, fun deny_sub_to_send/1} 19 | } 20 | , { 21 | "Should deliver message for all subscribers", 22 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_without_multipart/1} 23 | } 24 | , { 25 | "Should deliver message for all subscribers using multipart", 26 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_multipart/1} 27 | } 28 | , { 29 | "Should deliver message for all subscribers that matching with pattern", 30 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_matching/1} 31 | } 32 | , { 33 | "Should resend subscriptions when reconnection occurred", 34 | {setup, fun start/0, fun stop/1, fun negotiate_subcriptions_with_reconnect/1} 35 | } 36 | , { 37 | "Should allow to cancel and make other subscriptions", 38 | {setup, fun start/0, fun stop/1, fun cancel_and_remake_subscriptions/1} 39 | } 40 | ]. 41 | 42 | start() -> 43 | application:ensure_started(chumak), 44 | {ok, Socket} = chumak:socket(pub), 45 | {ok, _BindPid} = chumak:bind(Socket, tcp, "localhost", ?PORT), 46 | Socket. 47 | 48 | start_worker(Identity, SubscribeTopic, Func) -> 49 | Parent = self(), 50 | spawn_link( 51 | fun () -> 52 | {ok, Socket} = chumak:socket(sub, Identity), 53 | chumak:subscribe(Socket, SubscribeTopic), 54 | {ok, PeerPid} = chumak:connect(Socket, tcp, "localhost", ?PORT), 55 | 56 | case erlang:fun_info(Func, arity) of 57 | {arity, 3} -> 58 | Func(Socket, Identity, Parent); 59 | {arity, 4} -> 60 | Func(Socket, PeerPid, Identity, Parent) 61 | end 62 | end 63 | ). 64 | 65 | stop(Pid) -> 66 | gen_server:stop(Pid). 67 | 68 | deny_pub_to_receive(Socket) -> 69 | R1 = chumak:recv(Socket), 70 | R2 = chumak:recv_multipart(Socket), 71 | 72 | [ 73 | ?_assertEqual({error, not_use}, R1), 74 | ?_assertEqual({error, not_use}, R2) 75 | ]. 76 | 77 | deny_sub_to_send(_Socket) -> 78 | {ok, ClientSocket} = chumak:socket(sub), 79 | {ok, _PeerPid} = chumak:connect(ClientSocket, tcp, "localhost", ?PORT), 80 | 81 | R1 = chumak:send(ClientSocket, <<"oi">>), 82 | R2 = chumak:send(ClientSocket, <<"oi">>), 83 | [ 84 | ?_assertEqual({error, not_use}, R1), 85 | ?_assertEqual({error, not_use}, R2) 86 | ]. 87 | 88 | negotiate_subcriptions_without_multipart(Socket) -> 89 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 90 | {ok, Message} = chumak:recv(ClientSocket), 91 | Parent ! {recv, Identity, Message} 92 | end, 93 | start_worker("SUB-A", <<>>, NegociateFunc), 94 | start_worker("SUB-B", <<>>, NegociateFunc), 95 | timer:sleep(200), 96 | ok = chumak:send(Socket, <<"Ready">>), 97 | 98 | MessageA = receive 99 | {recv, "SUB-A", MultipartA} -> 100 | MultipartA 101 | end, 102 | 103 | MessageB = receive 104 | {recv, "SUB-B", MultipartB} -> 105 | MultipartB 106 | end, 107 | 108 | [ 109 | ?_assertEqual(MessageA, <<"Ready">>), 110 | ?_assertEqual(MessageB, <<"Ready">>) 111 | ]. 112 | 113 | negotiate_subcriptions_with_multipart(Socket) -> 114 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 115 | {ok, Message} = chumak:recv_multipart(ClientSocket), 116 | Parent ! {recv, Identity, Message} 117 | end, 118 | start_worker("SUB-C", <<>>, NegociateFunc), 119 | start_worker("SUB-D", <<>>, NegociateFunc), 120 | timer:sleep(200), 121 | ok = chumak:send_multipart(Socket, [<<"Ready">>, <<"OK">>]), 122 | 123 | MessageA = receive 124 | {recv, "SUB-C", MultipartA} -> 125 | MultipartA 126 | end, 127 | 128 | MessageB = receive 129 | {recv, "SUB-D", MultipartB} -> 130 | MultipartB 131 | end, 132 | 133 | [ 134 | ?_assertEqual(MessageA, [<<"Ready">>, <<"OK">>]), 135 | ?_assertEqual(MessageB, [<<"Ready">>, <<"OK">>]) 136 | ]. 137 | 138 | negotiate_subcriptions_with_matching(Socket) -> 139 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 140 | {ok, Message} = chumak:recv_multipart(ClientSocket), 141 | Parent ! {recv, Identity, Message} 142 | end, 143 | start_worker("SUB-E", <<"debug">>, NegociateFunc), 144 | start_worker("SUB-F", <<"info">>, NegociateFunc), 145 | timer:sleep(200), 146 | ok = chumak:send_multipart(Socket, [<<"debug">>,<<"DebugReady">>]), 147 | ok = chumak:send_multipart(Socket, [<<"info">>,<<"InfoReady">>]), 148 | 149 | MessageA = receive 150 | {recv, "SUB-E", MultipartA} -> 151 | MultipartA 152 | end, 153 | 154 | MessageB = receive 155 | {recv, "SUB-F", MultipartB} -> 156 | MultipartB 157 | end, 158 | 159 | [ 160 | ?_assertEqual([<<"debug">>, <<"DebugReady">>], MessageA), 161 | ?_assertEqual([<<"info">>, <<"InfoReady">>], MessageB) 162 | ]. 163 | 164 | negotiate_subcriptions_with_reconnect(Socket) -> 165 | NegociateFunc = fun (ClientSocket, PeerPid, Identity, Parent) -> 166 | {ok, Message} = chumak:recv_multipart(ClientSocket), 167 | Parent ! {recv, Identity, Message}, 168 | chumak_peer:reconnect(PeerPid), 169 | {ok, Message} = chumak:recv_multipart(ClientSocket), 170 | Parent ! {recv, Identity, Message} 171 | end, 172 | start_worker("SUB-G", <<"A">>, NegociateFunc), 173 | timer:sleep(200), 174 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"Message A">>]), 175 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"Message B">>]), 176 | 177 | Message1 = receive 178 | {recv, "SUB-G", MultipartA} -> 179 | MultipartA 180 | end, 181 | timer:sleep(300), %% waits for reconnection 182 | ok = chumak:send_multipart(Socket, [<<"A">>, <<"Message A">>]), 183 | ok = chumak:send_multipart(Socket, [<<"B">>, <<"Message B">>]), 184 | 185 | Message2 = receive 186 | {recv, "SUB-G", MultipartB} -> 187 | MultipartB 188 | end, 189 | 190 | [ 191 | ?_assertEqual([<<"A">>, <<"Message A">>], Message1), 192 | ?_assertEqual([<<"A">>, <<"Message A">>], Message2) 193 | ]. 194 | 195 | cancel_and_remake_subscriptions(Socket) -> 196 | NegociateFunc = fun (ClientSocket, Identity, Parent) -> 197 | chumak:cancel(ClientSocket, <<"Z">>), 198 | chumak:subscribe(ClientSocket, <<"W">>), 199 | {ok, Message} = chumak:recv_multipart(ClientSocket), 200 | Parent ! {recv, Identity, Message} 201 | end, 202 | start_worker("SUB-H", <<"Z">>, NegociateFunc), 203 | 204 | %% waits the negotiation 205 | timer:sleep(400), 206 | ok = chumak:send_multipart(Socket, [<<"Z">>, <<"Message Z">>]), 207 | ok = chumak:send_multipart(Socket, [<<"W">>, <<"Message W">>]), 208 | 209 | Message = receive 210 | {recv, "SUB-H", MultipartA} -> 211 | MultipartA 212 | end, 213 | [ 214 | ?_assertEqual([<<"W">>, <<"Message W">>], Message) 215 | ]. 216 | --------------------------------------------------------------------------------