View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        jan@swi-prolog.org
    5    WWW:           https://www.swi-prolog.org
    6    Copyright (c)  2019-2026, VU University Amsterdam
    7			      SWI-Prolog Solutions b.v.
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(ssh_server,
   37          [ ssh_server/0,
   38            ssh_server/1,       % +Options
   39	    capture_messages/1  % +Level
   40	  ]).   41:- use_module(library(debug)).   42:- use_module(library(option)).   43:- use_module(library(settings)).   44:- use_module(library(threadutil)).   45
   46:- use_foreign_library(foreign(sshd4pl)).

Embedded SSH server

This module defines an embedded SSH server for SWI-Prolog on top of libssh. This module allows for a safe secondary access point to a running Prolog process. A typical use case is to provide a safe channal or inspection and maintenance of servers or embedded Prolog instances.

If possible, a login to the Prolog process uses a pseudo terminal to realise normal terminal interaction, including processing of ^C to interrupt running queries. If libedit (editline) is used as the command line editor this is installed (see el_wrap/0), providing advanced command line editing and history.

The library currently support login to the Prolog process. Future versions may also use the client access and exploit the SSH subsystem interface to achieve safe interaction between Prolog peers.

The client session

A new connection creates a Prolog thread that handles the connection. The new thread's standard streams (user_input, user_output, user_error, current_input and current_output) are attached to the new connection. Some of the environment is shared as Prolog flags. The following flags are defined:

ssh_tty
Provides the name of the pseudo terminal if such a terminal us allocated for this connection.
ssh_term
Provides the TERM environment variable passed from the client.
ssh_user
Provides the name of the user logged on.

If a pseudo terminal is used and the ssh_term flag is not dump, library(ansi_term) is connected to provide colorized output.

If a pseudo terminal is used and library(editline) is available, this library is used to enable command line editing.

Executing commands

Using ssh <options> <server> <command>, <command> is executed without a terminal (unless the -t option is given to ssh to force a terminal) and otherwise as a single Prolog toplevel command. For example:

ssh -p 2020 localhost "writeln('Hello world')"
Hello world
true.

If the query is nondeterministic alternative answers can be requested in the same way as using the interactive toplevel. The exit code is defined as follows:

0
The query succeeded
1
The query failed
2
The query produced an exception (the system prints a backtrace)
3
The query itself was not syntactically correct.

Aborting the server

If a Prolor process with an embedded ssh server misbehaves it can be forcefully aborted using the abort command. This calls C abort() as soon as possible and thus should function even if Prolog is, for example, stuck in a deadlock.

ssh -p 2020 localhost abort
To be done
- Currently only supports Unix. A Windows port is probably doable. It mostly requires finding a sensible replacement for the Unix pseudo terminal.
- Implement running other commands than the Prolog toplevel. */
  130:- multifile
  131    verify_password/3.                  % +ServerName, +User, +Password
  132
  133:- predicate_options(
  134       ssh_server/1, 1,
  135       [ name(atom),
  136         port(integer),
  137         bind_address(atom),
  138         host_key_file(atom),
  139         authorized_keys_file(atom),
  140         auth_methods(list(oneof([password,public_key])))
  141       ]).  142
  143:- setting(port, positive_integer, 2020,
  144           "Default port for SWI-Prolog SSH server").  145:- setting(color_term, boolean, true,
  146           "Enable ANSI color output on SSH terminal").
 ssh_server is det
 ssh_server(+PortOrOptions) is det
Create an embedded SSH server in the current Prolog process. If the argument is an integer it is interpreted as ssh_server([port(Integer)]). Options:
name(+Atom)
Name the server. Passed as first argument to verify_password/3 to identify multiple servers.
port(+Integer)
Port to listen on. Default is 2020.
bind_address(+Name)
Interface to listen to. Default is localhost. Use * to grant acccess from all network interfaces.
host_key_file(+File)
File name for the host private key. If omitted it searches for etc/ssh below the current directory and user_app_config('etc/ssh') (normally ~/.config/swi-prolog/etc/ssh). On failure it creates, a directory etc/ssh with default host keys and uses these.
auth_methods(+ListOfMethod)
Set allowed authentication methods. ListOfMethod is a list of
password
Allow password login (see verify_password/3)
public_key
Allow key based login (see authorized_keys_file below) The default is derived from the authorized_keys_file option and whether or not verify_password/3 is defined.
authorized_keys_file(+File)
File name for a file holding the public keys for users that are allows to login. Activates auth_methods([public_key]). This file is in OpenSSH format and contains a certificate per line in the format
<type> <base64-key> <comment>

The the file `~/.ssh/authorized_keys` is present, this will be used as default, granting anyone with access to this account to access the server with the same keys. If the option is present with value [] (empty list), no key file is used.

  192ssh_server :-
  193    ssh_server([]).
  194
  195ssh_server(Port) :-
  196    integer(Port),
  197    !,
  198    ssh_server([port(Port)]).
  199ssh_server(Options) :-
  200    setting(port, DefPort),
  201    merge_options(Options,
  202                  [ port(DefPort),
  203                    bind_address(localhost)
  204                  ], Options1),
  205    (   option(name(Name), Options)
  206    ->  Alias = Name
  207    ;   option(port(Port), Options1),
  208        format(atom(Alias), 'sshd@~w', [Port])
  209    ),
  210    ensure_host_keys(Options1, Options2),
  211    add_authorized_keys(Options2, Options3),
  212    add_auth_methods(Options3, Options4),
  213    setup_signals(Options4),
  214    thread_create(ssh_server_nt(Options4), _,
  215                  [ alias(Alias),
  216                    detached(true),
  217                    class(service)
  218                  ]).
 ensure_host_keys(+Options0, -Options) is det
Provide a host key:
  1. If the key file is given, use it.
  2. If there is a key in etc/ssh, use it.
  3. If there is a key in user_app_config('etc/ssh'), use it.
  4. Try to create a key in user_app_config('etc/ssh')
  5. Try to create a key in etc/ssh
  230ensure_host_keys(Options, Options) :-
  231    option(host_key_file(KeyFile), Options),
  232    !,
  233    (   access_file(KeyFile, read)
  234    ->  true
  235    ;   permission_error(read, ssh_host_key_file, KeyFile)
  236    ).
  237ensure_host_keys(Options0, Options) :-
  238    exists_file('etc/ssh/ssh_host_ecdsa_key'),
  239    !,
  240    Options = [host_key_file('etc/ssh/ssh_host_ecdsa_key')|Options0].
  241ensure_host_keys(Options0, Options) :-
  242    absolute_file_name(user_app_config('etc/ssh'), Dir,
  243                       [ file_type(directory),
  244                         access(exist),
  245                         file_errors(fail)
  246                       ]),
  247    !,
  248    directory_file_path(Dir, ssh_host_ecdsa_key, KeyFile),
  249    Options = [host_key_file(KeyFile)|Options0].
  250ensure_host_keys(Options0, Options) :-
  251    absolute_file_name(user_app_config('etc/ssh'), Dir,
  252                       [ solutions(all),
  253                         file_errors(fail)
  254                       ]),
  255    Error = error(_,_),
  256    catch(make_directory_path(Dir), Error, fail),
  257    file_directory_name(Dir, P0),
  258    file_directory_name(P0, ConfigDir),
  259    format(string(KeyCmd), 'ssh-keygen -A -f ~w', [ConfigDir]),
  260    print_message(informational, ssh_server(create_host_keys(Dir))),
  261    shell(KeyCmd),
  262    !,
  263    directory_file_path(Dir, ssh_host_ecdsa_key, KeyFile),
  264    Options = [host_key_file(KeyFile)|Options0].
  265ensure_host_keys(Options,
  266                 [ host_key_file('etc/ssh/ssh_host_ecdsa_key')
  267                 | Options
  268                 ]) :-
  269    print_message(informational, ssh_server(create_host_keys('etc/ssh'))),
  270    make_directory_path('etc/ssh'),
  271    shell('ssh-keygen -A -f .').
  272
  273add_auth_methods(Options, Options) :-
  274    option(auth_methods(_), Options),
  275    !.
  276add_auth_methods(Options, [auth_methods(Methods)|Options]) :-
  277    findall(Method, option_auth_method(Options, Method), Methods).
  278
  279option_auth_method(Options, public_key) :-
  280    option(authorized_keys_file(_), Options).
  281option_auth_method(_Options, password) :-
  282    predicate_property(verify_password(_,_,_), number_of_clauses(N)),
  283    N > 0.
  284
  285add_authorized_keys(Options0, Options) :-
  286    option(authorized_keys_file(AuthKeysFile), Options0),
  287    !,
  288    (   AuthKeysFile == []
  289    ->  select_option(authorized_keys_file(AuthKeysFile), Options0, Options)
  290    ;   Options = Options0
  291    ).
  292add_authorized_keys(Options, [authorized_keys_file(AuthKeysFile)|Options]) :-
  293    expand_file_name('~/.ssh/authorized_keys', [AuthKeysFile]),
  294    access_file(AuthKeysFile, read),
  295    !.
  296add_authorized_keys(Options, Options).
 setup_signals(+Options)
Re-installs the int signal to start the debugger. Notably library(http/http_unix_daemon) binds this to terminates the process.
  303setup_signals(_Options) :-
  304    E = error(_,_),
  305    catch(on_signal(int, _, debug), E, print_message(warning, E)).
 run_client(+Server, +In, +Out, +Err, +Command, -RetCode) is det
Run Command using I/O from the triple <In, Out, Err> and bind RetCode to the ssh shell return code.
  312:- public run_client/6.  313
  314run_client(Server, In, Out, Err, Command, RetCode) :-
  315    set_properties,
  316    setup_console(Server, In, Out, Err, Cleanup),
  317    call_cleanup(ssh_toplevel(Command, RetCode),
  318                 shutdown_console(Cleanup)).
  319
  320:- if(thread_property(main,class(_))).  321set_properties :-
  322    current_prolog_flag(ssh_user, User),
  323    thread_self(Me),
  324    thread_property(Me, id(Id)),
  325    format(atom(Alias), '~w@ssh/~w', [User, Id]),
  326    set_thread(Me, alias(Alias)),
  327    set_thread(Me, class(console)).
  328:- elif(current_predicate(thread_alias/1)).  329set_properties :-
  330    current_prolog_flag(ssh_user, User),
  331    thread_self(Me),
  332    thread_property(Me, id(Id)),
  333    format(atom(Alias), '~w@ssh/~w', [User, Id]),
  334    thread_alias(Alias).
  335:- endif.  336set_properties.
  337
  338% Used by has_console/0 in thread_util.
  339
  340:- dynamic thread_util:has_console/4.  341
  342setup_console(Server, In, Out, Err, clean(Me, Cleanup)) :-
  343    thread_self(Me),
  344    set_stream(In,  alias(user_input)),
  345    set_stream(Out, alias(user_output)),
  346    set_stream(Err, alias(user_error)),
  347    set_stream(In,  alias(current_input)),
  348    set_stream(Out, alias(current_output)),
  349    enable_colors,
  350    enable_line_editing(Mode),
  351    load_history(Mode, Server, Cleanup).
  352
  353shutdown_console(clean(_TID, History)) :-
  354    save_history(History),
  355    disable_line_editing.
  356
  357:- if(setting(color_term, true)).  358:- use_module(library(ansi_term)).  359:- endif.
 enable_colors is det
Enable ANSI colors on the remote shell. This is controlled by the setting color_term. Note that we do not wish to inherit this as the server may have different preferences.
  367enable_colors :-
  368    stream_property(user_input, tty(true)),
  369    setting(color_term, true),
  370    current_prolog_flag(ssh_term, Term),
  371    Term \== dump,
  372    !,
  373    set_prolog_flag(color_term, true).
  374enable_colors :-
  375    set_prolog_flag(color_term, false).
 enable_line_editing is det
Enable line editing for the SSH session. We can do this if the SSH session uses a pseudo terminal and we are using library(editline) as command line editor (GNU readline uses global variables and thus can only handle a single tty in the process).
  384use_editline :-
  385    exists_source(library(editline)),
  386    (   current_prolog_flag(readline, editline)
  387    ->  true
  388    ;   \+ current_prolog_flag(readline, _)
  389    ).
  390
  391:- if(use_editline).  392:- use_module(library(editline)).  393enable_line_editing(editline) :-
  394    stream_property(user_input, tty(true)),
  395    !,
  396    debug(ssh(server), 'Setting up line editing', []),
  397    set_prolog_flag(tty_control, true),
  398    el_wrap.
  399:- else.  400enable_line_editing(tty) :-
  401    stream_property(user_input, tty(true)),
  402    !,
  403    set_prolog_flag(tty_control, true).
  404:- endif.  405enable_line_editing(none) :-
  406    set_prolog_flag(tty_control, false).
  407
  408:- if(current_predicate(el_unwrap/1)).  409disable_line_editing :-
  410    el_wrapped(user_input),
  411    !,
  412    Error = error(_,_),
  413    catch(el_unwrap(user_input), Error, true).
  414:- endif.  415disable_line_editing.
 verify_password(+ServerName, +User:atom, +Passwd:string) is semidet
Hook that can be used to accept password based logins. This predicate must succeeds to accept the User/Passwd combination.
Arguments:
ServerName- is the name provided with the name(Name) option when creating the server or the empty list.
  426		 /*******************************
  427		 *            HISTORY		*
  428		 *******************************/
  429
  430:- multifile
  431    prolog:history/2.
 load_history(+EditMode, +Server, -Cleanup) is det
Load command line history for Server, binding Cleanup to the required command for save_history/1
  438load_history(editline, Server, save(File)) :-
  439    history_file(Server, File,
  440                 [ access(read),
  441                   file_errors(fail)
  442                 ]),
  443    !,
  444    prolog:history(user_input, load(File)).
  445load_history(editline, Server, create(Server)) :-
  446    !.
  447load_history(_, _, nosave).
 save_history(+Action) is det
Save the history information according to action.
  453save_history(save(File)) :-
  454    catch(write_history(File), _, true),
  455    !.
  456save_history(create(Server)) :-
  457    history_file(Server, File,
  458                 [ file_errors(fail),
  459                   solutions(all)
  460                 ]),
  461    catch(write_history(File), _, true),
  462    !.
  463save_history(_).
  464
  465write_history(File) :-
  466    file_directory_name(File, Dir),
  467    make_directory_path(Dir),
  468    prolog:history(user_input, save(File)).
  469
  470history_file(Server, Path, Options) :-
  471    (   Server == []
  472    ->  SName = default
  473    ;   SName = Server
  474    ),
  475    current_prolog_flag(ssh_user, User),
  476    atomic_list_concat([ssh, history, SName, User], /, File),
  477    absolute_file_name(user_app_config(File), Path, Options).
 ssh_toplevel(+Command, -RetCode)
Run the toplevel goal for the SSH session. The default is prolog, running the toplevel. Otherwise the argument is processed as a single toplevel goal.
  487ssh_toplevel(prolog, 0) :-
  488    !,
  489    version,
  490    prolog.
  491ssh_toplevel(Command, RetCode) :-
  492    catch(term_string(Query, Command, [variable_names(Bindings)]),
  493          Error, true),
  494    (   var(Error)
  495    ->  catch_with_backtrace('$execute_query'(Query, Bindings, Truth), E2, true),
  496        toplevel_finish(Truth, E2, RetCode)
  497    ;   print_message(error, Error),
  498        RetCode = 3
  499    ).
  500
  501toplevel_finish(_, Error, 2) :-
  502    nonvar(Error),
  503    !,
  504    print_message(error, Error).
  505toplevel_finish(true, _, 0).
  506toplevel_finish(false, _, 1).
  507
  508
  509		 /*******************************
  510		 *       CAPTURE MESSAGES       *
  511		 *******************************/
  512
  513:- dynamic
  514       captured_messages/3.  515:- thread_local
  516       thread_error_stream/1.  517
  518user:message_property(Level, stream(S)) :-
  519    captured_messages(Level, S, _).
 capture_messages(+Level) is det
Redirect all messages of the indicated level to the console of the current thread. This is part of the SSH library as it is notably practical when connected through SSH. Consider using trace/1 on some predicate. We catch capture the output using:
?- capture_messages(debug).
?- trace(p/1).
  531capture_messages(Level) :-
  532    (   thread_error_stream(S)
  533    ->  true
  534    ;   thread_self(Me),
  535	stream_property(S, alias(user_error)),
  536	asserta(thread_error_stream(S)),
  537	thread_at_exit(cleanup_message_capture)
  538    ),
  539    asserta(captured_messages(Level, S, Me)).
  540
  541cleanup_message_capture :-
  542    thread_self(Me),
  543    retractall(captured_messages(_,_,Me)).
  544
  545
  546		 /*******************************
  547		 *           MESSAGES		*
  548		 *******************************/
  549
  550:- multifile
  551    prolog:message//1.  552
  553prolog:message(ssh_server(create_host_keys(Dir))) -->
  554    [ 'SSH Server: Creating host keys in "~w"'-[Dir] ]