2004-03-02 22:16:55 +01:00
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% File : ejabberd_http.erl
|
|
|
|
%%% Author : Alexey Shchepin <alexey@sevcom.net>
|
|
|
|
%%% Purpose :
|
|
|
|
%%% Created : 27 Feb 2004 by Alexey Shchepin <alexey@sevcom.net>
|
|
|
|
%%% Id : $Id$
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
|
|
|
-module(ejabberd_http).
|
|
|
|
-author('alexey@sevcom.net').
|
2006-02-03 04:28:15 +01:00
|
|
|
-vsn('$Revision$ ').
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
%% External exports
|
|
|
|
-export([start/2,
|
|
|
|
start_link/2,
|
2006-01-13 02:55:20 +01:00
|
|
|
become_controller/1,
|
2005-11-22 19:25:02 +01:00
|
|
|
receive_headers/1,
|
|
|
|
url_encode/1]).
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
-include("ejabberd.hrl").
|
|
|
|
-include("jlib.hrl").
|
2004-03-04 21:56:32 +01:00
|
|
|
-include("ejabberd_http.hrl").
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
-record(state, {sockmod,
|
|
|
|
socket,
|
|
|
|
request_method,
|
2004-07-06 23:34:50 +02:00
|
|
|
request_version,
|
2004-03-02 22:16:55 +01:00
|
|
|
request_path,
|
|
|
|
request_auth,
|
2004-09-30 23:54:39 +02:00
|
|
|
request_keepalive,
|
2004-05-01 22:10:25 +02:00
|
|
|
request_content_length,
|
2004-05-22 21:48:35 +02:00
|
|
|
request_lang = "en",
|
|
|
|
use_http_poll = false,
|
2004-09-25 22:52:20 +02:00
|
|
|
use_web_admin = false,
|
|
|
|
end_of_request = false,
|
|
|
|
trail = ""
|
2004-03-02 22:16:55 +01:00
|
|
|
}).
|
|
|
|
|
|
|
|
|
|
|
|
-define(XHTML_DOCTYPE,
|
2004-05-04 21:47:24 +02:00
|
|
|
"<?xml version='1.0'?>\n"
|
2004-03-02 22:16:55 +01:00
|
|
|
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
|
|
|
|
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n").
|
|
|
|
|
2004-04-26 17:38:07 +02:00
|
|
|
-define(HTML_DOCTYPE,
|
2005-11-26 19:56:39 +01:00
|
|
|
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
|
|
|
|
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n").
|
2004-04-26 17:38:07 +02:00
|
|
|
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
start(SockData, Opts) ->
|
|
|
|
supervisor:start_child(ejabberd_http_sup, [SockData, Opts]).
|
|
|
|
|
|
|
|
start_link({SockMod, Socket}, Opts) ->
|
2004-09-25 22:52:20 +02:00
|
|
|
TLSEnabled = lists:member(tls, Opts),
|
|
|
|
TLSOpts = lists:filter(fun({certfile, _}) -> true;
|
|
|
|
(_) -> false
|
|
|
|
end, Opts),
|
|
|
|
{SockMod1, Socket1} =
|
|
|
|
if
|
|
|
|
TLSEnabled ->
|
|
|
|
inet:setopts(Socket, [{recbuf, 8192}]),
|
|
|
|
{ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts),
|
|
|
|
{tls, TLSSocket};
|
|
|
|
true ->
|
|
|
|
{SockMod, Socket}
|
|
|
|
end,
|
|
|
|
case SockMod1 of
|
2004-03-02 22:16:55 +01:00
|
|
|
gen_tcp ->
|
2004-09-25 22:52:20 +02:00
|
|
|
inet:setopts(Socket1, [{packet, http}, {recbuf, 8192}]);
|
|
|
|
_ ->
|
|
|
|
ok
|
2004-03-02 22:16:55 +01:00
|
|
|
end,
|
2004-05-22 21:48:35 +02:00
|
|
|
UseHTTPPoll = lists:member(http_poll, Opts),
|
|
|
|
UseWebAdmin = lists:member(web_admin, Opts),
|
2004-09-25 22:52:20 +02:00
|
|
|
?DEBUG("S: ~p~n", [{UseHTTPPoll, UseWebAdmin}]),
|
|
|
|
?INFO_MSG("started: ~p", [{SockMod1, Socket1}]),
|
2004-03-02 22:16:55 +01:00
|
|
|
{ok, proc_lib:spawn_link(ejabberd_http,
|
|
|
|
receive_headers,
|
2004-09-25 22:52:20 +02:00
|
|
|
[#state{sockmod = SockMod1,
|
|
|
|
socket = Socket1,
|
2004-05-22 21:48:35 +02:00
|
|
|
use_http_poll = UseHTTPPoll,
|
|
|
|
use_web_admin = UseWebAdmin}])}.
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
|
2006-01-13 02:55:20 +01:00
|
|
|
become_controller(_Pid) ->
|
|
|
|
ok.
|
|
|
|
|
2004-03-02 22:16:55 +01:00
|
|
|
send_text(State, Text) ->
|
|
|
|
(State#state.sockmod):send(State#state.socket, Text).
|
|
|
|
|
|
|
|
|
|
|
|
receive_headers(State) ->
|
2004-04-26 17:38:07 +02:00
|
|
|
SockMod = State#state.sockmod,
|
|
|
|
Socket = State#state.socket,
|
|
|
|
Data = SockMod:recv(Socket, 0, 300000),
|
2004-09-25 22:52:20 +02:00
|
|
|
case State#state.sockmod of
|
|
|
|
gen_tcp ->
|
|
|
|
NewState = process_header(State, Data),
|
|
|
|
case NewState#state.end_of_request of
|
|
|
|
true ->
|
|
|
|
ok;
|
|
|
|
_ ->
|
|
|
|
receive_headers(NewState)
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
case Data of
|
|
|
|
{ok, Binary} ->
|
|
|
|
{Request, Trail} = parse_request(
|
|
|
|
State,
|
|
|
|
State#state.trail ++ binary_to_list(Binary)),
|
|
|
|
State1 = State#state{trail = Trail},
|
|
|
|
NewState = lists:foldl(
|
|
|
|
fun(D, S) ->
|
|
|
|
case S#state.end_of_request of
|
|
|
|
true ->
|
|
|
|
S;
|
|
|
|
_ ->
|
|
|
|
process_header(S, D)
|
|
|
|
end
|
|
|
|
end, State1, Request),
|
|
|
|
case NewState#state.end_of_request of
|
|
|
|
true ->
|
|
|
|
ok;
|
|
|
|
_ ->
|
|
|
|
receive_headers(NewState)
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
ok
|
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
|
|
|
process_header(State, Data) ->
|
|
|
|
SockMod = State#state.sockmod,
|
|
|
|
Socket = State#state.socket,
|
2004-03-02 22:16:55 +01:00
|
|
|
case Data of
|
2004-07-06 23:34:50 +02:00
|
|
|
{ok, {http_request, Method, Path, Version}} ->
|
2004-09-30 23:54:39 +02:00
|
|
|
KeepAlive = case Version of
|
|
|
|
{1, 1} ->
|
|
|
|
true;
|
|
|
|
_ ->
|
|
|
|
false
|
|
|
|
end,
|
2004-09-25 22:52:20 +02:00
|
|
|
State#state{request_method = Method,
|
|
|
|
request_version = Version,
|
2004-09-30 23:54:39 +02:00
|
|
|
request_path = Path,
|
|
|
|
request_keepalive = KeepAlive};
|
|
|
|
{ok, {http_header, _, 'Connection', _, Conn}} ->
|
2005-05-23 01:29:54 +02:00
|
|
|
KeepAlive1 = case jlib:tolower(Conn) of
|
|
|
|
"keep-alive" ->
|
|
|
|
true;
|
|
|
|
"close" ->
|
|
|
|
false;
|
|
|
|
_ ->
|
|
|
|
State#state.request_keepalive
|
|
|
|
end,
|
2004-09-30 23:54:39 +02:00
|
|
|
State#state{request_keepalive = KeepAlive1};
|
2004-03-02 22:16:55 +01:00
|
|
|
{ok, {http_header, _, 'Authorization', _, Auth}} ->
|
2004-09-25 22:52:20 +02:00
|
|
|
State#state{request_auth = parse_auth(Auth)};
|
2004-03-02 22:16:55 +01:00
|
|
|
{ok, {http_header, _, 'Content-Length', _, SLen}} ->
|
|
|
|
case catch list_to_integer(SLen) of
|
|
|
|
Len when is_integer(Len) ->
|
2004-09-25 22:52:20 +02:00
|
|
|
State#state{request_content_length = Len};
|
2004-03-02 22:16:55 +01:00
|
|
|
_ ->
|
2004-09-25 22:52:20 +02:00
|
|
|
State
|
2004-03-02 22:16:55 +01:00
|
|
|
end;
|
2004-05-01 22:10:25 +02:00
|
|
|
{ok, {http_header, _, 'Accept-Language', _, Langs}} ->
|
2004-09-25 22:52:20 +02:00
|
|
|
State#state{request_lang = parse_lang(Langs)};
|
2004-03-02 22:16:55 +01:00
|
|
|
{ok, {http_header, _, _, _, _}} ->
|
2004-09-25 22:52:20 +02:00
|
|
|
State;
|
2004-03-02 22:16:55 +01:00
|
|
|
{ok, http_eoh} ->
|
2004-03-10 22:54:54 +01:00
|
|
|
?INFO_MSG("(~w) http query: ~w ~s~n",
|
|
|
|
[State#state.socket,
|
|
|
|
State#state.request_method,
|
|
|
|
element(2, State#state.request_path)]),
|
2004-03-02 22:16:55 +01:00
|
|
|
Out = process_request(State),
|
|
|
|
send_text(State, Out),
|
2004-09-30 23:54:39 +02:00
|
|
|
case State#state.request_keepalive of
|
|
|
|
true ->
|
2004-07-06 23:34:50 +02:00
|
|
|
case SockMod of
|
|
|
|
gen_tcp ->
|
|
|
|
inet:setopts(Socket, [{packet, http}]);
|
2004-09-25 22:52:20 +02:00
|
|
|
_ ->
|
|
|
|
ok
|
2004-07-06 23:34:50 +02:00
|
|
|
end,
|
2004-09-25 22:52:20 +02:00
|
|
|
#state{sockmod = SockMod,
|
|
|
|
socket = Socket,
|
|
|
|
use_http_poll = State#state.use_http_poll,
|
|
|
|
use_web_admin = State#state.use_web_admin};
|
2004-07-06 23:34:50 +02:00
|
|
|
_ ->
|
2004-09-25 22:52:20 +02:00
|
|
|
#state{end_of_request = true}
|
2004-07-06 23:34:50 +02:00
|
|
|
end;
|
2004-03-02 22:16:55 +01:00
|
|
|
{error, _Reason} ->
|
2004-09-25 22:52:20 +02:00
|
|
|
#state{end_of_request = true};
|
2004-03-02 22:16:55 +01:00
|
|
|
_ ->
|
2004-09-25 22:52:20 +02:00
|
|
|
#state{end_of_request = true}
|
2004-03-02 22:16:55 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
process_request(#state{request_method = 'GET',
|
|
|
|
request_path = {abs_path, Path},
|
2004-05-01 22:10:25 +02:00
|
|
|
request_auth = Auth,
|
2004-05-22 21:48:35 +02:00
|
|
|
request_lang = Lang,
|
|
|
|
use_http_poll = UseHTTPPoll,
|
2005-05-23 01:29:54 +02:00
|
|
|
use_web_admin = UseWebAdmin} = State) ->
|
2006-02-03 04:28:15 +01:00
|
|
|
case (catch url_decode_q_split(Path)) of
|
|
|
|
{'EXIT', _} ->
|
|
|
|
process_request(false);
|
|
|
|
{NPath, Query} ->
|
|
|
|
LQuery = case (catch parse_urlencoded(Query)) of
|
|
|
|
{'EXIT', _Reason} ->
|
|
|
|
[];
|
|
|
|
LQ ->
|
|
|
|
LQ
|
|
|
|
end,
|
|
|
|
LPath = string:tokens(NPath, "/"),
|
|
|
|
Request = #request{method = 'GET',
|
|
|
|
path = LPath,
|
|
|
|
q = LQuery,
|
|
|
|
auth = Auth,
|
|
|
|
lang = Lang},
|
|
|
|
case ejabberd_web:process_get({UseHTTPPoll, UseWebAdmin},
|
|
|
|
Request) of
|
|
|
|
El when element(1, El) == xmlelement ->
|
|
|
|
make_xhtml_output(State, 200, [], El);
|
|
|
|
{Status, Headers, El} when
|
|
|
|
element(1, El) == xmlelement ->
|
|
|
|
make_xhtml_output(State, Status, Headers, El);
|
|
|
|
Text when is_list(Text) ->
|
|
|
|
make_text_output(State, 200, [], Text);
|
|
|
|
{Status, Headers, Text} when
|
|
|
|
is_list(Text) ->
|
|
|
|
make_text_output(State, Status, Headers, Text)
|
2004-03-04 21:56:32 +01:00
|
|
|
end
|
2004-03-02 22:16:55 +01:00
|
|
|
end;
|
|
|
|
|
|
|
|
process_request(#state{request_method = 'POST',
|
|
|
|
request_path = {abs_path, Path},
|
2004-03-04 21:56:32 +01:00
|
|
|
request_auth = Auth,
|
2004-03-02 22:16:55 +01:00
|
|
|
request_content_length = Len,
|
2004-05-01 22:10:25 +02:00
|
|
|
request_lang = Lang,
|
2004-03-02 22:16:55 +01:00
|
|
|
sockmod = SockMod,
|
2004-05-22 21:48:35 +02:00
|
|
|
socket = Socket,
|
|
|
|
use_http_poll = UseHTTPPoll,
|
|
|
|
use_web_admin = UseWebAdmin} = State)
|
|
|
|
when is_integer(Len) ->
|
2006-02-03 04:28:15 +01:00
|
|
|
case SockMod of
|
|
|
|
gen_tcp ->
|
|
|
|
inet:setopts(Socket, [{packet, 0}]);
|
2004-03-04 21:56:32 +01:00
|
|
|
_ ->
|
2006-02-03 04:28:15 +01:00
|
|
|
ok
|
|
|
|
end,
|
|
|
|
Data = recv_data(State, Len),
|
|
|
|
?DEBUG("client data: ~p~n", [Data]),
|
|
|
|
case (catch url_decode_q_split(Path)) of
|
|
|
|
{'EXIT', _} ->
|
|
|
|
process_request(false);
|
|
|
|
{NPath, Query} ->
|
|
|
|
LPath = string:tokens(NPath, "/"),
|
|
|
|
LQuery = case (catch parse_urlencoded(Data)) of
|
|
|
|
{'EXIT', _Reason} ->
|
|
|
|
[];
|
|
|
|
LQ ->
|
|
|
|
LQ
|
|
|
|
end,
|
|
|
|
Request = #request{method = 'POST',
|
|
|
|
path = LPath,
|
|
|
|
q = LQuery,
|
|
|
|
auth = Auth,
|
|
|
|
data = Data,
|
|
|
|
lang = Lang},
|
|
|
|
case ejabberd_web:process_get({UseHTTPPoll, UseWebAdmin},
|
|
|
|
Request) of
|
|
|
|
El when element(1, El) == xmlelement ->
|
|
|
|
make_xhtml_output(State, 200, [], El);
|
|
|
|
{Status, Headers, El} when
|
|
|
|
element(1, El) == xmlelement ->
|
|
|
|
make_xhtml_output(State, Status, Headers, El);
|
|
|
|
Text when is_list(Text) ->
|
|
|
|
make_text_output(State, 200, [], Text);
|
|
|
|
{Status, Headers, Text} when is_list(Text) ->
|
|
|
|
make_text_output(State, Status, Headers, Text)
|
2004-03-04 21:56:32 +01:00
|
|
|
end
|
2004-03-02 22:16:55 +01:00
|
|
|
end;
|
|
|
|
|
2005-05-23 01:29:54 +02:00
|
|
|
process_request(State) ->
|
|
|
|
make_xhtml_output(State,
|
2004-03-02 22:16:55 +01:00
|
|
|
400,
|
|
|
|
[],
|
|
|
|
ejabberd_web:make_xhtml([{xmlelement, "h1", [],
|
|
|
|
[{xmlcdata, "400 Bad Request"}]}])).
|
|
|
|
|
|
|
|
|
|
|
|
recv_data(State, Len) ->
|
|
|
|
recv_data(State, Len, []).
|
|
|
|
|
|
|
|
recv_data(State, 0, Acc) ->
|
|
|
|
binary_to_list(list_to_binary(Acc));
|
|
|
|
recv_data(State, Len, Acc) ->
|
2004-09-25 22:52:20 +02:00
|
|
|
case State#state.trail of
|
|
|
|
[] ->
|
|
|
|
case (State#state.sockmod):recv(State#state.socket, Len, 300000) of
|
|
|
|
{ok, Data} ->
|
|
|
|
recv_data(State, Len - size(Data), [Acc | Data]);
|
|
|
|
_ ->
|
|
|
|
""
|
|
|
|
end;
|
2004-03-02 22:16:55 +01:00
|
|
|
_ ->
|
2004-09-25 22:52:20 +02:00
|
|
|
Trail = State#state.trail,
|
|
|
|
recv_data(State#state{trail = ""}, Len - length(Trail), [Acc | Trail])
|
2004-03-02 22:16:55 +01:00
|
|
|
end.
|
|
|
|
|
|
|
|
|
2005-05-23 01:29:54 +02:00
|
|
|
make_xhtml_output(State, Status, Headers, XHTML) ->
|
2004-04-26 17:38:07 +02:00
|
|
|
Data = case lists:member(html, Headers) of
|
|
|
|
true ->
|
|
|
|
list_to_binary([?HTML_DOCTYPE,
|
|
|
|
xml:element_to_string(XHTML)]);
|
|
|
|
_ ->
|
|
|
|
list_to_binary([?XHTML_DOCTYPE,
|
|
|
|
xml:element_to_string(XHTML)])
|
|
|
|
end,
|
2004-03-13 21:01:37 +01:00
|
|
|
Headers1 = case lists:keysearch("Content-Type", 1, Headers) of
|
|
|
|
{value, _} ->
|
|
|
|
[{"Content-Length", integer_to_list(size(Data))} |
|
|
|
|
Headers];
|
|
|
|
_ ->
|
|
|
|
[{"Content-Type", "text/html; charset=utf-8"},
|
|
|
|
{"Content-Length", integer_to_list(size(Data))} |
|
|
|
|
Headers]
|
|
|
|
end,
|
2005-05-23 01:29:54 +02:00
|
|
|
HeadersOut = case {State#state.request_version,
|
|
|
|
State#state.request_keepalive} of
|
|
|
|
{{1, 1}, true} -> Headers1;
|
|
|
|
{_, true} ->
|
|
|
|
[{"Connection", "keep-alive"} | Headers1];
|
|
|
|
{_, false} ->
|
|
|
|
% not required for http versions < 1.1
|
|
|
|
% but would make no harm
|
|
|
|
[{"Connection", "close"} | Headers1]
|
|
|
|
end,
|
|
|
|
|
|
|
|
Version = case State#state.request_version of
|
|
|
|
{1, 1} -> "HTTP/1.1 ";
|
|
|
|
_ -> "HTTP/1.0 "
|
|
|
|
end,
|
|
|
|
|
2004-03-02 22:16:55 +01:00
|
|
|
H = lists:map(fun({Attr, Val}) ->
|
2004-04-26 17:38:07 +02:00
|
|
|
[Attr, ": ", Val, "\r\n"];
|
|
|
|
(_) ->
|
|
|
|
[]
|
2005-05-23 01:29:54 +02:00
|
|
|
end, HeadersOut),
|
|
|
|
SL = [Version, integer_to_list(Status), " ",
|
2004-03-02 22:16:55 +01:00
|
|
|
code_to_phrase(Status), "\r\n"],
|
|
|
|
[SL, H, "\r\n", Data].
|
2004-03-06 22:13:16 +01:00
|
|
|
|
2005-05-23 01:29:54 +02:00
|
|
|
make_text_output(State, Status, Headers, Text) ->
|
2004-03-06 22:13:16 +01:00
|
|
|
Data = list_to_binary(Text),
|
2004-03-13 21:01:37 +01:00
|
|
|
Headers1 = case lists:keysearch("Content-Type", 1, Headers) of
|
|
|
|
{value, _} ->
|
|
|
|
[{"Content-Length", integer_to_list(size(Data))} |
|
|
|
|
Headers];
|
|
|
|
_ ->
|
|
|
|
[{"Content-Type", "text/html; charset=utf-8"},
|
|
|
|
{"Content-Length", integer_to_list(size(Data))} |
|
|
|
|
Headers]
|
|
|
|
end,
|
2005-05-23 01:29:54 +02:00
|
|
|
|
|
|
|
HeadersOut = case {State#state.request_version,
|
|
|
|
State#state.request_keepalive} of
|
|
|
|
{{1, 1}, true} -> Headers1;
|
|
|
|
{_, true} ->
|
|
|
|
[{"Connection", "keep-alive"} | Headers1];
|
|
|
|
{_, false} ->
|
|
|
|
% not required for http versions < 1.1
|
|
|
|
% but would make no harm
|
|
|
|
[{"Connection", "close"} | Headers1]
|
|
|
|
end,
|
|
|
|
|
|
|
|
Version = case State#state.request_version of
|
|
|
|
{1, 1} -> "HTTP/1.1 ";
|
|
|
|
_ -> "HTTP/1.0 "
|
|
|
|
end,
|
|
|
|
|
2004-03-06 22:13:16 +01:00
|
|
|
H = lists:map(fun({Attr, Val}) ->
|
|
|
|
[Attr, ": ", Val, "\r\n"]
|
2005-05-23 01:29:54 +02:00
|
|
|
end, HeadersOut),
|
|
|
|
SL = [Version, integer_to_list(Status), " ",
|
2004-03-06 22:13:16 +01:00
|
|
|
code_to_phrase(Status), "\r\n"],
|
|
|
|
[SL, H, "\r\n", Data].
|
2005-05-23 01:29:54 +02:00
|
|
|
|
2004-03-02 22:16:55 +01:00
|
|
|
|
2004-05-01 22:10:25 +02:00
|
|
|
parse_lang(Langs) ->
|
|
|
|
case string:tokens(Langs, ",; ") of
|
|
|
|
[First | _] ->
|
|
|
|
First;
|
|
|
|
[] ->
|
|
|
|
"en"
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
2004-03-02 22:16:55 +01:00
|
|
|
|
|
|
|
% Code below is taken (with some modifications) from the yaws webserver, which
|
|
|
|
% is distributed under the folowing license:
|
|
|
|
%
|
|
|
|
% This software (the yaws webserver) is free software.
|
|
|
|
% Parts of this software is Copyright (c) Claes Wikstrom <klacke@hyber.org>
|
|
|
|
% Any use or misuse of the source code is hereby freely allowed.
|
|
|
|
%
|
|
|
|
% 1. Redistributions of source code must retain the above copyright
|
|
|
|
% notice as well as this list of conditions.
|
|
|
|
%
|
|
|
|
% 2. Redistributions in binary form must reproduce the above copyright
|
|
|
|
% notice as well as this list of conditions.
|
|
|
|
|
|
|
|
|
|
|
|
%% url decode the path and return {Path, QueryPart}
|
|
|
|
|
|
|
|
url_decode_q_split(Path) ->
|
|
|
|
url_decode_q_split(Path, []).
|
|
|
|
|
|
|
|
url_decode_q_split([$%, $C, $2, $%, Hi, Lo | Tail], Ack) ->
|
|
|
|
Hex = hex_to_integer([Hi, Lo]),
|
|
|
|
url_decode_q_split(Tail, [Hex|Ack]);
|
|
|
|
url_decode_q_split([$%, $C, $3, $%, Hi, Lo | Tail], Ack) when Hi > $9 ->
|
|
|
|
Hex = hex_to_integer([Hi+4, Lo]),
|
|
|
|
url_decode_q_split(Tail, [Hex|Ack]);
|
|
|
|
url_decode_q_split([$%, $C, $3, $%, Hi, Lo | Tail], Ack) when Hi < $A ->
|
|
|
|
Hex = hex_to_integer([Hi+4+7, Lo]),
|
|
|
|
url_decode_q_split(Tail, [Hex|Ack]);
|
|
|
|
url_decode_q_split([$%, Hi, Lo | Tail], Ack) ->
|
|
|
|
Hex = hex_to_integer([Hi, Lo]),
|
|
|
|
url_decode_q_split(Tail, [Hex|Ack]);
|
|
|
|
url_decode_q_split([$?|T], Ack) ->
|
|
|
|
%% Don't decode the query string here, that is parsed separately.
|
|
|
|
{path_norm_reverse(Ack), T};
|
|
|
|
url_decode_q_split([H|T], Ack) ->
|
|
|
|
url_decode_q_split(T, [H|Ack]);
|
|
|
|
url_decode_q_split([], Ack) ->
|
|
|
|
{path_norm_reverse(Ack), []}.
|
|
|
|
|
|
|
|
path_norm_reverse("/" ++ T) -> start_dir(0, "/", T);
|
|
|
|
path_norm_reverse( T) -> start_dir(0, "", T).
|
|
|
|
|
|
|
|
start_dir(N, Path, ".." ) -> rest_dir(N, Path, "");
|
|
|
|
start_dir(N, Path, "/" ++ T ) -> start_dir(N , Path, T);
|
|
|
|
start_dir(N, Path, "./" ++ T ) -> start_dir(N , Path, T);
|
|
|
|
start_dir(N, Path, "../" ++ T ) -> start_dir(N + 1, Path, T);
|
|
|
|
start_dir(N, Path, T ) -> rest_dir (N , Path, T).
|
|
|
|
|
|
|
|
rest_dir (_N, Path, [] ) -> case Path of
|
|
|
|
[] -> "/";
|
|
|
|
_ -> Path
|
|
|
|
end;
|
|
|
|
rest_dir (0, Path, [ $/ | T ] ) -> start_dir(0 , [ $/ | Path ], T);
|
|
|
|
rest_dir (N, Path, [ $/ | T ] ) -> start_dir(N - 1, Path , T);
|
|
|
|
rest_dir (0, Path, [ H | T ] ) -> rest_dir (0 , [ H | Path ], T);
|
|
|
|
rest_dir (N, Path, [ _H | T ] ) -> rest_dir (N , Path , T).
|
|
|
|
|
|
|
|
|
|
|
|
%% hex_to_integer
|
|
|
|
|
|
|
|
|
|
|
|
hex_to_integer(Hex) ->
|
|
|
|
case catch erlang:list_to_integer(Hex, 16) of
|
|
|
|
{'EXIT', _} ->
|
|
|
|
old_hex_to_integer(Hex);
|
|
|
|
X ->
|
|
|
|
X
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
old_hex_to_integer(Hex) ->
|
|
|
|
DEHEX = fun (H) when H >= $a, H =< $f -> H - $a + 10;
|
|
|
|
(H) when H >= $A, H =< $F -> H - $A + 10;
|
|
|
|
(H) when H >= $0, H =< $9 -> H - $0
|
|
|
|
end,
|
|
|
|
lists:foldl(fun(E, Acc) -> Acc*16+DEHEX(E) end, 0, Hex).
|
|
|
|
|
|
|
|
code_to_phrase(100) -> "Continue";
|
|
|
|
code_to_phrase(101) -> "Switching Protocols ";
|
|
|
|
code_to_phrase(200) -> "OK";
|
|
|
|
code_to_phrase(201) -> "Created";
|
|
|
|
code_to_phrase(202) -> "Accepted";
|
|
|
|
code_to_phrase(203) -> "Non-Authoritative Information";
|
|
|
|
code_to_phrase(204) -> "No Content";
|
|
|
|
code_to_phrase(205) -> "Reset Content";
|
|
|
|
code_to_phrase(206) -> "Partial Content";
|
|
|
|
code_to_phrase(300) -> "Multiple Choices";
|
|
|
|
code_to_phrase(301) -> "Moved Permanently";
|
|
|
|
code_to_phrase(302) -> "Found";
|
|
|
|
code_to_phrase(303) -> "See Other";
|
|
|
|
code_to_phrase(304) -> "Not Modified";
|
|
|
|
code_to_phrase(305) -> "Use Proxy";
|
|
|
|
code_to_phrase(306) -> "(Unused)";
|
|
|
|
code_to_phrase(307) -> "Temporary Redirect";
|
|
|
|
code_to_phrase(400) -> "Bad Request";
|
|
|
|
code_to_phrase(401) -> "Unauthorized";
|
|
|
|
code_to_phrase(402) -> "Payment Required";
|
|
|
|
code_to_phrase(403) -> "Forbidden";
|
|
|
|
code_to_phrase(404) -> "Not Found";
|
|
|
|
code_to_phrase(405) -> "Method Not Allowed";
|
|
|
|
code_to_phrase(406) -> "Not Acceptable";
|
|
|
|
code_to_phrase(407) -> "Proxy Authentication Required";
|
|
|
|
code_to_phrase(408) -> "Request Timeout";
|
|
|
|
code_to_phrase(409) -> "Conflict";
|
|
|
|
code_to_phrase(410) -> "Gone";
|
|
|
|
code_to_phrase(411) -> "Length Required";
|
|
|
|
code_to_phrase(412) -> "Precondition Failed";
|
|
|
|
code_to_phrase(413) -> "Request Entity Too Large";
|
|
|
|
code_to_phrase(414) -> "Request-URI Too Long";
|
|
|
|
code_to_phrase(415) -> "Unsupported Media Type";
|
|
|
|
code_to_phrase(416) -> "Requested Range Not Satisfiable";
|
|
|
|
code_to_phrase(417) -> "Expectation Failed";
|
|
|
|
code_to_phrase(500) -> "Internal Server Error";
|
|
|
|
code_to_phrase(501) -> "Not Implemented";
|
|
|
|
code_to_phrase(502) -> "Bad Gateway";
|
|
|
|
code_to_phrase(503) -> "Service Unavailable";
|
|
|
|
code_to_phrase(504) -> "Gateway Timeout";
|
|
|
|
code_to_phrase(505) -> "HTTP Version Not Supported".
|
|
|
|
|
|
|
|
|
|
|
|
parse_auth(Orig = "Basic " ++ Auth64) ->
|
|
|
|
case decode_base64(Auth64) of
|
|
|
|
{error, _Err} ->
|
|
|
|
undefined;
|
|
|
|
Auth ->
|
|
|
|
case string:tokens(Auth, ":") of
|
|
|
|
[User, Pass] ->
|
|
|
|
{User, Pass};
|
|
|
|
_ ->
|
|
|
|
undefined
|
|
|
|
end
|
|
|
|
end;
|
|
|
|
parse_auth(_) ->
|
|
|
|
undefined.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
decode_base64([]) ->
|
|
|
|
[];
|
|
|
|
decode_base64([Sextet1,Sextet2,$=,$=|Rest]) ->
|
|
|
|
Bits2x6=
|
|
|
|
(d(Sextet1) bsl 18) bor
|
|
|
|
(d(Sextet2) bsl 12),
|
|
|
|
Octet1=Bits2x6 bsr 16,
|
|
|
|
[Octet1|decode_base64(Rest)];
|
|
|
|
decode_base64([Sextet1,Sextet2,Sextet3,$=|Rest]) ->
|
|
|
|
Bits3x6=
|
|
|
|
(d(Sextet1) bsl 18) bor
|
|
|
|
(d(Sextet2) bsl 12) bor
|
|
|
|
(d(Sextet3) bsl 6),
|
|
|
|
Octet1=Bits3x6 bsr 16,
|
|
|
|
Octet2=(Bits3x6 bsr 8) band 16#ff,
|
|
|
|
[Octet1,Octet2|decode_base64(Rest)];
|
|
|
|
decode_base64([Sextet1,Sextet2,Sextet3,Sextet4|Rest]) ->
|
|
|
|
Bits4x6=
|
|
|
|
(d(Sextet1) bsl 18) bor
|
|
|
|
(d(Sextet2) bsl 12) bor
|
|
|
|
(d(Sextet3) bsl 6) bor
|
|
|
|
d(Sextet4),
|
|
|
|
Octet1=Bits4x6 bsr 16,
|
|
|
|
Octet2=(Bits4x6 bsr 8) band 16#ff,
|
|
|
|
Octet3=Bits4x6 band 16#ff,
|
|
|
|
[Octet1,Octet2,Octet3|decode_base64(Rest)];
|
|
|
|
decode_base64(_CatchAll) ->
|
|
|
|
{error, bad_base64}.
|
|
|
|
|
|
|
|
d(X) when X >= $A, X =<$Z ->
|
|
|
|
X-65;
|
|
|
|
d(X) when X >= $a, X =<$z ->
|
|
|
|
X-71;
|
|
|
|
d(X) when X >= $0, X =<$9 ->
|
|
|
|
X+4;
|
|
|
|
d($+) -> 62;
|
|
|
|
d($/) -> 63;
|
|
|
|
d(_) -> 63.
|
|
|
|
|
|
|
|
|
|
|
|
parse_urlencoded(S) ->
|
|
|
|
parse_urlencoded(S, nokey, [], key).
|
|
|
|
|
|
|
|
parse_urlencoded([$%, Hi, Lo | Tail], Last, Cur, State) ->
|
|
|
|
Hex = hex_to_integer([Hi, Lo]),
|
|
|
|
parse_urlencoded(Tail, Last, [Hex | Cur], State);
|
|
|
|
|
|
|
|
parse_urlencoded([$& | Tail], _Last, Cur, key) ->
|
|
|
|
[{lists:reverse(Cur), ""} |
|
|
|
|
parse_urlencoded(Tail, nokey, [], key)]; %% cont keymode
|
|
|
|
|
|
|
|
parse_urlencoded([$& | Tail], Last, Cur, value) ->
|
|
|
|
V = {Last, lists:reverse(Cur)},
|
|
|
|
[V | parse_urlencoded(Tail, nokey, [], key)];
|
|
|
|
|
|
|
|
parse_urlencoded([$+ | Tail], Last, Cur, State) ->
|
|
|
|
parse_urlencoded(Tail, Last, [$\s | Cur], State);
|
|
|
|
|
|
|
|
parse_urlencoded([$= | Tail], _Last, Cur, key) ->
|
|
|
|
parse_urlencoded(Tail, lists:reverse(Cur), [], value); %% change mode
|
|
|
|
|
|
|
|
parse_urlencoded([H | Tail], Last, Cur, State) ->
|
|
|
|
parse_urlencoded(Tail, Last, [H|Cur], State);
|
|
|
|
|
|
|
|
parse_urlencoded([], Last, Cur, _State) ->
|
|
|
|
[{Last, lists:reverse(Cur)}];
|
|
|
|
|
|
|
|
parse_urlencoded(undefined, _, _, _) ->
|
|
|
|
[].
|
|
|
|
|
|
|
|
|
2005-11-22 19:25:02 +01:00
|
|
|
url_encode([H|T]) ->
|
|
|
|
if
|
|
|
|
H >= $a, $z >= H ->
|
|
|
|
[H|url_encode(T)];
|
|
|
|
H >= $A, $Z >= H ->
|
|
|
|
[H|url_encode(T)];
|
|
|
|
H >= $0, $9 >= H ->
|
|
|
|
[H|url_encode(T)];
|
|
|
|
H == $_; H == $.; H == $-; H == $/; H == $: -> % FIXME: more..
|
|
|
|
[H|url_encode(T)];
|
|
|
|
true ->
|
|
|
|
case integer_to_hex(H) of
|
|
|
|
[X, Y] ->
|
|
|
|
[$%, X, Y | url_encode(T)];
|
|
|
|
[X] ->
|
|
|
|
[$%, $0, X | url_encode(T)]
|
|
|
|
end
|
|
|
|
end;
|
|
|
|
|
|
|
|
url_encode([]) ->
|
|
|
|
[].
|
|
|
|
|
|
|
|
integer_to_hex(I) ->
|
|
|
|
case catch erlang:integer_to_list(I, 16) of
|
|
|
|
{'EXIT', _} ->
|
|
|
|
old_integer_to_hex(I);
|
|
|
|
Int ->
|
|
|
|
Int
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
old_integer_to_hex(I) when I<10 ->
|
|
|
|
integer_to_list(I);
|
|
|
|
old_integer_to_hex(I) when I<16 ->
|
|
|
|
[I-10+$A];
|
|
|
|
old_integer_to_hex(I) when I>=16 ->
|
|
|
|
N = trunc(I/16),
|
|
|
|
old_integer_to_hex(N) ++ old_integer_to_hex(I rem 16).
|
|
|
|
|
|
|
|
|
2004-09-25 22:52:20 +02:00
|
|
|
% The following code is mostly taken from yaws_ssl.erl
|
|
|
|
|
|
|
|
parse_request(State, Data) ->
|
|
|
|
case Data of
|
|
|
|
[] ->
|
|
|
|
{[], []};
|
|
|
|
_ ->
|
|
|
|
?DEBUG("GOT ssl data ~p~n", [Data]),
|
|
|
|
{R, Trail} = case State#state.request_method of
|
|
|
|
undefined ->
|
|
|
|
{R1, Trail1} = get_req(Data),
|
|
|
|
?DEBUG("Parsed request ~p~n", [R1]),
|
|
|
|
{[R1], Trail1};
|
|
|
|
_ ->
|
|
|
|
{[], Data}
|
|
|
|
end,
|
|
|
|
{H, Trail2} = get_headers(Trail),
|
|
|
|
{R ++ H, Trail2}
|
|
|
|
end.
|
|
|
|
|
|
|
|
get_req("\r\n\r\n" ++ _) ->
|
|
|
|
bad_request;
|
|
|
|
get_req("\r\n" ++ Data) ->
|
|
|
|
get_req(Data);
|
|
|
|
get_req(Data) ->
|
|
|
|
{FirstLine, Trail} = lists:splitwith(fun not_eol/1, Data),
|
|
|
|
R = parse_req(FirstLine),
|
|
|
|
{R, Trail}.
|
|
|
|
|
|
|
|
|
|
|
|
not_eol($\r)->
|
|
|
|
false;
|
|
|
|
not_eol($\n) ->
|
|
|
|
false;
|
|
|
|
not_eol(_) ->
|
|
|
|
true.
|
|
|
|
|
|
|
|
|
|
|
|
get_word(Line)->
|
|
|
|
{Word, T} = lists:splitwith(fun(X)-> X /= $\ end, Line),
|
|
|
|
{Word, lists:dropwhile(fun(X) -> X == $\ end, T)}.
|
|
|
|
|
|
|
|
|
|
|
|
parse_req(Line) ->
|
|
|
|
{MethodStr, L1} = get_word(Line),
|
|
|
|
?DEBUG("Method: ~p~n", [MethodStr]),
|
|
|
|
case L1 of
|
|
|
|
[] ->
|
|
|
|
bad_request;
|
|
|
|
_ ->
|
|
|
|
{URI, L2} = get_word(L1),
|
|
|
|
{VersionStr, L3} = get_word(L2),
|
|
|
|
?DEBUG("URI: ~p~nVersion: ~p~nL3: ~p~n",
|
|
|
|
[URI, VersionStr, L3]),
|
|
|
|
case L3 of
|
|
|
|
[] ->
|
|
|
|
Method = case MethodStr of
|
|
|
|
"GET" -> 'GET';
|
|
|
|
"POST" -> 'POST';
|
|
|
|
"HEAD" -> 'HEAD';
|
|
|
|
"OPTIONS" -> 'OPTIONS';
|
|
|
|
"TRACE" -> 'TRACE';
|
|
|
|
"PUT" -> 'PUT';
|
|
|
|
"DELETE" -> 'DELETE';
|
|
|
|
S -> S
|
|
|
|
end,
|
|
|
|
Path = case URI of
|
|
|
|
"*" ->
|
|
|
|
% Is this correct?
|
|
|
|
"*";
|
|
|
|
P ->
|
|
|
|
% FIXME: Handle
|
|
|
|
% absolute URIs
|
|
|
|
{abs_path, P}
|
|
|
|
end,
|
|
|
|
case VersionStr of
|
|
|
|
[] ->
|
|
|
|
{ok, {http_request, Method, Path, {0,9}}};
|
|
|
|
"HTTP/1.0" ->
|
|
|
|
{ok, {http_request, Method, Path, {1,0}}};
|
|
|
|
"HTTP/1.1" ->
|
|
|
|
{ok, {http_request, Method, Path, {1,1}}};
|
|
|
|
_ ->
|
|
|
|
bad_request
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
bad_request
|
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
get_headers(Tail) ->
|
|
|
|
get_headers([], Tail).
|
|
|
|
|
|
|
|
get_headers(H, Tail) ->
|
|
|
|
case get_line(Tail) of
|
|
|
|
{incomplete, Tail2} ->
|
|
|
|
{H, Tail2};
|
|
|
|
{line, Line, Tail2} ->
|
|
|
|
get_headers(H ++ parse_line(Line), Tail2);
|
|
|
|
{lastline, Line, Tail2} ->
|
|
|
|
{H ++ parse_line(Line) ++ [{ok, http_eoh}], Tail2}
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
parse_line("Connection:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Connection', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Host:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Host', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Accept:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Accept', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("If-Modified-Since:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'If-Modified-Since', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("If-Match:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'If-Match', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("If-None-Match:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'If-None-Match', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("If-Range:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'If-Range', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("If-Unmodified-Since:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'If-Unmodified-Since', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Range:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Range', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("User-Agent:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'User-Agent', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Accept-Ranges:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Accept-Ranges', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Authorization:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Authorization', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Keep-Alive:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Keep-Alive', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Referer:" ++ Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Referer', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Content-type:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Content-Type', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Content-Type:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Content-Type', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Content-Length:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Content-Length', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Content-length:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Content-Length', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Cookie:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Cookie', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Accept-Language:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Accept-Language', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line("Accept-Encoding:"++Con) ->
|
|
|
|
[{ok, {http_header, undefined, 'Accept-Encoding', undefined, strip_spaces(Con)}}];
|
|
|
|
parse_line(S) ->
|
|
|
|
case lists:splitwith(fun(C)->C /= $: end, S) of
|
|
|
|
{Name, [$:|Val]} ->
|
|
|
|
[{ok, {http_header, undefined, Name, undefined, strip_spaces(Val)}}];
|
|
|
|
_ ->
|
|
|
|
[]
|
|
|
|
end.
|
|
|
|
|
|
|
|
|
|
|
|
is_space($\s) ->
|
|
|
|
true;
|
|
|
|
is_space($\r) ->
|
|
|
|
true;
|
|
|
|
is_space($\n) ->
|
|
|
|
true;
|
2005-05-23 01:29:54 +02:00
|
|
|
is_space($\t) ->
|
2004-09-25 22:52:20 +02:00
|
|
|
true;
|
|
|
|
is_space(_) ->
|
|
|
|
false.
|
|
|
|
|
|
|
|
|
|
|
|
strip_spaces(String) ->
|
|
|
|
strip_spaces(String, both).
|
|
|
|
|
|
|
|
strip_spaces(String, left) ->
|
|
|
|
drop_spaces(String);
|
|
|
|
strip_spaces(String, right) ->
|
|
|
|
lists:reverse(drop_spaces(lists:reverse(String)));
|
|
|
|
strip_spaces(String, both) ->
|
|
|
|
strip_spaces(drop_spaces(String), right).
|
|
|
|
|
|
|
|
drop_spaces([]) ->
|
|
|
|
[];
|
|
|
|
drop_spaces(YS=[X|XS]) ->
|
|
|
|
case is_space(X) of
|
|
|
|
true ->
|
|
|
|
drop_spaces(XS);
|
|
|
|
false ->
|
|
|
|
YS
|
|
|
|
end.
|
|
|
|
|
|
|
|
is_nb_space(X) ->
|
|
|
|
lists:member(X, [$\s, $\t]).
|
|
|
|
|
|
|
|
|
|
|
|
% ret: {line, Line, Trail} | {lastline, Line, Trail}
|
|
|
|
|
|
|
|
get_line(L) ->
|
|
|
|
get_line(L, []).
|
|
|
|
get_line("\r\n\r\n" ++ Tail, Cur) ->
|
|
|
|
{lastline, lists:reverse(Cur), Tail};
|
|
|
|
get_line("\r\n" ++ Tail, Cur) ->
|
|
|
|
case Tail of
|
|
|
|
[] ->
|
|
|
|
{incomplete, lists:reverse(Cur) ++ "\r\n"};
|
|
|
|
_ ->
|
|
|
|
case is_nb_space(hd(Tail)) of
|
|
|
|
true -> %% multiline ... continue
|
|
|
|
get_line(Tail, [$\n, $\r | Cur]);
|
|
|
|
false ->
|
|
|
|
{line, lists:reverse(Cur), Tail}
|
|
|
|
end
|
|
|
|
end;
|
|
|
|
get_line([H|T], Cur) ->
|
|
|
|
get_line(T, [H|Cur]).
|
|
|
|
|