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)).
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([port(Integer)]). Options:
localhost. Use *
to grant acccess from all network interfaces.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.authorized_keys_file below)
The default is derived from the authorized_keys_file option
and whether or not verify_password/3 is defined.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 ]).
etc/ssh, use it.user_app_config('etc/ssh'), use it.user_app_config('etc/ssh')etc/ssh230ensure_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 (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).
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)).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.
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).
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.
426 /******************************* 427 * HISTORY * 428 *******************************/ 429 430:- multifile 431 prolog:history/2.
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).
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).
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 518usermessage_property(Level, stream(S)) :- 519 captured_messages(Level, S, _).
?- 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 553prologmessage(ssh_server(create_host_keys(Dir))) --> 554 [ 'SSH Server: Creating host keys in "~w"'-[Dir] ]
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_inputandcurrent_output) are attached to the new connection. Some of the environment is shared as Prolog flags. The following flags are defined:TERMenvironment variable passed from the client.If a pseudo terminal is used and the
ssh_termflag is notdump, 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-toption is given tosshto 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:
Aborting the server
If a Prolor process with an embedded ssh server misbehaves it can be forcefully aborted using the
abortcommand. This calls Cabort()as soon as possible and thus should function even if Prolog is, for example, stuck in a deadlock.