From f9060599aeab81cb9282ddf51cc057bf1353208f Mon Sep 17 00:00:00 2001 From: Micael Karlberg Date: Tue, 25 Oct 2011 12:34:56 +0200 Subject: The XSS prevention methods used was confused if the URL was encoded (hex-encoded). OTP-9655 --- lib/inets/test/httpc_SUITE.erl | 3 +- lib/inets/test/httpd_basic_SUITE.erl | 144 ++++++++++++++++++++++++++++------- 2 files changed, 119 insertions(+), 28 deletions(-) (limited to 'lib/inets/test') diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl index 71f017dae6..c752b89aa0 100644 --- a/lib/inets/test/httpc_SUITE.erl +++ b/lib/inets/test/httpc_SUITE.erl @@ -3112,7 +3112,8 @@ tsp(F) -> tsp(F, []). tsp(F, A) -> Timestamp = formated_timestamp(), - test_server:format("** ~s ** ~p ~p:" ++ F ++ "~n", [Timestamp, self(), ?MODULE | A]). + test_server:format("** ~s ** ~p ~p:" ++ F ++ "~n", + [Timestamp, self(), ?MODULE | A]). formated_timestamp() -> format_timestamp( os:timestamp() ). diff --git a/lib/inets/test/httpd_basic_SUITE.erl b/lib/inets/test/httpd_basic_SUITE.erl index ed0fe942cf..581c9c5782 100644 --- a/lib/inets/test/httpd_basic_SUITE.erl +++ b/lib/inets/test/httpd_basic_SUITE.erl @@ -49,9 +49,28 @@ all(suite) -> init_per_suite(Config) -> ok = inets:start(), PrivDir = ?config(priv_dir, Config), - HttpdConf = [{port, 0}, {ipfamily, inet}, - {server_name, "httpd_test"}, {server_root, PrivDir}, - {document_root, PrivDir}, {bind_address, "localhost"}], + + Dummy = +" + +/index.html + + +DUMMY + +", + + DummyFile = filename:join([PrivDir,"dummy.html"]), + {ok, Fd} = file:open(DummyFile, [write]), + ok = file:write(Fd, Dummy), + ok = file:close(Fd), + HttpdConf = [{port, 0}, + {ipfamily, inet}, + {server_name, "httpd_test"}, + {server_root, PrivDir}, + {document_root, PrivDir}, + {bind_address, "localhost"}], + [{httpd_conf, HttpdConf} | Config]. %%-------------------------------------------------------------------- @@ -115,6 +134,10 @@ uri_too_long_414(Config) when is_list(Config) -> {version, "HTTP/0.9"}]), inets:stop(httpd, Pid). + +%%------------------------------------------------------------------------- +%%------------------------------------------------------------------------- + header_too_long_413(doc) -> ["Test that too long headers's get 413 HTTP code"]; header_too_long_413(suite) -> @@ -135,49 +158,92 @@ header_too_long_413(Config) when is_list(Config) -> inets:stop(httpd, Pid). +%%------------------------------------------------------------------------- +%%------------------------------------------------------------------------- + escaped_url_in_error_body(doc) -> ["Test Url-encoding see OTP-8940"]; escaped_url_in_error_body(suite) -> []; escaped_url_in_error_body(Config) when is_list(Config) -> + tsp("escaped_url_in_error_body -> entry"), HttpdConf = ?config(httpd_conf, Config), {ok, Pid} = inets:start(httpd, [{port, 0} | HttpdConf]), Info = httpd:info(Pid), - Port = proplists:get_value(port, Info), + Port = proplists:get_value(port, Info), _Address = proplists:get_value(bind_address, Info), - Path = "/this_is_bold", - URL = ?URL_START ++ integer_to_list(Port) ++ Path, - EscapedPath = http_uri:encode(Path), - case httpc:request(get, {URL, []}, - [{url_encode, true}, - {version, "HTTP/1.0"}], + + %% Request 1 + tsp("escaped_url_in_error_body -> request 1"), + URL1 = ?URL_START ++ integer_to_list(Port), + %% Make sure the server is ok, by making a request for a valid page + case httpc:request(get, {URL1 ++ "/dummy.html", []}, + [{url_encode, false}, + {version, "HTTP/1.0"}], [{full_result, false}]) of - {ok, {404, Body1}} -> - case find_URL_path(string:tokens(Body1, " ")) of - EscapedPath -> - ok; - BadPath1 -> - tsf({unexpected_path_1, EscapedPath, BadPath1}) - end; + {ok, {200, _}} -> + %% Don't care about the the body, just that we get a ok response + ok; {ok, UnexpectedOK1} -> tsf({unexpected_ok_1, UnexpectedOK1}) end, - case httpc:request(get, {URL, []}, - [{version, "HTTP/1.0"}], + %% Request 2 + tsp("escaped_url_in_error_body -> request 2"), + %% Make sure the server is ok, by making a request for a valid page + case httpc:request(get, {URL1 ++ "/dummy.html", []}, + [{url_encode, true}, + {version, "HTTP/1.0"}], + [{full_result, false}]) of + {ok, {200, _}} -> + %% Don't care about the the body, just that we get a ok response + ok; + {ok, UnexpectedOK2} -> + tsf({unexpected_ok_2, UnexpectedOK2}) + end, + + %% Request 3 + tsp("escaped_url_in_error_body -> request 3"), + %% Ask for a non-existing page(1) + Path = "/this_is_bold", + HTMLEncodedPath = http_util:html_encode(Path), + URL2 = URL1 ++ Path, + case httpc:request(get, {URL2, []}, + [{url_encode, true}, + {version, "HTTP/1.0"}], [{full_result, false}]) of - {ok, {404, Body2}} -> - HTMLEncodedPath = http_util:html_encode(Path), - case find_URL_path(string:tokens(Body2, " ")) of + {ok, {404, Body3}} -> + case find_URL_path(string:tokens(Body3, " ")) of HTMLEncodedPath -> ok; - BadPath2 -> - tsf({unexpected_path_2, EscapedPath, BadPath2}) + BadPath3 -> + tsf({unexpected_path_3, HTMLEncodedPath, BadPath3}) end; - {ok, UnexpectedOK2} -> - tsf({unexpected_ok_2, UnexpectedOK2}) + {ok, UnexpectedOK3} -> + tsf({unexpected_ok_1, UnexpectedOK3}) + end, + + %% Request 4 + tsp("escaped_url_in_error_body -> request 4"), + %% Ask for a non-existing page(2) + case httpc:request(get, {URL2, []}, + [{url_encode, false}, + {version, "HTTP/1.0"}], + [{full_result, false}]) of + {ok, {404, Body4}} -> + case find_URL_path(string:tokens(Body4, " ")) of + HTMLEncodedPath -> + ok; + BadPath4 -> + tsf({unexpected_path_2, HTMLEncodedPath, BadPath4}) + end; + {ok, UnexpectedOK4} -> + tsf({unexpected_ok_4, UnexpectedOK4}) end, - inets:stop(httpd, Pid). + tsp("escaped_url_in_error_body -> stop inets"), + inets:stop(httpd, Pid), + tsp("escaped_url_in_error_body -> done"), + ok. find_URL_path([]) -> ""; @@ -189,3 +255,27 @@ find_URL_path([_ | Rest]) -> tsf(Reason) -> test_server:fail(Reason). + +tsp(F) -> + tsp(F, []). +tsp(F, A) -> + Timestamp = formated_timestamp(), + test_server:format("** ~s ** ~p ~p:" ++ F ++ "~n", + [Timestamp, self(), ?MODULE | A]). + +formated_timestamp() -> + format_timestamp( os:timestamp() ). + +format_timestamp({_N1, _N2, N3} = Now) -> + {Date, Time} = calendar:now_to_datetime(Now), + {YYYY,MM,DD} = Date, + {Hour,Min,Sec} = Time, + FormatDate = + io_lib:format("~.4w:~.2.0w:~.2.0w ~.2.0w:~.2.0w:~.2.0w 4~w", + [YYYY,MM,DD,Hour,Min,Sec,round(N3/1000)]), + lists:flatten(FormatDate). + + +skip(Reason) -> + {skip, Reason}. + -- cgit v1.2.3 From 500557baeab33eb1999d34910aa2bb76a0e38f48 Mon Sep 17 00:00:00 2001 From: Micael Karlberg Date: Tue, 25 Oct 2011 15:23:36 +0200 Subject: Problems with proxy test cases. OTP-9655 --- lib/inets/test/httpc_SUITE.erl | 85 +++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 26 deletions(-) (limited to 'lib/inets/test') diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl index c752b89aa0..c52e3d25cd 100644 --- a/lib/inets/test/httpc_SUITE.erl +++ b/lib/inets/test/httpc_SUITE.erl @@ -180,8 +180,9 @@ init_per_testcase(Case, Config) -> init_per_testcase(Case, 2, Config). init_per_testcase(Case, Timeout, Config) -> - io:format(user, "~n~n*** INIT ~w:~w[~w] ***~n~n", - [?MODULE, Timeout, Case]), + io:format(user, + "~n~n*** INIT ~w:~w[~w] ***" + "~n~n", [?MODULE, Case, Timeout]), PrivDir = ?config(priv_dir, Config), application:stop(inets), Dog = test_server:timetrap(inets_test_lib:minutes(Timeout)), @@ -200,28 +201,55 @@ init_per_testcase(Case, Timeout, Config) -> inets_test_lib:start_http_server( filename:join(PrivDir, SslConfFile)), [{watchdog, Dog}, {local_ssl_server, Server} | TmpConfig2]; - "proxy" ++ Rest -> - case Rest of - "_https_not_supported" -> - inets:start(), - case (catch application:start(ssl)) of - ok -> - [{watchdog, Dog} | TmpConfig]; - _ -> - [{skip, - "SSL does not seem to be supported"} - | TmpConfig] - end; - _ -> - case is_proxy_available(?PROXY, ?PROXY_PORT) of - true -> - inets:start(), - [{watchdog, Dog} | TmpConfig]; - false -> - [{skip, "Failed to contact proxy"} | - TmpConfig] - end - end; + "proxy_" ++ Rest -> + case Rest of + "https_not_supported" -> + inets:start(), + case (catch application:start(ssl)) of + ok -> + [{watchdog, Dog} | TmpConfig]; + _ -> + [{skip, + "SSL does not seem to be supported"} + | TmpConfig] + end; + _ -> + %% We use erlang.org for the proxy tests + %% and after the switch to erlang-web, many + %% of the test cases no longer work (erlang.org + %% previously run on Apache). + %% Until we have had time to update inets + %% (and updated erlang.org to use that inets) + %% and the test cases, we simply skip the + %% problematic test cases. + %% This is not ideal, but I am busy.... + case is_proxy_available(?PROXY, ?PROXY_PORT) of + true -> + BadCases = + [ + "delete", + "get", + "head", + "not_modified_otp_6821", + "options", + "page_does_not_exist", + "post", + "put", + "stream" + ], + case lists:member(Rest, BadCases) of + true -> + [{skip, + "TC and server not compatible"}| + TmpConfig]; + false -> + inets:start(), + [{watchdog, Dog} | TmpConfig] + end; + false -> + [{skip, "proxy not responding"} | TmpConfig] + end + end; _ -> TmpConfig2 = lists:keydelete(local_server, 1, TmpConfig), Server = @@ -231,11 +259,14 @@ init_per_testcase(Case, Timeout, Config) -> [{watchdog, Dog}, {local_server, Server} | TmpConfig2] end, - http:set_options([{proxy, {{?PROXY, ?PROXY_PORT}, - ["localhost", ?IPV6_LOCAL_HOST]}}]), + ProxyExceptions = ["localhost", ?IPV6_LOCAL_HOST], + http:set_options([{proxy, {{?PROXY, ?PROXY_PORT}, ProxyExceptions}}]), inets:enable_trace(max, io, httpc), %% inets:enable_trace(max, io, all), %% snmp:set_trace([gen_tcp, inet_tcp, prim_inet]), + io:format(user, "~n~n*** INIT ~w:~w ***~n" + " NewConfig: ~p~n~n", + [?MODULE, Case, NewConfig]), NewConfig. %%-------------------------------------------------------------------- @@ -1194,6 +1225,8 @@ proxy_head(doc) -> proxy_head(suite) -> []; proxy_head(Config) when is_list(Config) -> + tsp("proxy_head -> entry with" + "~n Config: ~p", [Config]), case ?config(skip, Config) of undefined -> Command = -- cgit v1.2.3 From 1577983b9b3883b74e3e460ed4f8f6916ffaa3a5 Mon Sep 17 00:00:00 2001 From: Micael Karlberg Date: Wed, 26 Oct 2011 12:20:29 +0200 Subject: Fixed hex-decoding. OTP-9655 --- lib/inets/test/httpc_SUITE.erl | 27 ++++++++---------- lib/inets/test/httpd_SUITE.erl | 4 +-- lib/inets/test/httpd_test_lib.erl | 58 ++++++++++++++++++++++++++------------- 3 files changed, 52 insertions(+), 37 deletions(-) (limited to 'lib/inets/test') diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl index c52e3d25cd..3a1f4cc83d 100644 --- a/lib/inets/test/httpc_SUITE.erl +++ b/lib/inets/test/httpc_SUITE.erl @@ -197,9 +197,7 @@ init_per_testcase(Case, Timeout, Config) -> TmpConfig2 = lists:keydelete(local_ssl_server, 1, TmpConfig), %% Will start inets - Server = - inets_test_lib:start_http_server( - filename:join(PrivDir, SslConfFile)), + Server = start_http_server(PrivDir, SslConfFile), [{watchdog, Dog}, {local_ssl_server, Server} | TmpConfig2]; "proxy_" ++ Rest -> case Rest of @@ -209,9 +207,8 @@ init_per_testcase(Case, Timeout, Config) -> ok -> [{watchdog, Dog} | TmpConfig]; _ -> - [{skip, - "SSL does not seem to be supported"} - | TmpConfig] + [skip("SSL does not seem to be supported") | + TmpConfig] end; _ -> %% We use erlang.org for the proxy tests @@ -239,23 +236,20 @@ init_per_testcase(Case, Timeout, Config) -> ], case lists:member(Rest, BadCases) of true -> - [{skip, - "TC and server not compatible"}| + [skip("TC and server not compatible") | TmpConfig]; false -> inets:start(), [{watchdog, Dog} | TmpConfig] end; false -> - [{skip, "proxy not responding"} | TmpConfig] + [skip("proxy not responding") | TmpConfig] end end; _ -> TmpConfig2 = lists:keydelete(local_server, 1, TmpConfig), - Server = - %% Will start inets - inets_test_lib:start_http_server( - filename:join(PrivDir, IpConfFile)), + %% Will start inets + Server = start_http_server(PrivDir, IpConfFile), [{watchdog, Dog}, {local_server, Server} | TmpConfig2] end, @@ -264,11 +258,12 @@ init_per_testcase(Case, Timeout, Config) -> inets:enable_trace(max, io, httpc), %% inets:enable_trace(max, io, all), %% snmp:set_trace([gen_tcp, inet_tcp, prim_inet]), - io:format(user, "~n~n*** INIT ~w:~w ***~n" - " NewConfig: ~p~n~n", - [?MODULE, Case, NewConfig]), NewConfig. +start_http_server(ConfDir, ConfFile) -> + inets_test_lib:start_http_server( filename:join(ConfDir, ConfFile) ). + + %%-------------------------------------------------------------------- %% Function: end_per_testcase(Case, Config) -> _ %% Case - atom() diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl index 7403d4a643..7d95cdbebd 100644 --- a/lib/inets/test/httpd_SUITE.erl +++ b/lib/inets/test/httpd_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2005-2010. All Rights Reserved. +%% Copyright Ericsson AB 2005-2011. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -1627,7 +1627,7 @@ ticket_6003(Config) -> ?IP_PORT, ?config(node, Config), "GET http://www.erlang.org/%skalle " "HTTP/1.0\r\n\r\n", - [{statuscode, 400}, + [{statuscode, 404}, {version, "HTTP/1.0"}]), ok. diff --git a/lib/inets/test/httpd_test_lib.erl b/lib/inets/test/httpd_test_lib.erl index 6abee5be2c..becb54e479 100644 --- a/lib/inets/test/httpd_test_lib.erl +++ b/lib/inets/test/httpd_test_lib.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2001-2009. All Rights Reserved. +%% Copyright Ericsson AB 2001-2011. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -80,7 +80,9 @@ verify_request(SocketType, Host, Port, Node, RequestStr, Options) -> verify_request(SocketType, Host, Port, Node, RequestStr, Options, 30000). verify_request(SocketType, Host, Port, Node, RequestStr, Options, TimeOut) -> + tsp("verify_request -> connect to [~w] ~p:~w", [SocketType, Host, Port]), {ok, Socket} = inets_test_lib:connect_bin(SocketType, Host, Port), + inets_test_lib:send(SocketType, Socket, RequestStr), State = case inets_regexp:match(RequestStr, "printenv") of @@ -200,10 +202,9 @@ handle_http_body(Body, State = #state{headers = Headers, end. validate(RequestStr, #state{status_line = {Version, StatusCode, _}, - headers = Headers, - body = Body}, Options, N, P) -> - - %io:format("Status~p: H:~p B:~p~n", [StatusCode, Headers, Body]), + headers = Headers, + body = Body}, Options, N, P) -> + check_version(Version, Options), case lists:keysearch(statuscode, 1, Options) of {value, _} -> @@ -217,6 +218,7 @@ validate(RequestStr, #state{status_line = {Version, StatusCode, _}, list_to_integer(Headers#http_response_h.'content-length'), Body). + %%-------------------------------------------------------------------- %% Internal functions %%------------------------------------------------------------------ @@ -225,21 +227,20 @@ check_version(Version, Options) -> {value, {version, Version}} -> ok; {value, {version, Ver}} -> - test_server:fail({wrong_version, [{got, Version}, - {expected, Ver}]}); + tsf({wrong_version, [{got, Version}, + {expected, Ver}]}); _ -> case Version of "HTTP/1.1" -> ok; _ -> - test_server:fail({wrong_version, [{got, Version}, - {expected, "HTTP/1.1"}]}) + tsf({wrong_version, [{got, Version}, + {expected, "HTTP/1.1"}]}) end end. check_status_code(StatusCode, [], Options) -> - test_server:fail({wrong_status_code, [{got, StatusCode}, - {expected, Options}]}); + tsf({wrong_status_code, [{got, StatusCode}, {expected, Options}]}); check_status_code(StatusCode, Current = [_ | Rest], Options) -> case lists:keysearch(statuscode, 1, Current) of {value, {statuscode, StatusCode}} -> @@ -247,8 +248,7 @@ check_status_code(StatusCode, Current = [_ | Rest], Options) -> {value, {statuscode, _OtherStatus}} -> check_status_code(StatusCode, Rest, Options); false -> - test_server:fail({wrong_status_code, [{got, StatusCode}, - {expected, Options}]}) + tsf({wrong_status_code, [{got, StatusCode}, {expected, Options}]}) end. do_validate(_, [], _, _) -> @@ -279,8 +279,7 @@ do_validate(Header, [{header, HeaderField, Value}|Rest],N,P) -> Header}) end, do_validate(Header, Rest, N, P); -do_validate(Header,[{no_last_modified,HeaderField}|Rest],N,P) -> -% io:format("Header: ~p~nHeaderField: ~p~n",[Header,HeaderField]), +do_validate(Header,[{no_last_modified, HeaderField}|Rest],N,P) -> case lists:keysearch(HeaderField,1,Header) of {value,_} -> test_server:fail({wrong_header_field_value, HeaderField, @@ -293,7 +292,6 @@ do_validate(Header, [_Unknown | Rest], N, P) -> do_validate(Header, Rest, N, P). is_expect(RequestStr) -> - case inets_regexp:match(RequestStr, "xpect:100-continue") of {match, _, _}-> true; @@ -302,15 +300,15 @@ is_expect(RequestStr) -> end. %% OTP-5775, content-length -check_body("GET /cgi-bin/erl/httpd_example:get_bin HTTP/1.0\r\n\r\n", 200, "text/html", Length, _Body) when Length /= 274-> - test_server:fail(content_length_error); +check_body("GET /cgi-bin/erl/httpd_example:get_bin HTTP/1.0\r\n\r\n", 200, "text/html", Length, _Body) when (Length =/= 274) -> + tsf(content_length_error); check_body("GET /cgi-bin/cgi_echo HTTP/1.0\r\n\r\n", 200, "text/plain", _, Body) -> case size(Body) of 100 -> ok; _ -> - test_server:fail(content_length_error) + tsf(content_length_error) end; check_body(RequestStr, 200, "text/html", _, Body) -> @@ -330,3 +328,25 @@ print(Proto, Data, #state{print = true}) -> print(_, _, #state{print = false}) -> ok. +tsf(Reason) -> + test_server:fail(Reason). + +%% tsp(F) -> +%% tsp(F, []). +tsp(F, A) -> + Timestamp = formated_timestamp(), + test_server:format("** ~s ** ~p ~p:" ++ F ++ "~n", + [Timestamp, self(), ?MODULE | A]). + +formated_timestamp() -> + format_timestamp( os:timestamp() ). + +format_timestamp({_N1, _N2, N3} = Now) -> + {Date, Time} = calendar:now_to_datetime(Now), + {YYYY,MM,DD} = Date, + {Hour,Min,Sec} = Time, + FormatDate = + io_lib:format("~.4w:~.2.0w:~.2.0w ~.2.0w:~.2.0w:~.2.0w 4~w", + [YYYY,MM,DD,Hour,Min,Sec,round(N3/1000)]), + lists:flatten(FormatDate). + -- cgit v1.2.3 From 3ab1cc0eae216ce101bc6d9011dddeaf89019c8a Mon Sep 17 00:00:00 2001 From: Micael Karlberg Date: Wed, 26 Oct 2011 14:12:36 +0200 Subject: Uncommented ipv6 test cases. OTP-9655 --- lib/inets/test/httpd_SUITE.erl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'lib/inets/test') diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl index 7d95cdbebd..4a510f013e 100644 --- a/lib/inets/test/httpd_SUITE.erl +++ b/lib/inets/test/httpd_SUITE.erl @@ -126,7 +126,7 @@ all(suite) -> http_1_1_ip, http_1_0_ip, http_0_9_ip, - %% ipv6, + ipv6, tickets ]. @@ -1611,24 +1611,24 @@ ticket_5913(doc) -> ["Tests that a header without last-modified is handled"]; ticket_5913(suite) -> []; ticket_5913(Config) -> - ok=httpd_test_lib:verify_request(ip_comm, ?config(host, Config), - ?IP_PORT, ?config(node, Config), + ok = httpd_test_lib:verify_request(ip_comm, ?config(host, Config), + ?IP_PORT, ?config(node, Config), "GET /cgi-bin/erl/httpd_example:get_bin " "HTTP/1.0\r\n\r\n", [{statuscode, 200}, - {version, "HTTP/1.0"}]), + {version, "HTTP/1.0"}]), ok. ticket_6003(doc) -> ["Tests that a URI with a bad hexadecimal code is handled"]; ticket_6003(suite) -> []; ticket_6003(Config) -> - ok=httpd_test_lib:verify_request(ip_comm, ?config(host, Config), - ?IP_PORT, ?config(node, Config), - "GET http://www.erlang.org/%skalle " - "HTTP/1.0\r\n\r\n", - [{statuscode, 404}, - {version, "HTTP/1.0"}]), + ok = httpd_test_lib:verify_request(ip_comm, ?config(host, Config), + ?IP_PORT, ?config(node, Config), + "GET http://www.erlang.org/%skalle " + "HTTP/1.0\r\n\r\n", + [{statuscode, 400}, + {version, "HTTP/1.0"}]), ok. ticket_7304(doc) -> -- cgit v1.2.3