aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ftp/src/ftp_response.erl
blob: 8d00153ba8543bd90941468a72d4ec9542fb3754 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2005-2018. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%
%%
%% Description: This module impements handling of ftp server responses.

-module(ftp_response).

%% Internal API
-export([parse_lines/3, interpret/1, error_string/1]).

-include("ftp_internal.hrl").

%% First group of reply code digits 
-define(POS_PREL, 1).
-define(POS_COMPL, 2).
-define(POS_INTERM, 3).
-define(TRANS_NEG_COMPL, 4).
-define(PERM_NEG_COMPL, 5).
%% Second group of reply code digits
-define(SYNTAX,0).
-define(INFORMATION,1).
-define(CONNECTION,2).
-define(AUTH_ACC,3).
-define(UNSPEC,4).
-define(FILE_SYSTEM,5).

%%%=========================================================================
%%%  INTERNAL API 
%%%=========================================================================

%%--------------------------------------------------------------------------
%% parse_lines(Data, AccLines, StatusCode)  -> {ok, Lines} | 
%%                                             {continue, {Data, 
%%                                                       AccLines, StatusCode}}
%%
%% Data = binary() - data recived on the control connection from the 
%%                   ftp-server. 
%% AccLines = [string()] 
%% StatusCode     =  start | {byte(), byte(), byte()} | finish -
%%                   Indicates where in the parsing process we are.
%%                   start - (looking for the status code of the message)
%%                   {byte(), byte(), byte()} - status code found, now
%%                   looking for the last line indication.
%%                   finish - now on the last line.
%% Description: Parses a ftp control response message. 
%%      "A reply is defined to contain the 3-digit code, followed by Space
%%      <SP>, followed by one line of text (where some maximum line length
%%      has been specified), and terminated by the Telnet end-of-line
%%      code (CRLF), or a so called multilined reply for example:
%%
%%                                123-First line
%%                                Second line
%%                                  234 A line beginning with numbers
%%                                123 The last line
%%
%%         The user-process then simply needs to search for the second
%%         occurrence of the same reply code, followed by <SP> (Space), at
%%         the beginning of a line, and ignore all intermediary lines.  If
%%         an intermediary line begins with a 3-digit number, the Server
%%         will pad the front to avoid confusion.
%%--------------------------------------------------------------------------

%% Make sure we received the first 4 bytes so we know how to parse
%% the FTP server response e.i. is the response composed of one
%% or multiple lines.
parse_lines(Bin, Lines, start) when size(Bin) < 4 ->
    {continue, {Bin, Lines, start}};
%% Multiple lines exist
parse_lines(<<C1, C2, C3, $-, Rest/binary>>, Lines, start) ->
    parse_lines(Rest, [$-, C3, C2, C1 | Lines], {C1, C2, C3});
%% Only one line exists
parse_lines(<<C1, C2, C3, ?WHITE_SPACE, Bin/binary>>, Lines, start) ->
    parse_lines(Bin, [?WHITE_SPACE, C3, C2, C1 | Lines], finish);

%% Last line found
parse_lines(<<?CR, ?LF, C1, C2, C3, ?WHITE_SPACE, Rest/binary>>, Lines, {C1, C2, C3}) ->
    parse_lines(Rest, [?WHITE_SPACE, C3, C2, C1, ?LF, ?CR | Lines], finish);
%% Potential end found  wait for more data 
parse_lines(<<?CR, ?LF, C1, C2, C3>> = Bin, Lines, {C1, C2, C3}) ->
    {continue, {Bin, Lines, {C1, C2, C3}}};
%% Intermidate line begining with status code
parse_lines(<<?CR, ?LF, C1, C2, C3, Rest/binary>>, Lines, {C1, C2, C3}) ->
    parse_lines(Rest, [C3, C2, C1, ?LF, ?CR  | Lines], {C1, C2, C3});

%% Potential last line wait for more data
parse_lines(<<?CR, ?LF, C1, C2>> = Data, Lines, {C1, C2, _} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?CR, ?LF, C1>> = Data, Lines, {C1, _, _} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?CR, ?LF>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<?LF>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
parse_lines(<<>> = Data, Lines, {_,_,_} = StatusCode) ->
    {continue, {Data, Lines, StatusCode}};
%% Part of the multiple lines
parse_lines(<<Octet, Rest/binary>>, Lines, {_,_, _} = StatusCode) ->
    parse_lines(Rest, [Octet | Lines], StatusCode);

%% End of FTP server response found
parse_lines(<<?CR, ?LF>>, Lines, finish) ->
    {ok, lists:reverse([?LF, ?CR | Lines]), <<>>}; 
parse_lines(<<?CR, ?LF, Rest/binary>>, Lines, finish) ->
    {ok, lists:reverse([?LF, ?CR | Lines]), Rest}; 

%% Potential end found  wait for more data 
parse_lines(<<?CR>> = Data, Lines, finish) ->
    {continue, {Data, Lines, finish}};
parse_lines(<<>> = Data, Lines, finish) ->
    {continue, {Data, Lines, finish}};
%% Part of last line
parse_lines(<<Octet, Rest/binary>>, Lines, finish) ->
    parse_lines(Rest, [Octet | Lines], finish).

%%--------------------------------------------------------------------------
%% interpret(Lines) ->  {Status, Text} 
%%	Lines  = [byte(), byte(), byte() | Text] - ftp server response as
%%      returned by parse_lines/3
%%	Stauts = atom() (see interpret_status/3)
%%      Text = [string()]
%%
%% Description: Create nicer data to match on.
%%--------------------------------------------------------------------------
interpret([Didgit1, Didgit2, Didgit3 | Data]) ->
    Code1 = Didgit1 - $0,
    Code2 = Didgit2 - $0,
    Code3 = Didgit3 - $0,
    {interpret_status(Code1, Code2, Code3), Data}.

%%--------------------------------------------------------------------------
%% error_string(Error) -> string()
%%	Error =  {error, term()} | term()
%%
%% Description: Translates error codes into strings intended for
%% human interpretation.
%%--------------------------------------------------------------------------
error_string({error, Reason}) ->
    error_string(Reason);

error_string(echunk) -> "Synchronisation error during chunk sending.";
error_string(eclosed) -> "Session has been closed.";
error_string(econn) ->  "Connection to remote server prematurely closed.";
error_string(eexists) ->"File or directory already exists.";
error_string(ehost) ->  "Host not found, FTP server not found, "
		      "or connection rejected.";
error_string(elogin) -> "User not logged in.";
error_string(enotbinary) -> "Term is not a binary.";
error_string(epath) ->  "No such file or directory, already exists, "
		      "or permission denied.";
error_string(etype) ->  "No such type.";
error_string(euser) ->  "User name or password not valid.";
error_string(etnospc) -> "Insufficient storage space in system.";
error_string(enofile) -> "No files found or file unavailable";
error_string(epnospc) -> "Exceeded storage allocation "
		       "(for current directory or dataset).";
error_string(efnamena) -> "File name not allowed.";
error_string(Reason) -> 
    lists:flatten(io_lib:format("Unknown error: ~w", [Reason])).

%%%========================================================================
%%% Internal functions
%%%========================================================================

%% Positive Preleminary Reply
interpret_status(?POS_PREL,_,_)                   -> pos_prel; 
%%FIXME ??? 3??? interpret_status(?POS_COMPL, ?AUTH_ACC, 3)        -> tls_upgrade; 
interpret_status(?POS_COMPL, ?AUTH_ACC, 4)        -> tls_upgrade; 
%% Positive Completion Reply
interpret_status(?POS_COMPL,_,_)                  -> pos_compl;
%% Positive Intermediate Reply nedd account
interpret_status(?POS_INTERM,?AUTH_ACC,2)         -> pos_interm_acct;
%% Positive Intermediate Reply
interpret_status(?POS_INTERM,_,_)                 -> pos_interm; 
%% No files found or file not available
interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,0) -> enofile;
%% No storage area no action taken
interpret_status(?TRANS_NEG_COMPL,?FILE_SYSTEM,2) -> etnospc;
%% Temporary Error, no action taken
interpret_status(?TRANS_NEG_COMPL,_,_)            -> trans_neg_compl;
%% Permanent disk space error, the user shall not try again
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,0)  -> epath;
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,2)  -> epnospc;
interpret_status(?PERM_NEG_COMPL,?FILE_SYSTEM,3)  -> efnamena; 
interpret_status(?PERM_NEG_COMPL,?AUTH_ACC,0)     -> elogin; 
interpret_status(?PERM_NEG_COMPL,_,_)             -> perm_neg_compl.