From 8f3009715ee9ccdd7ecb54fea1244a32a29b62c0 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Tue, 3 Jul 2018 15:42:17 +0200 Subject: Initialize shard repo with code from somewhere --- .formatter.exs | 4 ++ .gitignore | 26 ++++++++++ README.md | 16 ++++++ config/config.exs | 30 +++++++++++ lib/app/chat.ex | 21 ++++++++ lib/application.ex | 33 ++++++++++++ lib/cli/cli.ex | 30 +++++++++++ lib/data/merklelist.ex | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/identity.ex | 32 ++++++++++++ lib/net/tcpconn.ex | 134 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/net/tcpserver.ex | 35 +++++++++++++ lib/web/httprouter.ex | 53 +++++++++++++++++++ mix.exs | 33 ++++++++++++ mix.lock | 9 ++++ test/conn_test.exs | 47 +++++++++++++++++ test/mkllst_test.exs | 15 ++++++ test/test_helper.exs | 1 + 17 files changed, 655 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 lib/app/chat.ex create mode 100644 lib/application.ex create mode 100644 lib/cli/cli.ex create mode 100644 lib/data/merklelist.ex create mode 100644 lib/identity.ex create mode 100644 lib/net/tcpconn.ex create mode 100644 lib/net/tcpserver.ex create mode 100644 lib/web/httprouter.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/conn_test.exs create mode 100644 test/mkllst_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed31458 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +shard-*.tar + +# vim files +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..7abb1ce --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Shard + +Tests of peer-to-peer stuff. Right now it's a little chat application with replication of message history over peers. Nothing is secure, contrary to the name of the project. + +## Installation + +Download and install [Elixir](https://elixir-lang.org/). + +``` +mix deps.get +mix compile +mix run --no-halt +``` + +P2P port is 4044 by default, can be changed by setting the `$PORT` environment variable. HTTP interface is on port `$PORT`+1000. + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..3139220 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :shard, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:shard, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/app/chat.ex b/lib/app/chat.ex new file mode 100644 index 0000000..4a56085 --- /dev/null +++ b/lib/app/chat.ex @@ -0,0 +1,21 @@ +defmodule SApp.Chat do + def send(msg) do + msgitem = {(System.os_time :seconds), + Shard.Identity.get_nickname(), + msg} + GenServer.cast(SApp.Chat.Log, {:insert, msgitem}) + + SNet.ConnSupervisor + |> DynamicSupervisor.which_children + |> Enum.each(fn {_, pid, _, _} -> GenServer.cast(pid, :init_push) end) + end + + def msg_callback({ts, nick, msg}) do + IO.puts "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601} <#{nick}> #{msg}" + end + + def msg_cmp({ts1, nick1, msg1}, {ts2, nick2, msg2}) do + SData.MerkleList.cmp_ts_str({ts1, nick1<>"|"<>msg1}, + {ts2, nick2<>"|"<>msg2}) + end +end diff --git a/lib/application.ex b/lib/application.ex new file mode 100644 index 0000000..a199e6c --- /dev/null +++ b/lib/application.ex @@ -0,0 +1,33 @@ +defmodule Shard.Application do + @moduledoc """ + Documentation for Shard. + """ + + use Application + + def start(_type, _args) do + import Supervisor.Spec, warn: false + + {listen_port, _} = Integer.parse ((System.get_env "PORT") || "4044") + + # Define workers and child supervisors to be supervised + children = [ + Shard.Identity, + + # Networking + { DynamicSupervisor, strategy: :one_for_one, name: SNet.ConnSupervisor }, + { SNet.TCPServer, listen_port }, + + # Applications & data store + { SData.MerkleList, [&SApp.Chat.msg_cmp/2, name: SApp.Chat.Log] }, + + # Web UI + Plug.Adapters.Cowboy.child_spec(:http, SWeb.HTTPRouter, [], port: listen_port + 1000) + ] + + # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Shard.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/cli/cli.ex b/lib/cli/cli.ex new file mode 100644 index 0000000..4643351 --- /dev/null +++ b/lib/cli/cli.ex @@ -0,0 +1,30 @@ +defmodule SCLI do + def run() do + str = "say: " |> IO.gets |> String.trim + cond do + str == "/quit" -> + nil + String.slice(str, 0..0) == "/" -> + command = str |> String.slice(1..-1) |> String.split(" ") + handle_command(command) + run() + true -> + SApp.Chat.send(str) + run() + end + end + + def handle_command(["connect", ipstr, portstr]) do + {:ok, ip} = :inet.parse_address (to_charlist ipstr) + {port, _} = Integer.parse portstr + SNet.TCPServer.add_peer(ip, port) + end + + def handle_command(["nick", nick]) do + Shard.Identity.set_nickname nick + end + + def handle_command(_cmd) do + IO.puts "Invalid command" + end +end diff --git a/lib/data/merklelist.ex b/lib/data/merklelist.ex new file mode 100644 index 0000000..c9e27f6 --- /dev/null +++ b/lib/data/merklelist.ex @@ -0,0 +1,136 @@ +defmodule SData.MerkleList do + use GenServer + + def start_link([cmp, name: name]) do + GenServer.start_link(__MODULE__, cmp, [name: name]) + end + + defp term_hash(term) do + :crypto.hash(:sha256, (:erlang.term_to_binary term)) + end + + @doc """ + Initialize a Merkle List storage. + `cmp` is a function that compares stored items and provides a total order. + + It must return: + - `:after` if the first argument is more recent + - `:duplicate` if the two items are the same + - `:before` if the first argument is older + """ + def init(cmp) do + root_item = :root + root_hash = term_hash root_item + state = %{ + root: root_hash, + top: root_hash, + cmp: cmp, + store: %{ root_hash => root_item } + } + {:ok, state} + end + + defp state_push(item, state) do + new_item = {item, state.top} + new_item_hash = term_hash new_item + new_store = Map.put(state.store, new_item_hash, new_item) + %{ state | :top => new_item_hash, :store => new_store } + end + + defp state_pop(state) do + if state.top == state.root do + :error + else + {item, next} = Map.get(state.store, state.top) + new_store = Map.delete(state.store, state.top) + new_state = %{ state | :top => next, :store => new_store } + {:ok, item, new_state} + end + end + + defp insert_many(state, [], _callback) do + state + end + + defp insert_many(state, [item | rest], callback) do + case state_pop(state) do + :error -> + new_state = state_push(item, insert_many(state, rest, callback)) + callback.(item) + new_state + {:ok, front, state_rest} -> + case state.cmp.(item, front) do + :after -> + new_state = state_push(item, insert_many(state, rest, callback)) + callback.(item) + new_state + :duplicate -> insert_many(state, rest, callback) + :before -> state_push(front, insert_many(state_rest, [item | rest], callback)) + end + end + end + + def handle_cast({:insert, item}, state) do + handle_cast({:insert_many, [item]}, state) + end + + def handle_cast({:insert_many, items}, state) do + handle_cast({:insert_many, items, fn _ -> nil end}, state) + end + + def handle_cast({:insert_many, items, callback}, state) do + items_sorted = Enum.sort(items, fn (x, y) -> state.cmp.(x, y) == :after end) + new_state = insert_many(state, items_sorted, callback) + {:noreply, new_state} + end + + def handle_call({:read, qbegin, qlimit}, _from, state) do + begin = qbegin || state.top + limit = qlimit || 20 + items = get_items_list(state, begin, limit) + {:reply, items, state} + end + + def handle_call(:top, _from, state) do + {:reply, state.top, state} + end + + def handle_call(:root, _from, state) do + {:reply, state.root, state} + end + + def handle_call({:has, hash}, _from, state) do + {:reply, Map.has_key?(state.store, hash), state} + end + + defp get_items_list(state, begin, limit) do + case limit do + 0 -> {:ok, [], begin} + _ -> + case Map.fetch(state.store, begin) do + {:ok, :root} -> + {:ok, [], nil } + {:ok, {item, next}} -> + case get_items_list(state, next, limit - 1) do + {:ok, rest, past} -> + {:ok, [ item | rest ], past } + {:error, reason} -> {:error, reason} + end + :error -> {:error, begin} + end + end + end + + @doc """ + Compare function for timestamped strings + """ + def cmp_ts_str({ts1, str1}, {ts2, str2}) do + cond do + ts1 > ts2 -> :after + ts1 < ts2 -> :before + str1 == str2 -> :duplicate + str1 > str2 -> :after + str1 < str2 -> :before + end + end +end diff --git a/lib/identity.ex b/lib/identity.ex new file mode 100644 index 0000000..229d6c7 --- /dev/null +++ b/lib/identity.ex @@ -0,0 +1,32 @@ +defmodule Shard.Identity do + use Agent + require Salty.Sign.Ed25519, as: Sign + + def start_link(_) do + Agent.start_link(__MODULE__, :init, [], name: __MODULE__) + end + + def init() do + {:ok, pk, sk} = Sign.keypair + nick_suffix = pk + |> binary_part(0, 3) + |> Base.encode16 + |> String.downcase + %{ + keypair: {pk, sk}, + nickname: "Anon" <> nick_suffix, + } + end + + def get_keypair() do + Agent.get(__MODULE__, &(&1.keypair)) + end + + def get_nickname() do + Agent.get(__MODULE__, &(&1.nickname)) + end + + def set_nickname(newnick) do + Agent.update(__MODULE__, &(%{&1 | nickname: newnick})) + end +end diff --git a/lib/net/tcpconn.ex b/lib/net/tcpconn.ex new file mode 100644 index 0000000..5d6c912 --- /dev/null +++ b/lib/net/tcpconn.ex @@ -0,0 +1,134 @@ +defmodule SNet.TCPConn do + use GenServer, restart: :temporary + require Salty.Box.Curve25519xchacha20poly1305, as: Box + require Salty.Sign.Ed25519, as: Sign + require Logger + + def start_link(state) do + GenServer.start_link(__MODULE__, state) + end + + def init(state) do + GenServer.cast(self(), :handshake) + {:ok, state} + end + + def handle_call(:get_host_str, _from, state) do + {:reply, "#{state.his_pkey|>Base.encode16|>String.downcase}@#{to_string(:inet_parse.ntoa(state.addr))}:#{state.port}", state} + end + + def handle_cast(:handshake, state) do + socket = state.socket + + {srv_pkey, srv_skey} = Shard.Identity.get_keypair + {:ok, sess_pkey, sess_skey} = Box.keypair + {:ok, challenge} = Salty.Random.buf 32 + + # Exchange public keys and challenge + :gen_tcp.send(socket, srv_pkey <> sess_pkey <> challenge) + {:ok, pkt} = :gen_tcp.recv(socket, 0) + cli_pkey = binary_part(pkt, 0, Sign.publickeybytes) + cli_sess_pkey = binary_part(pkt, Sign.publickeybytes, Box.publickeybytes) + cli_challenge = binary_part(pkt, Sign.publickeybytes + Box.publickeybytes, 32) + + # Do challenge and check their challenge + {:ok, cli_challenge_sign} = Sign.sign_detached(cli_challenge, srv_skey) + pkt = encode_pkt(cli_challenge_sign, cli_sess_pkey, sess_skey) + :gen_tcp.send(socket, pkt) + + {:ok, pkt} = :gen_tcp.recv(socket, 0) + challenge_sign = decode_pkt(pkt, cli_sess_pkey, sess_skey) + :ok = Sign.verify_detached(challenge_sign, challenge, cli_pkey) + + # Connected + :inet.setopts(socket, [active: true]) + + {:ok, {addr, port}} = :inet.peername socket + state =%{ socket: socket, + my_pkey: srv_pkey, + my_skey: srv_skey, + his_pkey: cli_pkey, + conn_my_pkey: sess_pkey, + conn_my_skey: sess_skey, + conn_his_pkey: cli_sess_pkey, + addr: addr, + port: port + } + Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" + + GenServer.cast(self(), :init_push) + + {:noreply, state} + end + + def handle_cast({:send_msg, msg}, state) do + send_msg(state, msg) + {:noreply, state} + end + + def handle_cast(:init_push, state) do + push_messages(state, nil, 10) + {:noreply, state} + end + + defp encode_pkt(pkt, pk, sk) do + {:ok, n} = Salty.Random.buf Box.noncebytes + {:ok, msg} = Box.easy(pkt, n, pk, sk) + n <> msg + end + + defp decode_pkt(pkt, pk, sk) do + n = binary_part(pkt, 0, Box.noncebytes) + enc = binary_part(pkt, Box.noncebytes, (byte_size pkt) - Box.noncebytes) + {:ok, msg} = Box.open_easy(enc, n, pk, sk) + msg + end + + defp send_msg(state, msg) do + msgbin = :erlang.term_to_binary msg + enc = encode_pkt(msgbin, state.conn_his_pkey, state.conn_my_skey) + :gen_tcp.send(state.socket, enc) + end + + def handle_info({:tcp, _socket, raw_data}, state) do + msg = decode_pkt(raw_data, state.conn_his_pkey, state.conn_my_skey) + handle_packet(:erlang.binary_to_term(msg, [:safe]), state) + {:noreply, state} + end + + def handle_info({:tcp_closed, _socket}, state) do + Logger.info "Disconnected: #{print_id state} at #{inspect state.addr}:#{state.port}" + exit(:normal) + end + + defp push_messages(state, start, num) do + case GenServer.call(SApp.Chat.Log, {:read, start, num}) do + {:ok, list, rest} -> + send_msg(state, {:info, start, list, rest}) + _ -> nil + end + end + + defp handle_packet(msg, state) do + # Logger.info "Message: #{inspect msg}" + case msg do + :get_top -> push_messages(state, nil, 10) + {:get, start} -> push_messages(state, start, 20) + {:info, _start, list, rest} -> + if rest != nil and not GenServer.call(SApp.Chat.Log, {:has, rest}) do + send_msg(state, {:get, rest}) + end + spawn_link(fn -> + Process.sleep 1000 + GenServer.cast(SApp.Chat.Log, {:insert_many, list, &SApp.Chat.msg_callback/1}) + end) + end + end + + defp print_id(state) do + state.his_pkey + |> binary_part(0, 8) + |> Base.encode16 + |> String.downcase + end +end diff --git a/lib/net/tcpserver.ex b/lib/net/tcpserver.ex new file mode 100644 index 0000000..e5ee996 --- /dev/null +++ b/lib/net/tcpserver.ex @@ -0,0 +1,35 @@ +defmodule SNet.TCPServer do + require Logger + use Task, restart: :permanent + + def start_link(port) do + Task.start_link(__MODULE__, :accept, [port]) + end + + + @doc """ + Starts accepting connections on the given `port`. + """ + def accept(port) do + {:ok, socket} = :gen_tcp.listen(port, + [:binary, packet: 2, active: false, reuseaddr: true]) + Logger.info "Accepting connections on port #{port}" + loop_acceptor(socket) + end + + defp loop_acceptor(socket) do + {:ok, client} = :gen_tcp.accept(socket) + {:ok, pid} = DynamicSupervisor.start_child(SNet.ConnSupervisor, {SNet.TCPConn, %{socket: client}}) + :ok = :gen_tcp.controlling_process(client, pid) + loop_acceptor(socket) + end + + def add_peer(ip, port) do + {:ok, client} = :gen_tcp.connect(ip, port, [:binary, packet: 2, active: false]) + {:ok, pid} = DynamicSupervisor.start_child(SNet.ConnSupervisor, {SNet.TCPConn, %{socket: client}}) + :ok = :gen_tcp.controlling_process(client, pid) + pid + end + +end + diff --git a/lib/web/httprouter.ex b/lib/web/httprouter.ex new file mode 100644 index 0000000..5027df4 --- /dev/null +++ b/lib/web/httprouter.ex @@ -0,0 +1,53 @@ +defmodule SWeb.HTTPRouter do + use Plug.Router + use Plug.ErrorHandler + + plug Plug.Parsers, parsers: [:urlencoded, :multipart] + + plug :match + plug :dispatch + + get "/" do + main_page(conn) + end + + post "/" do + if Map.has_key?(conn.params, "msg") do + SApp.Chat.send(conn.params["msg"]) + end + if Map.has_key?(conn.params, "nick") do + Shard.Identity.set_nickname(conn.params["nick"]) + end + if Map.has_key?(conn.params, "peer") do + [ipstr, portstr] = String.split(conn.params["peer"], ":") + {:ok, ip} = :inet.parse_address (to_charlist ipstr) + {port, _} = Integer.parse portstr + SNet.TCPServer.add_peer(ip, port) + end + + main_page(conn) + end + + match _ do + send_resp(conn, 404, "Oops!") + end + + def main_page(conn) do + {:ok, messages, _} = GenServer.call(SApp.Chat.Log, {:read, nil, 42}) + + msgtxt = messages + |> Enum.map(fn {ts, nick, msg} -> "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601} <#{nick}> #{msg}\n" end) + + peerlist = SNet.ConnSupervisor + |> DynamicSupervisor.which_children + |> Enum.map(fn {_, pid, _, _} -> "#{GenServer.call(pid, :get_host_str)}\n" end) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, "
#{msgtxt}
" <> + "
" <> + "
" <> + "
#{peerlist}
" <> + "
") + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..3486509 --- /dev/null +++ b/mix.exs @@ -0,0 +1,33 @@ +defmodule Shard.MixProject do + use Mix.Project + + def project do + [ + app: :shard, + version: "0.1.0", + elixir: "~> 1.6", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {Shard.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, + {:cowboy, "~> 1.1.2"}, + {:plug, "~> 1.3.4"}, + {:salty, "~> 0.1.3", hex: :libsalty} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..110ecd7 --- /dev/null +++ b/mix.lock @@ -0,0 +1,9 @@ +%{ + "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, + "plug": {:hex, :plug, "1.3.6", "bcdf94ac0f4bc3b804bdbdbde37ebf598bd7ed2bfa5106ed1ab5984a09b7e75f", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, + "salty": {:hex, :libsalty, "0.1.3", "13332eb13ac995f5deb76903b44f96f740e1e3a6e511222bffdd8b42cd079ffb", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, +} diff --git a/test/conn_test.exs b/test/conn_test.exs new file mode 100644 index 0000000..48c92f8 --- /dev/null +++ b/test/conn_test.exs @@ -0,0 +1,47 @@ +defmodule ShardTest.Conn do + use ExUnit.Case + doctest Shard.Application + + require Salty.Box.Curve25519xchacha20poly1305, as: Box + require Salty.Sign.Ed25519, as: Sign + + test "the truth" do + assert 1 + 1 == 2 + end + + test "crypto connection" do + {:ok, srv_pkey, srv_skey} = Sign.keypair + {:ok, sess_pkey, sess_skey} = Box.keypair + {:ok, challenge} = Salty.Random.buf 32 + {:ok, socket} = :gen_tcp.connect {127,0,0,1}, 4044, [:binary, packet: 2, active: false] + + :gen_tcp.send(socket, srv_pkey <> sess_pkey <> challenge) + {:ok, pkt} = :gen_tcp.recv(socket, 0) + cli_pkey = binary_part(pkt, 0, Sign.publickeybytes) + cli_sess_pkey = binary_part(pkt, Sign.publickeybytes, Box.publickeybytes) + cli_challenge = binary_part(pkt, Sign.publickeybytes + Box.publickeybytes, 32) + + {:ok, cli_challenge_sign} = Sign.sign_detached(cli_challenge, srv_skey) + sendmsg(socket, cli_challenge_sign, cli_sess_pkey, sess_skey) + + challenge_sign = recvmsg(socket, cli_sess_pkey, sess_skey) + :ok = Sign.verify_detached(challenge_sign, challenge, cli_pkey) + + pkt = :erlang.binary_to_term(recvmsg(socket, cli_sess_pkey, sess_skey), [:safe]) + IO.puts (inspect pkt) + end + + defp sendmsg(sock, msg, pk, sk) do + {:ok, n} = Salty.Random.buf Box.noncebytes + {:ok, msg} = Box.easy(msg, n, pk, sk) + :gen_tcp.send(sock, n <> msg) + end + + defp recvmsg(sock, pk, sk) do + {:ok, pkt} = :gen_tcp.recv(sock, 0) + n = binary_part(pkt, 0, Box.noncebytes) + enc = binary_part(pkt, Box.noncebytes, (byte_size pkt) - Box.noncebytes) + {:ok, msg} = Box.open_easy(enc, n, pk, sk) + msg + end +end diff --git a/test/mkllst_test.exs b/test/mkllst_test.exs new file mode 100644 index 0000000..7d1fadf --- /dev/null +++ b/test/mkllst_test.exs @@ -0,0 +1,15 @@ +defmodule ShardTest.MklLst do + use ExUnit.Case + doctest Shard.Application + + test "merkle list" do + {:ok, pid} = GenServer.start(SData.MerkleList, &SData.MerkleList.cmp_ts_str/2) + + {:ok, list, rt} = GenServer.call(pid, {:read, nil, nil}) + assert list == [] + assert rt == nil + + GenServer.cast(pid, {:insert, {12, "aa, bb"}}) + GenServer.cast(pid, {:insert_many, [{14, "qwerty"}, {8, "haha"}]}) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() -- cgit v1.2.3