Skip to content

Commit

Permalink
feat: handle VERSION capability
Browse files Browse the repository at this point in the history
- Always first in the capabilities list.
- Moves to first if the callback has it somewhere.
- Ignores capabilities not in standard.
  • Loading branch information
sntran committed Mar 2, 2021
1 parent 248b031 commit 484a8b6
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 7 deletions.
38 changes: 37 additions & 1 deletion src/gen_nntp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@
}).

-define(PORT, list_to_integer(os:getenv("PORT", "119"))).
-define(NNTP_VERSION, <<"2">>).
-define(CAPABILITIES, [
<<"HDR">>,
<<"IHAVE">>,
<<"LIST">>,
<<"MODE-READER">>,
<<"NEWNEWS">>,
<<"OVER">>,
<<"POST">>,
<<"READER">>
]).

-callback init(Args :: term()) ->
{ok, state()}
Expand Down Expand Up @@ -207,8 +218,21 @@ handle_info({tcp, Socket, <<"CAPABILITIES\r\n">>}, #client{transport = Transport
% Asks the callback module to provide the capacitities at this moment.
{ok, Capabilities, State1} = Module:handle_CAPABILITIES(State),

% Retrieve the VERSION capability from returned list if any.
Version = case lists:search(fun is_version/1, Capabilities) of
false -> <<"VERSION ", ?NNTP_VERSION/binary>>;
{value, Value} -> Value
end,

% Build multi-line data block responsefollowing the 101 response code.
Response = join(<<"\r\n">>, [<<"101 Capability list:">> | Capabilities]),
Response = join(<<"\r\n">>, [
<<"101 Capability list:">>, % Command response with code
Version % Then the version
| [
% And all the standard capabilities.
X || X <- Capabilities, is_capability(X)
]
]),

% Ends the multi-line data block with a termination line.
Transport:send(Socket, <<Response/binary, "\r\n.\r\n">>),
Expand Down Expand Up @@ -276,6 +300,18 @@ code_change(_OldVsn, Client, _Extra) ->
% ..code to convert state (and more) during code change
{ok, Client}.

% Checks if a text match "VERSION" capability.
is_version(<<"VERSION ", _N/binary>>) -> true;
is_version(_) -> false.

% VERSION is handled at server's level, so it's not a capability.
is_capability(<<"VERSION ", _N/binary>>) ->
false;

% Checks if the capability is in the standard list.
is_capability(Capability) ->
lists:member(Capability, ?CAPABILITIES).

% Join binary
join(_Separator, []) ->
<<>>;
Expand Down
65 changes: 60 additions & 5 deletions test/gen_nntp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,15 @@ defmodule GenNNTPTest do

{:ok, socket, _greeting} = GenNNTP.connect()

unless context[:skip_command] do
:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")
{:ok, _response} = :gen_tcp.recv(socket, 0, 1000)
end

%{socket: socket}
end

@tag skip_command: true
test "is called when the client asks for it", %{socket: socket} do

refute_receive(
Expand All @@ -159,22 +165,20 @@ defmodule GenNNTPTest do
)
end

@tag skip_command: true
test "is responded with 101 code", %{socket: socket} do
:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^101 /
end

@tag capabilities: ["VERSION 2", "HDR", "IHAVE", "LIST", "MODE-READER", "NEWNEWS", "OVER", "POST", "READER"]
@tag capabilities: ["VERSION 2", "READER", "IHAVE", "POST", "NEWNEWS", "HDR", "OVER", "LIST", "MODE-READER"]
test "is responded with capabilities returned from the callback", %{socket: socket, capabilities: capabilities} do
:ok = :gen_tcp.send(socket, "CAPABILITIES\r\n")
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^101 /

for capability <- capabilities do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "#{capability}\r\n"
end

# Receives the termination line.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === ".\r\n"
Expand All @@ -183,6 +187,57 @@ defmodule GenNNTPTest do
assert {:error, :timeout} = :gen_tcp.recv(socket, 0, 100)
end

@tag capabilities: ["READER", "IHAVE", "POST", "NEWNEWS"]
test "prepends with VERSION if not provided", %{socket: socket, capabilities: capabilities} do
# Asserts that "VERSION" is always first in the response.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^VERSION \d/

for capability <- capabilities do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "#{capability}\r\n"
end

# Receives the termination line.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === ".\r\n"
end

@tag capabilities: ["READER", "IHAVE", "VERSION 1", "POST", "NEWNEWS"]
test "moves VERSION to head", %{socket: socket, capabilities: capabilities} do
# Should respond with "VERSION 1" from the callback's return.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "VERSION 1\r\n"

# Then the rest of the capabilities, without "VERSION 1".
for capability <- capabilities, !(capability =~ ~r/^VERSION \d/) do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "#{capability}\r\n"
end

# Receives the termination line.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === ".\r\n"
end

@tag capabilities: ["READER", "IHAVE", "AUTOUPDATE", "POST", "NEWNEWS"]
test "only takes actual capabilities", %{socket: socket, capabilities: capabilities} do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response =~ ~r/^VERSION \d/

# Should not respond with "AUTOUPDATE" since it's not standard.
for capability <- capabilities, capability !== "AUTOUPDATE" do
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === "#{capability}\r\n"
end

# Receives the termination line.
{:ok, response} = :gen_tcp.recv(socket, 0, 1000)
assert response === ".\r\n"
end

end

end

describe "server interaction" do
Expand Down
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ExUnit.start()
ExUnit.start(exclude: [:skip])

0 comments on commit 484a8b6

Please sign in to comment.