Skip to content

Commit

Permalink
feat: HEAD, BODY and STAT commands
Browse files Browse the repository at this point in the history
They are all similar to ARTICLE, except:

- HEAD responds with 221 and just the headers on success.
- BODY responds with 222 and just the body on success.
- STAT responds with just the message ID on success.
  • Loading branch information
sntran committed Mar 10, 2021
1 parent 1587252 commit 73f3be7
Show file tree
Hide file tree
Showing 5 changed files with 516 additions and 126 deletions.
10 changes: 6 additions & 4 deletions include/gen_nntp.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
-type address() :: inet:socket_address() | inet:hostname() | binary().
-type port_number() :: inet:port_number().
-type message_id() :: binary().
-type article() :: {
message_id(),
Headers :: map(),
Body :: binary()
-type headers() :: map().
-type body() :: binary().
-type article() :: #{
id := message_id(),
headers => headers(),
body => body()
}.
23 changes: 22 additions & 1 deletion lib/gen_nntp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,28 @@ defmodule GenNNTP do
when group: String.t()

@callback handle_ARTICLE(arg, state) ::
{:ok, { number, :gen_nttp.article()}, state }|
{:ok, { number, :gen_nttp.article() }, state }|
{:ok, false, state} |
{:error, reason :: String.t(), state}
when number: non_neg_integer(),
arg: :gen_nttp.message_id() | number

@callback handle_HEAD(arg, state) ::
{:ok, { number, :gen_nttp.article() }, state }|
{:ok, false, state} |
{:error, reason :: String.t(), state}
when number: non_neg_integer(),
arg: :gen_nttp.message_id() | number

@callback handle_BODY(arg, state) ::
{:ok, { number, :gen_nttp.article() }, state }|
{:ok, false, state} |
{:error, reason :: String.t(), state}
when number: non_neg_integer(),
arg: :gen_nttp.message_id() | number

@callback handle_STAT(arg, state) ::
{:ok, { number, :gen_nttp.article() }, state }|
{:ok, false, state} |
{:error, reason :: String.t(), state}
when number: non_neg_integer(),
Expand Down
203 changes: 139 additions & 64 deletions src/gen_nntp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,28 @@
when Group :: binary().

-callback handle_ARTICLE(Arg, state()) ->
{ok, { Number, article()}, state()}
{ok, { Number, article() }, state()}
| {ok, false, state()}
| {error, Reason :: binary(), state()}
when Number :: non_neg_integer(),
Arg :: message_id() | Number.

-callback handle_HEAD(Arg, state()) ->
{ok, { Number, article() }, state()}
| {ok, false, state()}
| {error, Reason :: binary(), state()}
when Number :: non_neg_integer(),
Arg :: message_id() | Number.

-callback handle_BODY(Arg, state()) ->
{ok, { Number, article() }, state()}
| {ok, false, state()}
| {error, Reason :: binary(), state()}
when Number :: non_neg_integer(),
Arg :: message_id() | Number.

-callback handle_STAT(Arg, state()) ->
{ok, { Number, article() }, state()}
| {ok, false, state()}
| {error, Reason :: binary(), state()}
when Number :: non_neg_integer(),
Expand Down Expand Up @@ -455,106 +476,138 @@ handle_command(<<"LISTGROUP ", Group/binary>> = Cmd, Client) ->

{reply, Reply, Client#client{state = NewState}};

% Both ARTICLE, HEAD, BODY, and STAT have similar response.
handle_command(<<"ARTICLE", Arg/binary>>, Client) ->
handle_article(<<"ARTICLE">>, Arg, Client);

handle_command(<<"HEAD", Arg/binary>>, Client) ->
handle_article(<<"HEAD">>, Arg, Client);

handle_command(<<"BODY", Arg/binary>>, Client) ->
handle_article(<<"BODY">>, Arg, Client);

handle_command(<<"STAT", Arg/binary>>, Client) ->
handle_article(<<"STAT">>, Arg, Client);

% This command provides a short summary of the commands that are
% understood by this implementation of the server. The help text will
% be presented as a multi-line data block following the 100 response
% code. This text is not guaranteed to be in any particular format (but must
% be UTF-8) and MUST NOT be used by clients as a replacement for the
% CAPABILITIES command
handle_command(<<"HELP">>, Client) ->
#client{module = Module, state = State} = Client,
{ok, Help, NewState} = Module:handle_HELP(State),

Reply = [
<<"100 Help text follows\r\n">>,
Help, <<"\r\n">>,
<<".">>
],
{reply, Reply, Client#client{state = NewState}};

% The client uses the QUIT command to terminate the session. The server
% MUST acknowledge the QUIT command and then close the connection to
% the client.
handle_command(<<"QUIT">>, Client) ->
{stop, normal, <<"205 Connection closing">>, Client};

% Any other commands.
handle_command(Command, Client) ->
#client{module = Module, state = State} = Client,

case Module:handle_command(Command, State) of
{reply, Reply, State1} ->
{reply, Reply, Client#client{state = State1}};
{noreply, State1} ->
{noreply, Client#client{state = State1}};
{stop, Reason, State1} ->
{stop, Reason, Client#client{state = State1}};
{stop, Reason, Reply, State1} ->
{stop, Reason, Reply, Client#client{state = State1}}
end.

% Trim the leading whitespace if any.
handle_article(Type, <<" ", Arg/binary>>, Client) ->
handle_article(Type, Arg, Client);

% The currently selected group is invalid, and no argument is specified.
handle_command(<<"ARTICLE">>, #client{group = invalid} = Client) ->
handle_article(_Type, <<"">>, #client{group = invalid} = Client) ->
{reply, <<"412 No newsgroup selected">>, Client};

% Have currently selected group, but current article number is invalid.
handle_command(<<"ARTICLE">>, #client{article_number = invalid} = Client) ->
handle_article(_Type, <<"">>, #client{article_number = invalid} = Client) ->
{reply, <<"420 Current article number is invalid">>, Client};

handle_command(<<"ARTICLE">>, #client{article_number = ArticleNumber} = Client) ->
handle_article(Type, <<"">>, #client{article_number = ArticleNumber} = Client) ->
% @FIXME: Double conversion.
ArticleNumberBinary = integer_to_binary(ArticleNumber),
handle_command(<<"ARTICLE ", ArticleNumberBinary/binary>>, Client);
handle_article(Type, ArticleNumberBinary, Client);

% Client requests for an article by message ID or article number.
handle_command(<<"ARTICLE ", Arg/binary>> = Cmd, Client) ->
handle_article(Type, Arg, Client) ->
#client{module = Module, group = CurrentGroup, state = State} = Client,
% Asks the callback module to provide the capacitities at this moment.
{ok, Capabilities, State1} = Module:handle_CAPABILITIES(State),

{Reply, NewState} = case is_capable(Cmd, Capabilities) of
{Reply, NewState} = case is_capable(Type, Capabilities) of
true ->
SuccessCode = case Type of
<<"ARTICLE">> -> <<"220">>;
<<"HEAD">> -> <<"221">>;
<<"BODY">> -> <<"222">>;
<<"STAT">> -> <<"223">>
end,

Callback = binary_to_existing_atom(<<"handle_", Type/binary>>),

% Checks if the argument is a number or a message ID.
try {CurrentGroup, binary_to_integer(Arg)} of
% Argument is a number, but current group is invalid, responds with
{invalid, _ArticleNumber} ->
{<<"412 No newsgroup selected">>, State1};
% Argument is a number, and current group is valid, ask the callback for article.
{_, ArticleNumber} ->
{ok, ArticleInfo, State2} = Module:handle_ARTICLE({ArticleNumber, CurrentGroup}, State1),
{ok, ArticleInfo, State2} = apply(Module, Callback, [{ArticleNumber, CurrentGroup}, State1]),
case ArticleInfo of
false -> {<<"423 No article with that number">>, State2};

% Article specified by article number exists
{Number, {Id, Headers, Body}} ->
{Number, #{id := Id} = Article} ->
Response = join(<<"\r\n">>, [
join(<<" ">>, [<<"220">>, integer_to_binary(Number), Id]),
article(Headers, Body),
join(<<" ">>, [SuccessCode, integer_to_binary(Number), Id]),
to_binary(Article),
<<".">>
]),
{Response, State2}
end
catch
error:badarg ->
{ok, ArticleInfo, State2} = Module:handle_ARTICLE(Arg, State1),
{ok, ArticleInfo, State2} = apply(Module, Callback, [Arg, State1]),
case ArticleInfo of
false -> {<<"430 No article with that message-id">>, State2};

% Article specified by message ID exists
{Number, {Id, Headers, Body}} ->
Response = join(<<"\r\n">>, [
join(<<" ">>, [<<"220">>, integer_to_binary(Number), Id]),
article(Headers, Body),
<<".">>
]),
{Number, #{id := Id} = Article} ->
Line = join(<<" ">>, [SuccessCode, integer_to_binary(Number), Id]),

Response = case to_binary(Article) of
<<"">> -> Line;
MultiLine -> [
Line, <<"\r\n">>,
MultiLine, <<"\r\n">>,
<<".">>
]
end,

{Response, State2}
end
end;
false ->
{<<"430 No article with that message-id">>, State1}
end,

{reply, Reply, Client#client{state = NewState}};

% This command provides a short summary of the commands that are
% understood by this implementation of the server. The help text will
% be presented as a multi-line data block following the 100 response
% code. This text is not guaranteed to be in any particular format (but must
% be UTF-8) and MUST NOT be used by clients as a replacement for the
% CAPABILITIES command
handle_command(<<"HELP">>, Client) ->
#client{module = Module, state = State} = Client,
{ok, Help, NewState} = Module:handle_HELP(State),

Reply = [
<<"100 Help text follows\r\n">>,
Help, <<"\r\n">>,
<<".">>
],
{reply, Reply, Client#client{state = NewState}};

% The client uses the QUIT command to terminate the session. The server
% MUST acknowledge the QUIT command and then close the connection to
% the client.
handle_command(<<"QUIT">>, Client) ->
{stop, normal, <<"205 Connection closing">>, Client};

% Any other commands.
handle_command(Command, Client) ->
#client{module = Module, state = State} = Client,

case Module:handle_command(Command, State) of
{reply, Reply, State1} ->
{reply, Reply, Client#client{state = State1}};
{noreply, State1} ->
{noreply, Client#client{state = State1}};
{stop, Reason, State1} ->
{stop, Reason, Client#client{state = State1}};
{stop, Reason, Reply, State1} ->
{stop, Reason, Reply, Client#client{state = State1}}
end.
{reply, Reply, Client#client{state = NewState}}.

%% ==================================================================
%% Internal Funtions
Expand Down Expand Up @@ -599,21 +652,43 @@ is_capable(<<"LISTGROUP", _Arg/binary>>, Capabilities) ->
is_capable(<<"READER">>, Capabilities);
is_capable(<<"ARTICLE", _Arg/binary>>, Capabilities) ->
is_capable(<<"READER">>, Capabilities);
is_capable(<<"HEAD", _Arg/binary>>, Capabilities) ->
is_capable(<<"READER">>, Capabilities);
is_capable(<<"BODY", _Arg/binary>>, Capabilities) ->
is_capable(<<"READER">>, Capabilities);
is_capable(<<"STAT", _Arg/binary>>, Capabilities) ->
is_capable(<<"READER">>, Capabilities);

is_capable(Capability, Capabilities) ->
lists:member(Capability, Capabilities).

% Join binary
%% @private
join(_Separator, []) ->
<<>>;
<<>>;
join(Separator, [H|T]) ->
lists:foldl(fun (Value, Acc) -> <<Acc/binary, Separator/binary, Value/binary>> end, H, T).
lists:foldl(fun (Value, Acc) ->
<<Acc/binary, Separator/binary, Value/binary>>
end, H, T).

article(Headers, Body) ->
% Full article
to_binary(#{headers := Headers, body := Body}) ->
join(<<"\r\n">>, [
maps:fold(fun(Header, Content, AccIn) ->
<<AccIn/binary, Header/binary, ": ", Content/binary, "\r\n">>
end, <<"">>, Headers),
to_binary(#{headers => Headers}),
<<"">>,
Body
]).
]);
% Headers only
to_binary(#{headers := Headers}) ->
join(
<<"\r\n">>,
lists:map(fun({Header, Content}) ->
<<Header/binary, ": ", Content/binary>>
end, maps:to_list(Headers))
);
% Body only
to_binary(#{body := Body}) ->
Body;

to_binary(#{id := _Id}) ->
<<"">>.
Loading

0 comments on commit 73f3be7

Please sign in to comment.