diff --git a/README.md b/README.md
index 4847b981..8734538b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
This package contains an implementation of [Thrift](https://thrift.apache.org/)
for Elixir. It includes a Thrift IDL parser, an Elixir code generator, and
-binary framed client and server implementations.
+binary protocol clients and servers with framed and buffered transports.
The generated serialization code is highly optimized and has been measured at
**10 and 25 times faster**[why?](#why-is-it-faster-than-the-apache-implementation)
@@ -50,6 +50,23 @@ defmodule MyProject.Mixfile do
end
```
+## Buffered Transport
+
+Generated services also provide `Binary.Buffered.Client` and
+`Binary.Buffered.Server`, with the same RPC methods and connection options
+as their framed counterparts:
+
+```elixir
+{:ok, server} = MyService.Binary.Buffered.Server.start_link(MyHandler, 2345)
+{:ok, client} = MyService.Binary.Buffered.Client.start_link("localhost", 2345)
+```
+
+Both endpoints must use the same transport. Buffered transport sends binary
+protocol messages without frame-length prefixes, writing each RPC in one send.
+It accepts versioned and legacy headers and handles messages split across TCP
+reads. Incoming messages are limited to 16 MiB and 64 levels of nesting.
+Regenerate existing service code to obtain the buffered modules.
+
## RPC Service Support
We provide full client and server support for Thrift RPC services. The examples
diff --git a/lib/thrift/binary/buffered/client.ex b/lib/thrift/binary/buffered/client.ex
new file mode 100644
index 00000000..af428530
--- /dev/null
+++ b/lib/thrift/binary/buffered/client.ex
@@ -0,0 +1,20 @@
+defmodule Thrift.Binary.Buffered.Client do
+ @moduledoc """
+ Binary protocol client using buffered (unframed) transport.
+
+ Writes each RPC in one send and reads a complete binary protocol message.
+ Incoming messages are limited to 16 MiB and 64 levels of nesting.
+
+ Options are the same as `Thrift.Binary.Framed.Client.start_link/3`.
+ Generated services expose this as `YourService.Binary.Buffered.Client`.
+ """
+ alias Thrift.Binary.Framed.Client
+
+ def start_link(host, port, opts \\ []),
+ do: Client.start_link(host, port, Keyword.put(opts, :packet, 0))
+
+ defdelegate close(conn), to: Client
+ defdelegate connect(conn, opts), to: Client
+ defdelegate call(conn, name, args, module, opts), to: Client
+ defdelegate oneway(conn, name, args, opts), to: Client
+end
diff --git a/lib/thrift/binary/buffered/message.ex b/lib/thrift/binary/buffered/message.ex
new file mode 100644
index 00000000..3f0cda08
--- /dev/null
+++ b/lib/thrift/binary/buffered/message.ex
@@ -0,0 +1,102 @@
+defmodule Thrift.Binary.Buffered.Message do
+ @moduledoc false
+
+ # Exact reads leave subsequent messages in the socket buffer. Bound both
+ # message size and nesting, and use one deadline for the entire message.
+ @max_size 16 * 1024 * 1024
+ @max_depth 64
+
+ def recv(transport, socket, timeout) do
+ deadline = if timeout == :infinity, do: :infinity, else: now() + timeout
+ state = {transport, socket, deadline, [], 0}
+
+ try do
+ {header, state} = read(state, 4)
+
+ state =
+ case header do
+ <<0x80, 1, 0, type>> when type in 1..4 ->
+ state = value(11, state, 0)
+ elem(read(state, 4), 1)
+
+ <> when size >= 0 ->
+ {_, state} = read(state, size)
+ {<>, state} = read(state, 1)
+ if type not in 1..4, do: throw({:error, :invalid_message})
+ elem(read(state, 4), 1)
+
+ _ ->
+ throw({:error, :invalid_message})
+ end
+
+ {_, _, _, chunks, _} = value(12, state, 0)
+ {:ok, chunks |> Enum.reverse() |> IO.iodata_to_binary()}
+ catch
+ {:error, _} = error -> error
+ end
+ end
+
+ defp value(_, _, depth) when depth > @max_depth, do: throw({:error, :invalid_message})
+ defp value(type, state, _) when type in [2, 3], do: elem(read(state, 1), 1)
+ defp value(6, state, _), do: elem(read(state, 2), 1)
+ defp value(8, state, _), do: elem(read(state, 4), 1)
+ defp value(type, state, _) when type in [4, 10], do: elem(read(state, 8), 1)
+
+ defp value(11, state, _) do
+ {size, state} = count(state)
+ elem(read(state, size), 1)
+ end
+
+ defp value(12, state, depth) do
+ case read(state, 1) do
+ {<<0>>, state} ->
+ state
+
+ {<>, state} ->
+ {_, state} = read(state, 2)
+ state = value(type, state, depth + 1)
+ value(12, state, depth)
+ end
+ end
+
+ defp value(13, state, depth) do
+ {<>, state} = read(state, 2)
+ {size, state} = count(state)
+ repeat(size, [key, val], state, depth + 1)
+ end
+
+ defp value(type, state, depth) when type in [14, 15] do
+ {<>, state} = read(state, 1)
+ {size, state} = count(state)
+ repeat(size, [element], state, depth + 1)
+ end
+
+ defp value(_, _, _), do: throw({:error, :invalid_message})
+
+ defp repeat(0, _, state, _), do: state
+
+ defp repeat(count, types, state, depth) do
+ state = Enum.reduce(types, state, &value(&1, &2, depth))
+ repeat(count - 1, types, state, depth)
+ end
+
+ defp count(state) do
+ {<>, state} = read(state, 4)
+ if size < 0 or size > @max_size, do: throw({:error, :invalid_message})
+ {size, state}
+ end
+
+ defp read(state, 0), do: {<<>>, state}
+
+ defp read({transport, socket, deadline, chunks, total}, size) do
+ if total + size > @max_size, do: throw({:error, :message_too_large})
+ timeout = if deadline == :infinity, do: :infinity, else: max(deadline - now(), 0)
+
+ case transport.recv(socket, size, timeout) do
+ {:ok, bytes} -> {bytes, {transport, socket, deadline, [bytes | chunks], total + size}}
+ {:error, _} = error -> throw(error)
+ end
+ end
+
+ defp now, do: System.monotonic_time(:millisecond)
+end
diff --git a/lib/thrift/binary/buffered/server.ex b/lib/thrift/binary/buffered/server.ex
new file mode 100644
index 00000000..f731ad38
--- /dev/null
+++ b/lib/thrift/binary/buffered/server.ex
@@ -0,0 +1,16 @@
+defmodule Thrift.Binary.Buffered.Server do
+ @moduledoc """
+ Binary protocol server using buffered (unframed) transport.
+
+ Incoming messages are limited to 16 MiB and 64 levels of nesting.
+
+ Options are the same as `Thrift.Binary.Framed.Server.start_link/4`.
+ Generated services expose this as `YourService.Binary.Buffered.Server`.
+ """
+ alias Thrift.Binary.Framed.Server
+
+ def start_link(server_module, port, handler_module, opts \\ []),
+ do: Server.start_link(server_module, port, handler_module, Keyword.put(opts, :packet, 0))
+
+ defdelegate stop(pid), to: Server
+end
diff --git a/lib/thrift/binary/framed/client.ex b/lib/thrift/binary/framed/client.ex
index 18be648b..8a4db23b 100644
--- a/lib/thrift/binary/framed/client.ex
+++ b/lib/thrift/binary/framed/client.ex
@@ -55,7 +55,8 @@ defmodule Thrift.Binary.Framed.Client do
ssl_opts: [SSL.option()],
timeout: integer,
sock: {:gen_tcp, :gen_tcp.socket()} | {:ssl, :ssl.sslsocket()},
- seq_id: integer
+ seq_id: integer,
+ packet: 0 | 4
}
defstruct host: nil,
port: nil,
@@ -64,7 +65,8 @@ defmodule Thrift.Binary.Framed.Client do
ssl_opts: nil,
timeout: 5000,
sock: nil,
- seq_id: 0
+ seq_id: 0,
+ packet: 4
end
require Logger
@@ -82,7 +84,8 @@ defmodule Thrift.Binary.Framed.Client do
port: port,
tcp_opts: tcp_opts,
ssl_opts: ssl_opts,
- timeout: timeout
+ timeout: timeout,
+ packet: Keyword.get(opts, :packet, 4)
}
{:connect, :init, s}
@@ -141,6 +144,7 @@ defmodule Thrift.Binary.Framed.Client do
opts =
opts
|> Keyword.merge(@immutable_tcp_opts)
+ |> Keyword.put(:packet, s.packet)
|> Keyword.put_new(:send_timeout, 1000)
case :gen_tcp.connect(host, port, opts, timeout) do
@@ -253,7 +257,7 @@ defmodule Thrift.Binary.Framed.Client do
timeout = Keyword.get(tcp_opts, :timeout, default_timeout)
with :ok <- transport.send(sock, [message | serialized_args]),
- {:ok, message} <- transport.recv(sock, 0, timeout) do
+ {:ok, message} <- receive_message(transport, sock, timeout, s.packet) do
reply = deserialize_message_reply(message, rpc_name, seq_id)
{:reply, reply, s}
else
@@ -291,6 +295,12 @@ defmodule Thrift.Binary.Framed.Client do
end
end
+ defp receive_message(transport, socket, timeout, 0),
+ do: Thrift.Binary.Buffered.Message.recv(transport, socket, timeout)
+
+ defp receive_message(transport, socket, timeout, 4),
+ do: transport.recv(socket, 0, timeout)
+
def deserialize_message_reply(message, rpc_name, seq_id) do
handle_message(Binary.deserialize(:message_begin, message), seq_id, rpc_name)
end
@@ -333,9 +343,7 @@ defmodule Thrift.Binary.Framed.Client do
TApplicationException.exception(
type: :bad_sequence_id,
message:
- "Invalid sequence id. The client sent #{seq_id}, but the server replied with #{
- mismatched_seq_id
- }"
+ "Invalid sequence id. The client sent #{seq_id}, but the server replied with #{mismatched_seq_id}"
)
{:error, {:exception, exception}}
diff --git a/lib/thrift/binary/framed/protocol_handler.ex b/lib/thrift/binary/framed/protocol_handler.ex
index 839485bd..effa587f 100644
--- a/lib/thrift/binary/framed/protocol_handler.ex
+++ b/lib/thrift/binary/framed/protocol_handler.ex
@@ -25,7 +25,8 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
:server_module,
:handler_module,
:on_connect,
- :recv_timeout
+ :recv_timeout,
+ :packet
]
@spec start_link(reference, port, transport, {module, module, transport_opts, [SSL.option()]}) ::
@@ -60,7 +61,8 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
:ok = :ranch.accept_ack(ref)
{recv_timeout, tcp_opts} = Keyword.pop(tcp_opts, :recv_timeout, @default_timeout)
- :ok = :inet.setopts(socket, Keyword.put(tcp_opts, :packet, 4))
+ packet = Keyword.get(tcp_opts, :packet, 4)
+ :ok = :inet.setopts(socket, tcp_opts)
state = %__MODULE__{
transport: :gen_tcp,
@@ -68,7 +70,8 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
server_module: server_module,
handler_module: handler_module,
on_connect: on_connect,
- recv_timeout: recv_timeout
+ recv_timeout: recv_timeout,
+ packet: packet
}
case SSL.configuration(ssl_opts) do
@@ -97,7 +100,7 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
with :ok <- :inet.setopts(socket, packet: :raw),
{:ok, first_bytes} <- :gen_tcp.recv(socket, 4, recv_timeout),
:ok <- :gen_tcp.unrecv(socket, first_bytes),
- :ok <- :inet.setopts(socket, packet: 4) do
+ :ok <- :inet.setopts(socket, packet: state.packet) do
<> = first_bytes
{:ok, first_byte}
end
@@ -114,7 +117,7 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
finish_span(span, result: "ssl_rejected")
close(state)
- {:ok, @tcp_header_byte} when optional == :required ->
+ {:ok, byte} when byte in [@tcp_header_byte, 0x80] and optional == :required ->
Logger.error(fn ->
format_log("peek_first_byte", "SSL required", state)
end)
@@ -122,7 +125,7 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
finish_span(span, result: "tcp_rejected")
close(state)
- {:ok, @tcp_header_byte} when optional in [:disabled, :optional] ->
+ {:ok, byte} when byte in [@tcp_header_byte, 0x80] and optional in [:disabled, :optional] ->
finish_span(span, result: "tcp")
on_connect(state)
@@ -200,7 +203,7 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
) do
span = start_span(:receive_message, state)
- case transport.recv(socket, 0, recv_timeout) do
+ case receive_packet(transport, socket, recv_timeout, state.packet) do
{:ok, serialized_message} ->
finish_span(span, result: "success")
deserialize_message(serialized_message, state)
@@ -219,6 +222,12 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
end
end
+ defp receive_packet(transport, socket, timeout, 0),
+ do: Thrift.Binary.Buffered.Message.recv(transport, socket, timeout)
+
+ defp receive_packet(transport, socket, timeout, 4),
+ do: transport.recv(socket, 0, timeout)
+
defp deserialize_message(serialized_message, state) do
case Binary.deserialize(:message_begin, serialized_message) do
{:ok, message} ->
@@ -293,7 +302,11 @@ defmodule Thrift.Binary.Framed.ProtocolHandler do
server_module.handle_thrift(method_name, args_binary, handler_module)
end)
- send_reply(<<0>>, :continue, state)
+ if state.packet == 0 do
+ receive_message(state)
+ else
+ send_reply(<<0>>, :continue, state)
+ end
end
defp send_reply(data, after_reply, %__MODULE__{transport: transport, socket: socket} = state) do
diff --git a/lib/thrift/binary/framed/server.ex b/lib/thrift/binary/framed/server.ex
index 7195276d..d3625993 100644
--- a/lib/thrift/binary/framed/server.ex
+++ b/lib/thrift/binary/framed/server.ex
@@ -47,7 +47,10 @@ defmodule Thrift.Binary.Framed.Server do
def start_link(server_module, port, handler_module, opts) do
name = Keyword.get(opts, :name, handler_module)
worker_count = Keyword.get(opts, :worker_count, 1)
- tcp_opts = Keyword.get(opts, :tcp_opts, [])
+
+ tcp_opts =
+ Keyword.put(Keyword.get(opts, :tcp_opts, []), :packet, Keyword.get(opts, :packet, 4))
+
ssl_opts = Keyword.get(opts, :ssl_opts, [])
on_connect = Keyword.get(opts, :on_connect)
diff --git a/lib/thrift/generator/binary/framed/client.ex b/lib/thrift/generator/binary/framed/client.ex
index 32dcb6ed..60a81875 100644
--- a/lib/thrift/generator/binary/framed/client.ex
+++ b/lib/thrift/generator/binary/framed/client.ex
@@ -4,18 +4,21 @@ defmodule Thrift.Generator.Binary.Framed.Client do
alias Thrift.AST.Function
alias Thrift.Generator.{Service, Utils}
- def generate(service) do
+ def generate(service, transport \\ :framed) do
functions =
service.functions
|> Map.values()
|> Enum.map(&generate_handler_function(&1))
|> Utils.merge_blocks()
+ module = Module.concat([Binary, Macro.camelize(Atom.to_string(transport)), Client])
+ implementation = Module.concat([Thrift, module])
+
quote do
- defmodule Binary.Framed.Client do
+ defmodule unquote(module) do
@moduledoc false
- alias Thrift.Binary.Framed.Client, as: ClientImpl
+ alias unquote(implementation), as: ClientImpl
defdelegate close(conn), to: ClientImpl
defdelegate connect(conn, opts), to: ClientImpl
diff --git a/lib/thrift/generator/binary/framed/server.ex b/lib/thrift/generator/binary/framed/server.ex
index 08a789bb..909f4cd8 100644
--- a/lib/thrift/generator/binary/framed/server.ex
+++ b/lib/thrift/generator/binary/framed/server.ex
@@ -9,18 +9,21 @@ defmodule Thrift.Generator.Binary.Framed.Server do
alias Thrift.Parser.FileGroup
- def generate(service_module, service, file_group) do
+ def generate(service_module, service, file_group, transport \\ :framed) do
functions =
service.functions
|> Map.values()
|> Enum.map(&generate_handler_function(file_group, service_module, &1))
+ module = Module.concat([Binary, Macro.camelize(Atom.to_string(transport)), Server])
+ implementation = Module.concat([Thrift, module])
+
quote do
- defmodule Binary.Framed.Server do
+ defmodule unquote(module) do
@moduledoc false
require Logger
- alias Thrift.Binary.Framed.Server, as: ServerImpl
+ alias unquote(implementation), as: ServerImpl
defdelegate stop(name), to: ServerImpl
def start_link(handler_module, port, opts \\ []) do
diff --git a/lib/thrift/generator/service.ex b/lib/thrift/generator/service.ex
index 3d5acbca..3ec89534 100644
--- a/lib/thrift/generator/service.ex
+++ b/lib/thrift/generator/service.ex
@@ -26,6 +26,11 @@ defmodule Thrift.Generator.Service do
framed_client = Generator.Binary.Framed.Client.generate(service)
framed_server = Generator.Binary.Framed.Server.generate(dest_module, service, file_group)
+ buffered_client = Generator.Binary.Framed.Client.generate(service, :buffered)
+
+ buffered_server =
+ Generator.Binary.Framed.Server.generate(dest_module, service, file_group, :buffered)
+
service_module =
quote do
defmodule unquote(dest_module) do
@@ -36,6 +41,8 @@ defmodule Thrift.Generator.Service do
unquote(framed_client)
unquote(framed_server)
+ unquote(buffered_client)
+ unquote(buffered_server)
end
end
diff --git a/mix.lock b/mix.lock
index 423762a6..530850c6 100644
--- a/mix.lock
+++ b/mix.lock
@@ -20,7 +20,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
diff --git a/test/fixtures/ssl/README.md b/test/fixtures/ssl/README.md
new file mode 100644
index 00000000..4fe904f2
--- /dev/null
+++ b/test/fixtures/ssl/README.md
@@ -0,0 +1,6 @@
+These certificates and the private key are public test fixtures for localhost
+SSL communication only. Do not use them outside tests.
+
+`cacerts.pem` is a test CA; `cert.pem` is its signed localhost certificate,
+and `key.pem` is the corresponding unencrypted private key. The certificates
+are valid for ten years from generation in October 2026.
diff --git a/test/fixtures/ssl/cacerts.pem b/test/fixtures/ssl/cacerts.pem
new file mode 100644
index 00000000..b046c295
--- /dev/null
+++ b/test/fixtures/ssl/cacerts.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIUA0sUa5a6DW0djJufdAawj8ovbfowDQYJKoZIhvcNAQEL
+BQAwGTEXMBUGA1UEAwwOVGhyaWZ0IFRlc3QgQ0EwHhcNMjYxMDEyMjExNDUwWhcN
+MzYxMDA5MjExNDUwWjAZMRcwFQYDVQQDDA5UaHJpZnQgVGVzdCBDQTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBANgrTLaDqiYmVkmaZfLYsh8ePX+z9TI3
+GKKX7XpNdlkgFyw3jn2Oo3KvFzoj8iT/lc3F/LjZW0XaeC0gr6ClEKtMbrCd1PPX
+wwnjnfF+hKfQji7ksTQzGYBLFAerGiKq2QnRx9AlXOt/iJxHHmhjqB2+OuNkKJpV
+Kk/RIQFoBGOTIl2mEMouLYAE1DhmUUxCmHEo2vgTdGZX07ZmNwcjc1S40eviuZ4X
+NNI9u+q68r6OIh/VXL2tsuzn+WXv/iMqh6FR6CANtvdPYB1yHjkf2L/OBcBI3mIi
+5NC7Ud8SMTRj1xUSC/KTzTcmFuaJRvRd7f0HasspCEHmadaP5NW8398CAwEAAaNj
+MGEwHQYDVR0OBBYEFPMtrdjgVh0FJqmcdojhNsavmDPSMB8GA1UdIwQYMBaAFPMt
+rdjgVh0FJqmcdojhNsavmDPSMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMA0GCSqGSIb3DQEBCwUAA4IBAQDK9ZTTK25XVboEokF7LJOQ44hSNFVBUODa
+bZGTkczaPVDiSCQfiyWDc4VseJNkqOpn9cPotzosIJWPVoghDKkratDn35aktfGc
+eIRbiLqKm9t9wvk6OMFVKO3bScaHcLJVSvsFHzruEfKVwlbGM1XGkonQuiR1hJJT
+xIMQMw0RyMh06oIpWJkyFPfvJnZ9b39WPTfCv5h/85eqXsapnoEZekUGu6D+QWb2
+fg8HawTfRxAtRgbz6aXdC/2Eu9SdgGn49359ZBYfdj//aqUlynW65t7nfAQGXMPU
+vALZJZa11RH0mPByFR08BLsu28w/nlcVNkooFhyQDWYScjpGY+6/
+-----END CERTIFICATE-----
diff --git a/test/fixtures/ssl/cert.pem b/test/fixtures/ssl/cert.pem
new file mode 100644
index 00000000..adc8da99
--- /dev/null
+++ b/test/fixtures/ssl/cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5UaHJp
+ZnQgVGVzdCBDQTAeFw0yNjEwMTIyMTE0NTBaFw0zNjEwMDkyMTE0NTBaMBQxEjAQ
+BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJzXBqT/GEnHyeK+xsTnljpV8uMd4t3v1a1imTDJ5PpmZ2rgvUfH3q0IQVgePXkV
+Dg58zdKTz0BnIAfKRYQ3gzXDAujETDo0TXKEnglqy6h0VK5h2eMPCiCGN4fD0dG7
+Gbm013U7vKwK5nRhvTKAEkvy7x8WHPlzA96CZpnSze8QVNxjqkAI1WL1eqEmTzKc
+Qka/Y2oyRFtdsFrqrX8WyjlR6zRSpM0erE6imJp2yq2lCma0CUFz8OwheODnw8Dp
+ZkBvAam4vpCDgNnJ8CfzAulbredcoDVIGTuxWRy7xwvSPOUinlkxFFqQiJLIqIja
+wP+YUeoDVhS7FXGRA5qC2HsCAwEAAaNeMFwwGgYDVR0RBBMwEYIJbG9jYWxob3N0
+hwR/AAABMB0GA1UdDgQWBBSmtYxmhv7XSVutlyIBYNLmY6NYnzAfBgNVHSMEGDAW
+gBTzLa3Y4FYdBSapnHaI4TbGr5gz0jANBgkqhkiG9w0BAQsFAAOCAQEAnQZ3RK8O
+Xz63LhaF+EZ6AI+hXPkL9PiMvoxhCChncZ4n1glfq+K7FGzvB92EHduBE8W+kK8q
+dRtm+HHFw/FAsXWOM1bCaIqB4NbK6kMqKV01G1z9YT40mEP1vWAoGXUz5/TvcKxQ
+WEZ9JeE53Qu85OFh2/ONFHR29FEYj3Ypp76U62/26lzJxu7V5MPdr6pwVnC+GFtk
+coUHearNUCkE3VZRQzOfRMaBTSi75lnS0g+VH1sARrqNyuXw1czLhIhgsixy+wB5
+fbk30mbPYwwssWZcOV+4PuoJyijU0v0mcz5iPnQg0bAwTEDgugaG8h94StVOoSeG
+8mQAN6qwLVvwsA==
+-----END CERTIFICATE-----
diff --git a/test/fixtures/ssl/key.pem b/test/fixtures/ssl/key.pem
new file mode 100644
index 00000000..bbd60f89
--- /dev/null
+++ b/test/fixtures/ssl/key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc1wak/xhJx8ni
+vsbE55Y6VfLjHeLd79WtYpkwyeT6Zmdq4L1Hx96tCEFYHj15FQ4OfM3Sk89AZyAH
+ykWEN4M1wwLoxEw6NE1yhJ4JasuodFSuYdnjDwoghjeHw9HRuxm5tNd1O7ysCuZ0
+Yb0ygBJL8u8fFhz5cwPegmaZ0s3vEFTcY6pACNVi9XqhJk8ynEJGv2NqMkRbXbBa
+6q1/Fso5Ues0UqTNHqxOopiadsqtpQpmtAlBc/DsIXjg58PA6WZAbwGpuL6Qg4DZ
+yfAn8wLpW63nXKA1SBk7sVkcu8cL0jzlIp5ZMRRakIiSyKiI2sD/mFHqA1YUuxVx
+kQOagth7AgMBAAECggEADqQSAOK2po3fHkAXN7gIsZRmvTsue5pRFO5F2k9BL/t8
+Pva7efvB9ol1PspqjFGMrjSKACoqfYVtm4c50GW1kBYOwPeGHXmBcKXJnPEaZt9z
+15LXL1eEPKoFfV0odTl6kCKTabm0W05p2fEVI3rfemCItZsFBGeaCQec7VIc8D/X
+/DcUvd+PnleSfdrGyzbf8XqswnXfURWljrJ2T7y6Ds7kGGbU730K6uTyL/C8pw2U
+Vk9tcnDpovRZQ9+WqtwGdymwbDHR6d51MjNTT9oPH/U6/kw0uJQyOTOoRN80JP7S
+rygP1DkI+cOxkgdbgKA92OMqetKqtkUzuJDhvTcIBQKBgQDajT9C4qFdJW/0lS+4
+aR1Hh3/AhRGbKBHalh77JbNuxRZTspuMegSA5mruEet7rZeMQchV+pY9B0sIqJdW
+RkMc+E+4f7q03ASaXmPwtkIkPfW3FGHkDIiX5fbRlxMS6ENbQz1Bc5jI2VIZOrft
+uZByFSEA/PqFAYnExrCaW+alrQKBgQC3tsvR6SrYAaIxvw4ZgP/q+kegK2EuXdVE
+wwfJGrzdc17Aph3IPbTaZB2JnsKPlSZ2/IPvZkM1SnHR9A6uQAtyVKX1wEn7WfEZ
+aFga1MVhuhsZRnaEsiDWc54qsCH4LWSeFOkGYKxhEDiDie77A7DWGOuaA/PLj6ED
+bnnoAnQrxwKBgDGC9ETz0LioZY+USKZPcemlgcScoNt0nW7WuZgN93NtxNwbR2/w
+qJqHGMxsVNvE/OMyd+86gNS8lTwV7YsHCedDPdQ/Y43bllNedOewcpWBT+w0+uRO
+DpfS6jKbNh//bFr9haARTxiZWycKDWcWtemSbdKqfRqquYpRtOqTTVJVAoGARJbG
+ufQnvsW9XH1MMlFrOrt+ilonyjvWJ/n4qiiFYth0A9EBRYVkRDUkogDX+utglQPp
+7jNr3DWTESPhZ0UaAPGBlKLZ/DG0Rhwi6MGPf9EP1Pvtu9UVVXuNDbr8s7+e1TZR
+WcQ2F3cK4nFY+/aAyy46qCHZn6JcJv/UG77uyHkCgYBJP/4Q9V0JWym27CgaaRXa
+ByLAo+T/UPmdjM7vhQalcIo2v5PTa8UJau648A66Z6U+6SJ5Xmow59E7vuMNXCWQ
+PXXm/Y9Q2Zsi9xJqBDXSZtemEF9V/bdw8nOm/QQa9RfjaGUAV6t+a5HxUjfYBH4B
+lZGWGXgROc4a/TtbSGzjUQ==
+-----END PRIVATE KEY-----
diff --git a/test/support/lib/thrift_test_case.ex b/test/support/lib/thrift_test_case.ex
index 3020ddea..c19077c7 100644
--- a/test/support/lib/thrift_test_case.ex
+++ b/test/support/lib/thrift_test_case.ex
@@ -139,7 +139,17 @@ defmodule ThriftTestCase do
quote bind_quoted: [module: __MODULE__, message: message, var: var, block: block] do
if module.implement?(__MODULE__) do
contents = module.quoted_contents(__MODULE__, block)
- name = ExUnit.Case.register_test(__ENV__, :test, message, [])
+
+ name =
+ ExUnit.Case.register_test(
+ __ENV__.module,
+ __ENV__.file,
+ __ENV__.line,
+ :test,
+ message,
+ []
+ )
+
def unquote(name)(unquote(var)), do: unquote(contents)
else
ExUnit.Case.test message do
diff --git a/test/thrift/binary/buffered/message_test.exs b/test/thrift/binary/buffered/message_test.exs
new file mode 100644
index 00000000..79c993da
--- /dev/null
+++ b/test/thrift/binary/buffered/message_test.exs
@@ -0,0 +1,71 @@
+defmodule Thrift.Binary.Buffered.MessageTest do
+ use ExUnit.Case, async: true
+ alias Thrift.Binary.Buffered.Message
+ alias Thrift.Protocol.Binary
+
+ setup do
+ {:ok, listener} = :gen_tcp.listen(0, [:binary, active: false, packet: 0])
+ {:ok, port} = :inet.port(listener)
+
+ {:ok, sender} =
+ :gen_tcp.connect(String.to_charlist("localhost"), port, [:binary, active: false, packet: 0])
+
+ {:ok, receiver} = :gen_tcp.accept(listener)
+ :gen_tcp.close(listener)
+
+ on_exit(fn ->
+ :gen_tcp.close(sender)
+ :gen_tcp.close(receiver)
+ end)
+
+ {:ok, sender: sender, receiver: receiver}
+ end
+
+ test "reads every field type and leaves the next message buffered", ctx do
+ fields = [
+ {2, Binary.serialize(:bool, true)},
+ {3, Binary.serialize(:i8, -5)},
+ {6, Binary.serialize(:i16, -300)},
+ {8, Binary.serialize(:i32, 999)},
+ {10, Binary.serialize(:i64, -999)},
+ {4, Binary.serialize(:double, 1.5)},
+ {11, Binary.serialize(:string, "")},
+ {12, <<11, 1::16, 3::32, 0, 1, 2, 0>>},
+ {13, Binary.serialize({:map, {:string, {:list, :i64}}}, %{"a" => [1, 2]})},
+ {14, Binary.serialize({:set, :string}, MapSet.new(["a", "b"]))},
+ {15, Binary.serialize({:list, :i32}, [])}
+ ]
+
+ body = Enum.map(fields, fn {type, bytes} -> [<>, bytes] end)
+
+ message =
+ IO.iodata_to_binary([Binary.serialize(:message_begin, {:reply, 0, "all"}), body, <<0>>])
+
+ :ok = :gen_tcp.send(ctx.sender, [message, message])
+ assert {:ok, ^message} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ assert {:ok, ^message} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ end
+
+ test "rejects negative lengths and unknown field types", ctx do
+ header = Binary.serialize(:message_begin, {:reply, 0, "bad"})
+ :ok = :gen_tcp.send(ctx.sender, [header, <<11, 1::16, -1::signed-32>>])
+ assert {:error, :invalid_message} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ :ok = :gen_tcp.send(ctx.sender, [header, <<99, 1::16>>])
+ assert {:error, :invalid_message} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ end
+
+ test "bounds message size and nesting", ctx do
+ header = Binary.serialize(:message_begin, {:reply, 0, "bad"})
+ :ok = :gen_tcp.send(ctx.sender, [header, <<11, 1::16, 16_777_216::32>>])
+ assert {:error, :message_too_large} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ :ok = :gen_tcp.send(ctx.sender, [header, List.duplicate(<<12, 1::16>>, 65)])
+ assert {:error, :invalid_message} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ end
+
+ test "incomplete messages time out or report a closed connection", ctx do
+ :ok = :gen_tcp.send(ctx.sender, <<0x80, 1>>)
+ assert {:error, :timeout} = Message.recv(:gen_tcp, ctx.receiver, 20)
+ :gen_tcp.close(ctx.sender)
+ assert {:error, :closed} = Message.recv(:gen_tcp, ctx.receiver, 1000)
+ end
+end
diff --git a/test/thrift/binary/buffered/transport_test.exs b/test/thrift/binary/buffered/transport_test.exs
new file mode 100644
index 00000000..9a1c045e
--- /dev/null
+++ b/test/thrift/binary/buffered/transport_test.exs
@@ -0,0 +1,79 @@
+defmodule Thrift.Binary.Buffered.TransportTest do
+ use ThriftTestCase
+
+ @thrift_file name: "buffered.thrift",
+ contents: """
+ exception Rejected { 1: string message }
+ service Echo {
+ string echo(1: string text),
+ map> nested(1: map> value),
+ void fail() throws (1: Rejected rejected),
+ oneway void notify(),
+ }
+ """
+
+ alias __MODULE__.Echo.Binary.Buffered.{Client, Server}
+ alias Thrift.Binary.Buffered.Message
+ alias Thrift.Protocol.Binary
+
+ defmodule Handler do
+ def echo(text), do: text
+ def nested(value), do: value
+ def notify(), do: :ok
+ def fail(), do: raise(Thrift.Binary.Buffered.TransportTest.Rejected, message: "rejected")
+ end
+
+ setup do
+ {:ok, server} = Server.start_link(Handler, 0)
+ Process.unlink(server)
+ port = :ranch.get_port(Handler)
+ on_exit(fn -> if Process.alive?(server), do: Server.stop(server) end)
+ {:ok, port: port}
+ end
+
+ thrift_test "generated client supports repeated calls, containers, exceptions and oneway",
+ ctx do
+ {:ok, client} = Client.start_link("127.0.0.1", ctx.port)
+ assert {:ok, ""} = Client.echo(client, "")
+ assert {:ok, nil} = Client.notify(client)
+ assert {:ok, "after oneway"} = Client.echo(client, "after oneway")
+ nested = %{"empty" => [], "numbers" => [1, -2, 3]}
+ assert {:ok, ^nested} = Client.nested(client, nested)
+ assert {:error, {:exception, %Rejected{message: "rejected"}}} = Client.fail(client)
+ assert "still connected" = Client.echo!(client, "still connected")
+ assert :ok = Client.close(client)
+ end
+
+ thrift_test "raw peer can fragment and coalesce requests without frame prefixes", ctx do
+ {:ok, socket} =
+ :gen_tcp.connect(String.to_charlist("localhost"), ctx.port, [
+ :binary,
+ active: false,
+ packet: 0
+ ])
+
+ # Hand-written binary protocol requests, independent of our serializer.
+ request = <<0x80, 1, 0, 1, 4::32, "echo", 7::32, 11, 1::16, 3::32, "abc", 0>>
+ <> = request
+ :ok = :gen_tcp.send(socket, <>)
+ :ok = :gen_tcp.send(socket, [rest, request])
+ response = <<0x80, 1, 0, 2, 4::32, "echo", 7::32, 11, 0::16, 3::32, "abc", 0>>
+ assert {:ok, ^response} = :gen_tcp.recv(socket, byte_size(response), 1000)
+ assert {:ok, ^response} = :gen_tcp.recv(socket, byte_size(response), 1000)
+ :gen_tcp.close(socket)
+ end
+
+ thrift_test "server accepts legacy message headers", ctx do
+ {:ok, socket} =
+ :gen_tcp.connect(String.to_charlist("localhost"), ctx.port, [
+ :binary,
+ active: false,
+ packet: 0
+ ])
+
+ :ok = :gen_tcp.send(socket, <<4::32, "echo", 1, 9::32, 11, 1::16, 0::32, 0>>)
+ assert {:ok, response} = Message.recv(:gen_tcp, socket, 1000)
+ assert {:ok, {:reply, 9, "echo", _}} = Binary.deserialize(:message_begin, response)
+ :gen_tcp.close(socket)
+ end
+end
diff --git a/test/thrift/binary/framed/server_test.exs b/test/thrift/binary/framed/server_test.exs
index 48572cda..3edb170b 100644
--- a/test/thrift/binary/framed/server_test.exs
+++ b/test/thrift/binary/framed/server_test.exs
@@ -174,7 +174,7 @@ defmodule Servers.Binary.Framed.IntegrationTest do
end
thrift_test "it can handle bogus data", ctx do
- {:ok, socket} = :gen_tcp.connect('localhost', ctx.port, [:binary, packet: 4, active: false])
+ {:ok, socket} = :gen_tcp.connect(~c"localhost", ctx.port, [:binary, packet: 4, active: false])
:ok = :gen_tcp.send(socket, <<1, 2, 3, 4, 5>>)
assert {:error, :closed} == :gen_tcp.recv(socket, 0)
diff --git a/test/thrift/binary/framed/ssl_test.exs b/test/thrift/binary/framed/ssl_test.exs
index d53a04b7..4143202e 100644
--- a/test/thrift/binary/framed/ssl_test.exs
+++ b/test/thrift/binary/framed/ssl_test.exs
@@ -61,7 +61,9 @@ defmodule Servers.Binary.Framed.SSLTest do
def build_ssl_client(ctx) do
{:ok, ssl_client} =
- Client.start_link("localhost", ctx.port, ssl_opts: ctx[:ssl_opts] ++ [enabled: true])
+ Client.start_link("localhost", ctx.port,
+ ssl_opts: ctx[:ssl_opts] ++ [enabled: true, verify: :verify_none]
+ )
{:ok, ssl_client: ssl_client}
end
@@ -143,7 +145,7 @@ defmodule Servers.Binary.Framed.SSLTest do
## Helpers
defp get_certs() do
- certs_path = Application.app_dir(:ssl, "examples/certs/etc/server")
+ certs_path = Path.expand("../../../fixtures/ssl", __DIR__)
cacerts = Path.join(certs_path, "cacerts.pem")
cert = Path.join(certs_path, "cert.pem")
key = Path.join(certs_path, "key.pem")
diff --git a/test/thrift/generator/service_test.exs b/test/thrift/generator/service_test.exs
index 87d1b17a..8b551a90 100644
--- a/test/thrift/generator/service_test.exs
+++ b/test/thrift/generator/service_test.exs
@@ -460,6 +460,6 @@ defmodule Thrift.Generator.ServiceTest do
}
# Still connected.
- assert {:error, {:exception, exception}} = WrongClient.plang(client)
+ assert {:error, {:exception, ^exception}} = WrongClient.plang(client)
end
end
diff --git a/test/thrift/parser/lexer_test.exs b/test/thrift/parser/lexer_test.exs
index 205d2a50..901e3cf6 100644
--- a/test/thrift/parser/lexer_test.exs
+++ b/test/thrift/parser/lexer_test.exs
@@ -147,19 +147,19 @@ defmodule Thrift.Parser.LexerTest do
assert tokenize("{'hello':'world'}") == [
{:"{", 1},
- {:string, 1, 'hello'},
+ {:string, 1, ~c"hello"},
{:":", 1},
- {:string, 1, 'world'},
+ {:string, 1, ~c"world"},
{:"}", 1}
]
assert tokenize("{'a':1,'b':2}") == [
{:"{", 1},
- {:string, 1, 'a'},
+ {:string, 1, ~c"a"},
{:":", 1},
{:int, 1, 1},
{:",", 1},
- {:string, 1, 'b'},
+ {:string, 1, ~c"b"},
{:":", 1},
{:int, 1, 2},
{:"}", 1}
@@ -173,11 +173,11 @@ defmodule Thrift.Parser.LexerTest do
end
test "identifiers" do
- assert tokenize("abc_123.xzy") == [{:ident, 1, 'abc_123.xzy'}]
+ assert tokenize("abc_123.xzy") == [{:ident, 1, ~c"abc_123.xzy"}]
end
test "typedef" do
- assert tokenize("typedef i32 Integer") == [{:typedef, 1}, {:i32, 1}, {:ident, 1, 'Integer'}]
+ assert tokenize("typedef i32 Integer") == [{:typedef, 1}, {:i32, 1}, {:ident, 1, ~c"Integer"}]
end
test "enum definition" do
@@ -190,21 +190,21 @@ defmodule Thrift.Parser.LexerTest do
}
""") == [
{:enum, 1},
- {:ident, 1, 'Operation'},
+ {:ident, 1, ~c"Operation"},
{:"{", 1},
- {:ident, 2, 'ADD'},
+ {:ident, 2, ~c"ADD"},
{:=, 2},
{:int, 2, 1},
{:",", 2},
- {:ident, 3, 'SUBTRACT'},
+ {:ident, 3, ~c"SUBTRACT"},
{:=, 3},
{:int, 3, 2},
{:",", 3},
- {:ident, 4, 'MULTIPLY'},
+ {:ident, 4, ~c"MULTIPLY"},
{:=, 4},
{:int, 4, 3},
{:",", 4},
- {:ident, 5, 'DIVIDE'},
+ {:ident, 5, ~c"DIVIDE"},
{:=, 5},
{:int, 5, 4},
{:"}", 6}
@@ -221,30 +221,30 @@ defmodule Thrift.Parser.LexerTest do
}
""") == [
{:struct, 1},
- {:ident, 1, 'Work'},
+ {:ident, 1, ~c"Work"},
{:"{", 1},
{:int, 2, 1},
{:":", 2},
{:i32, 2},
- {:ident, 2, 'num1'},
+ {:ident, 2, ~c"num1"},
{:=, 2},
{:int, 2, 0},
{:",", 2},
{:int, 3, 2},
{:":", 3},
{:i32, 3},
- {:ident, 3, 'num2'},
+ {:ident, 3, ~c"num2"},
{:",", 3},
{:int, 4, 3},
{:":", 4},
- {:ident, 4, 'Operation'},
- {:ident, 4, 'op'},
+ {:ident, 4, ~c"Operation"},
+ {:ident, 4, ~c"op"},
{:",", 4},
{:int, 5, 4},
{:":", 5},
{:optional, 5},
{:string, 5},
- {:ident, 5, 'comment'},
+ {:ident, 5, ~c"comment"},
{:",", 5},
{:"}", 6}
]
@@ -258,17 +258,17 @@ defmodule Thrift.Parser.LexerTest do
}
""") == [
{:exception, 1},
- {:ident, 1, 'InvalidOperation'},
+ {:ident, 1, ~c"InvalidOperation"},
{:"{", 1},
{:int, 2, 1},
{:":", 2},
{:i32, 2},
- {:ident, 2, 'whatOp'},
+ {:ident, 2, ~c"whatOp"},
{:",", 2},
{:int, 3, 2},
{:":", 3},
{:string, 3},
- {:ident, 3, 'why'},
+ {:ident, 3, ~c"why"},
{:"}", 4}
]
end
@@ -282,32 +282,32 @@ defmodule Thrift.Parser.LexerTest do
}
""") == [
{:service, 1},
- {:ident, 1, 'Calculator'},
+ {:ident, 1, ~c"Calculator"},
{:extends, 1},
- {:ident, 1, 'shared.SharedService'},
+ {:ident, 1, ~c"shared.SharedService"},
{:"{", 1},
{:void, 2},
- {:ident, 2, 'ping'},
+ {:ident, 2, ~c"ping"},
{:"(", 2},
{:")", 2},
{:",", 2},
{:i32, 3},
- {:ident, 3, 'add'},
+ {:ident, 3, ~c"add"},
{:"(", 3},
{:int, 3, 1},
{:":", 3},
{:i32, 3},
- {:ident, 3, 'num1'},
+ {:ident, 3, ~c"num1"},
{:",", 3},
{:int, 3, 2},
{:":", 3},
{:i32, 3},
- {:ident, 3, 'num2'},
+ {:ident, 3, ~c"num2"},
{:")", 3},
{:",", 3},
{:oneway, 4},
{:void, 4},
- {:ident, 4, 'zip'},
+ {:ident, 4, ~c"zip"},
{:"(", 4},
{:")", 4},
{:"}", 5}
@@ -323,20 +323,20 @@ defmodule Thrift.Parser.LexerTest do
#@namespace elixir Bar
""") == [
{:namespace, 1},
- {:ident, 1, 'elixir'},
- {:ident, 1, 'Foo'},
+ {:ident, 1, ~c"elixir"},
+ {:ident, 1, ~c"Foo"},
{:namespace, 2},
- {:ident, 2, 'py'},
- {:ident, 2, 'foo.bar.baz'},
+ {:ident, 2, ~c"py"},
+ {:ident, 2, ~c"foo.bar.baz"},
{:namespace, 3},
- {:ident, 3, 'java'},
- {:ident, 3, 'com.pinterest.foo.bar.baz'},
+ {:ident, 3, ~c"java"},
+ {:ident, 3, ~c"com.pinterest.foo.bar.baz"},
{:namespace, 4},
{:*, 4},
- {:ident, 4, 'foo.bar'},
+ {:ident, 4, ~c"foo.bar"},
{:namespace, 5},
- {:ident, 5, 'elixir'},
- {:ident, 5, 'Bar'}
+ {:ident, 5, ~c"elixir"},
+ {:ident, 5, ~c"Bar"}
]
end
@@ -344,7 +344,7 @@ defmodule Thrift.Parser.LexerTest do
assert tokenize("const my_const = 32") ==
[
{:const, 1},
- {:ident, 1, 'my_const'},
+ {:ident, 1, ~c"my_const"},
{:=, 1},
{:int, 1, 32}
]
@@ -355,9 +355,9 @@ defmodule Thrift.Parser.LexerTest do
[
{:const, 1},
{:string, 1},
- {:ident, 1, 'my_const'},
+ {:ident, 1, ~c"my_const"},
{:=, 1},
- {:ident, 1, 'MyEnum.VALUE'}
+ {:ident, 1, ~c"MyEnum.VALUE"}
]
end
end