From 484a8b65b1e570789b420f1c8d75c92b89c60017 Mon Sep 17 00:00:00 2001 From: Son Tran-Nguyen Date: Tue, 2 Mar 2021 16:32:38 -0600 Subject: [PATCH] feat: handle VERSION capability - Always first in the capabilities list. - Moves to first if the callback has it somewhere. - Ignores capabilities not in standard. --- src/gen_nntp.erl | 38 +++++++++++++++++++++++- test/gen_nntp_test.exs | 65 ++++++++++++++++++++++++++++++++++++++---- test/test_helper.exs | 2 +- 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/gen_nntp.erl b/src/gen_nntp.erl index 29a0af0..acc6149 100644 --- a/src/gen_nntp.erl +++ b/src/gen_nntp.erl @@ -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()} @@ -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, <>), @@ -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, []) -> <<>>; diff --git a/test/gen_nntp_test.exs b/test/gen_nntp_test.exs index 0644800..b704689 100644 --- a/test/gen_nntp_test.exs +++ b/test/gen_nntp_test.exs @@ -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( @@ -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" @@ -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 diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..d13e5fc 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start() +ExUnit.start(exclude: [:skip])