diff options
49 files changed, 2084 insertions, 663 deletions
diff --git a/erts/etc/common/ct_run.c b/erts/etc/common/ct_run.c index acdfa8c8b8..898c8ccde0 100644 --- a/erts/etc/common/ct_run.c +++ b/erts/etc/common/ct_run.c @@ -82,6 +82,9 @@ static int eargc; /* Number of arguments in eargv. */ static void error(char* format, ...); static void* emalloc(size_t size); +#ifdef HAVE_COPYING_PUTENV +static void efree(void *p); +#endif static char* strsave(char* string); static void push_words(char* src); static int run_erlang(char* name, char** argv); @@ -119,6 +122,30 @@ char *strerror(int errnum) } #endif /* !HAVE_STRERROR */ + +static void +set_env(char *key, char *value) +{ +#ifdef __WIN32__ + WCHAR wkey[MAXPATHLEN]; + WCHAR wvalue[MAXPATHLEN]; + MultiByteToWideChar(CP_UTF8, 0, key, -1, wkey, MAXPATHLEN); + MultiByteToWideChar(CP_UTF8, 0, value, -1, wvalue, MAXPATHLEN); + if (!SetEnvironmentVariableW(wkey, wvalue)) + error("SetEnvironmentVariable(\"%s\", \"%s\") failed!", key, value); +#else + size_t size = strlen(key) + 1 + strlen(value) + 1; + char *str = emalloc(size); + sprintf(str, "%s=%s", key, value); + if (putenv(str) != 0) + error("putenv(\"%s\") failed!", str); +#ifdef HAVE_COPYING_PUTENV + efree(str); +#endif +#endif +} + + #ifdef __WIN32__ int wmain(int argc, wchar_t **wcargv) { @@ -155,6 +182,11 @@ int main(int argc, char** argv) emulator = get_default_emulator(argv[0]); /* + * Add scriptname to env + */ + set_env("ESCRIPT_NAME", argv[0]); + + /* * Allocate the argv vector to be used for arguments to Erlang. * Arrange for starting to pushing information in the middle of * the array, to allow easy addition of commands in the beginning. @@ -458,6 +490,14 @@ erealloc(void *p, size_t size) } #endif +#ifdef HAVE_COPYING_PUTENV +static void +efree(void *p) +{ + free(p); +} +#endif + static char* strsave(char* string) { diff --git a/erts/etc/common/dialyzer.c b/erts/etc/common/dialyzer.c index 6ba3605422..829984ef3f 100644 --- a/erts/etc/common/dialyzer.c +++ b/erts/etc/common/dialyzer.c @@ -64,6 +64,9 @@ static int eargc; /* Number of arguments in eargv. */ static void error(char* format, ...); static void* emalloc(size_t size); +#ifdef HAVE_COPYING_PUTENV +static void efree(void *p); +#endif static char* strsave(char* string); static void push_words(char* src); static int run_erlang(char* name, char** argv); @@ -147,6 +150,29 @@ free_env_val(char *value) #endif } +static void +set_env(char *key, char *value) +{ +#ifdef __WIN32__ + WCHAR wkey[MAXPATHLEN]; + WCHAR wvalue[MAXPATHLEN]; + MultiByteToWideChar(CP_UTF8, 0, key, -1, wkey, MAXPATHLEN); + MultiByteToWideChar(CP_UTF8, 0, value, -1, wvalue, MAXPATHLEN); + if (!SetEnvironmentVariableW(wkey, wvalue)) + error("SetEnvironmentVariable(\"%s\", \"%s\") failed!", key, value); +#else + size_t size = strlen(key) + 1 + strlen(value) + 1; + char *str = emalloc(size); + sprintf(str, "%s=%s", key, value); + if (putenv(str) != 0) + error("putenv(\"%s\") failed!", str); +#ifdef HAVE_COPYING_PUTENV + efree(str); +#endif +#endif +} + + #ifdef __WIN32__ int wmain(int argc, wchar_t **wcargv) { @@ -181,6 +207,11 @@ int main(int argc, char** argv) error("Value of environment variable DIALYZER_EMULATOR is too large"); /* + * Add scriptname to env + */ + set_env("ESCRIPT_NAME", argv[0]); + + /* * Allocate the argv vector to be used for arguments to Erlang. * Arrange for starting to pushing information in the middle of * the array, to allow easy addition of commands in the beginning. @@ -434,6 +465,14 @@ erealloc(void *p, size_t size) } #endif +#ifdef HAVE_COPYING_PUTENV +static void +efree(void *p) +{ + free(p); +} +#endif + static char* strsave(char* string) { diff --git a/erts/etc/common/erlc.c b/erts/etc/common/erlc.c index b54cb31bef..2abeff33a3 100644 --- a/erts/etc/common/erlc.c +++ b/erts/etc/common/erlc.c @@ -72,6 +72,9 @@ static int pause_after_execution = 0; static char* process_opt(int* pArgc, char*** pArgv, int offset); static void error(char* format, ...); static void* emalloc(size_t size); +#ifdef HAVE_COPYING_PUTENV +static void efree(void *p); +#endif static char* strsave(char* string); static void push_words(char* src); static int run_erlang(char* name, char** argv); @@ -147,6 +150,28 @@ get_env(char *key) } static void +set_env(char *key, char *value) +{ +#ifdef __WIN32__ + WCHAR wkey[MAXPATHLEN]; + WCHAR wvalue[MAXPATHLEN]; + MultiByteToWideChar(CP_UTF8, 0, key, -1, wkey, MAXPATHLEN); + MultiByteToWideChar(CP_UTF8, 0, value, -1, wvalue, MAXPATHLEN); + if (!SetEnvironmentVariableW(wkey, wvalue)) + error("SetEnvironmentVariable(\"%s\", \"%s\") failed!", key, value); +#else + size_t size = strlen(key) + 1 + strlen(value) + 1; + char *str = emalloc(size); + sprintf(str, "%s=%s", key, value); + if (putenv(str) != 0) + error("putenv(\"%s\") failed!", str); +#ifdef HAVE_COPYING_PUTENV + efree(str); +#endif +#endif +} + +static void free_env_val(char *value) { #ifdef __WIN32__ @@ -188,6 +213,11 @@ int main(int argc, char** argv) error("Value of environment variable ERLC_EMULATOR is too large"); /* + * Add scriptname to env + */ + set_env("ESCRIPT_NAME", argv[0]); + + /* * Allocate the argv vector to be used for arguments to Erlang. * Arrange for starting to pushing information in the middle of * the array, to allow easy adding of emulator options (like -pa) @@ -499,6 +529,13 @@ erealloc(void *p, size_t size) } #endif +#ifdef HAVE_COPYING_PUTENV +static void +efree(void *p) +{ + free(p); +} +#endif static char* strsave(char* string) { diff --git a/erts/etc/common/erlexec.c b/erts/etc/common/erlexec.c index 8b75199a1e..f11ec2320a 100644 --- a/erts/etc/common/erlexec.c +++ b/erts/etc/common/erlexec.c @@ -587,8 +587,12 @@ int main(int argc, char **argv) erts_snprintf(tmpStr, sizeof(tmpStr), "%s" DIRSEP "%s" BINARY_EXT, bindir, emu); emu = strsave(tmpStr); - add_Eargs(emu); /* Will be argv[0] -- necessary! */ - + s = get_env("ESCRIPT_NAME"); + if(s) { + add_Eargs(s); /* argv[0] = scriptname*/ + } else { + add_Eargs(progname); /* argv[0] = erl or cerl */ + } /* * Add the bindir to the path (unless it is there already). */ diff --git a/erts/etc/common/escript.c b/erts/etc/common/escript.c index 4134a3ff36..c28e45a044 100644 --- a/erts/etc/common/escript.c +++ b/erts/etc/common/escript.c @@ -155,6 +155,29 @@ free_env_val(char *value) efree(value); #endif } + +static void +set_env(char *key, char *value) +{ +#ifdef __WIN32__ + WCHAR wkey[MAXPATHLEN]; + WCHAR wvalue[MAXPATHLEN]; + MultiByteToWideChar(CP_UTF8, 0, key, -1, wkey, MAXPATHLEN); + MultiByteToWideChar(CP_UTF8, 0, value, -1, wvalue, MAXPATHLEN); + if (!SetEnvironmentVariableW(wkey, wvalue)) + error("SetEnvironmentVariable(\"%s\", \"%s\") failed!", key, value); +#else + size_t size = strlen(key) + 1 + strlen(value) + 1; + char *str = emalloc(size); + sprintf(str, "%s=%s", key, value); + if (putenv(str) != 0) + error("putenv(\"%s\") failed!", str); +#ifdef HAVE_COPYING_PUTENV + efree(str); +#endif +#endif +} + /* * Find absolute path to this program */ @@ -548,7 +571,12 @@ main(int argc, char** argv) while (--eargc_base >= 0) { UNSHIFT(eargv_base[eargc_base]); } - + + /* + * Add scriptname to env + */ + set_env("ESCRIPT_NAME", scriptname); + /* * Invoke Erlang with the collected options. */ diff --git a/erts/etc/common/typer.c b/erts/etc/common/typer.c index 77a95ccded..644c90a795 100644 --- a/erts/etc/common/typer.c +++ b/erts/etc/common/typer.c @@ -64,6 +64,9 @@ static int eargc; /* Number of arguments in eargv. */ static void error(char* format, ...); static void* emalloc(size_t size); +#ifdef HAVE_COPYING_PUTENV +static void efree(void *p); +#endif static char* strsave(char* string); static void push_words(char* src); static int run_erlang(char* name, char** argv); @@ -101,6 +104,29 @@ char *strerror(int errnum) } #endif /* !HAVE_STRERROR */ +static void +set_env(char *key, char *value) +{ +#ifdef __WIN32__ + WCHAR wkey[MAXPATHLEN]; + WCHAR wvalue[MAXPATHLEN]; + MultiByteToWideChar(CP_UTF8, 0, key, -1, wkey, MAXPATHLEN); + MultiByteToWideChar(CP_UTF8, 0, value, -1, wvalue, MAXPATHLEN); + if (!SetEnvironmentVariableW(wkey, wvalue)) + error("SetEnvironmentVariable(\"%s\", \"%s\") failed!", key, value); +#else + size_t size = strlen(key) + 1 + strlen(value) + 1; + char *str = emalloc(size); + sprintf(str, "%s=%s", key, value); + if (putenv(str) != 0) + error("putenv(\"%s\") failed!", str); +#ifdef HAVE_COPYING_PUTENV + efree(str); +#endif +#endif +} + + #ifdef __WIN32__ int wmain(int argc, wchar_t **wcargv) { @@ -129,6 +155,10 @@ main(int argc, char** argv) #endif emulator = get_default_emulator(argv[0]); + /* + * Add scriptname to env + */ + set_env("ESCRIPT_NAME", argv[0]); /* * Allocate the argv vector to be used for arguments to Erlang. @@ -353,6 +383,14 @@ erealloc(void *p, size_t size) } #endif +#ifdef HAVE_COPYING_PUTENV +static void +efree(void *p) +{ + free(p); +} +#endif + static char* strsave(char* string) { diff --git a/lib/common_test/doc/src/run_test_chapter.xml b/lib/common_test/doc/src/run_test_chapter.xml index aea4d05867..fa336b7e16 100644 --- a/lib/common_test/doc/src/run_test_chapter.xml +++ b/lib/common_test/doc/src/run_test_chapter.xml @@ -1322,8 +1322,8 @@ <title>The Unexpected I/O Log</title> <p>The test suites overview page includes a link to the Unexpected I/O Log. In this log, <c>Common Test</c> saves printouts made with - <seealso marker="ct#log-2"><c>ct:log/2</c></seealso> and - <seealso marker="ct#pal-2"><c>ct:pal/2</c></seealso>, as well as captured system + <seealso marker="ct#log-2"><c>ct:log/1,2,3,4,5</c></seealso> and + <seealso marker="ct#pal-2"><c>ct:pal/1,2,3,4,5</c></seealso>, as well as captured system error- and progress reports, which cannot be associated with particular test cases and therefore cannot be written to individual test case log files. This occurs, for example, if a log printout is made from an external process (not a test @@ -1338,7 +1338,7 @@ <title>The Pre- and Post Test I/O Log</title> <p>The <c>Common Test</c> Framework Log page includes links to the Pre- and Post Test I/O Log. In this log, <c>Common Test</c> saves printouts made - with <c>ct:log/2</c> and <c>ct:pal/2</c>, as well as captured system error- + with <c>ct:log/1,2,3,4,5</c> and <c>ct:pal/1,2,3,4,5</c>, as well as captured system error- and progress reports, which take place before, and after, the test run. Examples of this are printouts from a CT hook init- or terminate function, or progress reports generated when an OTP application is started from a CT hook @@ -1349,8 +1349,8 @@ applications, see section <seealso marker="ct_hooks_chapter#synchronizing">Synchronizing</seealso> in section Common Test Hooks.</p> - <note><p>Logging to file with <c>ct:log/2</c> or <c>ct:pal/2</c> - only works when <c>Common Test</c> is running. Printouts with <c>ct:pal/2</c> + <note><p>Logging to file with <c>ct:log/1,2,3,4,5</c> or <c>ct:pal/1,2,3,4,5</c> + only works when <c>Common Test</c> is running. Printouts with <c>ct:pal/1,2,3,4,5</c> are however always displayed on screen.</p></note> </section> @@ -1383,24 +1383,38 @@ <p><c>Common Test</c> includes an <em>optional</em> feature to allow user HTML style sheets for customizing printouts. The functions in <c>ct</c> that print to a test case HTML log - file (<c>log/3</c> and <c>pal/3</c>) accept <c>Category</c> + file (<c>log/3,4,5</c> and <c>pal/3,4,5</c>) accept <c>Category</c> as first argument. With this argument a category can be specified - that can be mapped to a selector in a CSS - definition. This is useful, especially for coloring text + that can be mapped to a named <c>div</c> selector in a CSS rule-set. + This is useful, especially for coloring text differently depending on the type of (or reason for) the - printout. Say you want one color for test system + printout. Say you want one particular background color for test system configuration information, a different one for test system state information, and finally one for errors detected by the test case functions. The corresponding style sheet can look as follows:</p> <pre> - div.sys_config { background:blue; color:white } - div.sys_state { background:yellow; color:black } - div.error { background:red; color:white }</pre> + div.sys_config { background:blue } + div.sys_state { background:yellow } + div.error { background:red }</pre> + + <p>Common Test prints the text from <c>ct:log/3,4,5</c> or + <c>ct:pal/3,4,5</c> inside a <c>pre</c> element + nested under the named <c>div</c> element. Since the <c>pre</c> selector + has a predefined CSS rule (in file <c>ct_default.css</c>) for the attributes + <c>color</c>, <c>font-family</c> and <c>font-size</c>, if a user wants to + change any of the predefined attribute settings, a new rule for <c>pre</c> + must be added to the user stylesheet. Example:</p> + + <pre> +div.error pre { color:white }</pre> + + <p>Here, white text is used instead of the default black for <c>div.error</c> + printouts (and no other attribute settings for <c>pre</c> are affected).</p> <p>To install the CSS file (<c>Common Test</c> inlines the definition in the - HTML code), the name can be provided when executing <c>ct_run</c>.</p> + HTML code), the file name can be provided when executing <c>ct_run</c>.</p> <p><em>Example:</em></p> <pre> @@ -1448,8 +1462,8 @@ suite.</p> <p>Argument <c>Category</c> in the previous example can have the - value (atom) <c>sys_config</c> (white on blue), <c>sys_state</c> - (black on yellow), or <c>error</c> (white on red).</p> + value (atom) <c>sys_config</c> (blue background), <c>sys_state</c> + (yellow background), or <c>error</c> (white text on red background).</p> </section> <section> diff --git a/lib/debugger/doc/src/debugger_chapter.xml b/lib/debugger/doc/src/debugger_chapter.xml index 45dfdb3776..3c37d4b924 100644 --- a/lib/debugger/doc/src/debugger_chapter.xml +++ b/lib/debugger/doc/src/debugger_chapter.xml @@ -4,7 +4,7 @@ <chapter> <header> <copyright> - <year>1997</year><year>2016</year> + <year>1997</year><year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -652,8 +652,10 @@ c_break(Bindings) -> <item><p>The Bindings area, displaying all variables bindings. If you click a variable name, the value is displayed in the Evaluator area. Double-click a variable name to open a window where - the variable value can be edited. Notice however that pid, - reference, binary, or port values cannot be edited.</p> + the variable value can be edited. Notice however that pid, port, + reference, or fun + values cannot be edited unless they can be represented in the + running system.</p> </item> <item><p>The Trace area, which displays a trace output for the diff --git a/lib/debugger/src/dbg_icmd.erl b/lib/debugger/src/dbg_icmd.erl index 57a3719a50..4cd3dce670 100644 --- a/lib/debugger/src/dbg_icmd.erl +++ b/lib/debugger/src/dbg_icmd.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1998-2016. All Rights Reserved. +%% Copyright Ericsson AB 1998-2017. 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. @@ -382,14 +382,19 @@ eval_restricted({From,_Mod,Cmd,SP}, Bs) -> case catch parse_cmd(Cmd, 1) of {'EXIT', _Reason} -> From ! {self(), {eval_rsp, 'Parse error'}}; - [{var,_,Var}] -> + {[{var,_,Var}], XBs} -> Bs2 = bindings(Bs, SP), Res = case get_binding(Var, Bs2) of {value, Value} -> Value; - unbound -> unbound + unbound -> + case get_binding(Var, XBs) of + {value, _} -> + 'Only possible to inspect variables'; + unbound -> unbound + end end, From ! {self(), {eval_rsp, Res}}; - _Forms -> + {_Forms, _XBs} -> Rsp = 'Only possible to inspect variables', From ! {self(), {eval_rsp, Rsp}} end. @@ -404,22 +409,22 @@ eval_nonrestricted({From, _Mod, Cmd, _SP}, Bs, {'EXIT', _Reason} -> From ! {self(), {eval_rsp, 'Parse error'}}, Bs; - Forms -> + {Forms, XBs} -> mark_running(Line, Le), + Bs1 = merge_bindings(Bs, XBs), {Res, Bs2} = lists:foldl(fun(Expr, {_Res, Bs0}) -> eval_nonrestricted_1(Expr,Bs0,Ieval) end, - {null, Bs}, + {null, Bs1}, Forms), mark_break(M, Line, Le), From ! {self(), {eval_rsp, Res}}, - Bs2 + remove_binding_structs(Bs2, XBs) end. eval_nonrestricted_1({match,_,{var,_,Var},Expr}, Bs, Ieval) -> - {value,Res,Bs2} = - dbg_ieval:eval_expr(Expr, Bs, Ieval#ieval{top=false}), + {Res,Bs2} = eval_expr(Expr, Bs, Ieval), Bs3 = case lists:keyfind(Var, 1, Bs) of {Var,_Value} -> lists:keyreplace(Var, 1, Bs2, {Var,Res}); @@ -433,10 +438,21 @@ eval_nonrestricted_1({var,_,Var}, Bs, _Ieval) -> end, {Res,Bs}; eval_nonrestricted_1(Expr, Bs, Ieval) -> - {value,Res,Bs2} = - dbg_ieval:eval_expr(Expr, Bs, Ieval#ieval{top=false}), + eval_expr(Expr, Bs, Ieval). + +eval_expr(Expr, Bs, Ieval) -> + {value,Res,Bs2} = + dbg_ieval:eval_expr(Expr, Bs, Ieval#ieval{top=false}), {Res,Bs2}. +%% XBs have unique keys. +merge_bindings(Bs1, XBs) -> + Bs1 ++ erl_eval:bindings(XBs). + +remove_binding_structs(Bs1, XBs) -> + lists:foldl(fun({N, _V}, Bs) -> lists:keydelete(N, 1, Bs) + end, Bs1, erl_eval:bindings(XBs)). + mark_running(LineNo, Le) -> put(next_break, running), put(user_eval, [{LineNo, Le} | get(user_eval)]), @@ -450,9 +466,9 @@ mark_break(Cm, LineNo, Le) -> dbg_iserver:cast(get(int), {set_status,self(),break,{Cm,LineNo}}). parse_cmd(Cmd, LineNo) -> - {ok,Tokens,_} = erl_scan:string(Cmd, LineNo), - {ok,Forms} = erl_parse:parse_exprs(Tokens), - Forms. + {ok,Tokens,_} = erl_scan:string(Cmd, LineNo, [text]), + {ok,Forms,Bs} = lib:extended_parse_exprs(Tokens), + {Forms, Bs}. %%==================================================================== %% Library functions for attached process handling diff --git a/lib/debugger/src/dbg_wx_win.erl b/lib/debugger/src/dbg_wx_win.erl index 2c9d83ea74..f1298154ab 100644 --- a/lib/debugger/src/dbg_wx_win.erl +++ b/lib/debugger/src/dbg_wx_win.erl @@ -273,10 +273,9 @@ entry(Parent, Title, Prompt, {Type, Value}) -> verify(Type, Str) -> - case erl_scan:string(Str) of + case erl_scan:string(Str, 1, [text]) of {ok, Tokens, _EndLine} when Type==term -> - - case erl_parse:parse_term(Tokens++[{dot, erl_anno:new(1)}]) of + case lib:extended_parse_term(Tokens++[{dot, erl_anno:new(1)}]) of {ok, Value} -> {edit, Value}; _Error -> ignore diff --git a/lib/kernel/src/net_kernel.erl b/lib/kernel/src/net_kernel.erl index 0a9f9316b0..9921a0adfd 100644 --- a/lib/kernel/src/net_kernel.erl +++ b/lib/kernel/src/net_kernel.erl @@ -1262,11 +1262,22 @@ create_name(Name, LongOrShortNames, Try) -> {Head,Host1} = create_hostpart(Name, LongOrShortNames), case Host1 of {ok,HostPart} -> - {ok,list_to_atom(Head ++ HostPart)}; + case valid_name_head(Head) of + true -> + {ok,list_to_atom(Head ++ HostPart)}; + false -> + error_logger:info_msg("Invalid node name!\n" + "Please check your configuration\n"), + {error, badarg} + end; {error,long} when Try =:= 1 -> %% It could be we haven't read domain name from resolv file yet inet_config:do_load_resolv(os:type(), longnames), create_name(Name, LongOrShortNames, 0); + {error, hostname_not_allowed} -> + error_logger:info_msg("Invalid node name!\n" + "Please check your configuration\n"), + {error, badarg}; {error,Type} -> error_logger:info_msg( lists:concat(["Can\'t set ", @@ -1279,12 +1290,13 @@ create_name(Name, LongOrShortNames, Try) -> create_hostpart(Name, LongOrShortNames) -> {Head,Host} = split_node(Name), Host1 = case {Host,LongOrShortNames} of - {[$@,_|_],longnames} -> - {ok,Host}; + {[$@,_|_] = Host,longnames} -> + validate_hostname(Host); {[$@,_|_],shortnames} -> case lists:member($.,Host) of true -> {error,short}; - _ -> {ok,Host} + _ -> + validate_hostname(Host) end; {_,shortnames} -> case inet_db:gethostname() of @@ -1304,6 +1316,24 @@ create_hostpart(Name, LongOrShortNames) -> end, {Head,Host1}. +validate_hostname([$@|HostPart] = Host) -> + {ok, MP} = re:compile("^[!-ÿ]*$", [unicode]), + case re:run(HostPart, MP) of + {match, _} -> + {ok, Host}; + nomatch -> + {error, hostname_not_allowed} + end. + +valid_name_head(Head) -> + {ok, MP} = re:compile("^[0-9A-Za-z_\\-]*$", [unicode]), + case re:run(Head, MP) of + {match, _} -> + true; + nomatch -> + false + end. + split_node(Name) -> lists:splitwith(fun(C) -> C =/= $@ end, atom_to_list(Name)). diff --git a/lib/kernel/test/erl_distribution_SUITE.erl b/lib/kernel/test/erl_distribution_SUITE.erl index 09c80a0956..ecfa3d6cdb 100644 --- a/lib/kernel/test/erl_distribution_SUITE.erl +++ b/lib/kernel/test/erl_distribution_SUITE.erl @@ -24,7 +24,9 @@ -export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2]). --export([tick/1, tick_change/1, illegal_nodenames/1, hidden_node/1, +-export([tick/1, tick_change/1, + nodenames/1, hostnames/1, + illegal_nodenames/1, hidden_node/1, setopts/1, table_waste/1, net_setuptime/1, inet_dist_options_options/1, @@ -53,7 +55,6 @@ -export([pinger/1]). - -define(DUMMY_NODE,dummy@test01). %%----------------------------------------------------------------- @@ -68,8 +69,8 @@ suite() -> {timetrap,{minutes,4}}]. all() -> - [tick, tick_change, illegal_nodenames, hidden_node, - setopts, + [tick, tick_change, nodenames, hostnames, illegal_nodenames, + hidden_node, setopts, table_waste, net_setuptime, inet_dist_options_options, {group, monitor_nodes}]. @@ -179,7 +180,97 @@ table_waste(Config) when is_list(Config) -> stop_node(N), ok. +%% Test that starting nodes with different legal name part works, and that illegal +%% ones are filtered +nodenames(Config) when is_list(Config) -> + legal("a1@b"), + legal("a-1@b"), + legal("a_1@b"), + + illegal("cdé@a"), + illegal("te欢st@a"). + +%% Test that starting nodes with different legal host part works, and that illegal +%% ones are filtered +hostnames(Config) when is_list(Config) -> + Host = gethostname(), + legal([$a,$@|atom_to_list(Host)]), + legal("1@b1"), + legal("b@b1-c"), + legal("c@b1_c"), + legal("d@b1#c"), + legal("f@::1"), + legal("g@1:bc3:4e3f:f20:0:1"), + + case file:native_name_encoding() of + latin1 -> ignore; + _ -> legal("e@b1é") + end, + long_hostnames(net_kernel:longnames()), + + illegal("h@testالع"), + illegal("i@языtest"), + illegal("j@te欢st"). + +long_hostnames(true) -> + legal("[email protected]"), + legal("[email protected]"), + legal("[email protected]_c.d"), + legal("[email protected]"), + legal("[email protected]"); +long_hostnames(false) -> + illegal("[email protected]"). + +legal(Name) -> + case test_node(Name) of + started -> + ok; + not_started -> + ct:fail("no ~p node started", [Name]) + end. + +illegal(Name) -> + case test_node(Name) of + not_started -> + ok; + started -> + ct:fail("~p node started with illegal name", [Name]) + end. +test_node(Name) -> + ProgName = atom_to_list(lib:progname()), + Command = ProgName ++ " -noinput " ++ long_or_short() ++ Name ++ + " -eval \"net_adm:ping('" ++ atom_to_list(node()) ++ "')\"", + net_kernel:monitor_nodes(true), + BinCommand = unicode:characters_to_binary(Command, utf8), + open_port({spawn, BinCommand}, [stream]), + Node = list_to_atom(Name), + receive + {nodeup, Node} -> + net_kernel:monitor_nodes(false), + slave:stop(Node), + started + after 5000 -> + net_kernel:monitor_nodes(false), + not_started + end. + +long_or_short() -> + case net_kernel:longnames() of + true -> " -name "; + false -> " -sname " + end. + +% get the localhost's name, depending on the using name policy +gethostname() -> + Hostname = case net_kernel:longnames() of + true-> + net_adm:localhost(); + _-> + {ok, Name}=inet:gethostname(), + Name + end, + list_to_atom(Hostname). %% Test that pinging an illegal nodename does not kill the node. illegal_nodenames(Config) when is_list(Config) -> diff --git a/lib/public_key/src/pubkey_ssh.erl b/lib/public_key/src/pubkey_ssh.erl index 90726b1eb3..816d7b3336 100644 --- a/lib/public_key/src/pubkey_ssh.erl +++ b/lib/public_key/src/pubkey_ssh.erl @@ -44,11 +44,6 @@ %%==================================================================== %%-------------------------------------------------------------------- --spec decode(binary(), public_key | public_key:ssh_file()) -> - [{public_key:public_key(), Attributes::list()}] - ; (binary(), ssh2_pubkey) -> public_key:public_key() - . -%% %% Description: Decodes a ssh file-binary. %%-------------------------------------------------------------------- decode(Bin, public_key)-> @@ -66,11 +61,6 @@ decode(Bin, Type) -> openssh_decode(Bin, Type). %%-------------------------------------------------------------------- --spec encode([{public_key:public_key(), Attributes::list()}], public_key:ssh_file()) -> - binary() - ; (public_key:public_key(), ssh2_pubkey) -> binary() - . -%% %% Description: Encodes a list of ssh file entries. %%-------------------------------------------------------------------- encode(Bin, ssh2_pubkey) -> @@ -81,10 +71,6 @@ encode(Entries, Type) -> end, Entries)). %%-------------------------------------------------------------------- --spec dh_gex_group(integer(), integer(), integer(), - undefined | [{integer(),[{integer(),integer()}]}]) -> - {ok,{integer(),{integer(),integer()}}} | {error,any()} . -%% %% Description: Returns Generator and Modulus given MinSize, WantedSize %% and MaxSize %%-------------------------------------------------------------------- @@ -421,14 +407,21 @@ comma_list_encode([Option | Rest], []) -> comma_list_encode([Option | Rest], Acc) -> comma_list_encode(Rest, Acc ++ "," ++ Option). + +%% An experimental fix adding the signature algorithm name as the last element in a tuple... + ssh2_pubkey_encode(#'RSAPublicKey'{modulus = N, publicExponent = E}) -> - TypeStr = <<"ssh-rsa">>, - StrLen = size(TypeStr), + ssh2_pubkey_encode({#'RSAPublicKey'{modulus = N, publicExponent = E}, 'ssh-rsa'}); +ssh2_pubkey_encode({#'RSAPublicKey'{modulus = N, publicExponent = E}, SignAlg}) -> + SignAlgName = list_to_binary(atom_to_list(SignAlg)), + StrLen = size(SignAlgName), EBin = mpint(E), NBin = mpint(N), - <<?UINT32(StrLen), TypeStr:StrLen/binary, + <<?UINT32(StrLen), SignAlgName:StrLen/binary, EBin/binary, NBin/binary>>; +ssh2_pubkey_encode({{_,#'Dss-Parms'{}}=Key, _}) -> + ssh2_pubkey_encode(Key); ssh2_pubkey_encode({Y, #'Dss-Parms'{p = P, q = Q, g = G}}) -> TypeStr = <<"ssh-dss">>, StrLen = size(TypeStr), @@ -441,6 +434,8 @@ ssh2_pubkey_encode({Y, #'Dss-Parms'{p = P, q = Q, g = G}}) -> QBin/binary, GBin/binary, YBin/binary>>; +ssh2_pubkey_encode({{#'ECPoint'{},_}=Key, _}) -> + ssh2_pubkey_encode(Key); ssh2_pubkey_encode(Key={#'ECPoint'{point = Q}, {namedCurve,OID}}) -> TypeStr = key_type(Key), StrLen = size(TypeStr), @@ -453,10 +448,16 @@ ssh2_pubkey_encode(Key={#'ECPoint'{point = Q}, {namedCurve,OID}}) -> ssh2_pubkey_decode(Bin = <<?UINT32(Len), Type:Len/binary, _/binary>>) -> ssh2_pubkey_decode(Type, Bin). -ssh2_pubkey_decode(<<"ssh-rsa">>, +%% An experimental fix with the Signature Algorithm Name +ssh2_pubkey_decode(SignAlgName, <<?UINT32(Len), _:Len/binary, ?UINT32(SizeE), E:SizeE/binary, - ?UINT32(SizeN), N:SizeN/binary>>) -> + ?UINT32(SizeN), N:SizeN/binary>>) + when SignAlgName == <<"ssh-rsa">> ; + SignAlgName == <<"rsa-sha2-256">> ; + SignAlgName == <<"rsa-sha2-384">> ; + SignAlgName == <<"rsa-sha2-512">> + -> #'RSAPublicKey'{modulus = erlint(SizeN, N), publicExponent = erlint(SizeE, E)}; diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl index 7b5819fa84..0894e1860b 100644 --- a/lib/public_key/src/public_key.erl +++ b/lib/public_key/src/public_key.erl @@ -900,6 +900,7 @@ ssh_decode(SshBin, Type) when is_binary(SshBin), %%-------------------------------------------------------------------- -spec ssh_encode([{public_key(), Attributes::list()}], ssh_file()) -> binary() ; (public_key(), ssh2_pubkey) -> binary() + ; ({public_key(),atom()}, ssh2_pubkey) -> binary() . %% %% Description: Encodes a list of ssh file entries (public keys and diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index 315310f700..cf2a359e6c 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -38,7 +38,6 @@ -define(MAX_RND_PADDING_LEN, 15). -define(SUPPORTED_AUTH_METHODS, "publickey,keyboard-interactive,password"). --define(SUPPORTED_USER_KEYS, ['ssh-rsa','ssh-dss','ecdsa-sha2-nistp256','ecdsa-sha2-nistp384','ecdsa-sha2-nistp521']). -define(FALSE, 0). -define(TRUE, 1). @@ -134,7 +133,7 @@ role :: client | role(), peer :: undefined | {inet:hostname(), - {inet:ip_adress(),inet:port_number()}}, %% string version of peer address + {inet:ip_address(),inet:port_number()}}, %% string version of peer address c_vsn, %% client version {Major,Minor} s_vsn, %% server version {Major,Minor} @@ -145,6 +144,9 @@ c_keyinit, %% binary payload of kexinit packet s_keyinit, %% binary payload of kexinit packet + send_ext_info, %% May send ext-info to peer + recv_ext_info, %% Expect ext-info from peer + algorithms, %% #alg{} kex, %% key exchange algorithm @@ -198,6 +200,7 @@ userauth_quiet_mode, % boolean() userauth_methods, % list( string() ) eg ["keyboard-interactive", "password"] userauth_supported_methods, % string() eg "keyboard-interactive,password" + userauth_pubkeys, kb_tries_left = 0, % integer(), num tries left for "keyboard-interactive" userauth_preference, available_host_keys, @@ -216,7 +219,9 @@ compress, decompress, c_lng, - s_lng + s_lng, + send_ext_info, + recv_ext_info }). -record(ssh_key, diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl index 88c8144063..9eb11a53dc 100644 --- a/lib/ssh/src/ssh_auth.erl +++ b/lib/ssh/src/ssh_auth.erl @@ -136,34 +136,40 @@ keyboard_interactive_msg([#ssh{user = User, Ssh) end. -publickey_msg([Alg, #ssh{user = User, +publickey_msg([SigAlg, #ssh{user = User, session_id = SessionId, service = Service, opts = Opts} = Ssh]) -> - Hash = ssh_transport:sha(Alg), + Hash = ssh_transport:sha(SigAlg), + KeyAlg = key_alg(SigAlg), {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:user_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + case KeyCb:user_key(KeyAlg, [{key_cb_private,KeyCbOpts}|UserOpts]) of {ok, PrivKey} -> - StrAlgo = atom_to_list(Alg), - case encode_public_key(StrAlgo, ssh_transport:extract_public_key(PrivKey)) of - not_ok -> - {not_ok, Ssh}; + SigAlgStr = atom_to_list(SigAlg), + try + Key = ssh_transport:extract_public_key(PrivKey), + public_key:ssh_encode(Key, ssh2_pubkey) + of PubKeyBlob -> - SigData = build_sig_data(SessionId, - User, Service, PubKeyBlob, StrAlgo), + SigData = build_sig_data(SessionId, User, Service, + PubKeyBlob, SigAlgStr), Sig = ssh_transport:sign(SigData, Hash, PrivKey), - SigBlob = list_to_binary([?string(StrAlgo), ?binary(Sig)]), + SigBlob = list_to_binary([?string(SigAlgStr), + ?binary(Sig)]), ssh_transport:ssh_packet( #ssh_msg_userauth_request{user = User, service = Service, method = "publickey", data = [?TRUE, - ?string(StrAlgo), + ?string(SigAlgStr), ?binary(PubKeyBlob), ?binary(SigBlob)]}, Ssh) - end; + catch + _:_ -> + {not_ok, Ssh} + end; _Error -> {not_ok, Ssh} end. @@ -175,6 +181,7 @@ service_request_msg(Ssh) -> %%%---------------------------------------------------------------- init_userauth_request_msg(#ssh{opts = Opts} = Ssh) -> + %% Client side case ?GET_OPT(user, Opts) of undefined -> ErrStr = "Could not determine the users name", @@ -183,25 +190,16 @@ init_userauth_request_msg(#ssh{opts = Opts} = Ssh) -> description = ErrStr}); User -> - Msg = #ssh_msg_userauth_request{user = User, - service = "ssh-connection", - method = "none", - data = <<>>}, - Algs0 = ?GET_OPT(pref_public_key_algs, Opts), - %% The following line is not strictly correct. The call returns the - %% supported HOST key types while we are interested in USER keys. However, - %% they "happens" to be the same (for now). This could change.... - %% There is no danger as long as the set of user keys is a subset of the set - %% of host keys. - CryptoSupported = ssh_transport:supported_algorithms(public_key), - Algs = [A || A <- Algs0, - lists:member(A, CryptoSupported)], - - Prefs = method_preference(Algs), - ssh_transport:ssh_packet(Msg, Ssh#ssh{user = User, - userauth_preference = Prefs, - userauth_methods = none, - service = "ssh-connection"}) + ssh_transport:ssh_packet( + #ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "none", + data = <<>>}, + Ssh#ssh{user = User, + userauth_preference = method_preference(Ssh#ssh.userauth_pubkeys), + userauth_methods = none, + service = "ssh-connection"} + ) end. %%%---------------------------------------------------------------- @@ -272,8 +270,7 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User, #ssh{opts = Opts, userauth_supported_methods = Methods} = Ssh) -> - case pre_verify_sig(User, binary_to_list(BAlg), - KeyBlob, Opts) of + case pre_verify_sig(User, KeyBlob, Opts) of true -> {not_authorized, {User, undefined}, ssh_transport:ssh_packet( @@ -299,8 +296,7 @@ handle_userauth_request(#ssh_msg_userauth_request{user = User, userauth_supported_methods = Methods} = Ssh) -> case verify_sig(SessionId, User, "ssh-connection", - binary_to_list(BAlg), - KeyBlob, SigWLen, Opts) of + BAlg, KeyBlob, SigWLen, Opts) of true -> {authorized, User, ssh_transport:ssh_packet( @@ -453,14 +449,14 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{}, %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -method_preference(Algs) -> - lists:foldr(fun(A, Acc) -> - [{"publickey", ?MODULE, publickey_msg, [A]} | Acc] - end, - [{"password", ?MODULE, password_msg, []}, - {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []} - ], - Algs). +method_preference(SigKeyAlgs) -> + %% PubKeyAlgs: List of user (client) public key algorithms to try to use. + %% All of the acceptable algorithms is the default values. + PubKeyDefs = [{"publickey", ?MODULE, publickey_msg, [A]} || A <- SigKeyAlgs], + NonPKmethods = [{"password", ?MODULE, password_msg, []}, + {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []} + ], + PubKeyDefs ++ NonPKmethods. check_password(User, Password, Opts, Ssh) -> case ?GET_OPT(pwdfun, Opts) of @@ -499,9 +495,9 @@ get_password_option(Opts, User) -> false -> ?GET_OPT(password, Opts) end. -pre_verify_sig(User, Alg, KeyBlob, Opts) -> +pre_verify_sig(User, KeyBlob, Opts) -> try - {ok, Key} = decode_public_key_v2(KeyBlob, Alg), + Key = public_key:ssh_decode(KeyBlob, ssh2_pubkey), % or exception {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]) @@ -510,23 +506,18 @@ pre_verify_sig(User, Alg, KeyBlob, Opts) -> false end. -verify_sig(SessionId, User, Service, Alg, KeyBlob, SigWLen, Opts) -> +verify_sig(SessionId, User, Service, AlgBin, KeyBlob, SigWLen, Opts) -> try - {ok, Key} = decode_public_key_v2(KeyBlob, Alg), - + Alg = binary_to_list(AlgBin), {KeyCb,KeyCbOpts} = ?GET_OPT(key_cb, Opts), UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]) of - true -> - PlainText = build_sig_data(SessionId, User, - Service, KeyBlob, Alg), - <<?UINT32(AlgSigLen), AlgSig:AlgSigLen/binary>> = SigWLen, - <<?UINT32(AlgLen), _Alg:AlgLen/binary, - ?UINT32(SigLen), Sig:SigLen/binary>> = AlgSig, - ssh_transport:verify(PlainText, ssh_transport:sha(list_to_atom(Alg)), Sig, Key); - false -> - false - end + Key = public_key:ssh_decode(KeyBlob, ssh2_pubkey), % or exception + true = KeyCb:is_auth_key(Key, User, [{key_cb_private,KeyCbOpts}|UserOpts]), + PlainText = build_sig_data(SessionId, User, Service, KeyBlob, Alg), + <<?UINT32(AlgSigLen), AlgSig:AlgSigLen/binary>> = SigWLen, + <<?UINT32(AlgLen), _Alg:AlgLen/binary, + ?UINT32(SigLen), Sig:SigLen/binary>> = AlgSig, + ssh_transport:verify(PlainText, ssh_transport:sha(Alg), Sig, Key) catch _:_ -> false @@ -598,18 +589,7 @@ keyboard_interact_fun(KbdInteractFun, Name, Instr, PromptInfos, NumPrompts) -> language = "en"}}) end. -decode_public_key_v2(Bin, _Type) -> - try - public_key:ssh_decode(Bin, ssh2_pubkey) - of - Key -> {ok, Key} - catch - _:_ -> {error, bad_format} - end. -encode_public_key(_Alg, Key) -> - try - public_key:ssh_encode(Key, ssh2_pubkey) - catch - _:_ -> not_ok - end. +key_alg('rsa-sha2-256') -> 'ssh-rsa'; +key_alg('rsa-sha2-512') -> 'ssh-rsa'; +key_alg(Alg) -> Alg. diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 84bb7dc23f..74e14a233f 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -365,7 +365,7 @@ init_connection_handler(Role, Socket, Opts) -> {ok, StartState, D} -> process_flag(trap_exit, true), gen_statem:enter_loop(?MODULE, - [], %%[{debug,[trace,log,statistics,debug]} || Role==server], + [], %%[{debug,[trace,log,statistics,debug]} ], %% [] StartState, D); @@ -453,7 +453,9 @@ init_ssh_record(Role, _Socket, PeerAddr, Opts) -> PeerName = case ?GET_INTERNAL_OPT(host, Opts) of PeerIP when is_tuple(PeerIP) -> inet_parse:ntoa(PeerIP); - PeerName0 -> + PeerName0 when is_atom(PeerName0) -> + atom_to_list(PeerName0); + PeerName0 when is_list(PeerName0) -> PeerName0 end, S0#ssh{c_vsn = Vsn, @@ -462,6 +464,7 @@ init_ssh_record(Role, _Socket, PeerAddr, Opts) -> true -> ssh_io; false -> ssh_no_io end, + userauth_pubkeys = ?GET_OPT(pref_public_key_algs, Opts), userauth_quiet_mode = ?GET_OPT(quiet_mode, Opts), peer = {PeerName, PeerAddr} }; @@ -487,25 +490,42 @@ init_ssh_record(Role, _Socket, PeerAddr, Opts) -> -type renegotiate_flag() :: init | renegotiate. -type state_name() :: - {hello, role()} - | {kexinit, role(), renegotiate_flag()} - | {key_exchange, role(), renegotiate_flag()} - | {key_exchange_dh_gex_init, server, renegotiate_flag()} + {hello, role() } + | {kexinit, role(), renegotiate_flag()} + | {key_exchange, role(), renegotiate_flag()} + | {key_exchange_dh_gex_init, server, renegotiate_flag()} | {key_exchange_dh_gex_reply, client, renegotiate_flag()} - | {new_keys, role()} - | {service_request, role()} - | {userauth, role()} - | {userauth_keyboard_interactive, role()} - | {connected, role()} + | {new_keys, role(), renegotiate_flag()} + | {ext_info, role(), renegotiate_flag()} + | {service_request, role() } + | {userauth, role() } + | {userauth_keyboard_interactive, role() } + | {userauth_keyboard_interactive_extra, server } + | {userauth_keyboard_interactive_info_response, client } + | {connected, role() } . --type handle_event_result() :: gen_statem:handle_event_result(). +%% The state names must fulfill some rules regarding +%% where the role() and the renegotiate_flag() is placed: + +-spec role(state_name()) -> role(). +role({_,Role}) -> Role; +role({_,Role,_}) -> Role. + +-spec renegotiation(state_name()) -> boolean(). +renegotiation({_,_,ReNeg}) -> ReNeg == renegotiation; +renegotiation(_) -> false. + + +-define(CONNECTED(StateName), + (element(1,StateName) == connected orelse + element(1,StateName) == ext_info ) ). -spec handle_event(gen_statem:event_type(), event_content(), state_name(), #data{} - ) -> handle_event_result(). + ) -> gen_statem:event_handler_result(state_name()) . %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @@ -589,13 +609,17 @@ handle_event(_, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, handle_event(_, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params), send_bytes(KexdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- diffie-hellman group exchange @@ -620,13 +644,17 @@ handle_event(_, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, handle_event(_, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params), send_bytes(KexEcdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; @@ -635,8 +663,10 @@ handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) -> {ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params), send_bytes(KexGexReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; @@ -645,30 +675,60 @@ handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,serv handle_event(_, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) -> {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), - {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh1}}; + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), + {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {new_keys, client|server} #### %% First key exchange round: -handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,init}, D) -> +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D) -> {ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - Ssh = case Role of - client -> - {MsgReq, Ssh2} = ssh_auth:service_request_msg(Ssh1), - send_bytes(MsgReq, D), - Ssh2; - server -> - Ssh1 - end, - {next_state, {service_request,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh2} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {MsgReq, Ssh} = ssh_auth:service_request_msg(Ssh1), + send_bytes(MsgReq, D), + {next_state, {ext_info,client,init}, D#data{ssh_params=Ssh}}; + +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,server,init}, D) -> + {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}}; %% Subsequent key exchange rounds (renegotiation): handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) -> {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - {next_state, {connected,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,Role,renegotiate}, D#data{ssh_params=Ssh}}; + + +%%% ######## {ext_info, client|server, init|renegotiate} #### + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,init}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {service_request,Role}, D}; + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,renegotiate}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {connected,Role}, D}; + +handle_event(_, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) -> + {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), + {keep_state, D#data{ssh_params = Ssh}}; + + +handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {service_request,Role}, D, [postpone]}; -%%% ######## {service_request, client|server} +handle_event(internal, Msg, {ext_info,Role,_ReNegFlag}, D) when is_tuple(Msg) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {connected,Role}, D, [postpone]}; + +%%% ######## {service_request, client|server} #### handle_event(_, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D) -> case ServiceName of @@ -747,6 +807,11 @@ handle_event(_, end; %%---- userauth success to client +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth,client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; + handle_event(_, #ssh_msg_userauth_success{}, {userauth,client}, D=#data{ssh_params = Ssh}) -> D#data.starter ! ssh_connected, {next_state, {connected,client}, D#data{ssh_params=Ssh#ssh{authenticated = true}}}; @@ -849,6 +914,11 @@ handle_event(_, #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info end, {next_state, {userauth,client}, D, [postpone]}; +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth_keyboard_interactive_info_response, client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; + handle_event(_, #ssh_msg_userauth_success{}, {userauth_keyboard_interactive_info_response, client}, D) -> {next_state, {userauth,client}, D, [postpone]}; @@ -967,12 +1037,10 @@ handle_event(cast, data_size, _, _) -> -handle_event(cast, _, StateName, _) when StateName /= {connected,server}, - StateName /= {connected,client} -> +handle_event(cast, _, StateName, _) when not ?CONNECTED(StateName) -> {keep_state_and_data, [postpone]}; - -handle_event(cast, {adjust_window,ChannelId,Bytes}, {connected,_}, D) -> +handle_event(cast, {adjust_window,ChannelId,Bytes}, StateName, D) when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_window_pending = Pending, @@ -998,7 +1066,7 @@ handle_event(cast, {adjust_window,ChannelId,Bytes}, {connected,_}, D) -> keep_state_and_data end; -handle_event(cast, {reply_request,success,ChannelId}, {connected,_}, D) -> +handle_event(cast, {reply_request,success,ChannelId}, StateName, D) when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = RemoteId} -> Msg = ssh_connection:channel_success_msg(RemoteId), @@ -1009,13 +1077,13 @@ handle_event(cast, {reply_request,success,ChannelId}, {connected,_}, D) -> keep_state_and_data end; -handle_event(cast, {request,ChannelPid, ChannelId, Type, Data}, {connected,_}, D) -> +handle_event(cast, {request,ChannelPid, ChannelId, Type, Data}, StateName, D) when ?CONNECTED(StateName) -> {keep_state, handle_request(ChannelPid, ChannelId, Type, Data, false, none, D)}; -handle_event(cast, {request,ChannelId,Type,Data}, {connected,_}, D) -> +handle_event(cast, {request,ChannelId,Type,Data}, StateName, D) when ?CONNECTED(StateName) -> {keep_state, handle_request(ChannelId, Type, Data, false, none, D)}; -handle_event(cast, {unknown,Data}, {connected,_}, D) -> +handle_event(cast, {unknown,Data}, StateName, D) when ?CONNECTED(StateName) -> Msg = #ssh_msg_unimplemented{sequence = Data}, {keep_state, send_msg(Msg,D)}; @@ -1076,30 +1144,34 @@ handle_event({call,From}, stop, StateName, D0) -> {Repls,D} = send_replies(Replies, D0), {stop_and_reply, normal, [{reply,From,ok}|Repls], D#data{connection_state=Connection}}; -handle_event({call,_}, _, StateName, _) when StateName /= {connected,server}, - StateName /= {connected,client} -> + +handle_event({call,_}, _, StateName, _) when not ?CONNECTED(StateName) -> {keep_state_and_data, [postpone]}; -handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> D = handle_request(ChannelPid, ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> D = handle_request(ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, StateName, D0) + when ?CONNECTED(StateName) -> {{replies, Replies}, Connection} = ssh_connection:channel_data(ChannelId, Type, Data, D0#data.connection_state, From), {Repls,D} = send_replies(Replies, D0#data{connection_state = Connection}), start_channel_request_timer(ChannelId, From, Timeout), % FIXME: No message exchange so why? {keep_state, D, Repls}; -handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {eof, ChannelId}, StateName, D0) + when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id, sent_close = false} -> D = send_msg(ssh_connection:channel_eof_msg(Id), D0), @@ -1110,8 +1182,8 @@ handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> handle_event({call,From}, {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, - {connected,_}, - D0) -> + StateName, + D0) when ?CONNECTED(StateName) -> erlang:monitor(process, ChannelPid), {ChannelId, D1} = new_channel_id(D0), D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, @@ -1131,7 +1203,8 @@ handle_event({call,From}, start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_cancel_idle_timer(D)}; -handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {send_window, ChannelId}, StateName, D) + when ?CONNECTED(StateName) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{send_window_size = WinSize, send_packet_size = Packsize} -> @@ -1141,7 +1214,8 @@ handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {recv_window, ChannelId}, StateName, D) + when ?CONNECTED(StateName) -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_packet_size = Packsize} -> @@ -1151,7 +1225,8 @@ handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {close, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {close, ChannelId}, StateName, D0) + when ?CONNECTED(StateName) -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id} = Channel -> D1 = send_msg(ssh_connection:channel_close_msg(Id), D0), @@ -1319,11 +1394,16 @@ handle_event(info, UnexpectedMessage, StateName, D = #data{ssh_params = Ssh}) -> handle_event(internal, {disconnect,Msg,_Reason}, StateName, D) -> disconnect(Msg, StateName, D); +handle_event(_Type, _Msg, {ext_info,Role,_ReNegFlag}, D) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {connected,Role}, D, [postpone]}; + handle_event(Type, Ev, StateName, D) -> Descr = case catch atom_to_list(element(1,Ev)) of "ssh_msg_" ++_ when Type==internal -> - "Message in wrong state"; +%% "Message in wrong state"; + lists:flatten(io_lib:format("Message ~p in wrong state (~p)", [element(1,Ev), StateName])); _ -> "Internal error" end, @@ -1463,16 +1543,6 @@ peer_role(client) -> server; peer_role(server) -> client. %%-------------------------------------------------------------------- -%% StateName to Role -role({_,Role}) -> Role; -role({_,Role,_}) -> Role. - -%%-------------------------------------------------------------------- -%% Check the StateName to see if we are in the renegotiation phase -renegotiation({_,_,ReNeg}) -> ReNeg == renegotiation; -renegotiation(_) -> false. - -%%-------------------------------------------------------------------- supported_host_keys(client, _, Options) -> try find_sup_hkeys(Options) @@ -1508,14 +1578,19 @@ find_sup_hkeys(Options) -> %% Alg :: atom() available_host_key({KeyCb,KeyCbOpts}, Alg, Opts) -> UserOpts = ?GET_OPT(user_options, Opts), - element(1, - catch KeyCb:host_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts])) == ok. + case KeyCb:host_key(Alg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + {ok,_} -> true; + _ -> false + end. + send_msg(Msg, State=#data{ssh_params=Ssh0}) when is_tuple(Msg) -> {Bytes, Ssh} = ssh_transport:ssh_packet(Msg, Ssh0), send_bytes(Bytes, State), State#data{ssh_params=Ssh}. +send_bytes("", _D) -> + ok; send_bytes(Bytes, #data{socket = Socket, transport_cb = Transport}) -> _ = Transport:send(Socket, Bytes), ok. @@ -1622,6 +1697,29 @@ cache(#data{connection_state=C}) -> C#connection.channel_cache. %%%---------------------------------------------------------------- +handle_ssh_msg_ext_info(#ssh_msg_ext_info{}, D=#data{ssh_params = #ssh{recv_ext_info=false}} ) -> + % The peer sent this although we didn't allow it! + D; + +handle_ssh_msg_ext_info(#ssh_msg_ext_info{data=Data}, D0) -> + lists:foldl(fun ext_info/2, D0, Data). + + +ext_info({"server-sig-algs",SigAlgs}, D0 = #data{ssh_params=#ssh{role=client}=Ssh0}) -> + %% Make strings to eliminate risk of beeing bombed with odd strings that fills the atom table: + SupportedAlgs = lists:map(fun erlang:atom_to_list/1, ssh_transport:supported_algorithms(public_key)), + Ssh = Ssh0#ssh{userauth_pubkeys = + [list_to_atom(SigAlg) || SigAlg <- string:tokens(SigAlgs,","), + %% length of SigAlg is implicitly checked by member: + lists:member(SigAlg, SupportedAlgs) + ]}, + D0#data{ssh_params = Ssh}; + +ext_info(_, D0) -> + %% Not implemented + D0. + +%%%---------------------------------------------------------------- handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = Id} = Channel -> @@ -1911,12 +2009,14 @@ handshake(Pid, Ref, Timeout) -> end. update_inet_buffers(Socket) -> - {ok, BufSzs0} = inet:getopts(Socket, [sndbuf,recbuf]), - MinVal = 655360, - case - [{Tag,MinVal} || {Tag,Val} <- BufSzs0, - Val < MinVal] + try + {ok, BufSzs0} = inet:getopts(Socket, [sndbuf,recbuf]), + MinVal = 655360, + [{Tag,MinVal} || {Tag,Val} <- BufSzs0, + Val < MinVal] of [] -> ok; NewOpts -> inet:setopts(Socket, NewOpts) + catch + _:_ -> ok end. diff --git a/lib/ssh/src/ssh_dbg.erl b/lib/ssh/src/ssh_dbg.erl index 0345bbdea7..9431bf1817 100644 --- a/lib/ssh/src/ssh_dbg.erl +++ b/lib/ssh/src/ssh_dbg.erl @@ -56,7 +56,7 @@ messages(Write, MangleArg) when is_function(Write,2), dbg_ssh_messages() -> dbg:tp(ssh_message,encode,1, x), dbg:tp(ssh_message,decode,1, x), - dbg:tpl(ssh_transport,select_algorithm,3, x), + dbg:tpl(ssh_transport,select_algorithm,4, x), dbg:tp(ssh_transport,hello_version_msg,1, x), dbg:tp(ssh_transport,handle_hello_version,1, x). @@ -77,7 +77,7 @@ msg_formater({trace_ts,Pid,return_from,{ssh_message,decode,1},Msg,TS}, D) -> msg_formater({trace_ts,_Pid,call,{ssh_transport,select_algorithm,_},_TS}, D) -> D; -msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,3},{ok,Alg},TS}, D) -> +msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,_},{ok,Alg},TS}, D) -> fmt("~n~s ~p ALGORITHMS~n~s~n", [ts(TS),Pid, wr_record(Alg)], D); msg_formater({trace_ts,_Pid,call,{ssh_transport,hello_version_msg,_},_TS}, D) -> @@ -160,6 +160,7 @@ shrink_bin(X) -> X. ?wr_record(ssh_msg_kexdh_init); ?wr_record(ssh_msg_kexdh_reply); ?wr_record(ssh_msg_newkeys); +?wr_record(ssh_msg_ext_info); ?wr_record(ssh_msg_kex_dh_gex_request); ?wr_record(ssh_msg_kex_dh_gex_request_old); ?wr_record(ssh_msg_kex_dh_gex_group); diff --git a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl index 88f4d10792..4498c70d34 100644 --- a/lib/ssh/src/ssh_file.erl +++ b/lib/ssh/src/ssh_file.erl @@ -75,17 +75,12 @@ host_key(Algorithm, Opts) -> Password = proplists:get_value(identity_pass_phrase(Algorithm), Opts, ignore), case decode(File, Password) of {ok,Key} -> - case {Key,Algorithm} of - {#'RSAPrivateKey'{}, 'ssh-rsa'} -> {ok,Key}; - {#'DSAPrivateKey'{}, 'ssh-dss'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp256r1'}}, 'ecdsa-sha2-nistp256'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp384r1'}}, 'ecdsa-sha2-nistp384'} -> {ok,Key}; - {#'ECPrivateKey'{parameters = {namedCurve, ?'secp521r1'}}, 'ecdsa-sha2-nistp521'} -> {ok,Key}; - _ -> - {error,bad_keytype_in_file} + case ssh_transport:valid_key_sha_alg(Key,Algorithm) of + true -> {ok,Key}; + false -> {error,bad_keytype_in_file} end; - Other -> - Other + {error,DecodeError} -> + {error,DecodeError} end. is_auth_key(Key, User,Opts) -> @@ -115,6 +110,9 @@ user_key(Algorithm, Opts) -> %% Internal functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% file_base_name('ssh-rsa' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-256' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-384' ) -> "ssh_host_rsa_key"; +file_base_name('rsa-sha2-512' ) -> "ssh_host_rsa_key"; file_base_name('ssh-dss' ) -> "ssh_host_dsa_key"; file_base_name('ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key"; file_base_name('ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key"; @@ -253,12 +251,18 @@ do_lookup_host_key(KeyToMatch, Host, Alg, Opts) -> identity_key_filename('ssh-dss' ) -> "id_dsa"; identity_key_filename('ssh-rsa' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-256' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-384' ) -> "id_rsa"; +identity_key_filename('rsa-sha2-512' ) -> "id_rsa"; identity_key_filename('ecdsa-sha2-nistp256') -> "id_ecdsa"; identity_key_filename('ecdsa-sha2-nistp384') -> "id_ecdsa"; identity_key_filename('ecdsa-sha2-nistp521') -> "id_ecdsa". identity_pass_phrase("ssh-dss" ) -> dsa_pass_phrase; identity_pass_phrase("ssh-rsa" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-256" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-384" ) -> rsa_pass_phrase; +identity_pass_phrase("rsa-sha2-512" ) -> rsa_pass_phrase; identity_pass_phrase("ecdsa-sha2-"++_) -> ecdsa_pass_phrase; identity_pass_phrase(P) when is_atom(P) -> identity_pass_phrase(atom_to_list(P)). diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl index 562f040477..21c0eabcd3 100644 --- a/lib/ssh/src/ssh_message.erl +++ b/lib/ssh/src/ssh_message.erl @@ -215,6 +215,16 @@ encode(#ssh_msg_service_accept{ }) -> <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring_utf8(Service)>>; +encode(#ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }) -> + lists:foldl(fun({ExtName,ExtVal}, Acc) -> + <<Acc/binary, ?Estring(ExtName), ?Estring(ExtVal)>> + end, + <<?Ebyte(?SSH_MSG_EXT_INFO), ?Euint32(N)>>, + Data); + encode(#ssh_msg_newkeys{}) -> <<?Ebyte(?SSH_MSG_NEWKEYS)>>; @@ -435,6 +445,18 @@ decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_RESPONSE), ?UINT32(Num), Data/binary>>) -> num_responses = Num, data = Data}; +decode(<<?BYTE(?SSH_MSG_EXT_INFO), ?UINT32(N), BinData/binary>>) -> + Data = bin_foldr( + fun(Bin,Acc) when length(Acc) == N -> + {Bin,Acc}; + (<<?DEC_BIN(V0,__0), ?DEC_BIN(V1,__1), Rest/binary>>, Acc) -> + {Rest,[{binary_to_list(V0),binary_to_list(V1)}|Acc]} + end, [], BinData), + #ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }; + %%% Keyexchange messages decode(<<?BYTE(?SSH_MSG_KEXINIT), Cookie:128, Data/binary>>) -> decode_kex_init(Data, [Cookie, ssh_msg_kexinit], 10); @@ -537,17 +559,28 @@ decode(<<?BYTE(?SSH_MSG_DEBUG), ?BYTE(Bool), ?DEC_BIN(Msg,__0), ?DEC_BIN(Lang,__ %%% Helper functions %%% +bin_foldr(Fun, Acc, Bin) -> + lists:reverse(bin_foldl(Fun, Acc, Bin)). + +bin_foldl(_, Acc, <<>>) -> Acc; +bin_foldl(Fun, Acc0, Bin0) -> + {Bin,Acc} = Fun(Bin0,Acc0), + bin_foldl(Fun, Acc, Bin). + +%%%---------------------------------------------------------------- decode_keyboard_interactive_prompts(<<>>, Acc) -> lists:reverse(Acc); decode_keyboard_interactive_prompts(<<?DEC_BIN(Prompt,__0), ?BYTE(Bool), Bin/binary>>, Acc) -> decode_keyboard_interactive_prompts(Bin, [{Prompt, erl_boolean(Bool)} | Acc]). +%%%---------------------------------------------------------------- erl_boolean(0) -> false; erl_boolean(1) -> true. +%%%---------------------------------------------------------------- decode_kex_init(<<?BYTE(Bool), ?UINT32(X)>>, Acc, 0) -> list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) -> @@ -569,11 +602,22 @@ decode_signature(<<?DEC_BIN(_Alg,__0), ?UINT32(_), Signature/binary>>) -> Signature. -encode_signature(#'RSAPublicKey'{}, Signature) -> - <<?Ebinary(<<"ssh-rsa">>), ?Ebinary(Signature)>>; -encode_signature({_, #'Dss-Parms'{}}, Signature) -> +encode_signature({#'RSAPublicKey'{},Sign}, Signature) -> + SignName = list_to_binary(atom_to_list(Sign)), + <<?Ebinary(SignName), ?Ebinary(Signature)>>; +encode_signature({{_, #'Dss-Parms'{}},_}, Signature) -> <<?Ebinary(<<"ssh-dss">>), ?Ebinary(Signature)>>; -encode_signature({#'ECPoint'{}, {namedCurve,OID}}, Signature) -> +encode_signature({{#'ECPoint'{}, {namedCurve,OID}},_}, Signature) -> CurveName = public_key:oid2ssh_curvename(OID), <<?Ebinary(<<"ecdsa-sha2-",CurveName/binary>>), ?Ebinary(Signature)>>. +%% encode_signature(#'RSAPublicKey'{}, Signature) -> +%% SignName = <<"ssh-rsa">>, +%% <<?Ebinary(SignName), ?Ebinary(Signature)>>; +%% encode_signature({_, #'Dss-Parms'{}}, Signature) -> +%% <<?Ebinary(<<"ssh-dss">>), ?Ebinary(Signature)>>; +%% encode_signature({#'ECPoint'{}, {namedCurve,OID}}, Signature) -> +%% CurveName = public_key:oid2ssh_curvename(OID), +%% <<?Ebinary(<<"ecdsa-sha2-",CurveName/binary>>), ?Ebinary(Signature)>>. + + diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index ee3cdbb8a0..0886d5b34d 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -437,9 +437,7 @@ default(client) -> {pref_public_key_algs, def} => #{default => - %% Get dynamically supported keys in the order of the ?SUPPORTED_USER_KEYS - [A || A <- ?SUPPORTED_USER_KEYS, - lists:member(A, ssh_transport:supported_algorithms(public_key))], + ssh_transport:supported_algorithms(public_key), chk => fun check_pref_public_key_algs/1, class => @@ -614,11 +612,23 @@ default(common) -> }, {max_random_length_padding, def} => - #{default => ?MAX_RND_PADDING_LEN, - chk => fun check_non_neg_integer/1, - class => user_options - } - }. + #{default => ?MAX_RND_PADDING_LEN, + chk => fun check_non_neg_integer/1, + class => user_options + }, + + {send_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + }, + + {recv_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + } + }. %%%================================================================ @@ -658,20 +668,8 @@ check_pref_public_key_algs(V) -> PKs = ssh_transport:supported_algorithms(public_key), CHK = fun(A, Ack) -> case lists:member(A, PKs) of - true -> - [A|Ack]; - false -> - %% Check with the documented options, that is, - %% the one we can handle - case lists:member(A,?SUPPORTED_USER_KEYS) of - false -> - %% An algorithm ssh never can handle - error_in_check(A, "Not supported public key"); - true -> - %% An algorithm ssh can handle, but not in - %% this very call - Ack - end + true -> [A|Ack]; + false -> error_in_check(A, "Not supported public key") end end, case lists:foldr( diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 6b47868d5c..7c7dda7a1e 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -38,6 +38,7 @@ handle_hello_version/1, key_exchange_init_msg/1, key_init/3, new_keys_message/1, + ext_info_message/1, handle_kexinit_msg/3, handle_kexdh_init/2, handle_kex_dh_gex_group/2, handle_kex_dh_gex_init/2, handle_kex_dh_gex_reply/2, handle_new_keys/2, handle_kex_dh_gex_request/2, @@ -47,6 +48,7 @@ parallell_gen_key/1, extract_public_key/1, ssh_packet/2, pack/2, + valid_key_sha_alg/2, sha/1, sign/3, verify/4]). %%% For test suites @@ -90,6 +92,10 @@ default_algorithms(cipher) -> default_algorithms(mac) -> supported_algorithms(mac, same(['AEAD_AES_128_GCM', 'AEAD_AES_256_GCM'])); +default_algorithms(public_key) -> + supported_algorithms(public_key, ['rsa-sha2-256', + 'rsa-sha2-384', + 'rsa-sha2-512']); default_algorithms(Alg) -> supported_algorithms(Alg, []). @@ -116,6 +122,9 @@ supported_algorithms(public_key) -> {'ecdsa-sha2-nistp384', [{public_keys,ecdsa}, {hashs,sha384}, {ec_curve,secp384r1}]}, {'ecdsa-sha2-nistp521', [{public_keys,ecdsa}, {hashs,sha512}, {ec_curve,secp521r1}]}, {'ecdsa-sha2-nistp256', [{public_keys,ecdsa}, {hashs,sha256}, {ec_curve,secp256r1}]}, + {'rsa-sha2-256', [{public_keys,rsa}, {hashs,sha256} ]}, + {'rsa-sha2-384', [{public_keys,rsa}, {hashs,sha384} ]}, + {'rsa-sha2-512', [{public_keys,rsa}, {hashs,sha512} ]}, {'ssh-rsa', [{public_keys,rsa}, {hashs,sha} ]}, {'ssh-dss', [{public_keys,dss}, {hashs,sha} ]} % Gone in OpenSSH 7.3.p1 ]); @@ -230,7 +239,7 @@ key_exchange_init_msg(Ssh0) -> kex_init(#ssh{role = Role, opts = Opts, available_host_keys = HostKeyAlgs}) -> Random = ssh_bits:random(16), PrefAlgs = ?GET_OPT(preferred_algorithms, Opts), - kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs). + kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs, Opts). key_init(client, Ssh, Value) -> Ssh#ssh{c_keyinit = Value}; @@ -238,10 +247,11 @@ key_init(server, Ssh, Value) -> Ssh#ssh{s_keyinit = Value}. -kexinit_message(_Role, Random, Algs, HostKeyAlgs) -> +kexinit_message(Role, Random, Algs, HostKeyAlgs, Opts) -> #ssh_msg_kexinit{ cookie = Random, - kex_algorithms = to_strings( get_algs(kex,Algs) ), + kex_algorithms = to_strings( get_algs(kex,Algs) ) + ++ kex_ext_info(Role,Opts), server_host_key_algorithms = HostKeyAlgs, encryption_algorithms_client_to_server = c2s(cipher,Algs), encryption_algorithms_server_to_client = s2c(cipher,Algs), @@ -263,39 +273,42 @@ get_algs(Key, Algs) -> proplists:get_value(Key, Algs, default_algorithms(Key)). to_strings(L) -> lists:map(fun erlang:atom_to_list/1, L). new_keys_message(Ssh0) -> - {SshPacket, Ssh} = - ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {SshPacket, Ssh1} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), + Ssh = install_alg(snd, Ssh1), {ok, SshPacket, Ssh}. handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = client} = Ssh0) -> - {ok, Algoritms} = select_algorithm(client, Own, CounterPart), - case verify_algorithm(Algoritms) of - true -> - key_exchange_first_msg(Algoritms#alg.kex, - Ssh0#ssh{algorithms = Algoritms}); - {false,Alg} -> - %% TODO: Correct code? - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = client} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(client, Own, CounterPart, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + key_exchange_first_msg(Algos#alg.kex, + Ssh#ssh{algorithms = Algos}) + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end; handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = server} = Ssh) -> - {ok, Algoritms} = select_algorithm(server, CounterPart, Own), - case verify_algorithm(Algoritms) of - true -> - {ok, Ssh#ssh{algorithms = Algoritms}}; - {false,Alg} -> - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = server} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(server, CounterPart, Own, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + {ok, Ssh#ssh{algorithms = Algos}} + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end. @@ -308,6 +321,8 @@ verify_algorithm(#alg{decrypt = undefined}) -> {false, "decrypt"}; verify_algorithm(#alg{compress = undefined}) -> {false, "compress"}; verify_algorithm(#alg{decompress = undefined}) -> {false, "decompress"}; verify_algorithm(#alg{kex = Kex}) -> + %% This also catches the error if 'ext-info-s' or 'ext-info-c' is selected. + %% (draft-ietf-curdle-ssh-ext-info-04 2.2) case lists:member(Kex, supported_algorithms(kex)) of true -> true; false -> {false, "kex"} @@ -370,7 +385,8 @@ key_exchange_first_msg(Kex, Ssh0) when Kex == 'ecdh-sha2-nistp256' ; %%% diffie-hellman-group18-sha512 %%% handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, - Ssh0 = #ssh{algorithms = #alg{kex=Kex} = Algs}) -> + Ssh0 = #ssh{algorithms = #alg{kex=Kex, + hkey=SignAlg} = Algs}) -> %% server {G, P} = dh_group(Kex), if @@ -378,12 +394,12 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, Sz = dh_bits(Algs), {Public, Private} = generate_key(dh, [P,G,2*Sz]), K = compute_key(dh, E, Private, [P,G]), - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, MyPubHostKey, E, Public, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Kex), {E,Public,K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh1} = - ssh_packet(#ssh_msg_kexdh_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kexdh_reply{public_host_key = {MyPubHostKey,SignAlg}, f = Public, h_sig = H_SIG }, Ssh0), @@ -404,19 +420,20 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey, f = F, h_sig = H_SIG}, - #ssh{keyex_key = {{Private, Public}, {G, P}}} = Ssh0) -> + #ssh{keyex_key = {{Private, Public}, {G, P}}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% client if 1=<F, F=<(P-1)-> K = compute_key(dh, F, Private, [P,G]), - H = kex_h(Ssh0, PeerPubHostKey, Public, F, K), - + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Kex), {Public,F,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -486,7 +503,7 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits}, ssh_packet(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0), {ok, SshPacket, Ssh#ssh{keyex_key = {x, {G, P}}, - keyex_info = {-1, -1, NBits} % flag for kex_h hash calc + keyex_info = {-1, -1, NBits} % flag for kex_hash calc }}; {error,_} -> ssh_connection_handler:disconnect( @@ -532,20 +549,21 @@ handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) -> handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E}, #ssh{keyex_key = {{Private, Public}, {G, P}}, - keyex_info = {Min, Max, NBits}} = - Ssh0) -> + keyex_info = {Min, Max, NBits}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% server if 1=<E, E=<(P-1) -> K = compute_key(dh, E, Private, [P,G]), if 1<K, K<(P-1) -> - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, MyPubHostKey, Min, NBits, Max, P, G, E, Public, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Kex), {Min,NBits,Max,P,G,E,Public,K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh} = - ssh_packet(#ssh_msg_kex_dh_gex_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kex_dh_gex_reply{public_host_key = {MyPubHostKey,SignAlg}, f = Public, h_sig = H_SIG}, Ssh0), {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), @@ -571,7 +589,9 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK f = F, h_sig = H_SIG}, #ssh{keyex_key = {{Private, Public}, {G, P}}, - keyex_info = {Min, Max, NBits}} = + keyex_info = {Min, Max, NBits}, + algorithms = #alg{kex=Kex, + hkey=SignAlg}} = Ssh0) -> %% client if @@ -579,14 +599,13 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK K = compute_key(dh, F, Private, [P,G]), if 1<K, K<(P-1) -> - H = kex_h(Ssh0, PeerPubHostKey, Min, NBits, Max, P, G, Public, F, K), - + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Kex), {Min,NBits,Max,P,G,Public,F,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; _Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -616,7 +635,8 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK %%% diffie-hellman-ecdh-sha2-* %%% handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, - Ssh0 = #ssh{algorithms = #alg{kex=Kex}}) -> + Ssh0 = #ssh{algorithms = #alg{kex=Kex, + hkey=SignAlg}}) -> %% at server Curve = ecdh_curve(Kex), {MyPublic, MyPrivate} = generate_key(ecdh, Curve), @@ -624,12 +644,12 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, compute_key(ecdh, PeerPublic, MyPrivate, Curve) of K -> - MyPrivHostKey = get_host_key(Ssh0), + MyPrivHostKey = get_host_key(Ssh0, SignAlg), MyPubHostKey = extract_public_key(MyPrivHostKey), - H = kex_h(Ssh0, Curve, MyPubHostKey, PeerPublic, MyPublic, K), - H_SIG = sign_host_key(Ssh0, MyPrivHostKey, H), + H = kex_hash(Ssh0, MyPubHostKey, SignAlg, sha(Curve), {PeerPublic, MyPublic, K}), + H_SIG = sign(H, sha(SignAlg), MyPrivHostKey), {SshPacket, Ssh1} = - ssh_packet(#ssh_msg_kex_ecdh_reply{public_host_key = MyPubHostKey, + ssh_packet(#ssh_msg_kex_ecdh_reply{public_host_key = {MyPubHostKey,SignAlg}, q_s = MyPublic, h_sig = H_SIG}, Ssh0), @@ -649,20 +669,21 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, q_s = PeerPublic, h_sig = H_SIG}, - #ssh{keyex_key = {{MyPublic,MyPrivate}, Curve}} = Ssh0 + #ssh{keyex_key = {{MyPublic,MyPrivate}, Curve}, + algorithms = #alg{hkey=SignAlg}} = Ssh0 ) -> %% at client try compute_key(ecdh, PeerPublic, MyPrivate, Curve) of K -> - H = kex_h(Ssh0, Curve, PeerPubHostKey, MyPublic, PeerPublic, K), + H = kex_hash(Ssh0, PeerPubHostKey, SignAlg, sha(Curve), {MyPublic,PeerPublic,K}), case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -682,7 +703,7 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, %%%---------------------------------------------------------------- handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> - try install_alg(Ssh0) of + try install_alg(rcv, Ssh0) of #ssh{} = Ssh -> {ok, Ssh} catch @@ -693,34 +714,49 @@ handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> }) end. + +%%%---------------------------------------------------------------- +kex_ext_info(Role, Opts) -> + case ?GET_OPT(recv_ext_info,Opts) of + true when Role==client -> ["ext-info-c"]; + true when Role==server -> ["ext-info-s"]; + false -> [] + end. + +ext_info_message(#ssh{role=client, + send_ext_info=true} = Ssh0) -> + %% FIXME: no extensions implemented + {ok, "", Ssh0}; + +ext_info_message(#ssh{role=server, + send_ext_info=true} = Ssh0) -> + AlgsList = lists:map(fun erlang:atom_to_list/1, + ssh_transport:default_algorithms(public_key)), + Msg = #ssh_msg_ext_info{nr_extensions = 1, + data = [{"server-sig-algs", string:join(AlgsList,",")}] + }, + {SshPacket, Ssh} = ssh_packet(Msg, Ssh0), + {ok, SshPacket, Ssh}; + +ext_info_message(Ssh0) -> + {ok, "", Ssh0}. % "" means: 'do not send' + +%%%---------------------------------------------------------------- %% select session id -sid(#ssh{session_id = undefined}, H) -> - H; -sid(#ssh{session_id = Id}, _) -> - Id. +sid(#ssh{session_id = undefined}, H) -> H; +sid(#ssh{session_id = Id}, _) -> Id. %% %% The host key should be read from storage %% -get_host_key(SSH) -> - #ssh{key_cb = {KeyCb,KeyCbOpts}, opts = Opts, algorithms = ALG} = SSH, +get_host_key(SSH, SignAlg) -> + #ssh{key_cb = {KeyCb,KeyCbOpts}, opts = Opts} = SSH, UserOpts = ?GET_OPT(user_options, Opts), - case KeyCb:host_key(ALG#alg.hkey, [{key_cb_private,KeyCbOpts}|UserOpts]) of - {ok, #'RSAPrivateKey'{} = Key} -> Key; - {ok, #'DSAPrivateKey'{} = Key} -> Key; - {ok, #'ECPrivateKey'{} = Key} -> Key; - Result -> - exit({error, {Result, unsupported_key_type}}) + case KeyCb:host_key(SignAlg, [{key_cb_private,KeyCbOpts}|UserOpts]) of + {ok, PrivHostKey} -> PrivHostKey; + Result -> exit({error, {Result, unsupported_key_type}}) end. -sign_host_key(_Ssh, PrivateKey, H) -> - sign(H, sign_host_key_sha(PrivateKey), PrivateKey). - -sign_host_key_sha(#'ECPrivateKey'{parameters = {namedCurve,OID}}) -> sha(OID); -sign_host_key_sha(#'RSAPrivateKey'{}) -> sha; -sign_host_key_sha(#'DSAPrivateKey'{}) -> sha. - - extract_public_key(#'RSAPrivateKey'{modulus = N, publicExponent = E}) -> #'RSAPublicKey'{modulus = N, publicExponent = E}; extract_public_key(#'DSAPrivateKey'{y = Y, p = P, q = Q, g = G}) -> @@ -730,8 +766,8 @@ extract_public_key(#'ECPrivateKey'{parameters = {namedCurve,OID}, {#'ECPoint'{point=Q}, {namedCurve,OID}}. -verify_host_key(SSH, PublicKey, Digest, Signature) -> - case verify(Digest, host_key_sha(PublicKey), Signature, PublicKey) of +verify_host_key(#ssh{algorithms=Alg}=SSH, PublicKey, Digest, Signature) -> + case verify(Digest, sha(Alg#alg.hkey), Signature, PublicKey) of false -> {error, bad_signature}; true -> @@ -739,16 +775,6 @@ verify_host_key(SSH, PublicKey, Digest, Signature) -> end. -host_key_sha(#'RSAPublicKey'{}) -> sha; -host_key_sha({_, #'Dss-Parms'{}}) -> sha; -host_key_sha({#'ECPoint'{},{namedCurve,OID}}) -> sha(OID). - -public_algo(#'RSAPublicKey'{}) -> 'ssh-rsa'; -public_algo({_, #'Dss-Parms'{}}) -> 'ssh-dss'; -public_algo({#'ECPoint'{},{namedCurve,OID}}) -> - Curve = public_key:oid2ssh_curvename(OID), - list_to_atom("ecdsa-sha2-" ++ binary_to_list(Curve)). - accepted_host(Ssh, PeerName, Public, Opts) -> case ?GET_OPT(silently_accept_hosts, Opts) of @@ -812,7 +838,7 @@ known_host_key(#ssh{opts = Opts, key_cb = {KeyCb,KeyCbOpts}, peer = {PeerName,_} %% %% 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) -> +select_algorithm(Role, Client, Server, Opts) -> {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), @@ -837,17 +863,34 @@ select_algorithm(Role, Client, Server) -> Kex = select(Client#ssh_msg_kexinit.kex_algorithms, Server#ssh_msg_kexinit.kex_algorithms), - Alg = #alg{kex = Kex, - hkey = HK, - encrypt = Encrypt, - decrypt = Decrypt, - send_mac = SendMac, - recv_mac = RecvMac, - compress = Compression, - decompress = Decompression, - c_lng = C_Lng, - s_lng = S_Lng}, - {ok, Alg}. + SendExtInfo = + %% To send we must have that option enabled and ... + ?GET_OPT(send_ext_info,Opts) andalso + %% ... the peer must have told us to send: + case Role of + server -> lists:member("ext-info-c", Client#ssh_msg_kexinit.kex_algorithms); + client -> lists:member("ext-info-s", Server#ssh_msg_kexinit.kex_algorithms) + end, + + RecvExtInfo = + %% The peer should not send unless told so by us (which is + %% guided by an option). + %% (However a malicious peer could send anyway, so we must be prepared) + ?GET_OPT(recv_ext_info,Opts), + + {ok, #alg{kex = Kex, + hkey = HK, + encrypt = Encrypt, + decrypt = Decrypt, + send_mac = SendMac, + recv_mac = RecvMac, + compress = Compression, + decompress = Decompression, + c_lng = C_Lng, + s_lng = S_Lng, + send_ext_info = SendExtInfo, + recv_ext_info = RecvExtInfo + }}. %%% It is an agreed problem with RFC 5674 that if the selection is @@ -928,45 +971,66 @@ select_compression_decompression(server, Client, Server) -> Server#ssh_msg_kexinit.compression_algorithms_server_to_client), {Compression, Decompression}. -install_alg(SSH) -> - SSH1 = alg_final(SSH), - SSH2 = alg_setup(SSH1), - alg_init(SSH2). +%% DIr = rcv | snd +install_alg(Dir, SSH) -> + SSH1 = alg_final(Dir, SSH), + SSH2 = alg_setup(Dir, SSH1), + alg_init(Dir, SSH2). -alg_setup(SSH) -> +alg_setup(snd, SSH) -> ALG = SSH#ssh.algorithms, SSH#ssh{kex = ALG#alg.kex, hkey = ALG#alg.hkey, encrypt = ALG#alg.encrypt, - decrypt = ALG#alg.decrypt, send_mac = ALG#alg.send_mac, send_mac_size = mac_digest_size(ALG#alg.send_mac), + compress = ALG#alg.compress, + c_lng = ALG#alg.c_lng, + s_lng = ALG#alg.s_lng, + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info + }; + +alg_setup(rcv, SSH) -> + ALG = SSH#ssh.algorithms, + SSH#ssh{kex = ALG#alg.kex, + hkey = ALG#alg.hkey, + decrypt = ALG#alg.decrypt, recv_mac = ALG#alg.recv_mac, recv_mac_size = mac_digest_size(ALG#alg.recv_mac), - compress = ALG#alg.compress, decompress = ALG#alg.decompress, c_lng = ALG#alg.c_lng, s_lng = ALG#alg.s_lng, - algorithms = undefined + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info }. -alg_init(SSH0) -> + +alg_init(snd, SSH0) -> {ok,SSH1} = send_mac_init(SSH0), - {ok,SSH2} = recv_mac_init(SSH1), - {ok,SSH3} = encrypt_init(SSH2), - {ok,SSH4} = decrypt_init(SSH3), - {ok,SSH5} = compress_init(SSH4), - {ok,SSH6} = decompress_init(SSH5), - SSH6. - -alg_final(SSH0) -> + {ok,SSH2} = encrypt_init(SSH1), + {ok,SSH3} = compress_init(SSH2), + SSH3; + +alg_init(rcv, SSH0) -> + {ok,SSH1} = recv_mac_init(SSH0), + {ok,SSH2} = decrypt_init(SSH1), + {ok,SSH3} = decompress_init(SSH2), + SSH3. + + +alg_final(snd, SSH0) -> {ok,SSH1} = send_mac_final(SSH0), - {ok,SSH2} = recv_mac_final(SSH1), - {ok,SSH3} = encrypt_final(SSH2), - {ok,SSH4} = decrypt_final(SSH3), - {ok,SSH5} = compress_final(SSH4), - {ok,SSH6} = decompress_final(SSH5), - SSH6. + {ok,SSH2} = encrypt_final(SSH1), + {ok,SSH3} = compress_final(SSH2), + SSH3; + +alg_final(rcv, SSH0) -> + {ok,SSH1} = recv_mac_final(SSH0), + {ok,SSH2} = decrypt_final(SSH1), + {ok,SSH3} = decompress_final(SSH2), + SSH3. + select_all(CL, SL) when length(CL) + length(SL) < ?MAX_NUM_ALGORITHMS -> A = CL -- SL, %% algortihms only used by client @@ -1130,29 +1194,37 @@ payload(<<PacketLen:32, PaddingLen:8, PayloadAndPadding/binary>>) -> <<Payload:PayloadLen/binary, _/binary>> = PayloadAndPadding, Payload. -sign(SigData, Hash, #'DSAPrivateKey'{} = Key) -> - DerSignature = public_key:sign(SigData, Hash, Key), +sign(SigData, HashAlg, #'DSAPrivateKey'{} = Key) -> + DerSignature = public_key:sign(SigData, HashAlg, Key), #'Dss-Sig-Value'{r = R, s = S} = public_key:der_decode('Dss-Sig-Value', DerSignature), <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>>; -sign(SigData, Hash, Key = #'ECPrivateKey'{}) -> - DerEncodedSign = public_key:sign(SigData, Hash, Key), +sign(SigData, HashAlg, Key = #'ECPrivateKey'{}) -> + DerEncodedSign = public_key:sign(SigData, HashAlg, Key), #'ECDSA-Sig-Value'{r=R, s=S} = public_key:der_decode('ECDSA-Sig-Value', DerEncodedSign), <<?Empint(R),?Empint(S)>>; -sign(SigData, Hash, Key) -> - public_key:sign(SigData, Hash, Key). - -verify(PlainText, Hash, Sig, {_, #'Dss-Parms'{}} = Key) -> - <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>> = Sig, - Signature = public_key:der_encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}), - public_key:verify(PlainText, Hash, Signature, Key); -verify(PlainText, Hash, Sig, {#'ECPoint'{},_} = Key) -> - <<?UINT32(Rlen),R:Rlen/big-signed-integer-unit:8, - ?UINT32(Slen),S:Slen/big-signed-integer-unit:8>> = Sig, - Sval = #'ECDSA-Sig-Value'{r=R, s=S}, - DerEncodedSig = public_key:der_encode('ECDSA-Sig-Value',Sval), - public_key:verify(PlainText, Hash, DerEncodedSig, Key); -verify(PlainText, Hash, Sig, Key) -> - public_key:verify(PlainText, Hash, Sig, Key). +sign(SigData, HashAlg, Key) -> + public_key:sign(SigData, HashAlg, Key). + +verify(PlainText, HashAlg, Sig, {_, #'Dss-Parms'{}} = Key) -> + case Sig of + <<R:160/big-unsigned-integer, S:160/big-unsigned-integer>> -> + Signature = public_key:der_encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}), + public_key:verify(PlainText, HashAlg, Signature, Key); + _ -> + false + end; +verify(PlainText, HashAlg, Sig, {#'ECPoint'{},_} = Key) -> + case Sig of + <<?UINT32(Rlen),R:Rlen/big-signed-integer-unit:8, + ?UINT32(Slen),S:Slen/big-signed-integer-unit:8>> -> + Sval = #'ECDSA-Sig-Value'{r=R, s=S}, + DerEncodedSig = public_key:der_encode('ECDSA-Sig-Value',Sval), + public_key:verify(PlainText, HashAlg, DerEncodedSig, Key); + _ -> + false + end; +verify(PlainText, HashAlg, Sig, Key) -> + public_key:verify(PlainText, HashAlg, Sig, Key). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -1659,39 +1731,63 @@ hash(K, H, Ki, N, HashAlg) -> hash(K, H, <<Ki/binary, Kj/binary>>, N-128, HashAlg). %%%---------------------------------------------------------------- -kex_h(SSH, Key, E, F, K) -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), - L = <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(E), ?Empint(F), ?Empint(K)>>, - crypto:hash(sha((SSH#ssh.algorithms)#alg.kex), L). - -kex_h(SSH, Curve, Key, Q_c, Q_s, K) -> - KeyBin = public_key:ssh_encode(Key, ssh2_pubkey), - L = <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(Q_c), ?Empint(Q_s), ?Empint(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 -> - %% flag from 'ssh_msg_kex_dh_gex_request_old' - %% It was like this before that message was supported, - %% why? - <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Empint(E), ?Empint(F), ?Empint(K)>>; - true -> - <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), - ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), ?Ebinary(KeyBin), - ?Euint32(Min), ?Euint32(NBits), ?Euint32(Max), - ?Empint(Prime), ?Empint(Gen), ?Empint(E), ?Empint(F), ?Empint(K)>> - end, - crypto:hash(sha((SSH#ssh.algorithms)#alg.kex), L). - +kex_hash(SSH, Key, SignAlg, HashAlg, Args) -> + crypto:hash(HashAlg, kex_plaintext(SSH,Key,SignAlg,Args)). + +kex_plaintext(SSH, Key, SignAlg, Args) -> + EncodedKey = public_key:ssh_encode({Key,SignAlg}, ssh2_pubkey), + <<?Estring(SSH#ssh.c_version), ?Estring(SSH#ssh.s_version), + ?Ebinary(SSH#ssh.c_keyinit), ?Ebinary(SSH#ssh.s_keyinit), + ?Ebinary(EncodedKey), + (kex_alg_dependent(Args))/binary>>. + +kex_alg_dependent({E, F, K}) -> + %% diffie-hellman and ec diffie-hellman (with E = Q_c, F = Q_s) + <<?Empint(E), ?Empint(F), ?Empint(K)>>; + +kex_alg_dependent({-1, _, -1, _, _, E, F, K}) -> + %% ssh_msg_kex_dh_gex_request_old + <<?Empint(E), ?Empint(F), ?Empint(K)>>; + +kex_alg_dependent({Min, NBits, Max, Prime, Gen, E, F, K}) -> + %% diffie-hellman group exchange + <<?Euint32(Min), ?Euint32(NBits), ?Euint32(Max), + ?Empint(Prime), ?Empint(Gen), ?Empint(E), ?Empint(F), ?Empint(K)>>. + +%%%---------------------------------------------------------------- + +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-512') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-384') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'rsa-sha2-256') -> true; +valid_key_sha_alg(#'RSAPublicKey'{}, 'ssh-rsa' ) -> true; + +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-512') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-384') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'rsa-sha2-256') -> true; +valid_key_sha_alg(#'RSAPrivateKey'{}, 'ssh-rsa' ) -> true; + +valid_key_sha_alg({_, #'Dss-Parms'{}}, 'ssh-dss') -> true; +valid_key_sha_alg(#'DSAPrivateKey'{}, 'ssh-dss') -> true; + +valid_key_sha_alg({#'ECPoint'{},{namedCurve,OID}}, Alg) -> sha(OID) == sha(Alg); +valid_key_sha_alg(#'ECPrivateKey'{parameters = {namedCurve,OID}}, Alg) -> sha(OID) == sha(Alg); +valid_key_sha_alg(_, _) -> false. + + + +public_algo(#'RSAPublicKey'{}) -> 'ssh-rsa'; % FIXME: Not right with draft-curdle-rsa-sha2 +public_algo({_, #'Dss-Parms'{}}) -> 'ssh-dss'; +public_algo({#'ECPoint'{},{namedCurve,OID}}) -> + Curve = public_key:oid2ssh_curvename(OID), + list_to_atom("ecdsa-sha2-" ++ binary_to_list(Curve)). + + + sha('ssh-rsa') -> sha; +sha('rsa-sha2-256') -> sha256; +sha('rsa-sha2-384') -> sha384; +sha('rsa-sha2-512') -> sha512; sha('ssh-dss') -> sha; sha('ecdsa-sha2-nistp256') -> sha(secp256r1); sha('ecdsa-sha2-nistp384') -> sha(secp384r1); @@ -1711,7 +1807,8 @@ sha(?'secp384r1') -> sha(secp384r1); sha(?'secp521r1') -> sha(secp521r1); sha('ecdh-sha2-nistp256') -> sha(secp256r1); sha('ecdh-sha2-nistp384') -> sha(secp384r1); -sha('ecdh-sha2-nistp521') -> sha(secp521r1). +sha('ecdh-sha2-nistp521') -> sha(secp521r1); +sha(Str) when is_list(Str), length(Str)<50 -> sha(list_to_atom(Str)). mac_key_bytes('hmac-sha1') -> 20; diff --git a/lib/ssh/src/ssh_transport.hrl b/lib/ssh/src/ssh_transport.hrl index 19b3f5c437..faae6008f2 100644 --- a/lib/ssh/src/ssh_transport.hrl +++ b/lib/ssh/src/ssh_transport.hrl @@ -48,6 +48,7 @@ -define(SSH_MSG_DEBUG, 4). -define(SSH_MSG_SERVICE_REQUEST, 5). -define(SSH_MSG_SERVICE_ACCEPT, 6). +-define(SSH_MSG_EXT_INFO, 7). -define(SSH_MSG_KEXINIT, 20). -define(SSH_MSG_NEWKEYS, 21). @@ -88,6 +89,20 @@ name %% string }). +-record(ssh_msg_ext_info, + { + nr_extensions, %% uint32 + + %% repeat the following 2 fields "nr-extensions" times: + %% string extension-name + %% string extension-value + + data %% [{extension-name, %% string + %% extension-value}, %% string + %% ... + %% ] + }). + -record(ssh_msg_kexinit, { cookie, %% random(16) diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl index 089d191fea..c271ff49ef 100644 --- a/lib/ssh/test/ssh_basic_SUITE.erl +++ b/lib/ssh/test/ssh_basic_SUITE.erl @@ -651,6 +651,7 @@ exec_key_differs_fail(Config) when is_list(Config) -> IO = ssh_test_lib:start_io_server(), ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}, + {recv_ext_info, false}, {preferred_algorithms,[{public_key,['ssh-rsa']}]}, {pref_public_key_algs,['ssh-dss']}]), receive @@ -1366,13 +1367,25 @@ new_do_shell(IO, N, Ops=[{Order,Arg}|More]) -> ct:log("Skip newline ~p",[_X]), new_do_shell(IO, N, Ops); - <<Pfx:PfxSize/binary,P1,"> ">> when (P1-$0)==N -> + <<P1,"> ">> when (P1-$0)==N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,"> ">> when (P1-$0)==N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,"> ">> when (P1-$0)==N -> new_do_shell_prompt(IO, N, Order, Arg, More); - <<Pfx:PfxSize/binary,P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + <<P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,P2,"> ">> when (P1-$0)*10 + (P2-$0) == N -> new_do_shell_prompt(IO, N, Order, Arg, More); - <<Pfx:PfxSize/binary,P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + <<P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"(",Pfx:PfxSize/binary,")",P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> + new_do_shell_prompt(IO, N, Order, Arg, More); + <<"('",Pfx:PfxSize/binary,"')",P1,P2,P3,"> ">> when (P1-$0)*100 + (P2-$0)*10 + (P3-$0) == N -> new_do_shell_prompt(IO, N, Order, Arg, More); Err when element(1,Err)==error -> @@ -1408,7 +1421,7 @@ prompt_prefix() -> case node() of nonode@nohost -> <<>>; Node -> list_to_binary( - lists:concat(["(",Node,")"])) + atom_to_list(Node)) end. diff --git a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl index 2c4fa8be88..9e7d1a5fa3 100644 --- a/lib/ssh/test/ssh_protocol_SUITE.erl +++ b/lib/ssh/test/ssh_protocol_SUITE.erl @@ -752,6 +752,7 @@ connect_and_kex(Config, InitialState) -> {cipher,?DEFAULT_CIPHERS} ]}, {silently_accept_hosts, true}, + {recv_ext_info, false}, {user_dir, user_dir(Config)}, {user_interaction, false}]}, receive_hello, diff --git a/lib/ssh/test/ssh_relay.erl b/lib/ssh/test/ssh_relay.erl index 1e3810e9d4..763130358b 100644 --- a/lib/ssh/test/ssh_relay.erl +++ b/lib/ssh/test/ssh_relay.erl @@ -242,11 +242,11 @@ handle_info(stop, State) -> {stop, normal, State}; handle_info({'DOWN', _Ref, _process, LPid, Reason}, S) when S#state.lpid == LPid -> - io:format("Acceptor has finished: ~p~n", [Reason]), + io:format("Acceptor in ~p has finished: ~p~n", [?MODULE,Reason]), {noreply, S}; handle_info(_Info, State) -> - io:format("Unhandled info: ~p~n", [_Info]), + io:format("~p:~p Unhandled info: ~p~n", [?MODULE,?LINE,_Info]), {noreply, State}. %%-------------------------------------------------------------------- diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl index 6186d44890..ded47ca4f6 100644 --- a/lib/ssh/test/ssh_test_lib.erl +++ b/lib/ssh/test/ssh_test_lib.erl @@ -39,8 +39,9 @@ connect(Port, Options) when is_integer(Port) -> connect(any, Port, Options) -> connect(hostname(), Port, Options); connect(Host, Port, Options) -> - ct:log("~p:~p Calling ssh:connect(~p, ~p, ~p)",[?MODULE,?LINE,Host, Port, Options]), - {ok, ConnectionRef} = ssh:connect(Host, Port, Options), + R = ssh:connect(Host, Port, Options), + ct:log("~p:~p ssh:connect(~p, ~p, ~p)~n -> ~p",[?MODULE,?LINE,Host, Port, Options, R]), + {ok, ConnectionRef} = R, ConnectionRef. %%%---------------------------------------------------------------- @@ -858,8 +859,9 @@ get_kex_init(Conn) -> get_kex_init(Conn, Ref, TRef) -> %% First, validate the key exchange is complete (StateName == connected) - case sys:get_state(Conn) of - {{connected,_}, S} -> + {State, S} = sys:get_state(Conn), + case expected_state(State) of + true -> timer:cancel(TRef), %% Next, walk through the elements of the #state record looking %% for the #ssh_msg_kexinit record. This method is robust against @@ -873,8 +875,8 @@ get_kex_init(Conn, Ref, TRef) -> KexInit end; - {OtherState, S} -> - ct:log("Not in 'connected' state: ~p",[OtherState]), + false -> + ct:log("Not in 'connected' state: ~p",[State]), receive {reneg_timeout,Ref} -> ct:log("S = ~p", [S]), @@ -886,6 +888,10 @@ get_kex_init(Conn, Ref, TRef) -> end end. +expected_state({ext_info,_,_}) -> true; +expected_state({connected,_}) -> true; +expected_state(_) -> false. + %%%---------------------------------------------------------------- %%% Return a string with N random characters %%% diff --git a/lib/ssh/test/ssh_to_openssh_SUITE.erl b/lib/ssh/test/ssh_to_openssh_SUITE.erl index 35e3ee3edf..6b3055ebab 100644 --- a/lib/ssh/test/ssh_to_openssh_SUITE.erl +++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl @@ -153,7 +153,7 @@ erlang_shell_client_openssh_server(Config) when is_list(Config) -> IO = ssh_test_lib:start_io_server(), Shell = ssh_test_lib:start_shell(?SSH_DEFAULT_PORT, IO), IO ! {input, self(), "echo Hej\n"}, - receive_data("Hej"), + receive_data("Hej", undefined), IO ! {input, self(), "exit\n"}, receive_logout(), receive_normal_exit(Shell). @@ -451,7 +451,6 @@ erlang_server_openssh_client_renegotiate(Config) -> %%-------------------------------------------------------------------- erlang_client_openssh_server_renegotiate(_Config) -> process_flag(trap_exit, true), - IO = ssh_test_lib:start_io_server(), Ref = make_ref(), Parent = self(), @@ -487,11 +486,11 @@ erlang_client_openssh_server_renegotiate(_Config) -> ct:fail("Error=~p",[Error]); {ok, Ref, ConnectionRef} -> IO ! {input, self(), "echo Hej1\n"}, - receive_data("Hej1"), + receive_data("Hej1", ConnectionRef), Kex1 = ssh_test_lib:get_kex_init(ConnectionRef), ssh_connection_handler:renegotiate(ConnectionRef), IO ! {input, self(), "echo Hej2\n"}, - receive_data("Hej2"), + receive_data("Hej2", ConnectionRef), Kex2 = ssh_test_lib:get_kex_init(ConnectionRef), IO ! {input, self(), "exit\n"}, receive_logout(), @@ -554,23 +553,29 @@ erlang_client_openssh_server_nonexistent_subsystem(Config) when is_list(Config) %%-------------------------------------------------------------------- %%% Internal functions ----------------------------------------------- %%-------------------------------------------------------------------- -receive_data(Data) -> +receive_data(Data, Conn) -> receive Info when is_binary(Info) -> Lines = string:tokens(binary_to_list(Info), "\r\n "), case lists:member(Data, Lines) of true -> - ct:log("Expected result found in lines: ~p~n", [Lines]), + ct:log("Expected result ~p found in lines: ~p~n", [Data,Lines]), ok; false -> ct:log("Extra info: ~p~n", [Info]), - receive_data(Data) + receive_data(Data, Conn) end; Other -> ct:log("Unexpected: ~p",[Other]), - receive_data(Data) - after - 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) + receive_data(Data, Conn) + after + 30000 -> + {State, _} = case Conn of + undefined -> {'??','??'}; + _ -> sys:get_state(Conn) + end, + ct:log("timeout ~p:~p~nExpect ~p~nState = ~p",[?MODULE,?LINE,Data,State]), + ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. receive_logout() -> diff --git a/lib/ssh/test/ssh_trpt_test_lib.erl b/lib/ssh/test/ssh_trpt_test_lib.erl index e1f4c65300..781889ddd1 100644 --- a/lib/ssh/test/ssh_trpt_test_lib.erl +++ b/lib/ssh/test/ssh_trpt_test_lib.erl @@ -397,6 +397,12 @@ send(S0, {special,Msg,PacketFun}) when is_tuple(Msg), send_bytes(Packet, S#s{ssh = C, %%inc_send_seq_num(C), return_value = Msg}); +send(S0, #ssh_msg_newkeys{} = Msg) -> + S = opt(print_messages, S0, + fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end), + {ok, Packet, C} = ssh_transport:new_keys_message(S#s.ssh), + send_bytes(Packet, S#s{ssh = C}); + send(S0, Msg) when is_tuple(Msg) -> S = opt(print_messages, S0, fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end), @@ -455,7 +461,10 @@ recv(S0 = #s{}) -> }; #ssh_msg_kexdh_reply{} -> {ok, _NewKeys, C} = ssh_transport:handle_kexdh_reply(PeerMsg, S#s.ssh), - S#s{ssh=C#ssh{send_sequence=S#s.ssh#ssh.send_sequence}}; % Back the number + S#s{ssh = (S#s.ssh)#ssh{shared_secret = C#ssh.shared_secret, + exchanged_hash = C#ssh.exchanged_hash, + session_id = C#ssh.session_id}}; + %%%S#s{ssh=C#ssh{send_sequence=S#s.ssh#ssh.send_sequence}}; % Back the number #ssh_msg_newkeys{} -> {ok, C} = ssh_transport:handle_new_keys(PeerMsg, S#s.ssh), S#s{ssh=C}; diff --git a/lib/ssl/src/dtls_record.erl b/lib/ssl/src/dtls_record.erl index de6b6e400f..bc2097c021 100644 --- a/lib/ssl/src/dtls_record.erl +++ b/lib/ssl/src/dtls_record.erl @@ -534,8 +534,7 @@ calc_mac_hash(Type, Version, #{mac_secret := MacSecret, security_parameters := #security_parameters{mac_algorithm = MacAlg}}, Epoch, SeqNo, Fragment) -> Length = erlang:iolist_size(Fragment), - NewSeq = (Epoch bsl 48) + SeqNo, - mac_hash(Version, MacAlg, MacSecret, NewSeq, Type, + mac_hash(Version, MacAlg, MacSecret, Epoch, SeqNo, Type, Length, Fragment). highest_protocol_version() -> @@ -548,9 +547,11 @@ sufficient_dtlsv1_2_crypto_support() -> CryptoSupport = crypto:supports(), proplists:get_bool(sha256, proplists:get_value(hashs, CryptoSupport)). -mac_hash(Version, MacAlg, MacSecret, SeqNo, Type, Length, Fragment) -> - dtls_v1:mac_hash(Version, MacAlg, MacSecret, SeqNo, Type, - Length, Fragment). - +mac_hash({Major, Minor}, MacAlg, MacSecret, Epoch, SeqNo, Type, Length, Fragment) -> + Value = [<<?UINT16(Epoch), ?UINT48(SeqNo), ?BYTE(Type), + ?BYTE(Major), ?BYTE(Minor), ?UINT16(Length)>>, + Fragment], + dtls_v1:hmac_hash(MacAlg, MacSecret, Value). + calc_aad(Type, {MajVer, MinVer}, Epoch, SeqNo) -> <<?UINT16(Epoch), ?UINT48(SeqNo), ?BYTE(Type), ?BYTE(MajVer), ?BYTE(MinVer)>>. diff --git a/lib/ssl/src/dtls_v1.erl b/lib/ssl/src/dtls_v1.erl index 4aaf8baa6c..7f7223cde7 100644 --- a/lib/ssl/src/dtls_v1.erl +++ b/lib/ssl/src/dtls_v1.erl @@ -21,7 +21,7 @@ -include("ssl_cipher.hrl"). --export([suites/1, all_suites/1, mac_hash/7, ecc_curves/1, +-export([suites/1, all_suites/1, hmac_hash/3, ecc_curves/1, corresponding_tls_version/1, corresponding_dtls_version/1, cookie_secret/0, cookie_timeout/0]). @@ -40,9 +40,8 @@ all_suites(Version) -> end, ssl_cipher:all_suites(corresponding_tls_version(Version))). -mac_hash(Version, MacAlg, MacSecret, SeqNo, Type, Length, Fragment) -> - tls_v1:mac_hash(MacAlg, MacSecret, SeqNo, Type, Version, - Length, Fragment). +hmac_hash(MacAlg, MacSecret, Value) -> + tls_v1:hmac_hash(MacAlg, MacSecret, Value). ecc_curves({_Major, Minor}) -> tls_v1:ecc_curves(corresponding_minor_tls_version(Minor)). diff --git a/lib/ssl/src/tls_v1.erl b/lib/ssl/src/tls_v1.erl index f52ee06e71..bc487fdca4 100644 --- a/lib/ssl/src/tls_v1.erl +++ b/lib/ssl/src/tls_v1.erl @@ -29,7 +29,7 @@ -include("ssl_internal.hrl"). -include("ssl_record.hrl"). --export([master_secret/4, finished/5, certificate_verify/3, mac_hash/7, +-export([master_secret/4, finished/5, certificate_verify/3, mac_hash/7, hmac_hash/3, setup_keys/8, suites/1, prf/5, ecc_curves/1, ecc_curves/2, oid_to_enum/1, enum_to_oid/1, default_signature_algs/1, signature_algs/2]). @@ -221,11 +221,7 @@ suites(Minor) when Minor == 1; Minor == 2 -> ?TLS_RSA_WITH_3DES_EDE_CBC_SHA ]; suites(3) -> - [ - ?TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - ?TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - - ?TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + [?TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, ?TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, ?TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, ?TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, @@ -234,7 +230,10 @@ suites(3) -> ?TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384, ?TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384, + ?TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + ?TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, ?TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + ?TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, ?TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, ?TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, diff --git a/lib/stdlib/doc/src/gen_fsm.xml b/lib/stdlib/doc/src/gen_fsm.xml index e70f347479..7187630b43 100644 --- a/lib/stdlib/doc/src/gen_fsm.xml +++ b/lib/stdlib/doc/src/gen_fsm.xml @@ -29,7 +29,7 @@ <rev></rev> </header> <module>gen_fsm</module> - <modulesummary>Deprecated and replaced by <seealso marker="gen_statem"><c>gen_statem</c></seealso> </modulesummary> + <modulesummary>Deprecated and replaced by gen_statem </modulesummary> <description> <p> Deprecated and replaced by <seealso marker="gen_statem"><c>gen_statem</c></seealso> </p> diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 18089a8191..17a3a3c83c 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -4,7 +4,7 @@ <erlref> <header> <copyright> - <year>2016-2017</year> + <year>2016</year><year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -67,13 +67,16 @@ It has the same features and adds some really useful: </p> <list type="bulleted"> - <item>State code is gathered.</item> - <item>The state can be any term.</item> - <item>Events can be postponed.</item> - <item>Events can be self-generated.</item> - <item>Automatic state enter code can be called.</item> - <item>A reply can be sent from a later state.</item> - <item>There can be multiple <c>sys</c> traceable replies.</item> + <item>Gathered state code.</item> + <item>Arbitrary term state.</item> + <item>Event postponing.</item> + <item>Self-generated events.</item> + <item>State time-out.</item> + <item>Multiple generic named time-outs.</item> + <item>Absolute time-out time.</item> + <item>Automatic state enter calls.</item> + <item>Reply from other state than the request.</item> + <item>Multiple <c>sys</c> traceable replies.</item> </list> <p> The callback model(s) for <c>gen_statem</c> differs from @@ -148,7 +151,7 @@ erlang:'!' -----> Module:StateName/3 This gathers all code for a specific state in one function as the <c>gen_statem</c> engine branches depending on state name. - Notice the fact that there is a mandatory callback function + Note the fact that the callback function <seealso marker="#Module:terminate/3"><c>Module:terminate/3</c></seealso> makes the state name <c>terminate</c> unusable in this mode. </p> @@ -533,10 +536,12 @@ handle_event(_, _, State, Data) -> originate from the corresponding API functions. For calls, the event contains whom to reply to. Type <c>info</c> originates from regular process messages sent - to the <c>gen_statem</c>. Also, the state machine - implementation can generate events of types - <c>timeout</c>, <c>state_timeout</c>, - and <c>internal</c> to itself. + to the <c>gen_statem</c>. The state machine + implementation can, in addition to the above, + generate + <seealso marker="#type-event_type"><c>events of types</c></seealso> + <c>timeout</c>, <c>{timeout,<anno>Name</anno>}</c>, + <c>state_timeout</c>, and <c>internal</c> to itself. </p> </desc> </datatype> @@ -703,13 +708,14 @@ handle_event(_, _, State, Data) -> </item> <item> <p> - Timeout timers - <seealso marker="#type-state_timeout"><c>state_timeout()</c></seealso> + Time-out timers + <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso>, + <seealso marker="#type-generic_timeout"><c>generic_timeout()</c></seealso> and - <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso> + <seealso marker="#type-state_timeout"><c>state_timeout()</c></seealso> are handled. Time-outs with zero time are guaranteed to be delivered to the state machine before any external - not yet received event so if there is such a timeout requested, + not yet received event so if there is such a time-out requested, the corresponding time-out zero event is enqueued as the newest event. </p> @@ -787,49 +793,102 @@ handle_event(_, _, State, Data) -> <name name="event_timeout"/> <desc> <p> - Generates an event of + Starts a timer set by + <seealso marker="#type-enter_action"><c>enter_action()</c></seealso> + <c>timeout</c>. + When the timer expires an event of <seealso marker="#type-event_type"><c>event_type()</c></seealso> - <c>timeout</c> - after this time (in milliseconds) unless another - event arrives or has arrived - in which case this time-out is cancelled. + <c>timeout</c> will be generated. + See + <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer/4</c></seealso> + for how <c>Time</c> and + <seealso marker="#type-timeout_option"><c>Options</c></seealso> + are interpreted. Future <c>erlang:start_timer/4</c> <c>Options</c> + will not necessarily be supported. + </p> + <p> + Any event that arrives cancels this time-out. Note that a retried or inserted event counts as arrived. So does a state time-out zero event, if it was generated - before this timer is requested. + before this time-out is requested. </p> <p> - If the value is <c>infinity</c>, no timer is started, as - it never would trigger anyway. + If <c>Time</c> is <c>infinity</c>, + no timer is started, as it never would expire anyway. </p> <p> - If the value is <c>0</c> no timer is actually started, + If <c>Time</c> is relative and <c>0</c> + no timer is actually started, instead the the time-out event is enqueued to ensure that it gets processed before any not yet received external event. </p> <p> - Note that it is not possible or needed to cancel this time-out, + Note that it is not possible nor needed to cancel this time-out, as it is cancelled automatically by any other event. </p> </desc> </datatype> <datatype> + <name name="generic_timeout"/> + <desc> + <p> + Starts a timer set by + <seealso marker="#type-enter_action"><c>enter_action()</c></seealso> + <c>{timeout,Name}</c>. + When the timer expires an event of + <seealso marker="#type-event_type"><c>event_type()</c></seealso> + <c>{timeout,Name}</c> will be generated. + See + <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer/4</c></seealso> + for how <c>Time</c> and + <seealso marker="#type-timeout_option"><c>Options</c></seealso> + are interpreted. Future <c>erlang:start_timer/4</c> <c>Options</c> + will not necessarily be supported. + </p> + <p> + If <c>Time</c> is <c>infinity</c>, + no timer is started, as it never would expire anyway. + </p> + <p> + If <c>Time</c> is relative and <c>0</c> + no timer is actually started, + instead the the time-out event is enqueued to ensure + that it gets processed before any not yet + received external event. + </p> + <p> + Setting a timer with the same <c>Name</c> while it is running + will restart it with the new time-out value. + Therefore it is possible to cancel + a specific time-out by setting it to <c>infinity</c>. + </p> + </desc> + </datatype> + <datatype> <name name="state_timeout"/> <desc> <p> - Generates an event of + Starts a timer set by + <seealso marker="#type-enter_action"><c>enter_action()</c></seealso> + <c>state_timeout</c>. + When the timer expires an event of <seealso marker="#type-event_type"><c>event_type()</c></seealso> - <c>state_timeout</c> - after this time (in milliseconds) unless the <c>gen_statem</c> - changes states (<c>NewState =/= OldState</c>) - which case this time-out is cancelled. + <c>state_timeout</c> will be generated. + See + <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer/4</c></seealso> + for how <c>Time</c> and + <seealso marker="#type-timeout_option"><c>Options</c></seealso> + are interpreted. Future <c>erlang:start_timer/4</c> <c>Options</c> + will not necessarily be supported. </p> <p> - If the value is <c>infinity</c>, no timer is started, as - it never would trigger anyway. + If <c>Time</c> is <c>infinity</c>, + no timer is started, as it never would expire anyway. </p> <p> - If the value is <c>0</c> no timer is actually started, + If <c>Time</c> is relative and <c>0</c> + no timer is actually started, instead the the time-out event is enqueued to ensure that it gets processed before any not yet received external event. @@ -842,6 +901,20 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="timeout_option"/> + <desc> + <p> + If <c>Abs</c> is <c>true</c> an absolute timer is started, + and if it is <c>false</c> a relative, which is the default. + See + <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer/4</c></seealso> + for details. + </p> + <p> + </p> + </desc> + </datatype> + <datatype> <name name="action"/> <desc> <p> @@ -955,7 +1028,21 @@ handle_event(_, _, State, Data) -> Sets the <seealso marker="#type-transition_option"><c>transition_option()</c></seealso> <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso> - to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c>. + to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c> + and time-out options + <seealso marker="#type-timeout_option"><c><anno>Options</anno></c></seealso>. + </p> + </item> + <tag><c>{timeout,<anno>Name</anno>}</c></tag> + <item> + <p> + Sets the + <seealso marker="#type-transition_option"><c>transition_option()</c></seealso> + <seealso marker="#type-generic_timeout"><c>generic_timeout()</c></seealso> + to <c><anno>Time</anno></c> for <c><anno>Name</anno></c> + with <c><anno>EventContent</anno></c> + and time-out options + <seealso marker="#type-timeout_option"><c><anno>Options</anno></c></seealso>. </p> </item> <tag><c>state_timeout</c></tag> @@ -964,7 +1051,9 @@ handle_event(_, _, State, Data) -> Sets the <seealso marker="#type-transition_option"><c>transition_option()</c></seealso> <seealso marker="#type-state_timeout"><c>state_timeout()</c></seealso> - to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c>. + to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c> + and time-out options + <seealso marker="#type-timeout_option"><c><anno>Options</anno></c></seealso>. </p> </item> </taglist> @@ -1236,7 +1325,7 @@ handle_event(_, _, State, Data) -> to avoid that the calling process dies when the call times out, you will have to be prepared to handle a late reply. - So why not just allow the calling process to die? + So why not just let the calling process die? </p> </note> <p> @@ -1645,6 +1734,16 @@ handle_event(_, _, State, Data) -> <v>Reason = term()</v> </type> <desc> + <note> + <p> + This callback is optional, so callback modules need not export it. + If a release upgrade/downgrade with + <c>Change={advanced,Extra}</c> + specified in the <c>.appup</c> file is made + when <c>code_change/4</c> is not implemented + the process will crash with exit reason <c>undef</c>. + </p> + </note> <p> This function is called by a <c>gen_statem</c> when it is to update its internal state during a release upgrade/downgrade, @@ -1705,7 +1804,7 @@ handle_event(_, _, State, Data) -> <func> <name>Module:init(Args) -> Result(StateType)</name> <fsummary> - Optional function for initializing process and internal state. + Initializing process and internal state. </fsummary> <type> <v>Args = term()</v> @@ -1721,7 +1820,7 @@ handle_event(_, _, State, Data) -> <seealso marker="#start_link/3"><c>start_link/3,4</c></seealso> or <seealso marker="#start/3"><c>start/3,4</c></seealso>, - this optional function is called by the new process to initialize + this function is called by the new process to initialize the implementation state and server data. </p> <p> @@ -1730,13 +1829,16 @@ handle_event(_, _, State, Data) -> </p> <note> <p> - This callback is optional, so a callback module does not need - to export it, but most do. If this function is not exported, - the <c>gen_statem</c> should be started through + Note that if the <c>gen_statem</c> is started trough <seealso marker="proc_lib"><c>proc_lib</c></seealso> and - <seealso marker="#enter_loop/4"><c>enter_loop/4-6</c></seealso>. + <seealso marker="#enter_loop/4"><c>enter_loop/4-6</c></seealso>, + this callback will never be called. + Since this callback is not optional it can + in that case be implemented as: </p> + <pre> +init(Args) -> erlang:error(not_implemented, [Args]).</pre> </note> </desc> </func> diff --git a/lib/stdlib/doc/src/shell.xml b/lib/stdlib/doc/src/shell.xml index ab62c2fcdd..2593d3690b 100644 --- a/lib/stdlib/doc/src/shell.xml +++ b/lib/stdlib/doc/src/shell.xml @@ -569,7 +569,7 @@ Hello Number: 3378 <pre> 42> <input>E = ets:new(t, []).</input> -17</pre> +#Ref<0.1662103692.2407923716.214192></pre> <p>Command 42 creates an ETS table.</p> @@ -602,7 +602,7 @@ false</pre> <pre> 47> <input>E = ets:new(t, []).</input> -18 +#Ref<0.1662103692.2407923716.214197> 48> <input>ets:insert({d,1,2}).</input> * exception error: undefined function ets:insert/1</pre> @@ -617,10 +617,23 @@ true</pre> <p>Command 49 successfully inserts the tuple into the ETS table.</p> <pre> -50> <input>halt().</input> +50> <input>ets:insert(#Ref<0.1662103692.2407923716.214197>, {e,3,4}).</input> +true</pre> + + <p>Command 50 inserts another tuple into the ETS table. This time + the first argument is the table identifier itself. The shell can + parse commands with pids (<c><0.60.0></c>), ports + (<c>#Port<0.536></c>), references + (<c>#Ref<0.1662103692.2407792644.214210></c>), and external + functions (<c>#Fun<a.b.1></c>), but the command fails unless + the corresponding pid, port, reference, or function can be created + in the running system.</p> + + <pre> +51> <input>halt().</input> strider 2></pre> - <p>Command 50 exits the Erlang runtime system.</p> + <p>Command 51 exits the Erlang runtime system.</p> </section> <section> diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index cacc932ec4..6f566b8beb 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -78,8 +78,9 @@ -type data() :: term(). -type event_type() :: - {'call',From :: from()} | 'cast' | - 'info' | 'timeout' | 'state_timeout' | 'internal'. + {'call',From :: from()} | 'cast' | 'info' | + 'timeout' | {'timeout', Name :: term()} | 'state_timeout' | + 'internal'. -type callback_mode_result() :: callback_mode() | [callback_mode() | state_enter()]. @@ -88,7 +89,7 @@ -type transition_option() :: postpone() | hibernate() | - event_timeout() | state_timeout(). + event_timeout() | generic_timeout() | state_timeout(). -type postpone() :: %% If 'true' postpone the current event %% and retry it when the state changes (=/=) @@ -97,13 +98,17 @@ %% If 'true' hibernate the server instead of going into receive boolean(). -type event_timeout() :: - %% Generate a ('timeout', EventContent, ...) event after Time + %% Generate a ('timeout', EventContent, ...) event %% unless some other event is delivered - Time :: timeout(). + Time :: timeout() | integer(). +-type generic_timeout() :: + %% Generate a ({'timeout',Name}, EventContent, ...) event + Time :: timeout() | integer(). -type state_timeout() :: - %% Generate a ('state_timeout', EventContent, ...) event after Time + %% Generate a ('state_timeout', EventContent, ...) event %% unless the state is changed - Time :: timeout(). + Time :: timeout() | integer(). +-type timeout_option() :: {abs,Abs :: boolean()}. -type action() :: %% During a state change: @@ -137,8 +142,24 @@ (Timeout :: event_timeout()) | % {timeout,Timeout} {'timeout', % Set the event_timeout option Time :: event_timeout(), EventContent :: term()} | + {'timeout', % Set the event_timeout option + Time :: event_timeout(), + EventContent :: term(), + Options :: (timeout_option() | [timeout_option()])} | + %% + {{'timeout', Name :: term()}, % Set the generic_timeout option + Time :: generic_timeout(), EventContent :: term()} | + {{'timeout', Name :: term()}, % Set the generic_timeout option + Time :: generic_timeout(), + EventContent :: term(), + Options :: (timeout_option() | [timeout_option()])} | + %% {'state_timeout', % Set the state_timeout option Time :: state_timeout(), EventContent :: term()} | + {'state_timeout', % Set the state_timeout option + Time :: state_timeout(), + EventContent :: term(), + Options :: (timeout_option() | [timeout_option()])} | %% reply_action(). -type reply_action() :: @@ -287,8 +308,7 @@ StatusOption :: 'normal' | 'terminate'. -optional_callbacks( - [init/1, % One may use enter_loop/5,6,7 instead - format_status/2, % Has got a default implementation + [format_status/2, % Has got a default implementation terminate/3, % Has got a default implementation code_change/4, % Only needed by advanced soft upgrade %% @@ -303,37 +323,26 @@ %% Type validation functions callback_mode(CallbackMode) -> case CallbackMode of - state_functions -> - true; - handle_event_function -> - true; - _ -> - false + state_functions -> true; + handle_event_function -> true; + _ -> false end. %% -from({Pid,_}) when is_pid(Pid) -> - true; -from(_) -> - false. +from({Pid,_}) when is_pid(Pid) -> true; +from(_) -> false. %% event_type({call,From}) -> from(From); event_type(Type) -> case Type of - {call,From} -> - from(From); - cast -> - true; - info -> - true; - timeout -> - true; - state_timeout -> - true; - internal -> - true; - _ -> - false + {call,From} -> from(From); + cast -> true; + info -> true; + timeout -> true; + state_timeout -> true; + internal -> true; + {timeout,_} -> true; + _ -> false end. @@ -1313,7 +1322,7 @@ parse_enter_actions(Debug, S, State, Actions, Hibernate, TimeoutsR) -> parse_actions(Debug, S, State, Actions) -> Hibernate = false, - TimeoutsR = [{timeout,infinity,infinity}], %% Will cancel event timer + TimeoutsR = [infinity], %% Will cancel event timer Postpone = false, NextEventsR = [], parse_actions( @@ -1379,7 +1388,11 @@ parse_actions( ?STACKTRACE()} end; %% - {state_timeout,_,_} = Timeout -> + {{timeout,_},_,_} = Timeout -> + parse_actions_timeout( + Debug, S, State, Actions, + Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); + {{timeout,_},_,_,_} = Timeout -> parse_actions_timeout( Debug, S, State, Actions, Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); @@ -1387,6 +1400,18 @@ parse_actions( parse_actions_timeout( Debug, S, State, Actions, Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); + {timeout,_,_,_} = Timeout -> + parse_actions_timeout( + Debug, S, State, Actions, + Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); + {state_timeout,_,_} = Timeout -> + parse_actions_timeout( + Debug, S, State, Actions, + Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); + {state_timeout,_,_,_} = Timeout -> + parse_actions_timeout( + Debug, S, State, Actions, + Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout); Time -> parse_actions_timeout( Debug, S, State, Actions, @@ -1396,26 +1421,64 @@ parse_actions( parse_actions_timeout( Debug, S, State, Actions, Hibernate, TimeoutsR, Postpone, NextEventsR, Timeout) -> - Time = - case Timeout of - {_,T,_} -> T; - T -> T - end, - case validate_time(Time) of - true -> - parse_actions( - Debug, S, State, Actions, - Hibernate, [Timeout|TimeoutsR], - Postpone, NextEventsR); - false -> - {error, - {bad_action_from_state_function,Timeout}, - ?STACKTRACE()} + case Timeout of + {TimerType,Time,TimerMsg,TimerOpts} -> + case validate_timer_args(Time, listify(TimerOpts)) of + true -> + parse_actions( + Debug, S, State, Actions, + Hibernate, [Timeout|TimeoutsR], + Postpone, NextEventsR); + false -> + NewTimeout = {TimerType,Time,TimerMsg}, + parse_actions( + Debug, S, State, Actions, + Hibernate, [NewTimeout|TimeoutsR], + Postpone, NextEventsR); + error -> + {error, + {bad_action_from_state_function,Timeout}, + ?STACKTRACE()} + end; + {_,Time,_} -> + case validate_timer_args(Time, []) of + false -> + parse_actions( + Debug, S, State, Actions, + Hibernate, [Timeout|TimeoutsR], + Postpone, NextEventsR); + error -> + {error, + {bad_action_from_state_function,Timeout}, + ?STACKTRACE()} + end; + Time -> + case validate_timer_args(Time, []) of + false -> + parse_actions( + Debug, S, State, Actions, + Hibernate, [Timeout|TimeoutsR], + Postpone, NextEventsR); + error -> + {error, + {bad_action_from_state_function,Timeout}, + ?STACKTRACE()} + end end. -validate_time(Time) when is_integer(Time), Time >= 0 -> true; -validate_time(infinity) -> true; -validate_time(_) -> false. +validate_timer_args(Time, Opts) -> + validate_timer_args(Time, Opts, false). +%% +validate_timer_args(Time, [], true) when is_integer(Time) -> + true; +validate_timer_args(Time, [], false) when is_integer(Time), Time >= 0 -> + false; +validate_timer_args(infinity, [], Abs) -> + Abs; +validate_timer_args(Time, [{abs,Abs}|Opts], _) when is_boolean(Abs) -> + validate_timer_args(Time, Opts, Abs); +validate_timer_args(_, [_|_], _) -> + error. %% Stop and start timers as well as create timeout zero events %% and pending event timer @@ -1431,22 +1494,39 @@ parse_timers( TimerRefs, TimerTypes, CancelTimers, [Timeout|TimeoutsR], Seen, TimeoutEvents) -> case Timeout of + {TimerType,Time,TimerMsg,TimerOpts} -> + %% Absolute timer + parse_timers( + TimerRefs, TimerTypes, CancelTimers, TimeoutsR, + Seen, TimeoutEvents, + TimerType, Time, TimerMsg, listify(TimerOpts)); + %% Relative timers below + {TimerType,0,TimerMsg} -> + parse_timers( + TimerRefs, TimerTypes, CancelTimers, TimeoutsR, + Seen, TimeoutEvents, + TimerType, zero, TimerMsg, []); {TimerType,Time,TimerMsg} -> parse_timers( - TimerRefs, TimerTypes, CancelTimers, TimeoutsR, - Seen, TimeoutEvents, - TimerType, Time, TimerMsg); + TimerRefs, TimerTypes, CancelTimers, TimeoutsR, + Seen, TimeoutEvents, + TimerType, Time, TimerMsg, []); + 0 -> + parse_timers( + TimerRefs, TimerTypes, CancelTimers, TimeoutsR, + Seen, TimeoutEvents, + timeout, zero, 0, []); Time -> parse_timers( - TimerRefs, TimerTypes, CancelTimers, TimeoutsR, - Seen, TimeoutEvents, - timeout, Time, Time) + TimerRefs, TimerTypes, CancelTimers, TimeoutsR, + Seen, TimeoutEvents, + timeout, Time, Time, []) end. parse_timers( TimerRefs, TimerTypes, CancelTimers, TimeoutsR, Seen, TimeoutEvents, - TimerType, Time, TimerMsg) -> + TimerType, Time, TimerMsg, TimerOpts) -> case Seen of #{TimerType := _} -> %% Type seen before - ignore @@ -1465,7 +1545,7 @@ parse_timers( parse_timers( TimerRefs, NewTimerTypes, NewCancelTimers, TimeoutsR, NewSeen, TimeoutEvents); - 0 -> + zero -> %% Cancel any running timer {NewTimerTypes,NewCancelTimers} = cancel_timer_by_type( @@ -1478,7 +1558,8 @@ parse_timers( _ -> %% (Re)start the timer TimerRef = - erlang:start_timer(Time, self(), TimerMsg), + erlang:start_timer( + Time, self(), TimerMsg, TimerOpts), case TimerTypes of #{TimerType := OldTimerRef} -> %% Cancel the running timer @@ -1492,6 +1573,8 @@ parse_timers( NewCancelTimers, TimeoutsR, NewSeen, TimeoutEvents); #{} -> + %% Insert the new timer into + %% both TimerRefs and TimerTypes parse_timers( TimerRefs#{TimerRef => TimerType}, TimerTypes#{TimerType => TimerRef}, diff --git a/lib/stdlib/src/lib.erl b/lib/stdlib/src/lib.erl index 56654097d9..aa6797bce6 100644 --- a/lib/stdlib/src/lib.erl +++ b/lib/stdlib/src/lib.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2016. All Rights Reserved. +%% Copyright Ericsson AB 1996-2017. 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. @@ -22,6 +22,9 @@ -export([flush_receive/0, error_message/2, progname/0, nonl/1, send/2, sendw/2, eval_str/1]). +-export([extended_parse_exprs/1, extended_parse_term/1, + subst_values_for_vars/2]). + -export([format_exception/6, format_exception/7, format_stacktrace/4, format_stacktrace/5, format_call/4, format_call/5, format_fun/1]). @@ -127,6 +130,224 @@ all_white([$\t|T]) -> all_white(T); all_white([]) -> true; all_white(_) -> false. +%% `Tokens' is assumed to have been scanned with the 'text' option. +%% The annotations of the returned expressions are locations. +%% +%% Can handle pids, ports, references, and external funs ("items"). +%% Known items are represented by variables in the erl_parse tree, and +%% the items themselves are stored in the returned bindings. + +-spec extended_parse_exprs(Tokens) -> + {'ok', ExprList, Bindings} | {'error', ErrorInfo} when + Tokens :: [erl_scan:token()], + ExprList :: [erl_parse:abstract_expr()], + Bindings :: erl_eval:binding_struct(), + ErrorInfo :: erl_parse:error_info(). + +extended_parse_exprs(Tokens) -> + Ts = tokens_fixup(Tokens), + case erl_parse:parse_exprs(Ts) of + {ok, Exprs0} -> + {Exprs, Bs} = expr_fixup(Exprs0), + {ok, reset_expr_anno(Exprs), Bs}; + _ErrorInfo -> + erl_parse:parse_exprs(reset_token_anno(Ts)) + end. + +tokens_fixup([]) -> []; +tokens_fixup([T|Ts]=Ts0) -> + try token_fixup(Ts0) of + {NewT, NewTs} -> + [NewT|tokens_fixup(NewTs)] + catch + _:_ -> + [T|tokens_fixup(Ts)] + end. + +token_fixup(Ts) -> + {AnnoL, NewTs, FixupTag} = unscannable(Ts), + String = lists:append([erl_anno:text(A) || A <- AnnoL]), + _ = (fixup_fun(FixupTag))(String), + NewAnno = erl_anno:set_text(fixup_text(FixupTag), hd(AnnoL)), + {{string, NewAnno, String}, NewTs}. + +unscannable([{'#', A1}, {var, A2, 'Fun'}, {'<', A3}, {atom, A4, _}, + {'.', A5}, {float, A6, _}, {'>', A7}|Ts]) -> + {[A1, A2, A3, A4, A5, A6, A7], Ts, function}; +unscannable([{'#', A1}, {var, A2, 'Fun'}, {'<', A3}, {atom, A4, _}, + {'.', A5}, {atom, A6, _}, {'.', A7}, {integer, A8, _}, + {'>', A9}|Ts]) -> + {[A1, A2, A3, A4, A5, A6, A7, A8, A9], Ts, function}; +unscannable([{'<', A1}, {float, A2, _}, {'.', A3}, {integer, A4, _}, + {'>', A5}|Ts]) -> + {[A1, A2, A3, A4, A5], Ts, pid}; +unscannable([{'#', A1}, {var, A2, 'Port'}, {'<', A3}, {float, A4, _}, + {'>', A5}|Ts]) -> + {[A1, A2, A3, A4, A5], Ts, port}; +unscannable([{'#', A1}, {var, A2, 'Ref'}, {'<', A3}, {float, A4, _}, + {'.', A5}, {float, A6, _}, {'>', A7}|Ts]) -> + {[A1, A2, A3, A4, A5, A6, A7], Ts, reference}. + +expr_fixup(Expr0) -> + {Expr, Bs, _} = expr_fixup(Expr0, erl_eval:new_bindings(), 1), + {Expr, Bs}. + +expr_fixup({string,A,S}=T, Bs0, I) -> + try string_fixup(A, S) of + Value -> + Var = new_var(I), + Bs = erl_eval:add_binding(Var, Value, Bs0), + {{var, A, Var}, Bs, I+1} + catch + _:_ -> + {T, Bs0, I} + end; +expr_fixup(Tuple, Bs0, I0) when is_tuple(Tuple) -> + {L, Bs, I} = expr_fixup(tuple_to_list(Tuple), Bs0, I0), + {list_to_tuple(L), Bs, I}; +expr_fixup([E0|Es0], Bs0, I0) -> + {E, Bs1, I1} = expr_fixup(E0, Bs0, I0), + {Es, Bs, I} = expr_fixup(Es0, Bs1, I1), + {[E|Es], Bs, I}; +expr_fixup(T, Bs, I) -> + {T, Bs, I}. + +string_fixup(A, S) -> + Text = erl_anno:text(A), + FixupTag = fixup_tag(Text, S), + (fixup_fun(FixupTag))(S). + +new_var(I) -> + list_to_atom(lists:concat(['__ExtendedParseExprs_', I, '__'])). + +reset_token_anno(Tokens) -> + [setelement(2, T, (reset_anno())(element(2, T))) || T <- Tokens]. + +reset_expr_anno(Exprs) -> + [erl_parse:map_anno(reset_anno(), E) || E <- Exprs]. + +reset_anno() -> + fun(A) -> erl_anno:new(erl_anno:location(A)) end. + +fixup_fun(function) -> fun function/1; +fixup_fun(pid) -> fun erlang:list_to_pid/1; +fixup_fun(port) -> fun erlang:list_to_port/1; +fixup_fun(reference) -> fun erlang:list_to_ref/1. + +function(S) -> + %% External function. + {ok, [_, _, _, + {atom, _, Module}, _, + {atom, _, Function}, _, + {integer, _, Arity}|_], _} = erl_scan:string(S), + erlang:make_fun(Module, Function, Arity). + +fixup_text(function) -> "function"; +fixup_text(pid) -> "pid"; +fixup_text(port) -> "port"; +fixup_text(reference) -> "reference". + +fixup_tag("function", "#"++_) -> function; +fixup_tag("pid", "<"++_) -> pid; +fixup_tag("port", "#"++_) -> port; +fixup_tag("reference", "#"++_) -> reference. + +%%% End of extended_parse_exprs. + +%% `Tokens' is assumed to have been scanned with the 'text' option. +%% +%% Can handle pids, ports, references, and external funs. + +-spec extended_parse_term(Tokens) -> + {'ok', Term} | {'error', ErrorInfo} when + Tokens :: [erl_scan:token()], + Term :: term(), + ErrorInfo :: erl_parse:error_info(). + +extended_parse_term(Tokens) -> + case extended_parse_exprs(Tokens) of + {ok, [Expr], Bindings} -> + try normalise(Expr, Bindings) of + Term -> + {ok, Term} + catch + _:_ -> + Loc = erl_anno:location(element(2, Expr)), + {error,{Loc,?MODULE,"bad term"}} + end; + {ok, [_,Expr|_], _Bindings} -> + Loc = erl_anno:location(element(2, Expr)), + {error,{Loc,?MODULE,"bad term"}}; + {error, _} = Error -> + Error + end. + +%% From erl_parse. +normalise({var, _, V}, Bs) -> + {value, Value} = erl_eval:binding(V, Bs), + Value; +normalise({char,_,C}, _Bs) -> C; +normalise({integer,_,I}, _Bs) -> I; +normalise({float,_,F}, _Bs) -> F; +normalise({atom,_,A}, _Bs) -> A; +normalise({string,_,S}, _Bs) -> S; +normalise({nil,_}, _Bs) -> []; +normalise({bin,_,Fs}, Bs) -> + {value, B, _} = + eval_bits:expr_grp(Fs, [], + fun(E, _) -> + {value, normalise(E, Bs), []} + end, [], true), + B; +normalise({cons,_,Head,Tail}, Bs) -> + [normalise(Head, Bs)|normalise(Tail, Bs)]; +normalise({tuple,_,Args}, Bs) -> + list_to_tuple(normalise_list(Args, Bs)); +normalise({map,_,Pairs}, Bs) -> + maps:from_list(lists:map(fun + %% only allow '=>' + ({map_field_assoc,_,K,V}) -> + {normalise(K, Bs),normalise(V, Bs)} + end, Pairs)); +%% Special case for unary +/-. +normalise({op,_,'+',{char,_,I}}, _Bs) -> I; +normalise({op,_,'+',{integer,_,I}}, _Bs) -> I; +normalise({op,_,'+',{float,_,F}}, _Bs) -> F; +normalise({op,_,'-',{char,_,I}}, _Bs) -> -I; %Weird, but compatible! +normalise({op,_,'-',{integer,_,I}}, _Bs) -> -I; +normalise({op,_,'-',{float,_,F}}, _Bs) -> -F; +normalise({'fun',_,{function,{atom,_,M},{atom,_,F},{integer,_,A}}}, _Bs) -> + %% Since "#Fun<M.F.A>" is recognized, "fun M:F/A" should be too. + fun M:F/A. + +normalise_list([H|T], Bs) -> + [normalise(H, Bs)|normalise_list(T, Bs)]; +normalise_list([], _Bs) -> + []. + +%% To be used on ExprList and Bindings returned from extended_parse_exprs(). +%% Substitute {value, A, Item} for {var, A, ExtendedParseVar}. +%% {value, A, Item} is a shell/erl_eval convention, and for example +%% the linter cannot handle it. + +-spec subst_values_for_vars(ExprList, Bindings) -> [term()] when + ExprList :: [erl_parse:abstract_expr()], + Bindings :: erl_eval:binding_struct(). + +subst_values_for_vars({var, A, V}=Var, Bs) -> + case erl_eval:binding(V, Bs) of + {value, Value} -> + {value, A, Value}; + unbound -> + Var + end; +subst_values_for_vars(L, Bs) when is_list(L) -> + [subst_values_for_vars(E, Bs) || E <- L]; +subst_values_for_vars(T, Bs) when is_tuple(T) -> + list_to_tuple(subst_values_for_vars(tuple_to_list(T), Bs)); +subst_values_for_vars(T, _Bs) -> + T. + %%% Formatting of exceptions, mfa:s and funs. %% -> iolist() (no \n at end) diff --git a/lib/stdlib/src/qlc.erl b/lib/stdlib/src/qlc.erl index 8c4d835432..20aaa2638c 100644 --- a/lib/stdlib/src/qlc.erl +++ b/lib/stdlib/src/qlc.erl @@ -635,14 +635,25 @@ string_to_handle(Str, Options, Bindings) when is_list(Str) -> badarg -> erlang:error(badarg, [Str, Options, Bindings]); [Unique, Cache, MaxLookup, Join, Lookup] -> - case erl_scan:string(Str) of + case erl_scan:string(Str, 1, [text]) of {ok, Tokens, _} -> - case erl_parse:parse_exprs(Tokens) of - {ok, [Expr]} -> - case qlc_pt:transform_expression(Expr, Bindings) of + ScanRes = + case lib:extended_parse_exprs(Tokens) of + {ok, [Expr0], SBs} -> + {ok, Expr0, SBs}; + {ok, _ExprList, _SBs} -> + erlang:error(badarg, + [Str, Options, Bindings]); + E -> + E + end, + case ScanRes of + {ok, Expr, XBs} -> + Bs1 = merge_binding_structs(Bindings, XBs), + case qlc_pt:transform_expression(Expr, Bs1) of {ok, {call, _, _QlcQ, Handle}} -> {value, QLC_lc, _} = - erl_eval:exprs(Handle, Bindings), + erl_eval:exprs(Handle, Bs1), O = #qlc_opt{unique = Unique, cache = Cache, max_lookup = MaxLookup, @@ -652,8 +663,6 @@ string_to_handle(Str, Options, Bindings) when is_list(Str) -> {not_ok, [{error, Error} | _]} -> error(Error) end; - {ok, _ExprList} -> - erlang:error(badarg, [Str, Options, Bindings]); {error, ErrorInfo} -> error(ErrorInfo) end; @@ -770,6 +779,10 @@ all_selections([{I,Cs} | ICs]) -> %%% Local functions %%% +merge_binding_structs(Bs1, Bs2) -> + lists:foldl(fun({N, V}, Bs) -> erl_eval:add_binding(N, V, Bs) + end, Bs1, erl_eval:bindings(Bs2)). + aux_name1(Name, N, AllNames) -> SN = name_suffix(Name, N), case sets:is_element(SN, AllNames) of @@ -1180,9 +1193,12 @@ abstract1({table, {M, F, As0}}, _NElements, _Depth, Anno) abstract1({table, TableDesc}, _NElements, _Depth, _A) -> case io_lib:deep_char_list(TableDesc) of true -> - {ok, Tokens, _} = erl_scan:string(lists:flatten(TableDesc++".")), - {ok, [Expr]} = erl_parse:parse_exprs(Tokens), - Expr; + {ok, Tokens, _} = + erl_scan:string(lists:flatten(TableDesc++"."), 1, [text]), + {ok, Es, Bs} = + lib:extended_parse_exprs(Tokens), + [Expr] = lib:subst_values_for_vars(Es, Bs), + special(Expr); false -> % abstract expression TableDesc end; @@ -1210,6 +1226,15 @@ abstract1({list, L}, NElements, Depth, _A) when NElements =:= infinity; abstract1({list, L}, NElements, Depth, _A) -> abstract_term(depth(lists:sublist(L, NElements), Depth) ++ '...', 1). +special({value, _, Thing}) -> + abstract_term(Thing); +special(Tuple) when is_tuple(Tuple) -> + list_to_tuple(special(tuple_to_list(Tuple))); +special([E|Es]) -> + [special(E)|special(Es)]; +special(Expr) -> + Expr. + depth(List, infinity) -> List; depth(List, Depth) -> diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index 961f5f8a30..76a2789406 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -229,8 +229,9 @@ server_loop(N0, Eval_0, Bs00, RT, Ds00, History0, Results0) -> {Eval_1,Bs0,Ds0,Prompt} = prompt(N, Eval_0, Bs00, RT, Ds00), {Res,Eval0} = get_command(Prompt, Eval_1, Bs0, RT, Ds0), case Res of - {ok,Es0} -> - case expand_hist(Es0, N) of + {ok,Es0,XBs} -> + Es1 = lib:subst_values_for_vars(Es0, XBs), + case expand_hist(Es1, N) of {ok,Es} -> {V,Eval,Bs,Ds} = shell_cmd(Es, Eval0, Bs0, RT, Ds0, cmd), {History,Results} = check_and_get_history_and_results(), @@ -276,10 +277,10 @@ get_command(Prompt, Eval, Bs, RT, Ds) -> fun() -> exit( case - io:scan_erl_exprs(group_leader(), Prompt, 1) + io:scan_erl_exprs(group_leader(), Prompt, 1, [text]) of {ok,Toks,_EndPos} -> - erl_parse:parse_exprs(Toks); + lib:extended_parse_exprs(Toks); {eof,_EndPos} -> eof; {error,ErrorInfo,_EndPos} -> diff --git a/lib/stdlib/test/erl_internal_SUITE.erl b/lib/stdlib/test/erl_internal_SUITE.erl index 099f21f905..789a9d4363 100644 --- a/lib/stdlib/test/erl_internal_SUITE.erl +++ b/lib/stdlib/test/erl_internal_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1999-2016. All Rights Reserved. +%% Copyright Ericsson AB 1999-2017. 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. @@ -60,7 +60,7 @@ end_per_testcase(_Case, _Config) -> %% Check that the behaviour callbacks are correctly defined. behav(_) -> Modules = [application, gen_server, gen_fsm, gen_event, - supervisor_bridge, supervisor], + gen_statem, supervisor_bridge, supervisor], lists:foreach(fun check_behav/1, Modules). check_behav(Module) -> @@ -89,6 +89,10 @@ callbacks(gen_event) -> [{init,1}, {handle_event,2}, {handle_call,2}, {handle_info,2}, {terminate,2}, {code_change,3}, {format_status,2}]; +callbacks(gen_statem) -> + [{init, 1}, {callback_mode, 0}, {state_name, 3}, + {handle_event, 4}, {terminate, 3}, {code_change, 4}, + {format_status, 2}]; callbacks(supervisor_bridge) -> [{init,1}, {terminate,2}]; callbacks(supervisor) -> @@ -102,6 +106,9 @@ optional_callbacks(gen_fsm) -> [{handle_info, 3}, {terminate, 3}, {code_change, 4}, {format_status, 2}]; optional_callbacks(gen_event) -> [{handle_info, 2}, {terminate, 2}, {code_change, 3}, {format_status, 2}]; +optional_callbacks(gen_statem) -> + [{state_name, 3}, {handle_event, 4}, + {terminate, 3}, {code_change, 4}, {format_status, 2}]; optional_callbacks(supervisor_bridge) -> []; optional_callbacks(supervisor) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index ac27c9fc79..05934b3953 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2016. All Rights Reserved. +%% Copyright Ericsson AB 2016-2017. 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. @@ -38,9 +38,10 @@ all() -> {group, abnormal}, {group, abnormal_handle_event}, shutdown, stop_and_reply, state_enter, event_order, - state_timeout, event_types, code_change, + state_timeout, event_types, generic_timers, code_change, {group, sys}, - hibernate, enter_loop]. + hibernate, enter_loop, {group, undef_callbacks}, + undef_in_terminate]. groups() -> [{start, [], tcs(start)}, @@ -50,7 +51,8 @@ groups() -> {abnormal, [], tcs(abnormal)}, {abnormal_handle_event, [], tcs(abnormal)}, {sys, [], tcs(sys)}, - {sys_handle_event, [], tcs(sys)}]. + {sys_handle_event, [], tcs(sys)}, + {undef_callbacks, [], tcs(undef_callbacks)}]. tcs(start) -> [start1, start2, start3, start4, start5, start6, start7, @@ -62,8 +64,9 @@ tcs(abnormal) -> tcs(sys) -> [sys1, call_format_status, error_format_status, terminate_crash_format, - get_state, replace_state]. - + get_state, replace_state]; +tcs(undef_callbacks) -> + [undef_code_change, undef_terminate1, undef_terminate2]. init_per_suite(Config) -> Config. @@ -77,6 +80,11 @@ init_per_group(GroupName, Config) GroupName =:= abnormal_handle_event; GroupName =:= sys_handle_event -> [{callback_mode,handle_event_function}|Config]; +init_per_group(undef_callbacks, Config) -> + DataDir = ?config(data_dir, Config), + StatemPath = filename:join(DataDir, "oc_statem.erl"), + {ok, oc_statem} = compile:file(StatemPath), + Config; init_per_group(_GroupName, Config) -> Config. @@ -834,6 +842,7 @@ event_types(_Config) -> {next_event,timeout,3}, {next_event,info,4}, {next_event,cast,5}, + {next_event,{timeout,6}, 6}, {next_event,Call,Req}]} end, state1 => @@ -857,6 +866,10 @@ event_types(_Config) -> {next_state, state6, undefined} end, state6 => + fun ({timeout,6}, 6, undefined) -> + {next_state, state7, undefined} + end, + state7 => fun ({call,From}, stop, undefined) -> {stop_and_reply, shutdown, [{reply,From,stopped}]} @@ -884,6 +897,69 @@ event_types(_Config) -> +generic_timers(_Config) -> + process_flag(trap_exit, true), + + Machine = + %% Abusing the internal format of From... + #{init => + fun () -> + {ok, start, undefined} + end, + start => + fun ({call,_} = Call, Req, undefined) -> + {next_state, state1, undefined, + [{{timeout,a},1500,1}, + {state_timeout,1500,1}, + {{timeout,b},1000,1}, + {next_event,Call,Req}]} + end, + state1 => + fun ({call,_} = Call, Req, undefined) -> + T = erlang:monotonic_time(millisecond) + 500, + {next_state, state2, undefined, + [{{timeout,c},T,2,{abs,true}}, + {{timeout,d},0,2,[{abs,false}]}, + {timeout,0,2}, + {{timeout,b},infinity,2}, + {{timeout,a},1000,{Call,Req}}]} + end, + state2 => + fun ({timeout,d}, 2, undefined) -> + {next_state, state3, undefined} + end, + state3 => + fun ({timeout,c}, 2, undefined) -> + {next_state, state4, undefined} + end, + state4 => + fun ({timeout,a}, {{call,From},stop}, undefined) -> + {stop_and_reply, shutdown, + [{reply,From,stopped}]} + end}, + {ok,STM} = + gen_statem:start_link( + ?MODULE, {map_statem,Machine,[]}, [{debug,[trace]}]), + + stopped = gen_statem:call(STM, stop), + receive + {'EXIT',STM,shutdown} -> + ok + after 500 -> + ct:fail(did_not_stop) + end, + + {noproc,_} = + ?EXPECT_FAILURE(gen_statem:call(STM, hej), Reason), + case flush() of + [] -> + ok; + Other2 -> + ct:fail({unexpected,Other2}) + end. + + + sys1(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), @@ -1393,6 +1469,51 @@ enter_loop(Reg1, Reg2) -> gen_statem:enter_loop(?MODULE, [], state0, []) end. +undef_code_change(_Config) -> + {ok, Statem} = gen_statem:start(oc_statem, [], []), + {error, {'EXIT', + {undef, [{oc_statem, code_change, [_, _, _, _], _}|_]}}} + = fake_upgrade(Statem, oc_statem). + +fake_upgrade(Pid, Mod) -> + sys:suspend(Pid), + sys:replace_state(Pid, fun(State) -> {new, State} end), + Ret = sys:change_code(Pid, Mod, old_vsn, []), + ok = sys:resume(Pid), + Ret. + +undef_terminate1(_Config) -> + {ok, Statem} = gen_statem:start(oc_statem, [], []), + MRef = monitor(process, Statem), + ok = gen_statem:stop(Statem), + verify_down(Statem, MRef, normal), + ok. + +undef_terminate2(_Config) -> + Reason = {error, test}, + {ok, Statem} = oc_statem:start(), + MRef = monitor(process, Statem), + ok = gen_statem:stop(Statem, Reason, infinity), + verify_down(Statem, MRef, Reason). + +undef_in_terminate(_Config) -> + Data = {undef_in_terminate, {?MODULE, terminate}}, + {ok, Statem} = gen_statem:start(?MODULE, {data, Data}, []), + try + gen_statem:stop(Statem), + ct:fail(should_crash) + catch + exit:{undef, [{?MODULE, terminate, _, _}|_]} -> + ok + end. + +verify_down(Statem, MRef, Reason) -> + receive + {'DOWN', MRef, process, Statem, Reason} -> + ok + after 5000 -> + ct:fail(default_terminate_failed) + end. %% Test the order for multiple {next_event,T,C} next_events(Config) -> @@ -1571,6 +1692,9 @@ callback_mode() -> terminate(_, _State, crash_terminate) -> exit({crash,terminate}); +terminate(_, _State, {undef_in_terminate, {Mod, Fun}}) -> + Mod:Fun(), + ok; terminate({From,stopped}, State, _Data) -> From ! {self(),{stopped,State}}, ok; @@ -1597,8 +1721,9 @@ idle({call,From}, {delayed_answer,T}, Data) -> throw({keep_state,Data}) end; idle({call,From}, {timeout,Time}, _Data) -> + AbsTime = erlang:monotonic_time(millisecond) + Time, {next_state,timeout,{From,Time}, - {timeout,Time,idle}}; + {timeout,AbsTime,idle,[{abs,true}]}}; idle(cast, next_event, _Data) -> {next_state,next_events,[a,b,c], [{next_event,internal,a}, diff --git a/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl b/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl new file mode 100644 index 0000000000..27c9e0718d --- /dev/null +++ b/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl @@ -0,0 +1,37 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2017. 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% +%% +-module(oc_statem). + +-behaviour(gen_statem). + +%% API +-export([start/0]). + +%% gen_statem callbacks +-export([init/1, callback_mode/0]). + +start() -> + gen_statem:start({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + {ok, state_name, #{}}. + +callback_mode() -> + handle_event_function. diff --git a/lib/stdlib/test/qlc_SUITE.erl b/lib/stdlib/test/qlc_SUITE.erl index 2b5d52287e..5e9e03e410 100644 --- a/lib/stdlib/test/qlc_SUITE.erl +++ b/lib/stdlib/test/qlc_SUITE.erl @@ -1240,6 +1240,17 @@ string_to_handle(Config) when is_list(Config) -> {'EXIT', {no_lookup_to_carry_out, _}} = (catch qlc:e(qlc:string_to_handle(Q, {lookup,true}, Bs2))), ets:delete(Ets), + + %% References can be scanned and parsed. + E2 = ets:new(test, [bag]), + Ref = make_ref(), + true = ets:insert(E2, [{Ref,Ref}]), + S2 = "[{Val1} || {Ref1, Val1} <- ets:table("++io_lib:write(E2)++")," + "Ref1 =:= Ref].", + Bs = erl_eval:add_binding('Ref', Ref, erl_eval:new_bindings()), + [{Ref}] = qlc:e(qlc:string_to_handle(S2, [], Bs)), + ets:delete(E2), + ok. %% table @@ -4321,7 +4332,18 @@ ets(Config) when is_list(Config) -> R = qlc:e(Q), ets:delete(E), [] = R">>] - end + end, + + <<"E2 = ets:new(test, [bag]), + Ref = make_ref(), + true = ets:insert(E2, [{Ref,Ref}]), + Q2 = qlc:q([{Val1} || + {Ref1, Val1} <- ets:table(E2), + Ref1 =:= Ref]), + S = qlc:info(Q2), + true = is_list(S), + [{Ref}] = qlc:e(Q2), + ets:delete(E2)">> ], @@ -7071,7 +7093,7 @@ otp_12946(Config) when is_list(Config) -> %% Examples from qlc(3). manpage(Config) when is_list(Config) -> - + dets:start(), ok = compile_gb_table(Config), Ts = [ @@ -7138,11 +7160,14 @@ manpage(Config) when is_list(Config) -> \" [{X,Z}|{W,Y}] <- V2\n\" \" ])\n\" \"end\", - Info = + Info1 = re:replace(qlc:info(Q), - \"table\\\\(-*[0-9]*\", + \"table\\\\(#Ref<[\\.0-9]*>\", \"table(_\", [{return,list},global]), - L = Info, + F = fun(C) -> C =/= $\n andalso C =/= $\s end, + Info = lists:filter(F, Info1), + L1 = lists:filter(F, L), + L1 = Info, ets:delete(E1), ets:delete(E2)">>, @@ -7445,10 +7470,10 @@ etsc(F, Opts, Objs) -> V. join_info(H) -> - {qlc, S, Options} = strip_qlc_call(H), + {{qlc, S, Options}, Bs} = strip_qlc_call2(H), %% "Hide" the call to qlc_pt from the test in run_test(). LoadedPT = code:is_loaded(qlc_pt), - QH = qlc:string_to_handle(S, Options), + QH = qlc:string_to_handle(S, Options, Bs), _ = [unload_pt() || false <- [LoadedPT]], % doesn't take long... case {join_info_count(H), join_info_count(QH)} of {N, N} -> @@ -7458,30 +7483,34 @@ join_info(H) -> end. strip_qlc_call(H) -> + {Expr, _Bs} = strip_qlc_call2(H), + Expr. + +strip_qlc_call2(H) -> S = qlc:info(H, {flat, false}), - {ok, Tokens, _EndLine} = erl_scan:string(S++"."), - {ok, [Expr]} = erl_parse:parse_exprs(Tokens), - case Expr of - {call,_,{remote,_,{atom,_,qlc},{atom,_,q}},[LC]} -> - {qlc, lists:flatten([erl_pp:expr(LC), "."]), []}; - {call,_,{remote,_,{atom,_,qlc},{atom,_,q}},[LC, Opts]} -> - {qlc, lists:flatten([erl_pp:expr(LC), "."]), - erl_parse:normalise(Opts)}; - {call,_,{remote,_,{atom,_,ets},{atom,_,match_spec_run}},_} -> - {match_spec, Expr}; - {call,_,{remote,_,{atom,_,M},{atom,_,table}},_} -> - {table, M, Expr}; - _ -> - [] - end. + {ok, Tokens, _EndLine} = erl_scan:string(S++".", 1, [text]), + {ok, [Expr], Bs} = lib:extended_parse_exprs(Tokens), + {case Expr of + {call,_,{remote,_,{atom,_,qlc},{atom,_,q}},[LC]} -> + {qlc, lists:flatten([erl_pp:expr(LC), "."]), []}; + {call,_,{remote,_,{atom,_,qlc},{atom,_,q}},[LC, Opts]} -> + {qlc, lists:flatten([erl_pp:expr(LC), "."]), + erl_parse:normalise(Opts)}; + {call,_,{remote,_,{atom,_,ets},{atom,_,match_spec_run}},_} -> + {match_spec, Expr}; + {call,_,{remote,_,{atom,_,M},{atom,_,table}},_} -> + {table, M, Expr}; + _ -> + [] + end, Bs}. -record(ji, {nmerge = 0, nlookup = 0, nnested_loop = 0, nkeysort = 0}). %% Counts join options and (all) calls to qlc:keysort(). join_info_count(H) -> S = qlc:info(H, {flat, false}), - {ok, Tokens, _EndLine} = erl_scan:string(S++"."), - {ok, [Expr]} = erl_parse:parse_exprs(Tokens), + {ok, Tokens, _EndLine} = erl_scan:string(S++".", 1, [text]), + {ok, [Expr], _Bs} = lib:extended_parse_exprs(Tokens), #ji{nmerge = Nmerge, nlookup = Nlookup, nkeysort = NKeysort, nnested_loop = Nnested_loop} = ji(Expr, #ji{}), @@ -7524,8 +7553,8 @@ lookup_keys({list,Q,_}, L) -> lookup_keys({generate,_,Q}, L) -> lookup_keys(Q, L); lookup_keys({table,Chars}, L) when is_list(Chars) -> - {ok, Tokens, _} = erl_scan:string(lists:flatten(Chars++".")), - {ok, [Expr]} = erl_parse:parse_exprs(Tokens), + {ok, Tokens, _} = erl_scan:string(lists:flatten(Chars++"."), 1, [text]), + {ok, [Expr], _Bs} = lib:extended_parse_exprs(Tokens), case Expr of {call,_,_,[_fun,AKs]} -> case erl_parse:normalise(AKs) of @@ -7842,7 +7871,7 @@ run_test(Config, Extra, {cres, Body, Opts, ExpectedCompileReturn}) -> {module, _} = code:load_abs(AbsFile, Mod), Ms0 = erlang:process_info(self(),messages), - Before = {{get(), ets:all(), Ms0}, pps()}, + Before = {{get(), lists:sort(ets:all()), Ms0}, pps()}, %% Prepare the check that the qlc module does not call qlc_pt. _ = [unload_pt() || {file, Name} <- [code:is_loaded(qlc_pt)], @@ -7874,7 +7903,7 @@ run_test(Config, Extra, Body) -> wait_for_expected(R, {Strict0,PPS0}=Before, SourceFile, Wait) -> Ms = erlang:process_info(self(),messages), - After = {_,PPS1} = {{get(), ets:all(), Ms}, pps()}, + After = {_,PPS1} = {{get(), lists:sort(ets:all()), Ms}, pps()}, case {R, After} of {ok, Before} -> ok; diff --git a/lib/stdlib/test/shell_SUITE.erl b/lib/stdlib/test/shell_SUITE.erl index 56002dda25..99411bc8fd 100644 --- a/lib/stdlib/test/shell_SUITE.erl +++ b/lib/stdlib/test/shell_SUITE.erl @@ -31,7 +31,7 @@ progex_lc/1, progex_funs/1, otp_5990/1, otp_6166/1, otp_6554/1, otp_7184/1, otp_7232/1, otp_8393/1, otp_10302/1, otp_13719/1, - otp_14285/1]). + otp_14285/1, otp_14296/1]). -export([ start_restricted_from_shell/1, start_restricted_on_command_line/1,restricted_local/1]). @@ -92,7 +92,7 @@ groups() -> progex_funs]}, {tickets, [], [otp_5990, otp_6166, otp_6554, otp_7184, - otp_7232, otp_8393, otp_10302, otp_13719, otp_14285]}]. + otp_7232, otp_8393, otp_10302, otp_13719, otp_14285, otp_14296]}]. init_per_suite(Config) -> Config. @@ -2841,6 +2841,95 @@ otp_14285(Config) -> test_server:stop_node(Node), ok. +otp_14296(Config) when is_list(Config) -> + fun() -> + F = fun() -> a end, + LocalFun = term_to_string(F), + S = LocalFun ++ ".", + "1: syntax error before: Fun" = comm_err(S) + end(), + + fun() -> + F = fun mod:func/1, + ExternalFun = term_to_string(F), + S = ExternalFun ++ ".", + R = ExternalFun ++ ".\n", + R = t(S) + end(), + + fun() -> + UnknownPid = "<100000.0.0>", + S = UnknownPid ++ ".", + "1: syntax error before: '<'" = comm_err(S) + end(), + + fun() -> + KnownPid = term_to_string(self()), + S = KnownPid ++ ".", + R = KnownPid ++ ".\n", + R = t(S) + end(), + + fun() -> + Port = open_port({spawn, "ls"}, [line]), + KnownPort = erlang:port_to_list(Port), + S = KnownPort ++ ".", + R = KnownPort ++ ".\n", + R = t(S) + end(), + + fun() -> + UnknownPort = "#Port<100000.0>", + S = UnknownPort ++ ".", + "1: syntax error before: Port" = comm_err(S) + end(), + + fun() -> + UnknownRef = "#Ref<100000.0.0.0>", + S = UnknownRef ++ ".", + "1: syntax error before: Ref" = comm_err(S) + end(), + + fun() -> + KnownRef = term_to_string(make_ref()), + S = KnownRef ++ ".", + R = KnownRef ++ ".\n", + R = t(S) + end(), + + %% Test lib:extended_parse_term/1 + TF = fun(S) -> + {ok, Ts, _} = erl_scan:string(S++".", 1, [text]), + case lib:extended_parse_term(Ts) of + {ok, Term} -> Term; + {error, _}=Error -> Error + end + end, + Fun = fun m:f/1, + Fun = TF(term_to_string(Fun)), + Fun = TF("fun m:f/1"), + Pid = self(), + Pid = TF(term_to_string(Pid)), + Ref = make_ref(), + Ref = TF(term_to_string(Ref)), + Term = {[10, a], {"foo", []}, #{x => <<"bar">>}}, + Term = TF(lists:flatten(io_lib:format("~p", [Term]))), + {$a, F1, "foo"} = TF("{$a, 1.0, \"foo\"}"), + true = is_float(F1), + 3 = TF("+3"), + $a = TF("+$a"), + true = is_float(TF("+1.0")), + true = -3 =:= TF("-3"), + true = -$a =:= TF("-$a"), + true = is_float(TF("-1.0")), + {error, {_, _, ["syntax error"++_|_]}} = TF("{1"), + {error, {_,_,"bad term"}} = TF("fun() -> foo end"), + {error, {_,_,"bad term"}} = TF("1, 2"), + ok. + +term_to_string(T) -> + lists:flatten(io_lib:format("~w", [T])). + scan(B) -> F = fun(Ts) -> case erl_parse:parse_term(Ts) of diff --git a/lib/stdlib/test/sofs_SUITE.erl b/lib/stdlib/test/sofs_SUITE.erl index f67bf16f0f..39e56c6df6 100644 --- a/lib/stdlib/test/sofs_SUITE.erl +++ b/lib/stdlib/test/sofs_SUITE.erl @@ -1783,7 +1783,7 @@ multiple_relative_product(Conf) when is_list(Conf) -> ok. digraph(Conf) when is_list(Conf) -> - T0 = ets:all(), + T0 = lists:sort(ets:all()), E = empty_set(), R = relation([{a,b},{b,c},{c,d},{d,a}]), F = relation_to_family(R), @@ -1833,7 +1833,7 @@ digraph(Conf) when is_list(Conf) -> true -> ok end, - true = T0 == ets:all(), + true = T0 == lists:sort(ets:all()), ok. digraph_fail(ExitReason, Fail) -> diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index 2d86eb44d4..4ffc2ca172 100644 --- a/lib/tools/emacs/erlang.el +++ b/lib/tools/emacs/erlang.el @@ -928,7 +928,6 @@ resulting regexp is surrounded by \\_< and \\_>." "get_cookie" "get_module_info" "get_stacktrace" - "hash" "has_prepared_code_on_load" "hibernate" "insert_element" @@ -4707,6 +4706,7 @@ for a tag on the form `module:tag'." (if (fboundp 'advice-add) ;; Emacs 24.4+ + (require 'etags) (advice-add 'etags-tags-completion-table :around #'erlang-etags-tags-completion-table-advice) ;; Emacs 23.1-24.3 @@ -4873,6 +4873,7 @@ considered first when it is time to jump to the definition.") (and (erlang-soft-require 'xref) (erlang-soft-require 'cl-generic) (erlang-soft-require 'eieio) + (erlang-soft-require 'etags) ;; The purpose of using eval here is to avoid compilation ;; warnings in emacsen without cl-defmethod etc. (eval @@ -5351,7 +5352,7 @@ editing control characters: \\{erlang-shell-mode-map}" (interactive (when current-prefix-arg - (list ((read-shell-command "Erlang command: "))))) + (list (read-shell-command "Erlang command: ")))) (require 'comint) (let (cmd opts) (if command diff --git a/lib/wx/c_src/wxe_impl.cpp b/lib/wx/c_src/wxe_impl.cpp index 05d56667ab..90cd35455a 100644 --- a/lib/wx/c_src/wxe_impl.cpp +++ b/lib/wx/c_src/wxe_impl.cpp @@ -672,7 +672,7 @@ void * WxeApp::getPtr(char * bp, wxeMemEnv *memenv) { throw wxe_badarg(index); } void * temp = memenv->ref2ptr[index]; - if((index < memenv->next) && ((index == 0) || (temp > NULL))) + if((index < memenv->next) && ((index == 0) || (temp != (void *)NULL))) return temp; else { throw wxe_badarg(index); @@ -684,7 +684,7 @@ void WxeApp::registerPid(char * bp, ErlDrvTermData pid, wxeMemEnv * memenv) { if(!memenv) throw wxe_badarg(index); void * temp = memenv->ref2ptr[index]; - if((index < memenv->next) && ((index == 0) || (temp > NULL))) { + if((index < memenv->next) && ((index == 0) || (temp != (void *) NULL))) { ptrMap::iterator it; it = ptr2ref.find(temp); if(it != ptr2ref.end()) { diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index f4d84ab163..0667af7868 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -4,7 +4,7 @@ <chapter> <header> <copyright> - <year>2016</year> + <year>2016</year><year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -293,6 +293,13 @@ StateName(EventType, EventContent, Data) -> <seealso marker="#State Time-Outs">State Time-Outs</seealso> </item> <item> + Start a + <seealso marker="stdlib:gen_statem#type-generic_timeout"> + generic time-out</seealso>, + read more in section + <seealso marker="#Generic Time-Outs">Generic Time-Outs</seealso> + </item> + <item> Start an <seealso marker="stdlib:gen_statem#type-event_timeout">event time-out</seealso>, see more in section @@ -320,8 +327,9 @@ StateName(EventType, EventContent, Data) -> <c>gen_statem(3)</c> </seealso> manual page. - You can, for example, reply to many callers - and generate multiple next events to handle. + You can, for example, reply to many callers, + generate multiple next events, + and set time-outs to relative or absolute times. </p> </section> @@ -369,6 +377,14 @@ StateName(EventType, EventContent, Data) -> </seealso> state timer timing out. </item> + <tag><c>{timeout,Name}</c></tag> + <item> + Generated by state transition action + <seealso marker="stdlib:gen_statem#type-generic_timeout"> + <c>{{timeout,Name},Time,EventContent}</c> + </seealso> + generic timer timing out. + </item> <tag><c>timeout</c></tag> <item> Generated by state transition action @@ -450,7 +466,7 @@ locked( [Digit] -> do_unlock(), {next_state, open, Data#{remaining := Code}, - [{state_timeout,10000,lock}]; + [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete {next_state, locked, Data#{remaining := Rest}}; _Wrong -> @@ -779,7 +795,7 @@ handle_event(cast, {button,Digit}, State, #{code := Code} = Data) -> [Digit] -> % Complete do_unlock(), {next_state, open, Data#{remaining := Code}, - [{state_timeout,10000,lock}}; + [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete {keep_state, Data#{remaining := Rest}}; [_|_] -> % Wrong @@ -873,7 +889,7 @@ stop() -> <marker id="Event Time-Outs" /> <title>Event Time-Outs</title> <p> - A timeout feature inherited from <c>gen_statem</c>'s predecessor + A time-out feature inherited from <c>gen_statem</c>'s predecessor <seealso marker="stdlib:gen_fsm"><c>gen_fsm</c></seealso>, is an event time-out, that is, if an event arrives the timer is cancelled. @@ -906,24 +922,24 @@ locked( ... ]]></code> <p> - Whenever we receive a button event we start an event timeout + Whenever we receive a button event we start an event time-out of 30 seconds, and if we get an event type <c>timeout</c> we reset the remaining code sequence. </p> <p> - An event timeout is cancelled by any other event so you either - get some other event or the timeout event. It is therefore - not possible nor needed to cancel or restart an event timeout. + An event time-out is cancelled by any other event so you either + get some other event or the time-out event. It is therefore + not possible nor needed to cancel or restart an event time-out. Whatever event you act on has already cancelled - the event timeout... + the event time-out... </p> </section> <!-- =================================================================== --> <section> - <marker id="Erlang Timers" /> - <title>Erlang Timers</title> + <marker id="Generic Time-Outs" /> + <title>Generic Time-Outs</title> <p> The previous example of state time-outs only work if the state machine stays in the same state during the @@ -934,13 +950,68 @@ locked( You may want to start a timer in one state and respond to the time-out in another, maybe cancel the time-out without changing states, or perhaps run multiple - time-outs in parallel. All this can be accomplished - with Erlang Timers: + time-outs in parallel. All this can be accomplished with + <seealso marker="stdlib:gen_statem#type-generic_timeout">generic time-outs</seealso>. + They may look a little bit like + <seealso marker="stdlib:gen_statem#type-event_timeout">event time-outs</seealso> + but contain a name to allow for any number of them simultaneously + and they are not automatically cancelled. + </p> + <p> + Here is how to accomplish the state time-out + in the previous example by instead using a generic time-out + named <c>open_tm</c>: + </p> + <code type="erl"><![CDATA[ +... +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> + do_unlock(), + {next_state, open, Data#{remaining := Code}, + [{{timeout,open_tm},10000,lock}]}; +... + +open({timeout,open_tm}, lock, Data) -> + do_lock(), + {next_state,locked,Data}; +open(cast, {button,_}, Data) -> + {keep_state,Data}; +... + ]]></code> + <p> + Just as + <seealso marker="#State Time-Outs">state time-outs</seealso> + you can restart or cancel a specific generic time-out + by setting it to a new time or <c>infinity</c>. + </p> + <p> + Another way to handle a late time-out can be to not cancel it, + but to ignore it if it arrives in a state + where it is known to be late. + </p> + </section> + +<!-- =================================================================== --> + + <section> + <marker id="Erlang Timers" /> + <title>Erlang Timers</title> + <p> + The most versatile way to handle time-outs is to use + Erlang Timers; see <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer3,4</c></seealso>. + Most time-out tasks can be performed with the + time-out features in <c>gen_statem</c>, + but an example of one that can not is if you should need + the return value from + <seealso marker="erts:erlang#cancel_timer/2"><c>erlang:cancel_timer(Tref)</c></seealso>, that is; the remaining time of the timer. </p> <p> Here is how to accomplish the state time-out - in the previous example by insted using an Erlang Timer: + in the previous example by instead using an Erlang Timer: </p> <code type="erl"><![CDATA[ ... @@ -1596,7 +1667,7 @@ handle_event( {call,From}, code_length, {_StateName,_LockButton}, #{code := Code}) -> {keep_state_and_data, - [{reply,From,length(Code)}]}; + [{reply,From,length(Code)}]}; %% %% State: locked handle_event( @@ -1636,7 +1707,7 @@ handle_event( if Digit =:= LockButton -> {next_state, {locked,LockButton}, Data, - [{reply,From,locked}]); + [{reply,From,locked}]}; true -> {keep_state_and_data, [postpone]} @@ -1710,10 +1781,10 @@ handle_event( EventType, EventContent, {open,LockButton}, Data) -> case {EventType, EventContent} of - {enter, _OldState} -> - do_unlock(), - {keep_state_and_data, - [{state_timeout,10000,lock},hibernate]}; + {enter, _OldState} -> + do_unlock(), + {keep_state_and_data, + [{state_timeout,10000,lock},hibernate]}; ... ]]></code> <p> diff --git a/system/doc/efficiency_guide/processes.xml b/system/doc/efficiency_guide/processes.xml index afa4325d8e..3b64c863ff 100644 --- a/system/doc/efficiency_guide/processes.xml +++ b/system/doc/efficiency_guide/processes.xml @@ -4,7 +4,7 @@ <chapter> <header> <copyright> - <year>2001</year><year>2016</year> + <year>2001</year><year>2017</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -222,7 +222,7 @@ kilo_byte(N, Acc) -> <pre> 4> <input>T = ets:new(tab, []).</input> -17 +#Ref<0.1662103692.2407923716.214181> 5> <input>ets:insert(T, {key,efficiency_guide:kilo_byte()}).</input> true 6> <input>erts_debug:size(element(2, hd(ets:lookup(T, key)))).</input> |