diff options
Diffstat (limited to 'lib')
45 files changed, 1480 insertions, 437 deletions
diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl index 2dd4fdac05..5239ec1ff8 100644 --- a/lib/common_test/src/ct_conn_log_h.erl +++ b/lib/common_test/src/ct_conn_log_h.erl @@ -104,18 +104,26 @@ terminate(_,#state{logs=Logs}) -> %%%----------------------------------------------------------------- %%% Writing reports write_report(_Time,#conn_log{header=false,module=ConnMod}=Info,Data,GL,State) -> - {LogType,Fd} = get_log(Info,GL,State), - io:format(Fd,"~n~ts",[format_data(ConnMod,LogType,Data)]); + case get_log(Info,GL,State) of + {silent,_} -> + ok; + {LogType,Fd} -> + io:format(Fd,"~n~ts",[format_data(ConnMod,LogType,Data)]) + end; write_report(Time,#conn_log{module=ConnMod}=Info,Data,GL,State) -> - {LogType,Fd} = get_log(Info,GL,State), - io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time), - format_title(LogType,Info), - format_data(ConnMod,LogType,Data)]). + case get_log(Info,GL,State) of + {silent,_} -> + ok; + {LogType,Fd} -> + io:format(Fd,"~n~ts~ts~ts",[format_head(ConnMod,LogType,Time), + format_title(LogType,Info), + format_data(ConnMod,LogType,Data)]) + end. write_error(Time,#conn_log{module=ConnMod}=Info,Report,GL,State) -> case get_log(Info,GL,State) of - {html,_} -> + {LogType,_} when LogType==html; LogType==silent -> %% The error will anyway be written in the html log by the %% sasl error handler, so don't write it again. ok; diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl index 0de7bf03af..05977e5649 100644 --- a/lib/common_test/src/ct_netconfc.erl +++ b/lib/common_test/src/ct_netconfc.erl @@ -1272,6 +1272,14 @@ set_request_timer(T) -> {ok,TRef} = timer:send_after(T,{Ref,timeout}), {Ref,TRef}. +%%%----------------------------------------------------------------- +cancel_request_timer(undefined,undefined) -> + ok; +cancel_request_timer(Ref,TRef) -> + _ = timer:cancel(TRef), + receive {Ref,timeout} -> ok + after 0 -> ok + end. %%%----------------------------------------------------------------- client_hello(Options) when is_list(Options) -> @@ -1404,9 +1412,9 @@ handle_error(Reason, State) -> Pending -> %% Assuming the first request gets the %% first answer - P=#pending{tref=TRef,caller=Caller} = + P=#pending{tref=TRef,ref=Ref,caller=Caller} = lists:last(Pending), - _ = timer:cancel(TRef), + cancel_request_timer(Ref,TRef), Reason1 = {failed_to_parse_received_data,Reason}, ct_gen_conn:return(Caller,{error,Reason1}), lists:delete(P,Pending) @@ -1492,8 +1500,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) -> {error,Reason} -> {noreply,State#state{hello_status = {error,Reason}}} end; - #pending{tref=TRef,caller=Caller} -> - _ = timer:cancel(TRef), + #pending{tref=TRef,ref=Ref,caller=Caller} -> + cancel_request_timer(Ref,TRef), case decode_hello(E) of {ok,SessionId,Capabilities} -> ct_gen_conn:return(Caller,ok), @@ -1519,9 +1527,8 @@ decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) -> %% there is just one pending that matches (i.e. has %% undefined msg_id and op) case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of - [#pending{tref=TRef, - caller=Caller}] -> - _ = timer:cancel(TRef), + [#pending{tref=TRef,ref=Ref,caller=Caller}] -> + cancel_request_timer(Ref,TRef), ct_gen_conn:return(Caller,E), {noreply,State#state{pending=[]}}; _ -> @@ -1542,8 +1549,8 @@ get_msg_id(Attrs) -> decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) -> case lists:keytake(MsgId,#pending.msg_id,Pending) of - {value, #pending{tref=TRef,op=Op,caller=Caller}, Pending1} -> - _ = timer:cancel(TRef), + {value, #pending{tref=TRef,ref=Ref,op=Op,caller=Caller}, Pending1} -> + cancel_request_timer(Ref,TRef), Content = forward_xmlns_attr(Attrs,Content0), {CallerReply,{ServerReply,State2}} = do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}), @@ -1555,10 +1562,11 @@ decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) -> %% pending that matches (i.e. has undefined msg_id and op) case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of [#pending{tref=TRef, + ref=Ref, msg_id=undefined, op=undefined, caller=Caller}] -> - _ = timer:cancel(TRef), + cancel_request_timer(Ref,TRef), ct_gen_conn:return(Caller,E), {noreply,State#state{pending=[]}}; _ -> diff --git a/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl index 64ebfbc463..aaa0723488 100644 --- a/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl +++ b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl @@ -73,6 +73,7 @@ all() -> timeout_close_session, get, timeout_get, + flush_timeout_get, get_xpath, get_config, get_config_xpath, @@ -360,6 +361,28 @@ timeout_get(Config) -> ?ok = ct_netconfc:close_session(Client), ok. +%% Test OTP-13008 "ct_netconfc crash when receiving unknown timeout" +%% If the timer expires "at the same time" as the rpc reply is +%% received, the timeout message might already be sent when the timer +%% is cancelled. This test checks that the timeout message is flushed +%% from the message queue. If it isn't, the client crashes and the +%% session can not be closed afterwards. +%% Note that we can only hope that the test case triggers the problem +%% every now and then, as it is very timing dependent... +flush_timeout_get(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + ?NS:expect_reply('get',{data,Data}), + timer:sleep(1000), + case ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]},1) of + {error,timeout} -> ok; % problem not triggered + {ok,Data} -> ok % problem possibly triggered + end, + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + get_xpath(Config) -> DataDir = ?config(data_dir,Config), {ok,Client} = open_success(DataDir), diff --git a/lib/erl_interface/src/connect/ei_resolve.c b/lib/erl_interface/src/connect/ei_resolve.c index 3f1be2b17d..6381b02393 100644 --- a/lib/erl_interface/src/connect/ei_resolve.c +++ b/lib/erl_interface/src/connect/ei_resolve.c @@ -601,6 +601,16 @@ struct hostent *ei_gethostbyaddr(const char *addr, int len, int type) return gethostbyaddr(addr, len, type); } +/* + * Imprecise way to select the actually available gethostbyname_r and + * gethostbyaddr_r. + * + * TODO: check this properly in configure.in + */ +#if (defined(__linux__) || (__FreeBSD_version >= 602000) || defined(__DragonFly__)) + #define HAVE_GETHOSTBYADDR_R_8 1 +#endif + struct hostent *ei_gethostbyaddr_r(const char *addr, int length, int type, @@ -616,7 +626,7 @@ struct hostent *ei_gethostbyaddr_r(const char *addr, #ifndef HAVE_GETHOSTBYNAME_R return my_gethostbyaddr_r(addr,length,type,hostp,buffer,buflen,h_errnop); #else -#if (defined(__GLIBC__) || (__FreeBSD_version >= 602000) || defined(__DragonFly__)) +#ifdef HAVE_GETHOSTBYADDR_R_8 struct hostent *result; gethostbyaddr_r(addr, length, type, hostp, buffer, buflen, &result, @@ -643,7 +653,7 @@ struct hostent *ei_gethostbyname_r(const char *name, #ifndef HAVE_GETHOSTBYNAME_R return my_gethostbyname_r(name,hostp,buffer,buflen,h_errnop); #else -#if (defined(__GLIBC__) || (__FreeBSD_version >= 602000) || defined(__DragonFly__) || defined(__ANDROID__)) +#ifdef HAVE_GETHOSTBYADDR_R_8 struct hostent *result; gethostbyname_r(name, hostp, buffer, buflen, &result, h_errnop); diff --git a/lib/inets/src/http_client/httpc_handler.erl b/lib/inets/src/http_client/httpc_handler.erl index e6dcfee818..d1c52dcc78 100644 --- a/lib/inets/src/http_client/httpc_handler.erl +++ b/lib/inets/src/http_client/httpc_handler.erl @@ -1827,11 +1827,13 @@ host_header(_, URI) -> tls_upgrade(#state{status = {ssl_tunnel, #request{settings = - #http_options{ssl = {_, TLSOptions} = SocketType}, - address = Address} = Request}, + #http_options{ssl = {_, TLSOptions0} = SocketType}, + address = {Host, _} = Address} = Request}, session = #session{socket = TCPSocket} = Session0, options = Options} = State) -> + TLSOptions = maybe_add_sni(Host, TLSOptions0), + case ssl:connect(TCPSocket, TLSOptions) of {ok, TLSSocket} -> ClientClose = httpc_request:is_client_closing(Request#request.headers), @@ -1862,6 +1864,15 @@ tls_upgrade(#state{status = {stop, normal, State#state{request = Request}} end. +maybe_add_sni(Host, Options) -> + case http_util:is_hostname(Host) andalso + not lists:keymember(server_name_indication, 1, Options) of + true -> + [{server_name_indication, Host} | Options]; + false -> + Options + end. + %% --------------------------------------------------------------------- %% Session wrappers %% --------------------------------------------------------------------- diff --git a/lib/inets/src/http_lib/http_chunk.erl b/lib/inets/src/http_lib/http_chunk.erl index 9699856bf8..7325f24809 100644 --- a/lib/inets/src/http_lib/http_chunk.erl +++ b/lib/inets/src/http_lib/http_chunk.erl @@ -25,7 +25,7 @@ -include("http_internal.hrl"). %% API --export([decode/3, encode/1, encode_last/0, handle_headers/2]). +-export([decode/3, encode/1, encode_last/0, encode_last/1, handle_headers/2]). %% Callback API - used for example if the chunkedbody is received a %% little at a time on a socket. -export([decode_size/1, ignore_extensions/1, decode_data/1, decode_trailer/1]). @@ -85,6 +85,11 @@ encode(Chunk) when is_list(Chunk)-> encode_last() -> <<$0, ?CR, ?LF, ?CR, ?LF >>. +encode_last([]) -> + encode_last(); +encode_last(Trailers0) -> + Trailers = list_to_binary(encode_trailers(Trailers0)), + <<$0, ?CR, ?LF, Trailers/binary>>. %%------------------------------------------------------------------------- %% handle_headers(HeaderRecord, ChunkedHeaders) -> NewHeaderRecord @@ -276,10 +281,18 @@ decode_trailer(<<?CR, ?LF, Rest/binary>>, Header, Headers, Body, BodyLength, Rem Body, BodyLength, RemainingSize, TotalMaxHeaderSize); decode_trailer(<<Octet, Rest/binary>>, Header, Headers, Body, BodyLength, RemainingSize, TotalMaxHeaderSize) -> - decode_trailer(Rest, [Octet | Header], Headers, - Body, BodyLength, RemainingSize - 1, TotalMaxHeaderSize). + decode_trailer(Rest, [Octet | Header], Headers, + Body, BodyLength, remaing_size(RemainingSize, 1), TotalMaxHeaderSize). remaing_size(nolimit, _) -> nolimit; remaing_size(Total, Consumed) -> Total - Consumed. + +encode_trailers(Trailers) -> + encode_trailers(Trailers, ""). + +encode_trailers([], Acc) -> + Acc ++ ?CRLF ++ ?CRLF; +encode_trailers([{Header, Value} | Rest], Acc) -> + encode_trailers(Rest, Header ++ ":" ++ Value ++ ?CRLF ++ Acc). diff --git a/lib/inets/src/http_lib/http_response.erl b/lib/inets/src/http_lib/http_response.erl index d13670700c..42e5dd263d 100644 --- a/lib/inets/src/http_lib/http_response.erl +++ b/lib/inets/src/http_lib/http_response.erl @@ -65,6 +65,8 @@ header_list(Headers) -> %%%======================================================================== fill_headers([], _, Headers) -> Headers; +fill_headers([[]], _, Headers) -> + Headers; fill_headers([[Ch|HeaderFold]|Tail], Folded, Headers) when Ch == $\t; Ch == $\s -> fill_headers(Tail, [HeaderFold|Folded], Headers); diff --git a/lib/inets/src/http_server/httpd_example.erl b/lib/inets/src/http_server/httpd_example.erl index d729affd6d..0222487a4b 100644 --- a/lib/inets/src/http_server/httpd_example.erl +++ b/lib/inets/src/http_server/httpd_example.erl @@ -24,7 +24,7 @@ -export([newformat/3]). %% These are used by the inets test-suite --export([delay/1]). +-export([delay/1, chunk_timeout/3]). print(String) -> @@ -142,3 +142,11 @@ i(F) -> i(F,[]). i(F,A) -> io:format(F ++ "~n",A). sleep(T) -> receive after T -> ok end. + +%% ------------------------------------------------------ + +chunk_timeout(SessionID, _, StrInt) -> + mod_esi:deliver(SessionID, "Tranfer-Encoding:chunked/html\r\n\r\n"), + mod_esi:deliver(SessionID, top("Test chunk encoding timeout")), + timer:sleep(20000), + mod_esi:deliver(SessionID, footer()). diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl index 143d599edb..134576059d 100644 --- a/lib/inets/src/http_server/httpd_request_handler.erl +++ b/lib/inets/src/http_server/httpd_request_handler.erl @@ -630,21 +630,10 @@ decrease(N) when is_integer(N) -> decrease(N) -> N. -error_log(ReasonString, Info) -> +error_log(ReasonString, #mod{config_db = ConfigDB}) -> Error = lists:flatten( io_lib:format("Error reading request: ~s", [ReasonString])), - error_log(mod_log, Info, Error), - error_log(mod_disk_log, Info, Error). - -error_log(Mod, #mod{config_db = ConfigDB} = Info, String) -> - Modules = httpd_util:lookup(ConfigDB, modules, - [mod_get, mod_head, mod_log]), - case lists:member(Mod, Modules) of - true -> - Mod:error_log(Info, String); - _ -> - ok - end. + httpd_util:error_log(ConfigDB, Error). %%-------------------------------------------------------------------- diff --git a/lib/inets/src/http_server/httpd_response.erl b/lib/inets/src/http_server/httpd_response.erl index 71243f525a..c0b5f09faf 100644 --- a/lib/inets/src/http_server/httpd_response.erl +++ b/lib/inets/src/http_server/httpd_response.erl @@ -20,8 +20,8 @@ %% -module(httpd_response). -export([generate_and_send_response/1, send_status/3, send_header/3, - send_body/3, send_chunk/3, send_final_chunk/2, split_header/2, - is_disable_chunked_send/1, cache_headers/2]). + send_body/3, send_chunk/3, send_final_chunk/2, send_final_chunk/3, + split_header/2, is_disable_chunked_send/1, cache_headers/2]). -export([map_status_code/2]). -include_lib("inets/src/inets_app/inets_internal.hrl"). @@ -89,8 +89,7 @@ traverse_modules(ModData,[Module|Rest]) -> "~n Error: ~p" "~n Stack trace: ~p", [Module, T, E, ?STACK()])), - report_error(mod_log, ModData#mod.config_db, String), - report_error(mod_disk_log, ModData#mod.config_db, String), + httpd_util:error_log(ModData#mod.config_db, String), send_status(ModData, 500, none), done end. @@ -245,7 +244,6 @@ send_chunk(_, <<>>, _) -> ok; send_chunk(_, [], _) -> ok; - send_chunk(#mod{http_version = "HTTP/1.1", socket_type = Type, socket = Sock}, Response0, false) -> Response = http_chunk:encode(Response0), @@ -254,10 +252,13 @@ send_chunk(#mod{http_version = "HTTP/1.1", send_chunk(#mod{socket_type = Type, socket = Sock} = _ModData, Response, _) -> httpd_socket:deliver(Type, Sock, Response). +send_final_chunk(Mod, IsDisableChunkedSend) -> + send_final_chunk(Mod, [], IsDisableChunkedSend). + send_final_chunk(#mod{http_version = "HTTP/1.1", - socket_type = Type, socket = Sock}, false) -> - httpd_socket:deliver(Type, Sock, http_chunk:encode_last()); -send_final_chunk(#mod{socket_type = Type, socket = Sock}, _) -> + socket_type = Type, socket = Sock}, Trailers, false) -> + httpd_socket:deliver(Type, Sock, http_chunk:encode_last(Trailers)); +send_final_chunk(#mod{socket_type = Type, socket = Sock}, _, _) -> httpd_socket:close(Type, Sock). is_disable_chunked_send(Db) -> @@ -397,16 +398,6 @@ send_response_old(#mod{socket_type = Type, content_length(Body)-> integer_to_list(httpd_util:flatlength(Body)). -report_error(Mod, ConfigDB, Error) -> - Modules = httpd_util:lookup(ConfigDB, modules, - [mod_get, mod_head, mod_log]), - case lists:member(Mod, Modules) of - true -> - Mod:report_error(ConfigDB, Error); - _ -> - ok - end. - handle_headers([], NewHeaders) -> {ok, NewHeaders}; diff --git a/lib/inets/src/http_server/httpd_util.erl b/lib/inets/src/http_server/httpd_util.erl index 0387d71911..ab43f0b378 100644 --- a/lib/inets/src/http_server/httpd_util.erl +++ b/lib/inets/src/http_server/httpd_util.erl @@ -31,7 +31,7 @@ convert_netscapecookie_date/1, enable_debug/1, valid_options/3, modules_validate/1, module_validate/1, dir_validate/2, file_validate/2, mime_type_validate/1, - mime_types_validate/1, custom_date/0]). + mime_types_validate/1, custom_date/0, error_log/2]). -export([encode_hex/1, decode_hex/1]). -include_lib("kernel/include/file.hrl"). @@ -776,3 +776,17 @@ do_enable_debug([{Level,Modules}|Rest]) ok end, do_enable_debug(Rest). + +error_log(ConfigDb, Error) -> + error_log(mod_log, ConfigDb, Error), + error_log(mod_disk_log, ConfigDb, Error). + +error_log(Mod, ConfigDB, Error) -> + Modules = httpd_util:lookup(ConfigDB, modules, + [mod_get, mod_head, mod_log]), + case lists:member(Mod, Modules) of + true -> + Mod:report_error(ConfigDB, Error); + _ -> + ok + end. diff --git a/lib/inets/src/http_server/mod_esi.erl b/lib/inets/src/http_server/mod_esi.erl index b9a0797977..1923411449 100644 --- a/lib/inets/src/http_server/mod_esi.erl +++ b/lib/inets/src/http_server/mod_esi.erl @@ -376,7 +376,6 @@ erl_scheme_webpage_chunk(Mod, Func, Env, Input, ModData) -> end), Response = deliver_webpage_chunk(ModData, Pid), - process_flag(trap_exit,false), Response. @@ -418,7 +417,6 @@ deliver_webpage_chunk(#mod{config_db = Db} = ModData, Pid, Timeout) -> ?hdrv("deliver_webpage_chunk - timeout", []), send_headers(ModData, 504, [{"connection", "close"}]), httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket), - process_flag(trap_exit,false), {proceed,[{response, {already_sent, 200, 0}} | ModData#mod.data]} end. @@ -446,7 +444,6 @@ send_headers(ModData, StatusCode, HTTPHeaders) -> ExtraHeaders ++ HTTPHeaders). handle_body(_, #mod{method = "HEAD"} = ModData, _, _, Size, _) -> - process_flag(trap_exit,false), {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; handle_body(Pid, ModData, Body, Timeout, Size, IsDisableChunkedSend) -> @@ -454,34 +451,54 @@ handle_body(Pid, ModData, Body, Timeout, Size, IsDisableChunkedSend) -> httpd_response:send_chunk(ModData, Body, IsDisableChunkedSend), receive {esi_data, Data} when is_binary(Data) -> - ?hdrt("handle_body - received binary data (esi)", []), handle_body(Pid, ModData, Data, Timeout, Size + byte_size(Data), IsDisableChunkedSend); {esi_data, Data} -> - ?hdrt("handle_body - received data (esi)", []), handle_body(Pid, ModData, Data, Timeout, Size + length(Data), IsDisableChunkedSend); {ok, Data} -> - ?hdrt("handle_body - received data (ok)", []), handle_body(Pid, ModData, Data, Timeout, Size + length(Data), IsDisableChunkedSend); {'EXIT', Pid, normal} when is_pid(Pid) -> - ?hdrt("handle_body - exit:normal", []), httpd_response:send_final_chunk(ModData, IsDisableChunkedSend), {proceed, [{response, {already_sent, 200, Size}} | ModData#mod.data]}; {'EXIT', Pid, Reason} when is_pid(Pid) -> - ?hdrv("handle_body - exit", [{reason, Reason}]), - httpd_response:send_final_chunk(ModData, IsDisableChunkedSend), - exit({mod_esi_linked_process_died, Pid, Reason}) - + Error = lists:flatten(io_lib:format("mod_esi process failed with reason ~p", [Reason])), + httpd_util:error_log(ModData#mod.config_db, Error), + httpd_response:send_final_chunk(ModData, + [{"Warning", "199 inets server - body maybe incomplete, " + "internal server error"}], + IsDisableChunkedSend), + done after Timeout -> - ?hdrv("handle_body - timeout", []), - process_flag(trap_exit,false), - httpd_response:send_final_chunk(ModData, IsDisableChunkedSend), - exit({mod_esi_linked_process_timeout, Pid}) + kill_esi_delivery_process(Pid), + httpd_response:send_final_chunk(ModData, [{"Warning", "199 inets server - " + "body maybe incomplete, timed out"}], + IsDisableChunkedSend), + done end. +kill_esi_delivery_process(Pid) -> + exit(Pid, kill), + receive + {'EXIT', Pid, killed} -> + %% Clean message queue + receive + {esi_data, _} -> + ok + after 0 -> + ok + end, + receive + {ok, _} -> + ok + after 0 -> + ok + end + end. + + erl_script_timeout(Db) -> httpd_util:lookup(Db, erl_script_timeout, ?DEFAULT_ERL_TIMEOUT). diff --git a/lib/inets/test/httpd_1_1.erl b/lib/inets/test/httpd_1_1.erl index dd9d21bbfc..db6def9d17 100644 --- a/lib/inets/test/httpd_1_1.erl +++ b/lib/inets/test/httpd_1_1.erl @@ -24,7 +24,7 @@ -include_lib("kernel/include/file.hrl"). -export([host/4, chunked/4, expect/4, range/4, if_test/5, trace/4, - head/4, mod_cgi_chunked_encoding_test/5]). + head/4, mod_cgi_chunked_encoding_test/5, mod_esi_chunk_timeout/4]). %% -define(all_keys_lower_case,true). -ifndef(all_keys_lower_case). @@ -274,6 +274,15 @@ mod_cgi_chunked_encoding_test(Type, Port, Host, Node, [Request| Rest])-> [{statuscode, 200}]), mod_cgi_chunked_encoding_test(Type, Port, Host, Node, Rest). + +mod_esi_chunk_timeout(Type, Port, Host, Node) -> + ok = httpd_test_lib:verify_request(Type, Host, Port, Node, + "GET /cgi-bin/erl/httpd_example/chunk_timeout?input=20000 HTTP/1.1\r\n" + "Host:"++ Host ++"\r\n" + "\r\n", + [{statuscode, 200}, + {header, "warning"}]). + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl index 9bd6f3636c..1d8a603981 100644 --- a/lib/inets/test/httpd_SUITE.erl +++ b/lib/inets/test/httpd_SUITE.erl @@ -117,7 +117,7 @@ groups() -> {htaccess, [], [htaccess_1_1, htaccess_1_0, htaccess_0_9]}, {security, [], [security_1_1, security_1_0]}, %% Skip 0.9 as causes timing issus in test code {http_1_1, [], [host, chunked, expect, cgi, cgi_chunked_encoding_test, - trace, range, if_modified_since] ++ http_head() ++ http_get() ++ load()}, + trace, range, if_modified_since, mod_esi_chunk_timeout] ++ http_head() ++ http_get() ++ load()}, {http_1_0, [], [host, cgi, trace] ++ http_head() ++ http_get() ++ load()}, {http_0_9, [], http_head() ++ http_get() ++ load()} ]. @@ -757,6 +757,13 @@ esi(Config) when is_list(Config) -> Config, [{statuscode, 200}, {no_header, "cache-control"}]). %%------------------------------------------------------------------------- +mod_esi_chunk_timeout(Config) when is_list(Config) -> + ok = httpd_1_1:mod_esi_chunk_timeout(?config(type, Config), + ?config(port, Config), + ?config(host, Config), + ?config(node, Config)). + +%%------------------------------------------------------------------------- cgi() -> [{doc, "Test mod_cgi"}]. diff --git a/lib/inets/test/httpd_test_lib.erl b/lib/inets/test/httpd_test_lib.erl index a5b836f651..c58966ce10 100644 --- a/lib/inets/test/httpd_test_lib.erl +++ b/lib/inets/test/httpd_test_lib.erl @@ -235,11 +235,17 @@ validate(RequestStr, #state{status_line = {Version, StatusCode, _}, _ -> ok end, - do_validate(http_response:header_list(Headers), Options, N, P), - check_body(RequestStr, StatusCode, - Headers#http_response_h.'content-type', - list_to_integer(Headers#http_response_h.'content-length'), - Body). + HList = http_response:header_list(Headers), + do_validate(HList, Options, N, P), + case lists:keysearch("warning", 1, HList) of + {value, _} -> + ok; + _ -> + check_body(RequestStr, StatusCode, + Headers#http_response_h.'content-type', + list_to_integer(Headers#http_response_h.'content-length'), + Body) + end. %-------------------------------------------------------------------- %% Internal functions diff --git a/lib/kernel/src/file_io_server.erl b/lib/kernel/src/file_io_server.erl index bf8b9e2747..deb7b315b1 100644 --- a/lib/kernel/src/file_io_server.erl +++ b/lib/kernel/src/file_io_server.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2000-2013. All Rights Reserved. +%% Copyright Ericsson AB 2000-2015. 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. @@ -206,8 +206,8 @@ io_reply(From, ReplyAs, Reply) -> file_request({advise,Offset,Length,Advise}, #state{handle=Handle}=State) -> case ?PRIM_FILE:advise(Handle, Offset, Length, Advise) of - {error,_}=Reply -> - {stop,normal,Reply,State}; + {error,Reason}=Reply -> + {stop,Reason,Reply,State}; Reply -> {reply,Reply,State} end; @@ -215,62 +215,91 @@ file_request({allocate, Offset, Length}, #state{handle = Handle} = State) -> Reply = ?PRIM_FILE:allocate(Handle, Offset, Length), {reply, Reply, State}; +file_request({pread,At,Sz}, State) + when At =:= cur; + At =:= {cur,0} -> + case get_chars(Sz, latin1, State) of + {reply,Reply,NewState} + when is_list(Reply); + is_binary(Reply) -> + {reply,{ok,Reply},NewState}; + Other -> + Other + end; file_request({pread,At,Sz}, - #state{handle=Handle,buf=Buf,read_mode=ReadMode}=State) -> + #state{handle=Handle,buf=Buf}=State) -> case position(Handle, At, Buf) of - {ok,_Offs} -> - case ?PRIM_FILE:read(Handle, Sz) of - {ok,Bin} when ReadMode =:= list -> - std_reply({ok,binary_to_list(Bin)}, State); - Reply -> - std_reply(Reply, State) - end; - Reply -> - std_reply(Reply, State) + {error,_} = Reply -> + {error,Reply,State}; + _ -> + case get_chars(Sz, latin1, State#state{buf= <<>>}) of + {reply,Reply,NewState} + when is_list(Reply); + is_binary(Reply) -> + {reply,{ok,Reply},NewState}; + Other -> + Other + end end; +file_request({pwrite,At,Data}, + #state{buf= <<>>}=State) + when At =:= cur; + At =:= {cur,0} -> + put_chars(Data, latin1, State); file_request({pwrite,At,Data}, #state{handle=Handle,buf=Buf}=State) -> case position(Handle, At, Buf) of - {ok,_Offs} -> - std_reply(?PRIM_FILE:write(Handle, Data), State); - Reply -> - std_reply(Reply, State) + {error,_} = Reply -> + {error,Reply,State}; + _ -> + put_chars(Data, latin1, State) end; file_request(datasync, #state{handle=Handle}=State) -> case ?PRIM_FILE:datasync(Handle) of - {error,_}=Reply -> - {stop,normal,Reply,State}; + {error,Reason}=Reply -> + {stop,Reason,Reply,State}; Reply -> {reply,Reply,State} end; file_request(sync, #state{handle=Handle}=State) -> case ?PRIM_FILE:sync(Handle) of - {error,_}=Reply -> - {stop,normal,Reply,State}; + {error,Reason}=Reply -> + {stop,Reason,Reply,State}; Reply -> {reply,Reply,State} end; file_request(close, #state{handle=Handle}=State) -> - {stop,normal,?PRIM_FILE:close(Handle),State#state{buf= <<>>}}; + case ?PRIM_FILE:close(Handle) of + {error,Reason}=Reply -> + {stop,Reason,Reply,State#state{buf= <<>>}}; + Reply -> + {stop,normal,Reply,State#state{buf= <<>>}} + end; file_request({position,At}, #state{handle=Handle,buf=Buf}=State) -> - std_reply(position(Handle, At, Buf), State); + case position(Handle, At, Buf) of + {error,_} = Reply -> + {error,Reply,State}; + Reply -> + std_reply(Reply, State) + end; file_request(truncate, #state{handle=Handle}=State) -> case ?PRIM_FILE:truncate(Handle) of - {error,_Reason}=Reply -> - {stop,normal,Reply,State#state{buf= <<>>}}; + {error,Reason}=Reply -> + {stop,Reason,Reply,State#state{buf= <<>>}}; Reply -> - {reply,Reply,State} + std_reply(Reply, State) end; file_request(Unknown, #state{}=State) -> Reason = {request, Unknown}, {error,{error,Reason},State}. +%% Standard reply and clear buffer std_reply({error,_}=Reply, State) -> {error,Reply,State#state{buf= <<>>}}; std_reply(Reply, State) -> @@ -286,8 +315,8 @@ io_request({put_chars, Enc, Chars}, io_request({put_chars, Enc, Chars}, #state{handle=Handle,buf=Buf}=State) -> case position(Handle, cur, Buf) of - {error,_}=Reply -> - {stop,normal,Reply,State#state{buf= <<>>}}; + {error,Reason}=Reply -> + {stop,Reason,Reply,State}; _ -> put_chars(Chars, Enc, State#state{buf= <<>>}) end; @@ -368,23 +397,27 @@ io_request_loop([Request|Tail], %% I/O request put_chars %% put_chars(Chars, latin1, #state{handle=Handle, unic=latin1}=State) -> + NewState = State#state{buf = <<>>}, case ?PRIM_FILE:write(Handle, Chars) of - {error,_}=Reply -> - {stop,normal,Reply,State}; + {error,Reason}=Reply -> + {stop,Reason,Reply,NewState}; Reply -> - {reply,Reply,State} + {reply,Reply,NewState} end; put_chars(Chars, InEncoding, #state{handle=Handle, unic=OutEncoding}=State) -> + NewState = State#state{buf = <<>>}, case unicode:characters_to_binary(Chars,InEncoding,OutEncoding) of Bin when is_binary(Bin) -> case ?PRIM_FILE:write(Handle, Bin) of - {error,_}=Reply -> - {stop,normal,Reply,State}; + {error,Reason}=Reply -> + {stop,Reason,Reply,NewState}; Reply -> - {reply,Reply,State} + {reply,Reply,NewState} end; {error,_,_} -> - {stop,normal,{error,{no_translation, InEncoding, OutEncoding}},State} + {stop,no_translation, + {error,{no_translation, InEncoding, OutEncoding}}, + NewState} end. get_line(S, {<<>>, Cont}, OutEnc, @@ -884,11 +917,14 @@ cbv({utf32,little},_) -> %% Compensates ?PRIM_FILE:position/2 for the number of bytes %% we have buffered - -position(Handle, cur, Buf) -> - position(Handle, {cur, 0}, Buf); -position(Handle, {cur, Offs}, Buf) when is_binary(Buf) -> - ?PRIM_FILE:position(Handle, {cur, Offs-byte_size(Buf)}); -position(Handle, At, _Buf) -> - ?PRIM_FILE:position(Handle, At). - +position(Handle, At, Buf) -> + ?PRIM_FILE:position( + Handle, + case At of + cur -> + {cur, -byte_size(Buf)}; + {cur, Offs} -> + {cur, Offs-byte_size(Buf)}; + _ -> + At + end). diff --git a/lib/kernel/test/file_SUITE.erl b/lib/kernel/test/file_SUITE.erl index 8856be31c2..09d9a45197 100644 --- a/lib/kernel/test/file_SUITE.erl +++ b/lib/kernel/test/file_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2013. All Rights Reserved. +%% Copyright Ericsson AB 1996-2015. 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. @@ -48,7 +48,7 @@ -export([cur_dir_0/1, cur_dir_1/1, make_del_dir/1, list_dir/1,list_dir_error/1, untranslatable_names/1, untranslatable_names_error/1, - pos1/1, pos2/1]). + pos1/1, pos2/1, pos3/1]). -export([close/1, consult1/1, path_consult/1, delete/1]). -export([ eval1/1, path_eval/1, script1/1, path_script/1, open1/1, @@ -80,6 +80,7 @@ -export([interleaved_read_write/1]). +-export([unicode/1]). -export([altname/1]). -export([large_file/1, large_write/1]). @@ -114,7 +115,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> - [altname, read_write_file, {group, dirs}, + [unicode, altname, read_write_file, {group, dirs}, {group, files}, delete, rename, names, {group, errors}, {group, compression}, {group, links}, copy, delayed_write, read_ahead, segment_read, segment_write, @@ -136,7 +137,7 @@ groups() -> [open1, old_modes, new_modes, path_open, close, access, read_write, pread_write, append, open_errors, exclusive]}, - {pos, [], [pos1, pos2]}, + {pos, [], [pos1, pos2, pos3]}, {file_info, [], [file_info_basic_file, file_info_basic_directory, file_info_bad, file_info_times, file_write_file_info]}, @@ -1370,6 +1371,27 @@ pos2(Config) when is_list(Config) -> ?line test_server:timetrap_cancel(Dog), ok. +pos3(suite) -> []; +pos3(doc) -> ["When it does not use raw mode, file:position had a bug."]; +pos3(Config) when is_list(Config) -> + ?line Dog = test_server:timetrap(test_server:seconds(5)), + ?line RootDir = ?config(data_dir, Config), + ?line Name = filename:join(RootDir, "realmen.html.gz"), + + ?line {ok, Fd} = ?FILE_MODULE:open(Name, [read, binary]), + ?line {ok, _} = ?FILE_MODULE:read(Fd, 5), + ?line {error, einval} = ?FILE_MODULE:position(Fd, {bof, -1}), + + %% Here ok had returned =( + ?line {error, einval} = ?FILE_MODULE:position(Fd, {cur, -10}), + %% That test is actually questionable since file:position/2 + %% is documented to leave the file position undefined after + %% it has returned an error. But on Posix systems the position + %% is guaranteed to be unchanged after an error return. On e.g + %% Windows there is nothing stated about this in the documentation. + + ?line test_server:timetrap_cancel(Dog), + ok. file_info_basic_file(suite) -> []; file_info_basic_file(doc) -> []; @@ -2761,6 +2783,40 @@ compress_async_crash_loop(N, Path, ExpectedData) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +unicode(Config) when is_list(Config) -> + Dir = ?config(priv_dir, Config), + Name = filename:join(Dir, "data-utf8.txt"), + Txt = lists:seq(128, 255), + D = unicode:characters_to_binary(Txt, latin1, latin1), + {ok,Fd1} = + ?FILE_MODULE:open(Name, [write,read,binary,{encoding,unicode}]), + ok = ?FILE_MODULE:truncate(Fd1), + ok = ?FILE_MODULE:write(Fd1, Txt), + {ok,0} = ?FILE_MODULE:position(Fd1, bof), + {ok,D} = ?FILE_MODULE:read(Fd1, 129), + {ok,0} = ?FILE_MODULE:position(Fd1, bof), + {ok,D1} = ?FILE_MODULE:read(Fd1, 64), + {ok,Pos} = ?FILE_MODULE:position(Fd1, cur), + {ok,D2} = ?FILE_MODULE:pread(Fd1, {cur,0}, 65), + D = <<D1/binary, D2/binary>>, + {ok,D1} = ?FILE_MODULE:pread(Fd1, bof, 64), + {ok,Pos} = ?FILE_MODULE:position(Fd1, Pos), + {ok,D2} = ?FILE_MODULE:read(Fd1, 64), + ok = ?FILE_MODULE:close(Fd1), + %% + RawD = unicode:characters_to_binary(Txt, latin1, unicode), + {ok,RawD} = ?FILE_MODULE:read_file(Name), + %% + {ok,Fd2} = ?FILE_MODULE:open(Name, [read,{encoding,unicode}]), + {ok,Txt} = ?FILE_MODULE:read(Fd2, 129), + {Txt1,Txt2} = lists:split(64, Txt), + {ok,Txt2} = ?FILE_MODULE:pread(Fd2, Pos, 65), + {ok,0} = ?FILE_MODULE:position(Fd2, bof), + {ok,Txt1} = ?FILE_MODULE:read(Fd2, 64), + ok = ?FILE_MODULE:close(Fd2). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + altname(doc) -> "Test the file:altname/1 function"; altname(suite) -> diff --git a/lib/ssh/doc/src/notes.xml b/lib/ssh/doc/src/notes.xml index 012d7051eb..010b1b15c7 100644 --- a/lib/ssh/doc/src/notes.xml +++ b/lib/ssh/doc/src/notes.xml @@ -30,6 +30,22 @@ <file>notes.xml</file> </header> +<section><title>Ssh 4.1.3</title> + + <section><title>Known Bugs and Problems</title> + <list> + <item> + <p> + SSH_MSG_KEX_DH_GEX_REQUEST_OLD implemented to make PuTTY + work with erl server.</p> + <p> + Own Id: OTP-13140</p> + </item> + </list> + </section> + +</section> + <section><title>Ssh 4.1.2</title> <section><title>Fixed Bugs and Malfunctions</title> diff --git a/lib/ssh/doc/src/ssh.xml b/lib/ssh/doc/src/ssh.xml index 1e9acf4a99..18bced2d1d 100644 --- a/lib/ssh/doc/src/ssh.xml +++ b/lib/ssh/doc/src/ssh.xml @@ -85,6 +85,15 @@ <item><p><c>atom()</c> - Name of the Erlang module implementing the subsystem using the <c>ssh_channel</c> behavior, see <seealso marker="ssh_channel">ssh_channel(3)</seealso></p></item> + <tag><c>key_cb() =</c></tag> + <item> + <p><c>atom() | {atom(), list()}</c></p> + <p><c>atom()</c> - Name of the erlang module implementing the behaviours + <seealso marker="ssh_client_key_api">ssh_client_key_api</seealso> or + <seealso marker="ssh_client_key_api">ssh_client_key_api</seealso> as the + case maybe.</p> + <p><c>list()</c> - List of options that can be passed to the callback module.</p> + </item> <tag><c>channel_init_args() =</c></tag> <item><p><c>list()</c></p></item> @@ -272,11 +281,13 @@ kex is implicit but public_key is set explicitly.</p> password, if the password authentication method is attempted.</p> </item> - <tag><c><![CDATA[{key_cb, atom()}]]></c></tag> + <tag><c><![CDATA[{key_cb, key_cb()}]]></c></tag> <item> - <p>Module implementing the behaviour - <seealso marker="ssh_client_key_api">ssh_client_key_api</seealso>. - Can be used to customize the handling of public keys. + <p>Module implementing the behaviour <seealso + marker="ssh_client_key_api">ssh_client_key_api</seealso>. Can be used to + customize the handling of public keys. If callback options are provided + along with the module name, they are made available to the callback + module via the options passed to it under the key 'key_cb_private'. </p> </item> <tag><c><![CDATA[{quiet_mode, atom() = boolean()}]]></c></tag> @@ -607,11 +618,13 @@ kex is implicit but public_key is set explicitly.</p> </p> </item> - <tag><c><![CDATA[{key_cb, atom()}]]></c></tag> + <tag><c><![CDATA[{key_cb, key_cb()}]]></c></tag> <item> - <p>Module implementing the behaviour - <seealso marker="ssh_server_key_api">ssh_server_key_api</seealso>. - Can be used to customize the handling of public keys. + <p>Module implementing the behaviour <seealso + marker="ssh_server_key_api">ssh_server_key_api</seealso>. Can be used to + customize the handling of public keys. If callback options are provided + along with the module name, they are made available to the callback + module via the options passed to it under the key 'key_cb_private'. </p> </item> diff --git a/lib/ssh/doc/src/ssh_app.xml b/lib/ssh/doc/src/ssh_app.xml index 29cbbd79a2..79dd1e210e 100644 --- a/lib/ssh/doc/src/ssh_app.xml +++ b/lib/ssh/doc/src/ssh_app.xml @@ -137,6 +137,19 @@ <p>Supported algorithms are:</p> <taglist> + <tag>Key exchange algorithms</tag> + <item> + <list type="bulleted"> + <item>ecdh-sha2-nistp256</item> + <item>ecdh-sha2-nistp384</item> + <item>ecdh-sha2-nistp521</item> + <item>diffie-hellman-group-exchange-sha1</item> + <item>diffie-hellman-group-exchange-sha256</item> + <item>diffie-hellman-group14-sha1</item> + <item>diffie-hellman-group1-sha1</item> + </list> + </item> + <tag>Public key algorithms</tag> <item> <list type="bulleted"> @@ -157,30 +170,26 @@ </list> </item> - <tag>Encryption algorithms</tag> + <tag>Encryption algorithms (ciphers)</tag> <item> <list type="bulleted"> + <item>[email protected] (AEAD_AES_128_GCM)</item> + <item>[email protected] (AEAD_AES_256_GCM)</item> <item>aes128-ctr</item> <item>aes192-ctr</item> <item>aes256-ctr</item> <item>aes128-cbc</item> <item>3des-cbc</item> </list> + <p>Following the internet de-facto standard, the cipher and mac algorithm AEAD_AES_128_GCM is selected when the + cipher [email protected] is negotiated. The cipher and mac algorithm AEAD_AES_256_GCM is selected when the + cipher [email protected] is negotiated. + </p> + <p>See the text at the description of <seealso marker="#rfc5647_note">the rfc 5647 further down</seealso> + for more information. + </p> </item> - - <tag>Key exchange algorithms</tag> - <item> - <list type="bulleted"> - <item>ecdh-sha2-nistp256</item> - <item>ecdh-sha2-nistp384</item> - <item>ecdh-sha2-nistp521</item> - <item>diffie-hellman-group-exchange-sha1</item> - <item>diffie-hellman-group-exchange-sha256</item> - <item>diffie-hellman-group14-sha1</item> - <item>diffie-hellman-group1-sha1</item> - </list> - </item> - + <tag>Compression algorithms</tag> <item> <list type="bulleted"> @@ -255,6 +264,30 @@ <p></p> </item> + <item><url href="https://tools.ietf.org/html/rfc5647">RFC 5647</url>, AES Galois Counter Mode for + the Secure Shell Transport Layer Protocol. + <p><marker id="rfc5647_note"/>There is an ambiguity in the synchronized selection of cipher and mac algorithm. + This is resolved by OpenSSH in the ciphers [email protected] and [email protected] which are implemented. + If the explicit ciphers and macs AEAD_AES_128_GCM or AEAD_AES_256_GCM are needed, + they could be enabled with the option preferred_algorithms. + <warning> + If the client or the server is not Erlang/OTP, it is the users responsibility to check that + other implementation has the same interpretation of AEAD_AES_*_GCM as the Erlang/OTP SSH before + enabling them. The aes*[email protected] variants are always safe to use since they lack the + ambiguity. + </warning> + </p> + <p>The second paragraph in section 5.1 is resolved as: + <list type="ordered"> + <item>If the negotiated cipher is AEAD_AES_128_GCM, the mac algorithm is set to AEAD_AES_128_GCM.</item> + <item>If the negotiated cipher is AEAD_AES_256_GCM, the mac algorithm is set to AEAD_AES_256_GCM.</item> + <item>If the mac algorithm is AEAD_AES_128_GCM, the cipher is set to AEAD_AES_128_GCM.</item> + <item>If the mac algorithm is AEAD_AES_256_GCM, the cipher is set to AEAD_AES_256_GCM.</item> + </list> + The first rule that matches when read in order from the top is applied + </p> + </item> + <item><url href="https://tools.ietf.org/html/rfc5656">RFC 5656</url>, Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer. <p>Except @@ -266,6 +299,13 @@ </list> </p> </item> + + <item><url href="https://tools.ietf.org/html/rfc6668">RFC 6668</url>, SHA-2 Data Integrity Verification for + the Secure Shell (SSH) Transport Layer Protocol + <p>Comment: Defines hmac-sha2-256 and hmac-sha2-512 + </p> + </item> + </list> </section> diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl index 5bde184070..1d29c95229 100644 --- a/lib/ssh/src/ssh.erl +++ b/lib/ssh/src/ssh.erl @@ -235,10 +235,27 @@ start_daemon(Host, Port, Options, Inet) -> {error, _Reason} = Error -> Error; {SocketOptions, SshOptions}-> - do_start_daemon(Host, Port,[{role, server} |SshOptions] , [Inet | SocketOptions]) + try + do_start_daemon(Host, Port,[{role, server} |SshOptions] , [Inet | SocketOptions]) + catch + throw:bad_fd -> {error,bad_fd}; + _C:_E -> {error,{cannot_start_daemon,_C,_E}} + end end. -do_start_daemon(Host, Port, Options, SocketOptions) -> +do_start_daemon(Host0, Port0, Options, SocketOptions) -> + {Host,Port} = try + case proplists:get_value(fd, SocketOptions) of + undefined -> + {Host0,Port0}; + Fd when Port0==0 -> + find_hostport(Fd); + _ -> + {Host0,Port0} + end + catch + _:_ -> throw(bad_fd) + end, Profile = proplists:get_value(profile, Options, ?DEFAULT_PROFILE), case ssh_system_sup:system_supervisor(Host, Port, Profile) of undefined -> @@ -272,6 +289,22 @@ do_start_daemon(Host, Port, Options, SocketOptions) -> end end. +find_hostport(Fd) -> + %% Using internal functions inet:open/8 and inet:close/0. + %% Don't try this at home unless you know what you are doing! + {ok,S} = inet:open(Fd, {0,0,0,0}, 0, [], tcp, inet, stream, inet_tcp), + {ok, HostPort} = inet:sockname(S), + ok = inet:close(S), + HostPort. + +%% find_port(Fd) -> +%% %% Hack.... +%% {ok,TmpSock} = gen_tcp:listen(0,[{fd,Fd}]), +%% {ok, {_,ThePort}} = inet:sockname(TmpSock), +%% gen_tcp:close(TmpSock), +%% ThePort. + + handle_options(Opts) -> try handle_option(algs_compatibility(proplists:unfold(Opts)), [], []) of {Inet, Ssh} -> @@ -336,8 +369,12 @@ handle_option([{user_passwords, _} = Opt | Rest], SocketOptions, SshOptions) -> handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]); handle_option([{pwdfun, _} = Opt | Rest], SocketOptions, SshOptions) -> handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]); -handle_option([{key_cb, _} = Opt | Rest], SocketOptions, SshOptions) -> - handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]); +handle_option([{key_cb, {Module, Options}} | Rest], SocketOptions, SshOptions) -> + handle_option(Rest, SocketOptions, [handle_ssh_option({key_cb, Module}), + handle_ssh_priv_option({key_cb_private, Options}) | + SshOptions]); +handle_option([{key_cb, Module} | Rest], SocketOptions, SshOptions) -> + handle_option([{key_cb, {Module, []}} | Rest], SocketOptions, SshOptions); handle_option([{keyboard_interact_fun, _} = Opt | Rest], SocketOptions, SshOptions) -> handle_option(Rest, SocketOptions, [handle_ssh_option(Opt) | SshOptions]); %%Backwards compatibility @@ -511,6 +548,9 @@ handle_ssh_option({pwdfun, Value} = Opt) when is_function(Value,4) -> Opt; handle_ssh_option({key_cb, Value} = Opt) when is_atom(Value) -> Opt; +handle_ssh_option({key_cb, {CallbackMod, CallbackOptions}} = Opt) when is_atom(CallbackMod), + is_list(CallbackOptions) -> + Opt; handle_ssh_option({keyboard_interact_fun, Value} = Opt) when is_function(Value,3) -> Opt; handle_ssh_option({compression, Value} = Opt) when is_atom(Value) -> @@ -577,6 +617,9 @@ handle_ssh_option({profile, Value} = Opt) when is_atom(Value) -> handle_ssh_option(Opt) -> throw({error, {eoptions, Opt}}). +handle_ssh_priv_option({key_cb_private, Value} = Opt) when is_list(Value) -> + Opt. + handle_inet_option({active, _} = Opt) -> throw({error, {{eoptions, Opt}, "SSH has built in flow control, " "and active is handled internally, user is not allowed" diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index 4ad936f742..8efc743b67 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -29,7 +29,6 @@ -define(SSH_DEFAULT_PORT, 22). -define(SSH_MAX_PACKET_SIZE, (256*1024)). --define(SSH_LENGHT_INDICATOR_SIZE, 4). -define(REKEY_TIMOUT, 3600000). -define(REKEY_DATA_TIMOUT, 60000). -define(DEFAULT_PROFILE, default). diff --git a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl index c5ad1d7b6c..d94dedf1bf 100644 --- a/lib/ssh/src/ssh_acceptor.erl +++ b/lib/ssh/src/ssh_acceptor.erl @@ -56,7 +56,12 @@ acceptor_init(Parent, Port, Address, SockOpts, Opts, AcceptTimeout) -> error end. -do_socket_listen(Callback, Port, Opts) -> +do_socket_listen(Callback, Port0, Opts) -> + Port = + case proplists:get_value(fd, Opts) of + undefined -> Port0; + _ -> 0 + end, case Callback:listen(Port, Opts) of {error, nxdomain} -> Callback:listen(Port, lists:delete(inet6, Opts)); diff --git a/lib/ssh/src/ssh_connect.hrl b/lib/ssh/src/ssh_connect.hrl index 6db89c5d80..9f9f3de8fa 100644 --- a/lib/ssh/src/ssh_connect.hrl +++ b/lib/ssh/src/ssh_connect.hrl @@ -248,6 +248,9 @@ local_id, %% local channel id recv_window_size, + recv_window_pending = 0, %% Sum of window size updates that has not + %% yet been sent. This limits the number + %% of sent update msgs. recv_packet_size, recv_close = false, diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 8448218d91..516a09bf6a 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -433,6 +433,12 @@ key_exchange(#ssh_msg_kex_dh_gex_request{} = Msg, send_msg(GexGroup, State), {next_state, key_exchange_dh_gex_init, next_packet(State#state{ssh_params = Ssh})}; +key_exchange(#ssh_msg_kex_dh_gex_request_old{} = Msg, + #state{ssh_params = #ssh{role = server} = Ssh0} = State) -> + {ok, GexGroup, Ssh} = ssh_transport:handle_kex_dh_gex_request(Msg, Ssh0), + send_msg(GexGroup, State), + {next_state, key_exchange_dh_gex_init, next_packet(State#state{ssh_params = Ssh})}; + key_exchange(#ssh_msg_kex_dh_gex_group{} = Msg, #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> {ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, Ssh0), @@ -731,13 +737,28 @@ handle_event({adjust_window, ChannelId, Bytes}, StateName, #connection{channel_cache = Cache}} = State0) -> State = case ssh_channel:cache_lookup(Cache, ChannelId) of - #channel{recv_window_size = WinSize, remote_id = Id} = Channel -> - ssh_channel:cache_update(Cache, Channel#channel{recv_window_size = - WinSize + Bytes}), - Msg = ssh_connection:channel_adjust_window_msg(Id, Bytes), + #channel{recv_window_size = WinSize, + recv_window_pending = Pending, + recv_packet_size = PktSize} = Channel + when (WinSize-Bytes) >= 2*PktSize -> + %% The peer can send at least two more *full* packet, no hurry. + ssh_channel:cache_update(Cache, + Channel#channel{recv_window_pending = Pending + Bytes}), + State0; + + #channel{recv_window_size = WinSize, + recv_window_pending = Pending, + remote_id = Id} = Channel -> + %% Now we have to update the window - we can't receive so many more pkts + ssh_channel:cache_update(Cache, + Channel#channel{recv_window_size = + WinSize + Bytes + Pending, + recv_window_pending = 0}), + Msg = ssh_connection:channel_adjust_window_msg(Id, Bytes + Pending), send_replies([{connection_reply, Msg}], State0); - undefined -> - State0 + + undefined -> + State0 end, {next_state, StateName, next_packet(State)}; @@ -970,57 +991,39 @@ handle_info({Protocol, Socket, Info}, hello, transport_protocol = Protocol} = State) -> event({info_line, Info}, hello, State); -handle_info({Protocol, Socket, Data}, Statename, +handle_info({Protocol, Socket, Data}, StateName, #state{socket = Socket, transport_protocol = Protocol, - ssh_params = #ssh{decrypt_block_size = BlockSize, - recv_mac_size = MacSize} = Ssh0, - decoded_data_buffer = <<>>, - encoded_data_buffer = EncData0} = State0) -> - - %% Implementations SHOULD decrypt the length after receiving the - %% first 8 (or cipher block size, whichever is larger) bytes of a - %% packet. (RFC 4253: Section 6 - Binary Packet Protocol) - case size(EncData0) + size(Data) >= erlang:max(8, BlockSize) of - true -> - {Ssh, SshPacketLen, DecData, EncData} = - - ssh_transport:decrypt_first_block(<<EncData0/binary, - Data/binary>>, Ssh0), - case SshPacketLen > ?SSH_MAX_PACKET_SIZE of - true -> - DisconnectMsg = - #ssh_msg_disconnect{code = - ?SSH_DISCONNECT_PROTOCOL_ERROR, - description = "Bad packet length " - ++ integer_to_list(SshPacketLen), - language = "en"}, - handle_disconnect(DisconnectMsg, State0); - false -> - RemainingSshPacketLen = - (SshPacketLen + ?SSH_LENGHT_INDICATOR_SIZE) - - BlockSize + MacSize, - State = State0#state{ssh_params = Ssh}, - handle_ssh_packet_data(RemainingSshPacketLen, - DecData, EncData, Statename, - State) - end; - false -> - {next_state, Statename, - next_packet(State0#state{encoded_data_buffer = - <<EncData0/binary, Data/binary>>})} + ssh_params = Ssh0, + decoded_data_buffer = DecData0, + encoded_data_buffer = EncData0, + undecoded_packet_length = RemainingSshPacketLen0} = State0) -> + Encoded = <<EncData0/binary, Data/binary>>, + case ssh_transport:handle_packet_part(DecData0, Encoded, RemainingSshPacketLen0, Ssh0) of + {get_more, DecBytes, EncDataRest, RemainingSshPacketLen, Ssh1} -> + {next_state, StateName, + next_packet(State0#state{encoded_data_buffer = EncDataRest, + decoded_data_buffer = DecBytes, + undecoded_packet_length = RemainingSshPacketLen, + ssh_params = Ssh1})}; + {decoded, MsgBytes, EncDataRest, Ssh1} -> + generate_event(MsgBytes, StateName, + State0#state{ssh_params = Ssh1, + %% Important to be set for + %% next_packet +%%% FIXME: the following three seem to always be set in generate_event! + decoded_data_buffer = <<>>, + undecoded_packet_length = undefined, + encoded_data_buffer = EncDataRest}, + EncDataRest); + {bad_mac, Ssh1} -> + DisconnectMsg = + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Bad mac", + language = ""}, + handle_disconnect(DisconnectMsg, State0#state{ssh_params=Ssh1}) end; - -handle_info({Protocol, Socket, Data}, Statename, - #state{socket = Socket, - transport_protocol = Protocol, - decoded_data_buffer = DecData, - encoded_data_buffer = EncData, - undecoded_packet_length = Len} = - State) when is_integer(Len) -> - handle_ssh_packet_data(Len, DecData, <<EncData/binary, Data/binary>>, - Statename, State); - + handle_info({CloseTag, _Socket}, _StateName, #state{transport_close_tag = CloseTag, ssh_params = #ssh{role = _Role, opts = _Opts}} = State) -> @@ -1631,57 +1634,6 @@ after_new_keys_events({connection_reply, _Data} = Reply, {StateName, State}) -> NewState = send_replies([Reply], State), {next_state, StateName, NewState}. -handle_ssh_packet_data(RemainingSshPacketLen, DecData, EncData, StateName, - State) -> - EncSize = size(EncData), - case RemainingSshPacketLen > EncSize of - true -> - {next_state, StateName, - next_packet(State#state{decoded_data_buffer = DecData, - encoded_data_buffer = EncData, - undecoded_packet_length = - RemainingSshPacketLen})}; - false -> - handle_ssh_packet(RemainingSshPacketLen, StateName, - State#state{decoded_data_buffer = DecData, - encoded_data_buffer = EncData}) - - end. - -handle_ssh_packet(Length, StateName, #state{decoded_data_buffer = DecData0, - encoded_data_buffer = EncData0, - ssh_params = Ssh0, - transport_protocol = _Protocol, - socket = _Socket} = State0) -> - try - {Ssh1, DecData, EncData, Mac} = - ssh_transport:unpack(EncData0, Length, Ssh0), - SshPacket = <<DecData0/binary, DecData/binary>>, - case ssh_transport:is_valid_mac(Mac, SshPacket, Ssh1) of - true -> - PacketData = ssh_transport:msg_data(SshPacket), - {Ssh1, Msg} = ssh_transport:decompress(Ssh1, PacketData), - generate_event(Msg, StateName, - State0#state{ssh_params = Ssh1, - %% Important to be set for - %% next_packet - decoded_data_buffer = <<>>}, - EncData); - false -> - DisconnectMsg = - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, - description = "Bad mac", - language = "en"}, - handle_disconnect(DisconnectMsg, State0) - end - catch _:_ -> - Disconnect = - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, - description = "Bad input", - language = "en"}, - handle_disconnect(Disconnect, State0) - end. - handle_disconnect(DisconnectMsg, State) -> handle_disconnect(own, DisconnectMsg, State). diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl index a6549f1c73..819cba697e 100644 --- a/lib/ssh/src/ssh_sftpd.erl +++ b/lib/ssh/src/ssh_sftpd.erl @@ -30,6 +30,7 @@ -include("ssh.hrl"). -include("ssh_xfer.hrl"). +-include("ssh_connect.hrl"). %% For ?DEFAULT_PACKET_SIZE and ?DEFAULT_WINDOW_SIZE %%-------------------------------------------------------------------- %% External exports @@ -47,6 +48,7 @@ file_handler, % atom() - callback module file_state, % state for the file callback module max_files, % integer >= 0 max no files sent during READDIR + options, % from the subsystem declaration handles % list of open handles %% handle is either {<int>, directory, {Path, unread|eof}} or %% {<int>, file, {Path, IoDevice}} @@ -121,6 +123,7 @@ init(Options) -> MaxLength = proplists:get_value(max_files, Options, 0), Vsn = proplists:get_value(sftpd_vsn, Options, 5), {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength, + options = Options, handles = [], pending = <<>>, xf = #ssh_xfer{vsn = Vsn, ext = []}}}. @@ -164,7 +167,9 @@ handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) -> %% Description: Handles other messages %%-------------------------------------------------------------------- handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, - #state{xf =Xf} = State) -> + #state{xf = Xf, + options = Options} = State) -> + maybe_increase_recv_window(ConnectionManager, ChannelId, Options), {ok, State#state{xf = Xf#ssh_xfer{cm = ConnectionManager, channel = ChannelId}}}. @@ -934,3 +939,18 @@ rename(Path, Path2, ReqId, State0) -> {Status, FS1} = FileMod:rename(Path, Path2, FS0), State1 = State0#state{file_state = FS1}, send_status(Status, ReqId, State1). + + +maybe_increase_recv_window(ConnectionManager, ChannelId, Options) -> + WantedRecvWindowSize = + proplists:get_value(recv_window_size, Options, 1000000), + NumPkts = WantedRecvWindowSize div ?DEFAULT_PACKET_SIZE, + Increment = NumPkts*?DEFAULT_PACKET_SIZE - ?DEFAULT_WINDOW_SIZE, + + if + Increment > 0 -> + ssh_connection:adjust_window(ConnectionManager, ChannelId, + Increment); + Increment =< 0 -> + do_nothing + end. diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 0c999b96cc..67a0d29bb8 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -31,10 +31,10 @@ -include("ssh.hrl"). -export([versions/2, hello_version_msg/1]). --export([next_seqnum/1, decrypt_first_block/2, decrypt_blocks/3, +-export([next_seqnum/1, supported_algorithms/0, supported_algorithms/1, default_algorithms/0, default_algorithms/1, - is_valid_mac/3, + handle_packet_part/4, handle_hello_version/1, key_exchange_init_msg/1, key_init/3, new_keys_message/1, @@ -45,9 +45,13 @@ handle_kex_ecdh_init/2, handle_kex_ecdh_reply/2, extract_public_key/1, - unpack/3, decompress/2, ssh_packet/2, pack/2, pack/3, msg_data/1, + ssh_packet/2, pack/2, sign/3, verify/4]). +%%% For test suites +-export([pack/3]). +-export([decompress/2, decrypt_blocks/3, is_valid_mac/3 ]). % FIXME: remove + %%%---------------------------------------------------------------------------- %%% %%% There is a difference between supported and default algorithms. The @@ -66,10 +70,15 @@ default_algorithms() -> [{K,default_algorithms(K)} || K <- algo_classes()]. algo_classes() -> [kex, public_key, cipher, mac, compression]. -%% default_algorithms(kex) -> % Example of how to disable an algorithm -%% supported_algorithms(kex, ['ecdh-sha2-nistp521']); + +default_algorithms(cipher) -> + supported_algorithms(cipher, same(['AEAD_AES_128_GCM', + 'AEAD_AES_256_GCM'])); +default_algorithms(mac) -> + supported_algorithms(mac, same(['AEAD_AES_128_GCM', + 'AEAD_AES_256_GCM'])); default_algorithms(Alg) -> - supported_algorithms(Alg). + supported_algorithms(Alg, []). supported_algorithms() -> [{K,supported_algorithms(K)} || K <- algo_classes()]. @@ -97,19 +106,25 @@ supported_algorithms(public_key) -> supported_algorithms(cipher) -> same( select_crypto_supported( - [{'aes256-ctr', [{ciphers,{aes_ctr,256}}]}, - {'aes192-ctr', [{ciphers,{aes_ctr,192}}]}, - {'aes128-ctr', [{ciphers,{aes_ctr,128}}]}, - {'aes128-cbc', [{ciphers,aes_cbc128}]}, - {'3des-cbc', [{ciphers,des3_cbc}]} + [{'aes256-ctr', [{ciphers,{aes_ctr,256}}]}, + {'aes192-ctr', [{ciphers,{aes_ctr,192}}]}, + {'aes128-ctr', [{ciphers,{aes_ctr,128}}]}, + {'aes128-cbc', [{ciphers,aes_cbc128}]}, + {'[email protected]', [{ciphers,{aes_gcm,128}}]}, + {'[email protected]', [{ciphers,{aes_gcm,256}}]}, + {'AEAD_AES_128_GCM', [{ciphers,{aes_gcm,128}}]}, + {'AEAD_AES_256_GCM', [{ciphers,{aes_gcm,256}}]}, + {'3des-cbc', [{ciphers,des3_cbc}]} ] )); supported_algorithms(mac) -> same( select_crypto_supported( - [{'hmac-sha2-256', [{hashs,sha256}]}, - {'hmac-sha2-512', [{hashs,sha512}]}, - {'hmac-sha1', [{hashs,sha}]} + [{'hmac-sha2-256', [{hashs,sha256}]}, + {'hmac-sha2-512', [{hashs,sha512}]}, + {'hmac-sha1', [{hashs,sha}]}, + {'AEAD_AES_128_GCM', [{ciphers,{aes_gcm,128}}]}, + {'AEAD_AES_256_GCM', [{ciphers,{aes_gcm,256}}]} ] )); supported_algorithms(compression) -> @@ -118,46 +133,6 @@ supported_algorithms(compression) -> 'zlib' ]). -%% Dialyzer complains when not called...supported_algorithms(Key, [{client2server,BL1},{server2client,BL2}]) -> -%% Dialyzer complains when not called... [{client2server,As1},{server2client,As2}] = supported_algorithms(Key), -%% Dialyzer complains when not called... [{client2server,As1--BL1},{server2client,As2--BL2}]; -%% Dialyzer complains when not called...supported_algorithms(Key, BlackList) -> -%% Dialyzer complains when not called... supported_algorithms(Key) -- BlackList. - -select_crypto_supported(L) -> - Sup = [{ec_curve,crypto_supported_curves()} | crypto:supports()], - [Name || {Name,CryptoRequires} <- L, - crypto_supported(CryptoRequires, Sup)]. - -crypto_supported_curves() -> - try crypto:ec_curves() - catch _:_ -> [] - end. - -crypto_supported(Conditions, Supported) -> - lists:all( fun({Tag,CryptoName}) when is_atom(CryptoName) -> - crypto_name_supported(Tag,CryptoName,Supported); - ({Tag,{Name=aes_ctr,Len}}) when is_integer(Len) -> - crypto_name_supported(Tag,Name,Supported) andalso - ctr_len_supported(Name,Len) - end, Conditions). - -crypto_name_supported(Tag, CryptoName, Supported) -> - lists:member(CryptoName, proplists:get_value(Tag,Supported,[])). - -ctr_len_supported(Name, Len) -> - try - crypto:stream_encrypt(crypto:stream_init(Name, <<0:Len>>, <<0:128>>), <<"">>) - of - {_,X} -> is_binary(X) - catch - _:_ -> false - end. - - -same(Algs) -> [{client2server,Algs}, {server2client,Algs}]. - - %%%---------------------------------------------------------------------------- versions(client, Options)-> Vsn = proplists:get_value(vsn, Options, ?DEFAULT_CLIENT_VERSION), @@ -196,12 +171,6 @@ hello_version_msg(Data) -> next_seqnum(SeqNum) -> (SeqNum + 1) band 16#ffffffff. -decrypt_first_block(Bin, #ssh{decrypt_block_size = BlockSize} = Ssh0) -> - <<EncBlock:BlockSize/binary, EncData/binary>> = Bin, - {Ssh, <<?UINT32(PacketLen), _/binary>> = DecData} = - decrypt(Ssh0, EncBlock), - {Ssh, PacketLen, DecData, EncData}. - decrypt_blocks(Bin, Length, Ssh0) -> <<EncBlocks:Length/binary, EncData/binary>> = Bin, {Ssh, DecData} = decrypt(Ssh0, EncBlocks), @@ -464,6 +433,40 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request{min = Min0, language = ""}) end; +handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits}, + Ssh0=#ssh{opts=Opts}) -> + %% server + %% + %% This message was in the draft-00 of rfc4419 + %% (https://tools.ietf.org/html/draft-ietf-secsh-dh-group-exchange-00) + %% In later drafts and the rfc is "is used for backward compatibility". + %% Unfortunatly the rfc does not specify how to treat the parameter n + %% if there is no group of that modulus length :( + %% The draft-00 however specifies that n is the "... number of bits + %% the subgroup should have at least". + %% Further, it says that "Servers and clients SHOULD support groups + %% with a modulus length of k bits, where 1024 <= k <= 8192." + %% + Min0 = NBits, + Max0 = 8192, + {Min, Max} = adjust_gex_min_max(Min0, Max0, Opts), + case public_key:dh_gex_group(Min, NBits, Max, + proplists:get_value(dh_gex_groups,Opts)) of + {ok, {_Sz, {G,P}}} -> + {Public, Private} = generate_key(dh, [P,G]), + {SshPacket, Ssh} = + ssh_packet(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0), + {ok, SshPacket, + Ssh#ssh{keyex_key = {{Private, Public}, {G, P}}, + keyex_info = {-1, -1, NBits} % flag for kex_h hash calc + }}; + {error,_} -> + throw(#ssh_msg_disconnect{ + code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "No possible diffie-hellman-group-exchange group found", + language = ""}) + end; + handle_kex_dh_gex_request(_, _) -> throw({{error,bad_ssh_msg_kex_dh_gex_request}, #ssh_msg_disconnect{ @@ -757,8 +760,12 @@ known_host_key(#ssh{opts = Opts, key_cb = Mod, peer = Peer} = Ssh, %% The first algorithm in each list MUST be the preferred (guessed) %% algorithm. Each string MUST contain at least one algorithm name. select_algorithm(Role, Client, Server) -> - {Encrypt, Decrypt} = select_encrypt_decrypt(Role, Client, Server), - {SendMac, RecvMac} = select_send_recv_mac(Role, Client, Server), + {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), + {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), + + {Encrypt, SendMac} = aead_gcm_simultan(Encrypt0, SendMac0), + {Decrypt, RecvMac} = aead_gcm_simultan(Decrypt0, RecvMac0), + {Compression, Decompression} = select_compression_decompression(Role, Client, Server), @@ -789,6 +796,38 @@ select_algorithm(Role, Client, Server) -> s_lng = S_Lng}, {ok, Alg}. + +%%% It is an agreed problem with RFC 5674 that if the selection is +%%% Cipher = AEAD_AES_x_GCM and +%%% Mac = AEAD_AES_y_GCM (where x =/= y) +%%% then it is undefined what length should be selected. +%%% +%%% If only one of the two lengths (128,256) is available, I claim that +%%% there is no such ambiguity. + +%%% From https://anongit.mindrot.org/openssh.git/plain/PROTOCOL +%%% (read Nov 20, 2015) +%%% 1.6 transport: AES-GCM +%%% +%%% OpenSSH supports the AES-GCM algorithm as specified in RFC 5647. +%%% Because of problems with the specification of the key exchange +%%% the behaviour of OpenSSH differs from the RFC as follows: +%%% +%%% AES-GCM is only negotiated as the cipher algorithms +%%% "[email protected]" or "[email protected]" and never as +%%% an MAC algorithm. Additionally, if AES-GCM is selected as the cipher +%%% the exchanged MAC algorithms are ignored and there doesn't have to be +%%% a matching MAC. + +aead_gcm_simultan('[email protected]', _) -> {'AEAD_AES_128_GCM', 'AEAD_AES_128_GCM'}; +aead_gcm_simultan('[email protected]', _) -> {'AEAD_AES_256_GCM', 'AEAD_AES_256_GCM'}; +aead_gcm_simultan('AEAD_AES_128_GCM', _) -> {'AEAD_AES_128_GCM', 'AEAD_AES_128_GCM'}; +aead_gcm_simultan('AEAD_AES_256_GCM', _) -> {'AEAD_AES_256_GCM', 'AEAD_AES_256_GCM'}; +aead_gcm_simultan(_, 'AEAD_AES_128_GCM') -> {'AEAD_AES_128_GCM', 'AEAD_AES_128_GCM'}; +aead_gcm_simultan(_, 'AEAD_AES_256_GCM') -> {'AEAD_AES_256_GCM', 'AEAD_AES_256_GCM'}; +aead_gcm_simultan(Cipher, Mac) -> {Cipher,Mac}. + + select_encrypt_decrypt(client, Client, Server) -> Encrypt = select(Client#ssh_msg_kexinit.encryption_algorithms_client_to_server, @@ -823,18 +862,18 @@ select_compression_decompression(client, Client, Server) -> Compression = select(Client#ssh_msg_kexinit.compression_algorithms_client_to_server, Server#ssh_msg_kexinit.compression_algorithms_client_to_server), - Decomprssion = + Decompression = select(Client#ssh_msg_kexinit.compression_algorithms_server_to_client, Server#ssh_msg_kexinit.compression_algorithms_server_to_client), - {Compression, Decomprssion}; + {Compression, Decompression}; select_compression_decompression(server, Client, Server) -> - Decomprssion = + Decompression = select(Client#ssh_msg_kexinit.compression_algorithms_client_to_server, Server#ssh_msg_kexinit.compression_algorithms_client_to_server), Compression = select(Client#ssh_msg_kexinit.compression_algorithms_server_to_client, Server#ssh_msg_kexinit.compression_algorithms_server_to_client), - {Compression, Decomprssion}. + {Compression, Decompression}. install_alg(SSH) -> SSH1 = alg_final(SSH), @@ -911,14 +950,39 @@ pack(Data, Ssh=#ssh{}) -> %%% Note: pack/3 is only to be called from tests that wants %%% to deliberetly send packets with wrong PacketLength! %%% Use pack/2 for all other purposes! -pack(Data0, #ssh{encrypt_block_size = BlockSize, - send_sequence = SeqNum, send_mac = MacAlg, - send_mac_key = MacKey, - random_length_padding = RandomLengthPadding} - = Ssh0, - PacketLenDeviationForTests) when is_binary(Data0) -> - {Ssh1, Data} = compress(Ssh0, Data0), - PL = (BlockSize - ((4 + 1 + size(Data)) rem BlockSize)) rem BlockSize, +pack(PlainText, + #ssh{send_sequence = SeqNum, + send_mac = MacAlg, + send_mac_key = MacKey, + encrypt = CryptoAlg} = Ssh0, PacketLenDeviationForTests) when is_binary(PlainText) -> + + {Ssh1, CompressedPlainText} = compress(Ssh0, PlainText), + {EcryptedPacket, MAC, Ssh3} = + case pkt_type(CryptoAlg) of + common -> + PaddingLen = padding_length(4+1+size(CompressedPlainText), Ssh0), + Padding = ssh_bits:random(PaddingLen), + PlainPacketLen = 1 + PaddingLen + size(CompressedPlainText) + PacketLenDeviationForTests, + PlainPacketData = <<?UINT32(PlainPacketLen),?BYTE(PaddingLen), CompressedPlainText/binary, Padding/binary>>, + {Ssh2, EcryptedPacket0} = encrypt(Ssh1, PlainPacketData), + MAC0 = mac(MacAlg, MacKey, SeqNum, PlainPacketData), + {EcryptedPacket0, MAC0, Ssh2}; + aead -> + PaddingLen = padding_length(1+size(CompressedPlainText), Ssh0), + Padding = ssh_bits:random(PaddingLen), + PlainPacketLen = 1 + PaddingLen + size(CompressedPlainText) + PacketLenDeviationForTests, + PlainPacketData = <<?BYTE(PaddingLen), CompressedPlainText/binary, Padding/binary>>, + {Ssh2, {EcryptedPacket0,MAC0}} = encrypt(Ssh1, {<<?UINT32(PlainPacketLen)>>,PlainPacketData}), + {<<?UINT32(PlainPacketLen),EcryptedPacket0/binary>>, MAC0, Ssh2} + end, + FinalPacket = [EcryptedPacket, MAC], + Ssh = Ssh3#ssh{send_sequence = (SeqNum+1) band 16#ffffffff}, + {FinalPacket, Ssh}. + + +padding_length(Size, #ssh{encrypt_block_size = BlockSize, + random_length_padding = RandomLengthPadding}) -> + PL = (BlockSize - (Size rem BlockSize)) rem BlockSize, MinPaddingLen = if PL < 4 -> PL + BlockSize; true -> PL end, @@ -927,45 +991,94 @@ pack(Data0, #ssh{encrypt_block_size = BlockSize, ExtraPaddingLen = try crypto:rand_uniform(0,MaxExtraBlocks)*PadBlockSize catch _:_ -> 0 end, - PaddingLen = MinPaddingLen + ExtraPaddingLen, - Padding = ssh_bits:random(PaddingLen), - PacketLen = 1 + PaddingLen + size(Data) + PacketLenDeviationForTests, - PacketData = <<?UINT32(PacketLen),?BYTE(PaddingLen), - Data/binary, Padding/binary>>, - {Ssh2, EncPacket} = encrypt(Ssh1, PacketData), - MAC = mac(MacAlg, MacKey, SeqNum, PacketData), - Packet = [EncPacket, MAC], - Ssh = Ssh2#ssh{send_sequence = (SeqNum+1) band 16#ffffffff}, - {Packet, Ssh}. - -unpack(EncodedSoFar, ReminingLenght, #ssh{recv_mac_size = MacSize} = Ssh0) -> - SshLength = ReminingLenght - MacSize, - {NoMac, Mac, Rest} = case MacSize of - 0 -> - <<NoMac0:SshLength/binary, - Rest0/binary>> = EncodedSoFar, - {NoMac0, <<>>, Rest0}; - _ -> - <<NoMac0:SshLength/binary, - Mac0:MacSize/binary, - Rest0/binary>> = EncodedSoFar, - {NoMac0, Mac0, Rest0} - end, - {Ssh1, DecData, <<>>} = - case SshLength of - 0 -> - {Ssh0, <<>>, <<>>}; - _ -> - decrypt_blocks(NoMac, SshLength, Ssh0) - end, - {Ssh1, DecData, Rest, Mac}. + MinPaddingLen + ExtraPaddingLen. + + + +handle_packet_part(<<>>, Encrypted0, undefined, #ssh{decrypt = CryptoAlg} = Ssh0) -> + %% New ssh packet + case get_length(pkt_type(CryptoAlg), Encrypted0, Ssh0) of + get_more -> + %% too short to get the length + {get_more, <<>>, Encrypted0, undefined, Ssh0}; -msg_data(PacketData) -> - <<Len:32, PaddingLen:8, _/binary>> = PacketData, - DataLen = Len - PaddingLen - 1, - <<_:32, _:8, Data:DataLen/binary, - _:PaddingLen/binary>> = PacketData, - Data. + {ok, PacketLen, _, _, _} when PacketLen > ?SSH_MAX_PACKET_SIZE -> + %% far too long message than expected + throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Bad packet length " + ++ integer_to_list(PacketLen), + language = ""}); + + {ok, PacketLen, Decrypted, Encrypted1, + #ssh{recv_mac_size = MacSize} = Ssh1} -> + %% enough bytes so we got the length and can calculate how many + %% more bytes to expect for a full packet + TotalNeeded = (4 + PacketLen + MacSize), + handle_packet_part(Decrypted, Encrypted1, TotalNeeded, Ssh1) + end; + +handle_packet_part(DecryptedPfx, EncryptedBuffer, TotalNeeded, Ssh0) + when (size(DecryptedPfx)+size(EncryptedBuffer)) < TotalNeeded -> + %% need more bytes to finalize the packet + {get_more, DecryptedPfx, EncryptedBuffer, TotalNeeded, Ssh0}; + +handle_packet_part(DecryptedPfx, EncryptedBuffer, TotalNeeded, + #ssh{recv_mac_size = MacSize, + decrypt = CryptoAlg} = Ssh0) -> + %% enough bytes to decode the packet. + DecryptLen = TotalNeeded - size(DecryptedPfx) - MacSize, + <<EncryptedSfx:DecryptLen/binary, Mac:MacSize/binary, NextPacketBytes/binary>> = EncryptedBuffer, + case pkt_type(CryptoAlg) of + common -> + {Ssh1, DecryptedSfx} = decrypt(Ssh0, EncryptedSfx), + DecryptedPacket = <<DecryptedPfx/binary, DecryptedSfx/binary>>, + case is_valid_mac(Mac, DecryptedPacket, Ssh1) of + false -> + {bad_mac, Ssh1}; + true -> + {Ssh, DecompressedPayload} = decompress(Ssh1, payload(DecryptedPacket)), + {decoded, DecompressedPayload, NextPacketBytes, Ssh} + end; + aead -> + PacketLenBin = DecryptedPfx, + case decrypt(Ssh0, {PacketLenBin,EncryptedSfx,Mac}) of + {Ssh1, error} -> + {bad_mac, Ssh1}; + {Ssh1, DecryptedSfx} -> + DecryptedPacket = <<DecryptedPfx/binary, DecryptedSfx/binary>>, + {Ssh, DecompressedPayload} = decompress(Ssh1, payload(DecryptedPacket)), + {decoded, DecompressedPayload, NextPacketBytes, Ssh} + end + end. + + +get_length(common, EncryptedBuffer, #ssh{decrypt_block_size = BlockSize} = Ssh0) -> + case size(EncryptedBuffer) >= erlang:max(8, BlockSize) of + true -> + <<EncBlock:BlockSize/binary, EncryptedRest/binary>> = EncryptedBuffer, + {Ssh, + <<?UINT32(PacketLen),_/binary>> = Decrypted} = decrypt(Ssh0, EncBlock), + {ok, PacketLen, Decrypted, EncryptedRest, Ssh}; + false -> + get_more + end; +get_length(aead, EncryptedBuffer, Ssh) -> + case size(EncryptedBuffer) >= 4 of + true -> + <<?UINT32(PacketLen), EncryptedRest/binary>> = EncryptedBuffer, + {ok, PacketLen, <<?UINT32(PacketLen)>>, EncryptedRest, Ssh}; + false -> + get_more + end. + +pkt_type('AEAD_AES_128_GCM') -> aead; +pkt_type('AEAD_AES_256_GCM') -> aead; +pkt_type(_) -> common. + +payload(<<PacketLen:32, PaddingLen:8, PayloadAndPadding/binary>>) -> + PayloadLen = PacketLen - PaddingLen - 1, + <<Payload:PayloadLen/binary, _/binary>> = PayloadAndPadding, + Payload. sign(SigData, Hash, #'DSAPrivateKey'{} = Key) -> DerSignature = public_key:sign(SigData, Hash, Key), @@ -991,6 +1104,7 @@ verify(PlainText, Hash, Sig, {#'ECPoint'{},_} = Key) -> verify(PlainText, Hash, Sig, Key) -> public_key:verify(PlainText, Hash, Sig, Key). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Encryption @@ -999,6 +1113,30 @@ verify(PlainText, Hash, Sig, Key) -> encrypt_init(#ssh{encrypt = none} = Ssh) -> {ok, Ssh}; +encrypt_init(#ssh{encrypt = 'AEAD_AES_128_GCM', role = client} = Ssh) -> + IV = hash(Ssh, "A", 12*8), + <<K:16/binary>> = hash(Ssh, "C", 128), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = 'AEAD_AES_128_GCM', role = server} = Ssh) -> + IV = hash(Ssh, "B", 12*8), + <<K:16/binary>> = hash(Ssh, "D", 128), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = 'AEAD_AES_256_GCM', role = client} = Ssh) -> + IV = hash(Ssh, "A", 12*8), + <<K:32/binary>> = hash(Ssh, "C", 256), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = 'AEAD_AES_256_GCM', role = server} = Ssh) -> + IV = hash(Ssh, "B", 12*8), + <<K:32/binary>> = hash(Ssh, "D", 256), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}; encrypt_init(#ssh{encrypt = '3des-cbc', role = client} = Ssh) -> IV = hash(Ssh, "A", 64), <<K1:8/binary, K2:8/binary, K3:8/binary>> = hash(Ssh, "C", 192), @@ -1075,6 +1213,18 @@ encrypt_final(Ssh) -> encrypt(#ssh{encrypt = none} = Ssh, Data) -> {Ssh, Data}; +encrypt(#ssh{encrypt = 'AEAD_AES_128_GCM', + encrypt_keys = K, + encrypt_ctx = IV0} = Ssh, Data={_AAD,_Ptext}) -> + Enc = {_Ctext,_Ctag} = crypto:block_encrypt(aes_gcm, K, IV0, Data), + IV = next_gcm_iv(IV0), + {Ssh#ssh{encrypt_ctx = IV}, Enc}; +encrypt(#ssh{encrypt = 'AEAD_AES_256_GCM', + encrypt_keys = K, + encrypt_ctx = IV0} = Ssh, Data={_AAD,_Ptext}) -> + Enc = {_Ctext,_Ctag} = crypto:block_encrypt(aes_gcm, K, IV0, Data), + IV = next_gcm_iv(IV0), + {Ssh#ssh{encrypt_ctx = IV}, Enc}; encrypt(#ssh{encrypt = '3des-cbc', encrypt_keys = {K1,K2,K3}, encrypt_ctx = IV0} = Ssh, Data) -> @@ -1107,6 +1257,30 @@ encrypt(#ssh{encrypt = 'aes256-ctr', decrypt_init(#ssh{decrypt = none} = Ssh) -> {ok, Ssh}; +decrypt_init(#ssh{decrypt = 'AEAD_AES_128_GCM', role = client} = Ssh) -> + IV = hash(Ssh, "B", 12*8), + <<K:16/binary>> = hash(Ssh, "D", 128), + {ok, Ssh#ssh{decrypt_keys = K, + decrypt_block_size = 16, + decrypt_ctx = IV}}; +decrypt_init(#ssh{decrypt = 'AEAD_AES_128_GCM', role = server} = Ssh) -> + IV = hash(Ssh, "A", 12*8), + <<K:16/binary>> = hash(Ssh, "C", 128), + {ok, Ssh#ssh{decrypt_keys = K, + decrypt_block_size = 16, + decrypt_ctx = IV}}; +decrypt_init(#ssh{decrypt = 'AEAD_AES_256_GCM', role = client} = Ssh) -> + IV = hash(Ssh, "B", 12*8), + <<K:32/binary>> = hash(Ssh, "D", 256), + {ok, Ssh#ssh{decrypt_keys = K, + decrypt_block_size = 16, + decrypt_ctx = IV}}; +decrypt_init(#ssh{decrypt = 'AEAD_AES_256_GCM', role = server} = Ssh) -> + IV = hash(Ssh, "A", 12*8), + <<K:32/binary>> = hash(Ssh, "C", 256), + {ok, Ssh#ssh{decrypt_keys = K, + decrypt_block_size = 16, + decrypt_ctx = IV}}; decrypt_init(#ssh{decrypt = '3des-cbc', role = client} = Ssh) -> {IV, KD} = {hash(Ssh, "B", 64), hash(Ssh, "D", 192)}, @@ -1181,8 +1355,22 @@ decrypt_final(Ssh) -> decrypt_ctx = undefined, decrypt_block_size = 8}}. +decrypt(Ssh, <<>>) -> + {Ssh, <<>>}; decrypt(#ssh{decrypt = none} = Ssh, Data) -> {Ssh, Data}; +decrypt(#ssh{decrypt = 'AEAD_AES_128_GCM', + decrypt_keys = K, + decrypt_ctx = IV0} = Ssh, Data = {_AAD,_Ctext,_Ctag}) -> + Dec = crypto:block_decrypt(aes_gcm, K, IV0, Data), % Dec = PlainText | error + IV = next_gcm_iv(IV0), + {Ssh#ssh{decrypt_ctx = IV}, Dec}; +decrypt(#ssh{decrypt = 'AEAD_AES_256_GCM', + decrypt_keys = K, + decrypt_ctx = IV0} = Ssh, Data = {_AAD,_Ctext,_Ctag}) -> + Dec = crypto:block_decrypt(aes_gcm, K, IV0, Data), % Dec = PlainText | error + IV = next_gcm_iv(IV0), + {Ssh#ssh{decrypt_ctx = IV}, Dec}; decrypt(#ssh{decrypt = '3des-cbc', decrypt_keys = Keys, decrypt_ctx = IV0} = Ssh, Data) -> {K1, K2, K3} = Keys, @@ -1207,6 +1395,10 @@ decrypt(#ssh{decrypt = 'aes256-ctr', {State, Enc} = crypto:stream_decrypt(State0,Data), {Ssh#ssh{decrypt_ctx = State}, Enc}. + +next_gcm_iv(<<Fixed:32, InvCtr:64>>) -> <<Fixed:32, (InvCtr+1):64>>. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Compression %% @@ -1295,28 +1487,42 @@ decompress(#ssh{decompress = '[email protected]', decompress_ctx = Context, authe %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% send_mac_init(SSH) -> - case SSH#ssh.role of - client -> - KeySize =mac_key_size(SSH#ssh.send_mac), - Key = hash(SSH, "E", KeySize), - {ok, SSH#ssh { send_mac_key = Key }}; - server -> - KeySize = mac_key_size(SSH#ssh.send_mac), - Key = hash(SSH, "F", KeySize), - {ok, SSH#ssh { send_mac_key = Key }} + case pkt_type(SSH#ssh.send_mac) of + common -> + case SSH#ssh.role of + client -> + KeySize = mac_key_size(SSH#ssh.send_mac), + Key = hash(SSH, "E", KeySize), + {ok, SSH#ssh { send_mac_key = Key }}; + server -> + KeySize = mac_key_size(SSH#ssh.send_mac), + Key = hash(SSH, "F", KeySize), + {ok, SSH#ssh { send_mac_key = Key }} + end; + aead -> + %% Not applicable + {ok, SSH} end. send_mac_final(SSH) -> - {ok, SSH#ssh { send_mac = none, send_mac_key = undefined }}. + {ok, SSH#ssh {send_mac = none, + send_mac_key = undefined }}. + recv_mac_init(SSH) -> - case SSH#ssh.role of - client -> - Key = hash(SSH, "F", mac_key_size(SSH#ssh.recv_mac)), - {ok, SSH#ssh { recv_mac_key = Key }}; - server -> - Key = hash(SSH, "E", mac_key_size(SSH#ssh.recv_mac)), - {ok, SSH#ssh { recv_mac_key = Key }} + case pkt_type(SSH#ssh.recv_mac) of + common -> + case SSH#ssh.role of + client -> + Key = hash(SSH, "F", mac_key_size(SSH#ssh.recv_mac)), + {ok, SSH#ssh { recv_mac_key = Key }}; + server -> + Key = hash(SSH, "E", mac_key_size(SSH#ssh.recv_mac)), + {ok, SSH#ssh { recv_mac_key = Key }} + end; + aead -> + %% Not applicable + {ok, SSH} end. recv_mac_final(SSH) -> @@ -1399,8 +1605,11 @@ kex_h(SSH, Curve, Key, Q_c, Q_s, K) -> crypto:hash(sha(Curve), L). kex_h(SSH, Key, Min, NBits, Max, Prime, Gen, E, F, K) -> + KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), L = if Min==-1; Max==-1 -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), + %% flag from 'ssh_msg_kex_dh_gex_request_old' + %% It was like this before that message was supported, + %% why? Ts = [string,string,binary,binary,binary, uint32, mpint,mpint,mpint,mpint,mpint], @@ -1409,7 +1618,6 @@ kex_h(SSH, Key, Min, NBits, Max, Prime, Gen, E, F, K) -> KeyBin, NBits, Prime, Gen, E,F,K], Ts); true -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), Ts = [string,string,binary,binary,binary, uint32,uint32,uint32, mpint,mpint,mpint,mpint,mpint], @@ -1447,6 +1655,8 @@ mac_digest_size('hmac-md5') -> 20; mac_digest_size('hmac-md5-96') -> 12; mac_digest_size('hmac-sha2-256') -> 32; mac_digest_size('hmac-sha2-512') -> 64; +mac_digest_size('AEAD_AES_128_GCM') -> 16; +mac_digest_size('AEAD_AES_256_GCM') -> 16; mac_digest_size(none) -> 0. peer_name({Host, _}) -> @@ -1476,6 +1686,68 @@ ecdh_curve('ecdh-sha2-nistp256') -> secp256r1; ecdh_curve('ecdh-sha2-nistp384') -> secp384r1; ecdh_curve('ecdh-sha2-nistp521') -> secp521r1. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Utils for default_algorithms/1 and supported_algorithms/1 +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +supported_algorithms(Key, [{client2server,BL1},{server2client,BL2}]) -> + [{client2server,As1},{server2client,As2}] = supported_algorithms(Key), + [{client2server,As1--BL1},{server2client,As2--BL2}]; +supported_algorithms(Key, BlackList) -> + supported_algorithms(Key) -- BlackList. + + +select_crypto_supported(L) -> + Sup = [{ec_curve,crypto_supported_curves()} | crypto:supports()], + [Name || {Name,CryptoRequires} <- L, + crypto_supported(CryptoRequires, Sup)]. + +crypto_supported_curves() -> + try crypto:ec_curves() + catch _:_ -> [] + end. + +crypto_supported(Conditions, Supported) -> + lists:all( fun({Tag,CryptoName}) when is_atom(CryptoName) -> + crypto_name_supported(Tag,CryptoName,Supported); + ({Tag,{Name,Len}}) when is_integer(Len) -> + crypto_name_supported(Tag,Name,Supported) andalso + len_supported(Name,Len) + end, Conditions). + +crypto_name_supported(Tag, CryptoName, Supported) -> + lists:member(CryptoName, proplists:get_value(Tag,Supported,[])). + +len_supported(Name, Len) -> + try + case Name of + aes_ctr -> + {_, <<_/binary>>} = + %% Test encryption + crypto:stream_encrypt(crypto:stream_init(Name, <<0:Len>>, <<0:128>>), <<"">>); + aes_gcm -> + {<<_/binary>>, <<_/binary>>} = + crypto:block_encrypt(Name, + _Key = <<0:Len>>, + _IV = <<0:12/unsigned-unit:8>>, + {<<"AAD">>,"PT"}) + end + of + _ -> true + catch + _:_ -> false + end. + + +same(Algs) -> [{client2server,Algs}, {server2client,Algs}]. + + +%% default_algorithms(kex) -> % Example of how to disable an algorithm +%% supported_algorithms(kex, ['ecdh-sha2-nistp521']); + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Other utils diff --git a/lib/ssh/test/Makefile b/lib/ssh/test/Makefile index 96c74c6c8a..781a876723 100644 --- a/lib/ssh/test/Makefile +++ b/lib/ssh/test/Makefile @@ -47,6 +47,8 @@ MODULES= \ ssh_to_openssh_SUITE \ ssh_upgrade_SUITE \ ssh_test_lib \ + ssh_key_cb \ + ssh_key_cb_options \ ssh_trpt_test_lib \ ssh_echo_server \ ssh_peername_sockname_server \ diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl index 400edb4d2c..d4cb03f2f2 100644 --- a/lib/ssh/test/ssh_basic_SUITE.erl +++ b/lib/ssh/test/ssh_basic_SUITE.erl @@ -36,6 +36,8 @@ cli/1, close/1, daemon_already_started/1, + daemon_opt_fd/1, + multi_daemon_opt_fd/1, double_close/1, exec/1, exec_compressed/1, @@ -52,8 +54,10 @@ send/1, shell/1, shell_no_unicode/1, - shell_unicode_string/1, - ssh_info_print/1 + shell_unicode_string/1, + ssh_info_print/1, + key_callback/1, + key_callback_options/1 ]). %%% Common test callbacks @@ -82,9 +86,12 @@ all() -> {group, ecdsa_sha2_nistp521_key}, {group, dsa_pass_key}, {group, rsa_pass_key}, + {group, key_cb}, {group, internal_error}, daemon_already_started, double_close, + daemon_opt_fd, + multi_daemon_opt_fd, packet_size_zero, ssh_info_print ]. @@ -97,6 +104,7 @@ groups() -> {ecdsa_sha2_nistp521_key, [], basic_tests()}, {dsa_pass_key, [], [pass_phrase]}, {rsa_pass_key, [], [pass_phrase]}, + {key_cb, [], [key_callback, key_callback_options]}, {internal_error, [], [internal_error]} ]. @@ -176,6 +184,11 @@ init_per_group(dsa_pass_key, Config) -> PrivDir = ?config(priv_dir, Config), ssh_test_lib:setup_dsa_pass_pharse(DataDir, PrivDir, "Password"), [{pass_phrase, {dsa_pass_phrase, "Password"}}| Config]; +init_per_group(key_cb, Config) -> + DataDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + ssh_test_lib:setup_dsa(DataDir, PrivDir), + Config; init_per_group(internal_error, Config) -> DataDir = ?config(data_dir, Config), PrivDir = ?config(priv_dir, Config), @@ -243,6 +256,10 @@ end_per_group(rsa_pass_key, Config) -> PrivDir = ?config(priv_dir, Config), ssh_test_lib:clean_rsa(PrivDir), Config; +end_per_group(key_cb, Config) -> + PrivDir = ?config(priv_dir, Config), + ssh_test_lib:clean_dsa(PrivDir), + Config; end_per_group(internal_error, Config) -> PrivDir = ?config(priv_dir, Config), ssh_test_lib:clean_dsa(PrivDir), @@ -571,6 +588,56 @@ pass_phrase(Config) when is_list(Config) -> {ok, _ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), ssh:stop_daemon(Pid). +%%-------------------------------------------------------------------- +%%% Test that we can use key callback +key_callback(Config) when is_list(Config) -> + process_flag(trap_exit, true), + SystemDir = filename:join(?config(priv_dir, Config), system), + UserDir = ?config(priv_dir, Config), + NoPubKeyDir = filename:join(UserDir, "nopubkey"), + file:make_dir(NoPubKeyDir), + + {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, + {user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), + + ConnectOpts = [{silently_accept_hosts, true}, + {user_dir, NoPubKeyDir}, + {user_interaction, false}, + {key_cb, ssh_key_cb}], + + ConnectionRef = ssh_test_lib:connect(Host, Port, ConnectOpts), + + {ok, _ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), + ssh:stop_daemon(Pid). + + +%%-------------------------------------------------------------------- +%%% Test that we can use key callback with callback options +key_callback_options(Config) when is_list(Config) -> + process_flag(trap_exit, true), + SystemDir = filename:join(?config(priv_dir, Config), system), + UserDir = ?config(priv_dir, Config), + + NoPubKeyDir = filename:join(UserDir, "nopubkey"), + file:make_dir(NoPubKeyDir), + + {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, + {user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), + + {ok, PrivKey} = file:read_file(filename:join(UserDir, "id_dsa")), + + ConnectOpts = [{silently_accept_hosts, true}, + {user_dir, NoPubKeyDir}, + {user_interaction, false}, + {key_cb, {ssh_key_cb_options, [{priv_key, PrivKey}]}}], + + ConnectionRef = ssh_test_lib:connect(Host, Port, ConnectOpts), + + {ok, _ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), + ssh:stop_daemon(Pid). + %%-------------------------------------------------------------------- %%% Test that client does not hang if disconnects due to internal error @@ -705,6 +772,68 @@ double_close(Config) when is_list(Config) -> ok = ssh:close(CM). %%-------------------------------------------------------------------- +daemon_opt_fd(Config) -> + SystemDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), + + {ok,S1} = gen_tcp:listen(0,[]), + {ok,Fd1} = prim_inet:getfd(S1), + + {ok,Pid1} = ssh:daemon(0, [{system_dir, SystemDir}, + {fd,Fd1}, + {user_dir, UserDir}, + {user_passwords, [{"vego", "morot"}]}, + {failfun, fun ssh_test_lib:failfun/2}]), + + {ok,{_Host1,Port1}} = inet:sockname(S1), + {ok, C1} = ssh:connect("localhost", Port1, [{silently_accept_hosts, true}, + {user_dir, UserDir}, + {user, "vego"}, + {password, "morot"}, + {user_interaction, false}]), + exit(C1, {shutdown, normal}), + ssh:stop_daemon(Pid1), + gen_tcp:close(S1). + + +%%-------------------------------------------------------------------- +multi_daemon_opt_fd(Config) -> + SystemDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), + + Test = + fun() -> + {ok,S} = gen_tcp:listen(0,[]), + {ok,Fd} = prim_inet:getfd(S), + + {ok,Pid} = ssh:daemon(0, [{system_dir, SystemDir}, + {fd,Fd}, + {user_dir, UserDir}, + {user_passwords, [{"vego", "morot"}]}, + {failfun, fun ssh_test_lib:failfun/2}]), + + {ok,{_Host,Port}} = inet:sockname(S), + {ok, C} = ssh:connect("localhost", Port, [{silently_accept_hosts, true}, + {user_dir, UserDir}, + {user, "vego"}, + {password, "morot"}, + {user_interaction, false}]), + {S,Pid,C} + end, + + Tests = [Test(),Test(),Test(),Test(),Test(),Test()], + + [begin + gen_tcp:close(S), + ssh:stop_daemon(Pid), + exit(C, {shutdown, normal}) + end || {S,Pid,C} <- Tests]. + +%%-------------------------------------------------------------------- packet_size_zero(Config) -> SystemDir = ?config(data_dir, Config), PrivDir = ?config(priv_dir, Config), diff --git a/lib/ssh/test/ssh_key_cb.erl b/lib/ssh/test/ssh_key_cb.erl new file mode 100644 index 0000000000..388ec2ecc1 --- /dev/null +++ b/lib/ssh/test/ssh_key_cb.erl @@ -0,0 +1,45 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2015. 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% +%% + +%% +%%---------------------------------------------------------------------- + +%% Note: This module is used by ssh_basic_SUITE + +-module(ssh_key_cb). +-behaviour(ssh_client_key_api). +-compile(export_all). + +add_host_key(_, _, _) -> + ok. + +is_host_key(_, _, _, _) -> + true. + +user_key('ssh-dss', Opts) -> + UserDir = proplists:get_value(user_dir, Opts), + KeyFile = filename:join(filename:dirname(UserDir), "id_dsa"), + {ok, KeyBin} = file:read_file(KeyFile), + [Entry] = public_key:pem_decode(KeyBin), + Key = public_key:pem_entry_decode(Entry), + {ok, Key}; + +user_key(_Alg, _Opt) -> + {error, "Not Supported"}. diff --git a/lib/ssh/test/ssh_key_cb_options.erl b/lib/ssh/test/ssh_key_cb_options.erl new file mode 100644 index 0000000000..afccb34f0f --- /dev/null +++ b/lib/ssh/test/ssh_key_cb_options.erl @@ -0,0 +1,44 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2015. 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% +%% + +%% +%%---------------------------------------------------------------------- + +%% Note: This module is used by ssh_basic_SUITE + +-module(ssh_key_cb_options). +-behaviour(ssh_client_key_api). +-compile(export_all). + +add_host_key(_, _, _) -> + ok. + +is_host_key(_, _, _, _) -> + true. + +user_key('ssh-dss', Opts) -> + KeyCbOpts = proplists:get_value(key_cb_private, Opts), + KeyBin = proplists:get_value(priv_key, KeyCbOpts), + [Entry] = public_key:pem_decode(KeyBin), + Key = public_key:pem_entry_decode(Entry), + {ok, Key}; + +user_key(_Alg, _Opt) -> + {error, "Not Supported"}. diff --git a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl index 3a7f47c2dd..4639904061 100644 --- a/lib/ssh/test/ssh_protocol_SUITE.erl +++ b/lib/ssh/test/ssh_protocol_SUITE.erl @@ -69,7 +69,9 @@ groups() -> gex_client_init_option_groups, gex_server_gex_limit, gex_client_init_option_groups_moduli_file, - gex_client_init_option_groups_file + gex_client_init_option_groups_file, + gex_client_old_request_exact, + gex_client_old_request_noexact ]}, {service_requests, [], [bad_service_name, bad_long_service_name, @@ -94,7 +96,9 @@ init_per_testcase(no_common_alg_server_disconnects, Config) -> init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; TC == gex_client_init_option_groups_moduli_file ; TC == gex_client_init_option_groups_file ; - TC == gex_server_gex_limit -> + TC == gex_server_gex_limit ; + TC == gex_client_old_request_exact ; + TC == gex_client_old_request_noexact -> Opts = case TC of gex_client_init_option_groups -> [{dh_gex_groups, [{2345, 3, 41}]}]; @@ -106,8 +110,10 @@ init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; DataDir = ?config(data_dir, Config), F = filename:join(DataDir, "dh_group_test.moduli"), [{dh_gex_groups, {ssh_moduli_file,F}}]; - gex_server_gex_limit -> - [{dh_gex_groups, [{ 500, 3, 18}, + _ when TC == gex_server_gex_limit ; + TC == gex_client_old_request_exact ; + TC == gex_client_old_request_noexact -> + [{dh_gex_groups, [{ 500, 3, 17}, {1000, 7, 91}, {3000, 5, 61}]}, {dh_gex_limits,{500,1500}} @@ -126,7 +132,9 @@ end_per_testcase(no_common_alg_server_disconnects, Config) -> end_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; TC == gex_client_init_option_groups_moduli_file ; TC == gex_client_init_option_groups_file ; - TC == gex_server_gex_limit -> + TC == gex_server_gex_limit ; + TC == gex_client_old_request_exact ; + TC == gex_client_old_request_noexact -> stop_std_daemon(Config); end_per_testcase(_TestCase, Config) -> check_std_daemon_works(Config, ?LINE). @@ -381,6 +389,29 @@ do_gex_client_init(Config, {Min,N,Max}, {G,P}) -> ] ). +%%%-------------------------------------------------------------------- +gex_client_old_request_exact(Config) -> do_gex_client_init_old(Config, 500, {3,17}). +gex_client_old_request_noexact(Config) -> do_gex_client_init_old(Config, 800, {7,91}). + +do_gex_client_init_old(Config, N, {G,P}) -> + {ok,_} = + ssh_trpt_test_lib:exec( + [{set_options, [print_ops, print_seqnums, print_messages]}, + {connect, + server_host(Config),server_port(Config), + [{silently_accept_hosts, true}, + {user_dir, user_dir(Config)}, + {user_interaction, false}, + {preferred_algorithms,[{kex,['diffie-hellman-group-exchange-sha1']}]} + ]}, + receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, #ssh_msg_kex_dh_gex_request_old{n = N}}, + {match, #ssh_msg_kex_dh_gex_group{p=P, g=G, _='_'}, receive_msg} + ] + ). %%%-------------------------------------------------------------------- bad_service_name(Config) -> diff --git a/lib/ssh/test/ssh_renegotiate_SUITE.erl b/lib/ssh/test/ssh_renegotiate_SUITE.erl index ef631d54bd..227dfcddcd 100644 --- a/lib/ssh/test/ssh_renegotiate_SUITE.erl +++ b/lib/ssh/test/ssh_renegotiate_SUITE.erl @@ -32,9 +32,15 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. -all() -> [rekey, rekey_limit, renegotiate1, renegotiate2]. +all() -> [{group,default_algs}, + {group,aes_gcm} + ]. -groups() -> []. +groups() -> [{default_algs, [], tests()}, + {aes_gcm, [], tests()} + ]. + +tests() -> [rekey, rekey_limit, renegotiate1, renegotiate2]. %%-------------------------------------------------------------------- init_per_suite(Config) -> @@ -50,6 +56,18 @@ end_per_suite(_Config) -> crypto:stop(). %%-------------------------------------------------------------------- +init_per_group(aes_gcm, Config) -> + [{preferred_algorithms, [{cipher,[{client2server,['[email protected]']}, + {server2client,['[email protected]']}]}]} + | Config]; +init_per_group(_, Config) -> + [{preferred_algorithms, ssh:default_algorithms()} | Config]. + + +end_per_group(_, Config) -> + Config. + +%%-------------------------------------------------------------------- init_per_testcase(_TestCase, Config) -> ssh:start(), Config. diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl index 5816b708f2..424afc76fe 100644 --- a/lib/ssh/test/ssh_test_lib.erl +++ b/lib/ssh/test/ssh_test_lib.erl @@ -296,7 +296,7 @@ setup_dsa(DataDir, UserDir) -> file:make_dir(System), file:copy(filename:join(DataDir, "ssh_host_dsa_key"), filename:join(System, "ssh_host_dsa_key")), file:copy(filename:join(DataDir, "ssh_host_dsa_key.pub"), filename:join(System, "ssh_host_dsa_key.pub")), -ct:pal("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), +ct:log("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), setup_dsa_known_host(DataDir, UserDir), setup_dsa_auth_keys(DataDir, UserDir). @@ -306,7 +306,7 @@ setup_rsa(DataDir, UserDir) -> file:make_dir(System), file:copy(filename:join(DataDir, "ssh_host_rsa_key"), filename:join(System, "ssh_host_rsa_key")), file:copy(filename:join(DataDir, "ssh_host_rsa_key.pub"), filename:join(System, "ssh_host_rsa_key.pub")), -ct:pal("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), +ct:log("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), setup_rsa_known_host(DataDir, UserDir), setup_rsa_auth_keys(DataDir, UserDir). @@ -316,7 +316,7 @@ setup_ecdsa(Size, DataDir, UserDir) -> file:make_dir(System), file:copy(filename:join(DataDir, "ssh_host_ecdsa_key"++Size), filename:join(System, "ssh_host_ecdsa_key")), file:copy(filename:join(DataDir, "ssh_host_ecdsa_key"++Size++".pub"), filename:join(System, "ssh_host_ecdsa_key.pub")), -ct:pal("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), +ct:log("DataDir ~p:~n ~p~n~nSystDir ~p:~n ~p~n~nUserDir ~p:~n ~p",[DataDir, file:list_dir(DataDir), System, file:list_dir(System), UserDir, file:list_dir(UserDir)]), setup_ecdsa_known_host(Size, System, UserDir), setup_ecdsa_auth_keys(Size, UserDir, UserDir). @@ -502,7 +502,7 @@ default_algorithms(sshd, Host, Port) -> {user_interaction, false}]}])) catch _C:_E -> - ct:pal("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), + ct:log("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), [] end. @@ -522,7 +522,7 @@ default_algorithms(sshc, DaemonOptions) -> InitialState)) catch _C:_E -> - ct:pal("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), + ct:log("***~p:~p: ~p:~p",[?MODULE,?LINE,_C,_E]), [] end} end), diff --git a/lib/ssh/vsn.mk b/lib/ssh/vsn.mk index d828bccd29..25b19133b1 100644 --- a/lib/ssh/vsn.mk +++ b/lib/ssh/vsn.mk @@ -1,4 +1,5 @@ #-*-makefile-*- ; force emacs to enter makefile-mode SSH_VSN = 4.2 + APP_VSN = "ssh-$(SSH_VSN)" diff --git a/lib/ssl/src/inet_tls_dist.erl b/lib/ssl/src/inet_tls_dist.erl index b6e62a18c9..404ae93d20 100644 --- a/lib/ssl/src/inet_tls_dist.erl +++ b/lib/ssl/src/inet_tls_dist.erl @@ -30,7 +30,7 @@ childspecs() -> {ok, [{ssl_dist_sup,{ssl_dist_sup, start_link, []}, - permanent, 2000, worker, [ssl_dist_sup]}]}. + permanent, infinity, supervisor, [ssl_dist_sup]}]}. select(Node) -> case split_node(atom_to_list(Node), $@, []) of diff --git a/lib/ssl/src/ssl_connection.erl b/lib/ssl/src/ssl_connection.erl index f8afbdb41d..12a56df69f 100644 --- a/lib/ssl/src/ssl_connection.erl +++ b/lib/ssl/src/ssl_connection.erl @@ -1781,7 +1781,7 @@ handle_trusted_certs_db(#state{ssl_options = #ssl_options{cacertfile = <<>>, cac ok; handle_trusted_certs_db(#state{cert_db_ref = Ref, cert_db = CertDb, - ssl_options = #ssl_options{cacertfile = <<>>}}) -> + ssl_options = #ssl_options{cacertfile = <<>>}}) when CertDb =/= undefined -> %% Certs provided as DER directly can not be shared %% with other connections and it is safe to delete them when the connection ends. ssl_pkix_db:remove_trusted_certs(Ref, CertDb); diff --git a/lib/ssl/src/ssl_dist_sup.erl b/lib/ssl/src/ssl_dist_sup.erl index aa1fa57db8..435ad27a44 100644 --- a/lib/ssl/src/ssl_dist_sup.erl +++ b/lib/ssl/src/ssl_dist_sup.erl @@ -70,7 +70,7 @@ connection_manager_child_spec() -> Name = ssl_connection_dist, StartFunc = {tls_connection_sup, start_link_dist, []}, Restart = permanent, - Shutdown = 4000, + Shutdown = infinity, Modules = [tls_connection_sup], Type = supervisor, {Name, StartFunc, Restart, Shutdown, Type, Modules}. diff --git a/lib/ssl/src/ssl_internal.hrl b/lib/ssl/src/ssl_internal.hrl index 3851b2bc6e..8c7ed9c0d1 100644 --- a/lib/ssl/src/ssl_internal.hrl +++ b/lib/ssl/src/ssl_internal.hrl @@ -78,6 +78,9 @@ -define(ALL_DATAGRAM_SUPPORTED_VERSIONS, ['dtlsv1.2', dtlsv1]). -define(MIN_DATAGRAM_SUPPORTED_VERSIONS, ['dtlsv1.2', dtlsv1]). +-define('24H_in_msec', 86400000). +-define('24H_in_sec', 86400). + -record(ssl_options, { protocol :: tls | dtls, versions :: [ssl_record:ssl_version()], %% ssl_record:atom_version() in API diff --git a/lib/ssl/src/ssl_manager.erl b/lib/ssl/src/ssl_manager.erl index 2e05ba5aa5..cc15678f23 100644 --- a/lib/ssl/src/ssl_manager.erl +++ b/lib/ssl/src/ssl_manager.erl @@ -57,8 +57,6 @@ clear_pem_cache }). --define('24H_in_msec', 86400000). --define('24H_in_sec', 86400). -define(GEN_UNIQUE_ID_MAX_TRIES, 10). -define(SESSION_VALIDATION_INTERVAL, 60000). -define(CLEAR_PEM_CACHE, 120000). diff --git a/lib/ssl/src/ssl_session.erl b/lib/ssl/src/ssl_session.erl index 0d6cc93a20..1849a05314 100644 --- a/lib/ssl/src/ssl_session.erl +++ b/lib/ssl/src/ssl_session.erl @@ -31,8 +31,6 @@ %% Internal application API -export([is_new/2, client_id/4, server_id/6, valid_session/2]). --define('24H_in_sec', 8640). - -type seconds() :: integer(). %%-------------------------------------------------------------------- diff --git a/lib/ssl/src/ssl_tls_dist_proxy.erl b/lib/ssl/src/ssl_tls_dist_proxy.erl index 273d3b5521..d384264b53 100644 --- a/lib/ssl/src/ssl_tls_dist_proxy.erl +++ b/lib/ssl/src/ssl_tls_dist_proxy.erl @@ -48,6 +48,47 @@ accept(Listen) -> connect(Ip, Port) -> gen_server:call(?MODULE, {connect, Ip, Port}, infinity). + +do_listen(Options) -> + {First,Last} = case application:get_env(kernel,inet_dist_listen_min) of + {ok,N} when is_integer(N) -> + case application:get_env(kernel, + inet_dist_listen_max) of + {ok,M} when is_integer(M) -> + {N,M}; + _ -> + {N,N} + end; + _ -> + {0,0} + end, + do_listen(First, Last, listen_options([{backlog,128}|Options])). + +do_listen(First,Last,_) when First > Last -> + {error,eaddrinuse}; +do_listen(First,Last,Options) -> + case gen_tcp:listen(First, Options) of + {error, eaddrinuse} -> + do_listen(First+1,Last,Options); + Other -> + Other + end. + +listen_options(Opts0) -> + Opts1 = + case application:get_env(kernel, inet_dist_use_interface) of + {ok, Ip} -> + [{ip, Ip} | Opts0]; + _ -> + Opts0 + end, + case application:get_env(kernel, inet_dist_listen_options) of + {ok,ListenOpts} -> + ListenOpts ++ Opts1; + _ -> + Opts1 + end. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -62,13 +103,17 @@ init([]) -> handle_call({listen, Name}, _From, State) -> case gen_tcp:listen(0, [{active, false}, {packet,?PPRE}]) of {ok, Socket} -> - {ok, World} = gen_tcp:listen(0, [{active, false}, binary, {packet,?PPRE}]), + {ok, World} = do_listen([{active, false}, binary, {packet,?PPRE}, {reuseaddr, true}]), {ok, TcpAddress} = get_tcp_address(Socket), {ok, WorldTcpAddress} = get_tcp_address(World), {_,Port} = WorldTcpAddress#net_address.address, - {ok, Creation} = erl_epmd:register_node(Name, Port), - {reply, {ok, {Socket, TcpAddress, Creation}}, - State#state{listen={Socket, World}}}; + case erl_epmd:register_node(Name, Port) of + {ok, Creation} -> + {reply, {ok, {Socket, TcpAddress, Creation}}, + State#state{listen={Socket, World}}}; + {error, _} = Error -> + {reply, Error, State} + end; Error -> {reply, Error, State} end; @@ -134,6 +179,7 @@ accept_loop(Proxy, erts = Type, Listen, Extra) -> Extra ! {accept,self(),Socket,inet,proxy}, receive {_Kernel, controller, Pid} -> + inet:setopts(Socket, [nodelay()]), ok = gen_tcp:controlling_process(Socket, Pid), flush_old_controller(Pid, Socket), Pid ! {self(), controller}; @@ -167,7 +213,7 @@ accept_loop(Proxy, world = Type, Listen, Extra) -> accept_loop(Proxy, Type, Listen, Extra). try_connect(Port) -> - case gen_tcp:connect({127,0,0,1}, Port, [{active, false}, {packet,?PPRE}]) of + case gen_tcp:connect({127,0,0,1}, Port, [{active, false}, {packet,?PPRE}, nodelay()]) of R = {ok, _S} -> R; {error, _R} -> @@ -177,7 +223,7 @@ try_connect(Port) -> setup_proxy(Ip, Port, Parent) -> process_flag(trap_exit, true), Opts = get_ssl_options(client), - case ssl:connect(Ip, Port, [{active, true}, binary, {packet,?PPRE}] ++ Opts) of + case ssl:connect(Ip, Port, [{active, true}, binary, {packet,?PPRE}, nodelay()] ++ Opts) of {ok, World} -> {ok, ErtsL} = gen_tcp:listen(0, [{active, true}, {ip, {127,0,0,1}}, binary, {packet,?PPRE}]), {ok, #net_address{address={_,LPort}}} = get_tcp_address(ErtsL), @@ -193,25 +239,41 @@ setup_proxy(Ip, Port, Parent) -> Parent ! {self(), Err} end. + +%% we may not always want the nodelay behaviour +%% %% for performance reasons + +nodelay() -> + case application:get_env(kernel, dist_nodelay) of + undefined -> + {nodelay, true}; + {ok, true} -> + {nodelay, true}; + {ok, false} -> + {nodelay, false}; + _ -> + {nodelay, true} + end. + setup_connection(World, ErtsListen) -> process_flag(trap_exit, true), {ok, TcpAddress} = get_tcp_address(ErtsListen), {_Addr,Port} = TcpAddress#net_address.address, - {ok, Erts} = gen_tcp:connect({127,0,0,1}, Port, [{active, true}, binary, {packet,?PPRE}]), - ssl:setopts(World, [{active,true}, {packet,?PPRE}]), + {ok, Erts} = gen_tcp:connect({127,0,0,1}, Port, [{active, true}, binary, {packet,?PPRE}, nodelay()]), + ssl:setopts(World, [{active,true}, {packet,?PPRE}, nodelay()]), loop_conn_setup(World, Erts). loop_conn_setup(World, Erts) -> receive {ssl, World, Data = <<$a, _/binary>>} -> gen_tcp:send(Erts, Data), - ssl:setopts(World, [{packet,?PPOST}]), - inet:setopts(Erts, [{packet,?PPOST}]), + ssl:setopts(World, [{packet,?PPOST}, nodelay()]), + inet:setopts(Erts, [{packet,?PPOST}, nodelay()]), loop_conn(World, Erts); {tcp, Erts, Data = <<$a, _/binary>>} -> ssl:send(World, Data), - ssl:setopts(World, [{packet,?PPOST}]), - inet:setopts(Erts, [{packet,?PPOST}]), + ssl:setopts(World, [{packet,?PPOST}, nodelay()]), + inet:setopts(Erts, [{packet,?PPOST}, nodelay()]), loop_conn(World, Erts); {ssl, World, Data = <<_, _/binary>>} -> gen_tcp:send(Erts, Data), diff --git a/lib/ssl/test/ssl_basic_SUITE.erl b/lib/ssl/test/ssl_basic_SUITE.erl index 6f6107de2c..f032c769e2 100644 --- a/lib/ssl/test/ssl_basic_SUITE.erl +++ b/lib/ssl/test/ssl_basic_SUITE.erl @@ -35,7 +35,6 @@ -include("tls_record.hrl"). -include("tls_handshake.hrl"). --define('24H_in_sec', 86400). -define(TIMEOUT, 20000). -define(EXPIRE, 10). -define(SLEEP, 500). diff --git a/lib/ssl/test/ssl_dist_SUITE.erl b/lib/ssl/test/ssl_dist_SUITE.erl index 72d62b29a7..092015d3d8 100644 --- a/lib/ssl/test/ssl_dist_SUITE.erl +++ b/lib/ssl/test/ssl_dist_SUITE.erl @@ -40,7 +40,8 @@ %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- all() -> - [basic, payload, plain_options, plain_verify_options]. + [basic, payload, plain_options, plain_verify_options, nodelay_option, + listen_port_options, listen_options, use_interface]. groups() -> []. @@ -250,6 +251,146 @@ plain_verify_options(Config) when is_list(Config) -> stop_ssl_node(NH1), stop_ssl_node(NH2), success(Config). +%%-------------------------------------------------------------------- +nodelay_option() -> + [{doc,"Test specifying dist_nodelay option"}]. +nodelay_option(Config) -> + try + %% The default is 'true', so try setting it to 'false'. + application:set_env(kernel, dist_nodelay, false), + basic(Config) + after + application:unset_env(kernel, dist_nodelay) + end. + +listen_port_options() -> + [{doc, "Test specifying listening ports"}]. +listen_port_options(Config) when is_list(Config) -> + %% Start a node, and get the port number it's listening on. + NH1 = start_ssl_node(Config), + Node1 = NH1#node_handle.nodename, + Name1 = lists:takewhile(fun(C) -> C =/= $@ end, atom_to_list(Node1)), + {ok, NodesPorts} = apply_on_ssl_node(NH1, fun net_adm:names/0), + {Name1, Port1} = lists:keyfind(Name1, 1, NodesPorts), + + %% Now start a second node, configuring it to use the same port + %% number. + PortOpt1 = "-kernel inet_dist_listen_min " ++ integer_to_list(Port1) ++ + " inet_dist_listen_max " ++ integer_to_list(Port1), + + try start_ssl_node([{additional_dist_opts, PortOpt1} | Config]) of + #node_handle{} -> + %% If the node was able to start, it didn't take the port + %% option into account. + exit(unexpected_success) + catch + exit:{accept_failed, timeout} -> + %% The node failed to start, as expected. + ok + end, + + %% Try again, now specifying a high max port. + PortOpt2 = "-kernel inet_dist_listen_min " ++ integer_to_list(Port1) ++ + " inet_dist_listen_max 65535", + NH2 = start_ssl_node([{additional_dist_opts, PortOpt2} | Config]), + Node2 = NH2#node_handle.nodename, + Name2 = lists:takewhile(fun(C) -> C =/= $@ end, atom_to_list(Node2)), + {ok, NodesPorts2} = apply_on_ssl_node(NH2, fun net_adm:names/0), + {Name2, Port2} = lists:keyfind(Name2, 1, NodesPorts2), + + %% The new port should be higher: + if Port2 > Port1 -> + ok; + true -> + error({port, Port2, not_higher_than, Port1}) + end, + + stop_ssl_node(NH1), + stop_ssl_node(NH2), + success(Config). +%%-------------------------------------------------------------------- +listen_options() -> + [{doc, "Test inet_dist_listen_options"}]. +listen_options(Config) when is_list(Config) -> + Prio = 1, + case gen_udp:open(0, [{priority,Prio}]) of + {ok,Socket} -> + case inet:getopts(Socket, [priority]) of + {ok,[{priority,Prio}]} -> + ok = gen_udp:close(Socket), + do_listen_options(Prio, Config); + _ -> + ok = gen_udp:close(Socket), + {skip, + "Can not set priority "++integer_to_list(Prio)++ + " on socket"} + end; + {error,_} -> + {skip, "Can not set priority on socket"} + end. + +do_listen_options(Prio, Config) -> + PriorityString0 = "[{priority,"++integer_to_list(Prio)++"}]", + PriorityString = + case os:cmd("echo [{a,1}]") of + "[{a,1}]"++_ -> + PriorityString0; + _ -> + %% Some shells need quoting of [{}] + "'"++PriorityString0++"'" + end, + + Options = "-kernel inet_dist_listen_options " ++ PriorityString, + + NH1 = start_ssl_node([{additional_dist_opts, Options} | Config]), + NH2 = start_ssl_node([{additional_dist_opts, Options} | Config]), + Node2 = NH2#node_handle.nodename, + + pong = apply_on_ssl_node(NH1, fun () -> net_adm:ping(Node2) end), + + PrioritiesNode1 = + apply_on_ssl_node(NH1, fun get_socket_priorities/0), + PrioritiesNode2 = + apply_on_ssl_node(NH2, fun get_socket_priorities/0), + + Elevated1 = [P || P <- PrioritiesNode1, P =:= Prio], + ?t:format("Elevated1: ~p~n", [Elevated1]), + Elevated2 = [P || P <- PrioritiesNode2, P =:= Prio], + ?t:format("Elevated2: ~p~n", [Elevated2]), + [_|_] = Elevated1, + [_|_] = Elevated2, + + stop_ssl_node(NH1), + stop_ssl_node(NH2), + success(Config). +%%-------------------------------------------------------------------- +use_interface() -> + [{doc, "Test inet_dist_use_interface"}]. +use_interface(Config) when is_list(Config) -> + %% Force the node to listen only on the loopback interface. + IpString = "'{127,0,0,1}'", + Options = "-kernel inet_dist_use_interface " ++ IpString, + + %% Start a node, and get the port number it's listening on. + NH1 = start_ssl_node([{additional_dist_opts, Options} | Config]), + Node1 = NH1#node_handle.nodename, + Name = lists:takewhile(fun(C) -> C =/= $@ end, atom_to_list(Node1)), + {ok, NodesPorts} = apply_on_ssl_node(NH1, fun net_adm:names/0), + {Name, Port} = lists:keyfind(Name, 1, NodesPorts), + + %% Now find the socket listening on that port, and check its sockname. + Sockets = apply_on_ssl_node( + NH1, + fun() -> + [inet:sockname(P) || + P <- erlang:ports(), + {ok, Port} =:= (catch inet:port(P))] + end), + %% And check that it's actually listening on localhost. + [{ok,{{127,0,0,1},Port}}] = Sockets, + + stop_ssl_node(NH1), + success(Config). %%-------------------------------------------------------------------- %%% Internal functions ----------------------------------------------- @@ -264,6 +405,12 @@ tstsrvr_format(Fmt, ArgList) -> send_to_tstcntrl(Message) -> send_to_tstsrvr({message, Message}). +get_socket_priorities() -> + [Priority || + {ok,[{priority,Priority}]} <- + [inet:getopts(Port, [priority]) || + Port <- erlang:ports(), + element(2, erlang:port_info(Port, name)) =:= "tcp_inet"]]. %% %% test_server side api diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index f215a66812..ce1d9eb0ff 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -999,12 +999,7 @@ local_func(rl, [A], Bs0, _Shell, RT, Lf, Ef) -> {value,list_records(record_defs(RT, listify(Recs))),Bs}; local_func(rp, [A], Bs0, _Shell, RT, Lf, Ef) -> {[V],Bs} = expr_list([A], Bs0, Lf, Ef), - Cs = io_lib_pretty:print(V, ([{column, 1}, - {line_length, columns()}, - {depth, -1}, - {max_chars, ?CHAR_MAX}, - {record_print_fun, record_print_fun(RT)}] - ++ enc())), + Cs = pp(V, _Column=1, _Depth=-1, RT), io:requests([{put_chars, unicode, Cs}, nl]), {value,ok,Bs}; local_func(rr, [A], Bs0, _Shell, RT, Lf, Ef) -> @@ -1397,9 +1392,9 @@ get_history_and_results() -> {History, erlang:min(Results, History)}. pp(V, I, RT) -> - pp(V, I, RT, enc()). + pp(V, I, _Depth=?LINEMAX, RT). -pp(V, I, RT, Enc) -> +pp(V, I, D, RT) -> Strings = case application:get_env(stdlib, shell_strings) of {ok, false} -> @@ -1408,10 +1403,10 @@ pp(V, I, RT, Enc) -> true end, io_lib_pretty:print(V, ([{column, I}, {line_length, columns()}, - {depth, ?LINEMAX}, {max_chars, ?CHAR_MAX}, + {depth, D}, {max_chars, ?CHAR_MAX}, {strings, Strings}, {record_print_fun, record_print_fun(RT)}] - ++ Enc)). + ++ enc())). columns() -> case io:columns() of |