From 84adefa331c4159d432d22840663c38f155cd4c1 Mon Sep 17 00:00:00 2001 From: Erlang/OTP Date: Fri, 20 Nov 2009 14:54:40 +0000 Subject: The R13B03 release. --- lib/ssh/src/DSS.asn1 | 20 + lib/ssh/src/Makefile | 157 ++++ lib/ssh/src/PKCS-1.asn1 | 116 +++ lib/ssh/src/prebuild.skip | 2 + lib/ssh/src/ssh.app.src | 48 ++ lib/ssh/src/ssh.appup.src | 37 + lib/ssh/src/ssh.erl | 339 ++++++++ lib/ssh/src/ssh.hrl | 180 ++++ lib/ssh/src/ssh_acceptor.erl | 115 +++ lib/ssh/src/ssh_acceptor_sup.erl | 95 +++ lib/ssh/src/ssh_app.erl | 34 + lib/ssh/src/ssh_auth.erl | 423 +++++++++ lib/ssh/src/ssh_auth.hrl | 83 ++ lib/ssh/src/ssh_bits.erl | 483 +++++++++++ lib/ssh/src/ssh_channel.erl | 328 +++++++ lib/ssh/src/ssh_channel_sup.erl | 54 ++ lib/ssh/src/ssh_cli.erl | 500 +++++++++++ lib/ssh/src/ssh_cm.erl | 237 ++++++ lib/ssh/src/ssh_connect.hrl | 264 ++++++ lib/ssh/src/ssh_connection.erl | 1366 ++++++++++++++++++++++++++++++ lib/ssh/src/ssh_connection_controler.erl | 137 +++ lib/ssh/src/ssh_connection_handler.erl | 879 +++++++++++++++++++ lib/ssh/src/ssh_connection_manager.erl | 760 +++++++++++++++++ lib/ssh/src/ssh_dsa.erl | 95 +++ lib/ssh/src/ssh_file.erl | 530 ++++++++++++ lib/ssh/src/ssh_io.erl | 79 ++ lib/ssh/src/ssh_math.erl | 131 +++ lib/ssh/src/ssh_no_io.erl | 39 + lib/ssh/src/ssh_rsa.erl | 299 +++++++ lib/ssh/src/ssh_sftp.erl | 1148 +++++++++++++++++++++++++ lib/ssh/src/ssh_sftpd.erl | 932 ++++++++++++++++++++ lib/ssh/src/ssh_sftpd_file.erl | 83 ++ lib/ssh/src/ssh_sftpd_file_api.erl | 47 + lib/ssh/src/ssh_shell.erl | 178 ++++ lib/ssh/src/ssh_ssh.erl | 65 ++ lib/ssh/src/ssh_sshd.erl | 48 ++ lib/ssh/src/ssh_subsystem_sup.erl | 109 +++ lib/ssh/src/ssh_sup.erl | 101 +++ lib/ssh/src/ssh_system_sup.erl | 160 ++++ lib/ssh/src/ssh_transport.erl | 1161 +++++++++++++++++++++++++ lib/ssh/src/ssh_transport.hrl | 235 +++++ lib/ssh/src/ssh_userauth.hrl | 77 ++ lib/ssh/src/ssh_userreg.erl | 127 +++ lib/ssh/src/ssh_xfer.erl | 925 ++++++++++++++++++++ lib/ssh/src/ssh_xfer.hrl | 251 ++++++ lib/ssh/src/sshc_sup.erl | 65 ++ lib/ssh/src/sshd_sup.erl | 111 +++ 47 files changed, 13653 insertions(+) create mode 100755 lib/ssh/src/DSS.asn1 create mode 100644 lib/ssh/src/Makefile create mode 100755 lib/ssh/src/PKCS-1.asn1 create mode 100644 lib/ssh/src/prebuild.skip create mode 100644 lib/ssh/src/ssh.app.src create mode 100644 lib/ssh/src/ssh.appup.src create mode 100644 lib/ssh/src/ssh.erl create mode 100644 lib/ssh/src/ssh.hrl create mode 100644 lib/ssh/src/ssh_acceptor.erl create mode 100644 lib/ssh/src/ssh_acceptor_sup.erl create mode 100644 lib/ssh/src/ssh_app.erl create mode 100644 lib/ssh/src/ssh_auth.erl create mode 100644 lib/ssh/src/ssh_auth.hrl create mode 100755 lib/ssh/src/ssh_bits.erl create mode 100644 lib/ssh/src/ssh_channel.erl create mode 100644 lib/ssh/src/ssh_channel_sup.erl create mode 100644 lib/ssh/src/ssh_cli.erl create mode 100755 lib/ssh/src/ssh_cm.erl create mode 100755 lib/ssh/src/ssh_connect.hrl create mode 100644 lib/ssh/src/ssh_connection.erl create mode 100644 lib/ssh/src/ssh_connection_controler.erl create mode 100644 lib/ssh/src/ssh_connection_handler.erl create mode 100644 lib/ssh/src/ssh_connection_manager.erl create mode 100755 lib/ssh/src/ssh_dsa.erl create mode 100755 lib/ssh/src/ssh_file.erl create mode 100755 lib/ssh/src/ssh_io.erl create mode 100755 lib/ssh/src/ssh_math.erl create mode 100644 lib/ssh/src/ssh_no_io.erl create mode 100755 lib/ssh/src/ssh_rsa.erl create mode 100755 lib/ssh/src/ssh_sftp.erl create mode 100644 lib/ssh/src/ssh_sftpd.erl create mode 100644 lib/ssh/src/ssh_sftpd_file.erl create mode 100644 lib/ssh/src/ssh_sftpd_file_api.erl create mode 100644 lib/ssh/src/ssh_shell.erl create mode 100644 lib/ssh/src/ssh_ssh.erl create mode 100644 lib/ssh/src/ssh_sshd.erl create mode 100644 lib/ssh/src/ssh_subsystem_sup.erl create mode 100644 lib/ssh/src/ssh_sup.erl create mode 100644 lib/ssh/src/ssh_system_sup.erl create mode 100644 lib/ssh/src/ssh_transport.erl create mode 100644 lib/ssh/src/ssh_transport.hrl create mode 100755 lib/ssh/src/ssh_userauth.hrl create mode 100644 lib/ssh/src/ssh_userreg.erl create mode 100644 lib/ssh/src/ssh_xfer.erl create mode 100755 lib/ssh/src/ssh_xfer.hrl create mode 100644 lib/ssh/src/sshc_sup.erl create mode 100644 lib/ssh/src/sshd_sup.erl (limited to 'lib/ssh/src') diff --git a/lib/ssh/src/DSS.asn1 b/lib/ssh/src/DSS.asn1 new file mode 100755 index 0000000000..77aca3808b --- /dev/null +++ b/lib/ssh/src/DSS.asn1 @@ -0,0 +1,20 @@ +DSS DEFINITIONS EXPLICIT TAGS ::= + +BEGIN + +-- EXPORTS ALL +-- All types and values defined in this module are exported for use +-- in other ASN.1 modules. + +DSAPrivateKey ::= SEQUENCE { + version INTEGER, + p INTEGER, -- p + q INTEGER, -- q + g INTEGER, -- q + y INTEGER, -- y + x INTEGER -- x +} + +END + + diff --git a/lib/ssh/src/Makefile b/lib/ssh/src/Makefile new file mode 100644 index 0000000000..7abf06e52b --- /dev/null +++ b/lib/ssh/src/Makefile @@ -0,0 +1,157 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 2004-2009. All Rights Reserved. +# +# The contents of this file are subject to the Erlang Public License, +# Version 1.1, (the "License"); you may not use this file except in +# compliance with the License. You should have received a copy of the +# Erlang Public License along with this software. If not, it can be +# retrieved online at http://www.erlang.org/. +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# %CopyrightEnd% +# + +# +include $(ERL_TOP)/make/target.mk +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN=$(SSH_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/ssh-$(VSN) + +# ---------------------------------------------------- +# Common Macros +# ---------------------------------------------------- + +MODULES= \ + ssh \ + ssh_sup \ + sshc_sup \ + sshd_sup \ + ssh_channel \ + ssh_connection \ + ssh_connection_handler \ + ssh_connection_manager \ + ssh_connection_controler \ + ssh_shell \ + ssh_system_sup \ + ssh_subsystem_sup \ + ssh_channel_sup \ + ssh_acceptor_sup \ + ssh_acceptor \ + ssh_app \ + ssh_auth\ + ssh_bits \ + ssh_cli \ + ssh_cm \ + ssh_dsa \ + ssh_file \ + ssh_io \ + ssh_math \ + ssh_no_io \ + ssh_rsa \ + ssh_sftp \ + ssh_sftpd \ + ssh_sftpd_file\ + ssh_sftpd_file_api \ + ssh_ssh \ + ssh_sshd \ + ssh_transport \ + ssh_userreg \ + ssh_xfer + +PUBLIC_HRL_FILES= ssh.hrl ssh_userauth.hrl ssh_xfer.hrl + +ERL_FILES= $(MODULES:%=%.erl) $(ASN_ERLS) + +ALL_MODULES= $(MODULES) $(ASN_MODULES) + +TARGET_FILES= $(ALL_MODULES:%=$(EBIN)/%.$(EMULATOR)) $(APP_TARGET) $(APPUP_TARGET) + +APP_FILE= ssh.app +APPUP_FILE= ssh.appup + +APP_SRC= $(APP_FILE).src +APP_TARGET= $(EBIN)/$(APP_FILE) + +APPUP_SRC= $(APPUP_FILE).src +APPUP_TARGET= $(EBIN)/$(APPUP_FILE) + +ASN_MODULES = PKCS-1 DSS +ASN_ASNS = $(ASN_MODULES:%=%.asn1) +ASN_ERLS = $(ASN_MODULES:%=%.erl) +ASN_HRLS = $(ASN_MODULES:%=%.hrl) +ASN_DBS = $(ASN_MODULES:%=%.asn1db) +ASN_TABLES = $(ASN_MODULES:%=%.table) + +ASN_FLAGS = -bber_bin +der +compact_bit_string +optimize +noobj +inline + +INTERNAL_HRL_FILES = $(ASN_HRLS) ssh_auth.hrl ssh_connect.hrl ssh_transport.hrl + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- +ERL_COMPILE_FLAGS += -pa$(EBIN) + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug opt: $(TARGET_FILES) + +debug: ERLC_FLAGS += -Ddebug + +clean: + rm -f $(TARGET_FILES) + rm -f errs core *~ + rm -f $(ASN_ERLS) $(ASN_HRLS) $(ASN_DBS) + +$(TARGET_FILES): ssh.hrl + +# $(EBIN)/ssh_sftpd_file.$(EMULATOR): ERLC_FLAGS += -pa$(EBIN) +# $(EBIN)/ssh_sftpd_file.$(EMULATOR): $(EBIN)/ssh_sftpd_file_api.$(EMULATOR) + +$(APP_TARGET): $(APP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +%.hrl: %.asn1 + erlc $(ASN_FLAGS) $< + +DSS.hrl DSS.erl: DSS.asn1 +PKCS-1.hrl PKCS-1.erl: PKCS-1.asn1 + +$(EBIN)/ssh_file.$(EMULATOR): $(ASN_HRLS) + +docs: + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DATA) $(INTERNAL_HRL_FILES) $(ERL_FILES) $(RELSYSDIR)/src + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin + $(INSTALL_DIR) $(RELSYSDIR)/include + $(INSTALL_DATA) $(PUBLIC_HRL_FILES) $(RELSYSDIR)/include + +release_docs_spec: + diff --git a/lib/ssh/src/PKCS-1.asn1 b/lib/ssh/src/PKCS-1.asn1 new file mode 100755 index 0000000000..e7d6b18c63 --- /dev/null +++ b/lib/ssh/src/PKCS-1.asn1 @@ -0,0 +1,116 @@ +PKCS-1 { + iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) + modules(0) pkcs-1(1) +} + +-- $Revision: 1.1 $ + +DEFINITIONS EXPLICIT TAGS ::= + +BEGIN + +-- IMPORTS id-sha256, id-sha384, id-sha512 +-- FROM NIST-SHA2 { +-- joint-iso-itu-t(2) country(16) us(840) organization(1) +-- gov(101) csor(3) nistalgorithm(4) modules(0) sha2(1) +-- }; + +pkcs-1 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1 +} + +rsaEncryption OBJECT IDENTIFIER ::= { pkcs-1 1 } + +id-RSAES-OAEP OBJECT IDENTIFIER ::= { pkcs-1 7 } + +id-pSpecified OBJECT IDENTIFIER ::= { pkcs-1 9 } + +id-RSASSA-PSS OBJECT IDENTIFIER ::= { pkcs-1 10 } + +md2WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 2 } +md5WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 4 } +sha1WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 5 } +sha256WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 11 } +sha384WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 12 } +sha512WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 13 } + +id-sha1 OBJECT IDENTIFIER ::= { + iso(1) identified-organization(3) oiw(14) secsig(3) + algorithms(2) 26 +} + +id-md2 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) rsadsi(113549) digestAlgorithm(2) 2 +} + +id-md5 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) rsadsi(113549) digestAlgorithm(2) 5 +} + +id-mgf1 OBJECT IDENTIFIER ::= { pkcs-1 8 } + + +RSAPublicKey ::= SEQUENCE { + modulus INTEGER, -- n + publicExponent INTEGER -- e +} + +RSAPrivateKey ::= SEQUENCE { + version Version, + modulus INTEGER, -- n + publicExponent INTEGER, -- e + privateExponent INTEGER, -- d + prime1 INTEGER, -- p + prime2 INTEGER, -- q + exponent1 INTEGER, -- d mod (p-1) + exponent2 INTEGER, -- d mod (q-1) + coefficient INTEGER, -- (inverse of q) mod p + otherPrimeInfos OtherPrimeInfos OPTIONAL +} + +Version ::= INTEGER { two-prime(0), multi(1) } + (CONSTRAINED BY { + -- version must be multi if otherPrimeInfos present -- + }) + +OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo + +OtherPrimeInfo ::= SEQUENCE { + prime INTEGER, -- ri + exponent INTEGER, -- di + coefficient INTEGER -- ti +} + +Algorithm ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL +} + +AlgorithmNull ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters NULL +} + + +RSASSA-PSS-params ::= SEQUENCE { + hashAlgorithm [0] Algorithm, -- DEFAULT sha1, + maskGenAlgorithm [1] Algorithm, -- DEFAULT mgf1SHA1, + saltLength [2] INTEGER DEFAULT 20, + trailerField [3] TrailerField DEFAULT trailerFieldBC +} + +TrailerField ::= INTEGER { trailerFieldBC(1) } + +DigestInfo ::= SEQUENCE { + digestAlgorithm Algorithm, + digest OCTET STRING +} + +DigestInfoNull ::= SEQUENCE { + digestAlgorithm AlgorithmNull, + digest OCTET STRING +} + + +END -- PKCS1Definitions + diff --git a/lib/ssh/src/prebuild.skip b/lib/ssh/src/prebuild.skip new file mode 100644 index 0000000000..1d7552d98d --- /dev/null +++ b/lib/ssh/src/prebuild.skip @@ -0,0 +1,2 @@ +DSS.asn1db +PKCS-1.asn1db diff --git a/lib/ssh/src/ssh.app.src b/lib/ssh/src/ssh.app.src new file mode 100644 index 0000000000..9319f39591 --- /dev/null +++ b/lib/ssh/src/ssh.app.src @@ -0,0 +1,48 @@ +%%% This is an -*- erlang -*- file. + +{application, ssh, + [{description, "SSH-2 for Erlang/OTP"}, + {vsn, "%VSN%"}, + {modules, ['DSS', + 'PKCS-1', + ssh, + ssh_app, + ssh_acceptor, + ssh_acceptor_sup, + ssh_auth, + ssh_bits, + ssh_cli, + ssh_channel, + ssh_channel_sup, + ssh_cm, + ssh_connection, + ssh_connection_handler, + ssh_connection_manager, + ssh_connection_controler, + ssh_shell, + sshc_sup, + sshd_sup, + ssh_dsa, + ssh_file, + ssh_io, + ssh_math, + ssh_no_io, + ssh_rsa, + ssh_sftp, + ssh_sftpd, + ssh_sftpd_file, + ssh_sftpd_file_api, + ssh_ssh, + ssh_sshd, + ssh_subsystem_sup, + ssh_sup, + ssh_system_sup, + ssh_transport, + ssh_userreg, + ssh_xfer]}, + {registered, []}, + {applications, [kernel, stdlib, crypto]}, + {env, []}, + {mod, {ssh_app, []}}]}. + + diff --git a/lib/ssh/src/ssh.appup.src b/lib/ssh/src/ssh.appup.src new file mode 100644 index 0000000000..3bf772b42b --- /dev/null +++ b/lib/ssh/src/ssh.appup.src @@ -0,0 +1,37 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +{"%VSN%", + [ + {"1.1.6", [{restart_application, ssh}]}, + {"1.1.5", [{restart_application, ssh}]}, + {"1.1.4", [{restart_application, ssh}]}, + {"1.1.3", [{restart_application, ssh}]}, + {"1.1.2", [{restart_application, ssh}]} + ], + [ + {"1.1.6", [{restart_application, ssh}]}, + {"1.1.5", [{restart_application, ssh}]}, + {"1.1.4", [{restart_application, ssh}]}, + {"1.1.3", [{restart_application, ssh}]}, + {"1.1.2", [{restart_application, ssh}]} + ] +}. + + diff --git a/lib/ssh/src/ssh.erl b/lib/ssh/src/ssh.erl new file mode 100644 index 0000000000..f9a986a8b6 --- /dev/null +++ b/lib/ssh/src/ssh.erl @@ -0,0 +1,339 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). + +-export([start/0, start/1, stop/0, connect/3, close/1, connection_info/2, + channel_info/3, + daemon/1, daemon/2, daemon/3, + stop_listener/1, stop_listener/2, stop_daemon/1, stop_daemon/2, + shell/1, shell/2, shell/3]). + +%%-------------------------------------------------------------------- +%% Function: start([, Type]) -> ok +%% +%% Type = permanent | transient | temporary +%% +%% Description: Starts the inets application. Default type +%% is temporary. see application(3) +%%-------------------------------------------------------------------- +start() -> + application:start(ssh). + +start(Type) -> + application:start(ssh, Type). + +%%-------------------------------------------------------------------- +%% Function: stop() -> ok +%% +%% Description: Stops the inets application. +%%-------------------------------------------------------------------- +stop() -> + application:stop(ssh). + +%%-------------------------------------------------------------------- +%% Function: connect(Host, Port, Options) -> +%% connect(Host, Port, Options, Timeout -> ConnectionRef | {error, Reason} +%% +%% Host - string() +%% Port - integer() +%% Options - [{Option, Value}] +%% Timeout - infinity | integer(). +%% +%% Description: Starts an ssh connection. +%%-------------------------------------------------------------------- +connect(Host, Port, Options) -> + connect(Host, Port, Options, infinity). +connect(Host, Port, Options, Timeout) -> + {SocketOpts, Opts} = handle_options(Options), + DisableIpv6 = proplists:get_value(ip_v6_disabled, Opts, false), + Inet = inetopt(DisableIpv6), + try sshc_sup:start_child([[{address, Host}, {port, Port}, + {role, client}, + {channel_pid, self()}, + {socket_opts, [Inet | SocketOpts]}, + {ssh_opts, [{host, Host}| Opts]}]]) of + {ok, ConnectionSup} -> + {ok, Manager} = + ssh_connection_controler:connection_manager(ConnectionSup), + MRef = erlang:monitor(process, Manager), + receive + {Manager, is_connected} -> + do_demonitor(MRef, Manager), + {ok, Manager}; + %% When the connection fails + %% ssh_connection_sup:connection_manager + %% might return undefined as the connection manager + %% could allready have terminated, so we will not + %% match the Manager in this case + {_, not_connected, {error, Reason}} -> + do_demonitor(MRef, Manager), + {error, Reason}; + {_, not_connected, Other} -> + do_demonitor(MRef, Manager), + {error, Other}; + {'DOWN', MRef, _, Manager, Reason} when is_pid(Manager) -> + receive %% Clear EXIT message from queue + {'EXIT', Manager, _What} -> + {error, Reason} + after 0 -> + {error, Reason} + end + after Timeout -> + do_demonitor(MRef, Manager), + ssh_connection_manager:stop(Manager), + {error, timeout} + end + catch + exit:{noproc, _} -> + {error, ssh_not_started} + end. + +do_demonitor(MRef, Manager) -> + erlang:demonitor(MRef), + receive + {'DOWN', MRef, _, Manager, _} -> + ok + after 0 -> + ok + end. + + +%%-------------------------------------------------------------------- +%% Function: close(ConnectionRef) -> ok +%% +%% Description: Closes an ssh connection. +%%-------------------------------------------------------------------- +close(ConnectionRef) -> + ssh_connection_manager:stop(ConnectionRef). + +%%-------------------------------------------------------------------- +%% Function: connection_info(ConnectionRef) -> [{Option, Value}] +%% +%% Description: Retrieves information about a connection. +%%-------------------------------------------------------------------- +connection_info(ConnectionRef, Options) -> + ssh_connection_manager:connection_info(ConnectionRef, Options). + +%%-------------------------------------------------------------------- +%% Function: channel_info(ConnectionRef) -> [{Option, Value}] +%% +%% Description: Retrieves information about a connection. +%%-------------------------------------------------------------------- +channel_info(ConnectionRef, ChannelId, Options) -> + ssh_connection_manager:channel_info(ConnectionRef, ChannelId, Options). + +%%-------------------------------------------------------------------- +%% Function: daemon(Port) -> +%% daemon(Port, Options) -> +%% daemon(Address, Port, Options) -> SshSystemRef +%% +%% Description: Starts a server listening for SSH connections +%% on the given port. +%%-------------------------------------------------------------------- +daemon(Port) -> + daemon(Port, []). + +daemon(Port, Options) -> + daemon(any, Port, Options). + +daemon(HostAddr, Port, Options0) -> + Options1 = case proplists:get_value(shell, Options0) of + undefined -> + [{shell, {shell, start, []}} | Options0]; + _ -> + Options0 + end, + DisableIpv6 = proplists:get_value(ip_v6_disabled, Options0, false), + {Host, Inet, Options} = case HostAddr of + any -> + {ok, Host0} = inet:gethostname(), + {Host0, inetopt(DisableIpv6), Options1}; + {_,_,_,_} -> + {HostAddr, inet, + [{ip, HostAddr} | Options1]}; + {_,_,_,_,_,_,_,_} -> + {HostAddr, inet6, + [{ip, HostAddr} | Options1]} + end, + start_daemon(Host, Port, [{role, server} | Options], Inet). + +%%-------------------------------------------------------------------- +%% Function: stop_listener(SysRef) -> ok +%% stop_listener(Address, Port) -> ok +%% +%% +%% Description: Stops the listener, but leaves +%% existing connections started by the listener up and running. +%%-------------------------------------------------------------------- +stop_listener(SysSup) -> + ssh_system_sup:stop_listener(SysSup). +stop_listener(Address, Port) -> + ssh_system_sup:stop_listener(Address, Port). + +%%-------------------------------------------------------------------- +%% Function: stop_daemon(SysRef) -> ok +%%% stop_daemon(Address, Port) -> ok +%% +%% +%% Description: Stops the listener and all connections started by +%% the listener. +%%-------------------------------------------------------------------- +stop_daemon(SysSup) -> + ssh_system_sup:stop_system(SysSup). +stop_daemon(Address, Port) -> + ssh_system_sup:stop_system(Address, Port). + +%%-------------------------------------------------------------------- +%% Function: shell(Host [,Port,Options]) -> {ok, ConnectionRef} | +%% {error, Reason} +%% +%% Host = string() +%% Port = integer() +%% Options = [{Option, Value}] +%% +%% Description: Starts an interactive shell to an SSH server on the +%% given . The function waits for user input, +%% and will not return until the remote shell is ended.(e.g. on +%% exit from the shell) +%%-------------------------------------------------------------------- +shell(Host) -> + shell(Host, ?SSH_DEFAULT_PORT, []). +shell(Host, Options) -> + shell(Host, ?SSH_DEFAULT_PORT, Options). +shell(Host, Port, Options) -> + case connect(Host, Port, Options) of + {ok, ConnectionRef} -> + case ssh_connection:session_channel(ConnectionRef, infinity) of + {ok,ChannelId} -> + Args = [{channel_cb, ssh_shell}, + {init_args,[ConnectionRef, ChannelId]}, + {cm, ConnectionRef}, {channel_id, ChannelId}], + {ok, State} = ssh_channel:init([Args]), + ssh_channel:enter_loop(State); + Error -> + Error + end; + Error -> + Error + end. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +start_daemon(Host, Port, Options, Inet) -> + {SocketOpts, Opts} = handle_options(Options), + case ssh_system_sup:system_supervisor(Host, Port) of + undefined -> + %% TODO: It would proably make more sense to call the + %% address option host but that is a too big change at the + %% monent. The name is a legacy name! + try sshd_sup:start_child([{address, Host}, + {port, Port}, {role, server}, + {socket_opts, [Inet | SocketOpts]}, + {ssh_opts, Opts}]) of + {ok, SysSup} -> + {ok, SysSup}; + {error, {already_started, _}} -> + {error, eaddrinuse} + catch + exit:{noproc, _} -> + {error, ssh_not_started} + end; + Sup -> + case ssh_system_sup:restart_acceptor(Host, Port) of + {ok, _} -> + {ok, Sup}; + _ -> + {error, eaddrinuse} + end + end. + +handle_options(Opts) -> + handle_options(proplists:unfold(Opts), [], []). +handle_options([], SockOpts, Opts) -> + {SockOpts, Opts}; +%% TODO: Could do some type checks here on plain ssh-opts +handle_options([{system_dir, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user_dir, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user_dir_fun, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{silently_accept_hosts, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user_interaction, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{public_key_alg, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{connect_timeout, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{password, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user_passwords, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{pwdfun, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{user_auth, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{key_cb, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{role, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{channel, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{compression, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{allow_user_interaction, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{infofun, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{connectfun, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{disconnectfun , _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{failfun, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{ip_v6_disabled, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]); +handle_options([{ip, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, [Opt |SockOpts], Opts); +handle_options([{ifaddr, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, [Opt |SockOpts], Opts); +handle_options([{fd, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, [Opt | SockOpts], Opts); +handle_options([{nodelay, _} = Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, [Opt | SockOpts], Opts); +handle_options([Opt | Rest], SockOpts, Opts) -> + handle_options(Rest, SockOpts, [Opt | Opts]). + +inetopt(true) -> + inet6; + +inetopt(false) -> + inet. + + diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl new file mode 100644 index 0000000000..0e4285295c --- /dev/null +++ b/lib/ssh/src/ssh.hrl @@ -0,0 +1,180 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%% +%% SSH definitions +%% + +-ifndef(SSH_HRL). +-define(SSH_HRL, 1). + +-define(SSH_DEFAULT_PORT, 22). +-define(SSH_MAX_PACKET_SIZE, (256*1024)). +-define(SSH_LENGHT_INDICATOR_SIZE, 4). + +-define(FALSE, 0). +-define(TRUE, 1). +%% basic binary constructors +-define(BOOLEAN(X), X:8/unsigned-big-integer). +-define(BYTE(X), X:8/unsigned-big-integer). +-define(UINT16(X), X:16/unsigned-big-integer). +-define(UINT32(X), X:32/unsigned-big-integer). +-define(UINT64(X), X:64/unsigned-big-integer). +-define(STRING(X), ?UINT32((size(X))), (X)/binary). + +%% building macros +-define(boolean(X), + case X of + true -> <>; + false -> (<>) + end). + +-define(byte(X), << ?BYTE(X) >> ). +-define(uint16(X), << ?UINT16(X) >> ). +-define(uint32(X), << ?UINT32(X) >> ). +-define(uint64(X), << ?UINT64(X) >> ). +-define(string(X), << ?STRING(list_to_binary(X)) >> ). +-define(binary(X), << ?STRING(X) >>). + +-ifdef(debug). +-define(dbg(Debug, Fmt, As), + case (Debug) of + true -> + io:format([$# | (Fmt)], (As)); + _ -> + ok + end). +-else. +-define(dbg(Debug, Fmt, As), ok). +-endif. + +-define(SSH_CIPHER_NONE, 0). +-define(SSH_CIPHER_3DES, 3). +-define(SSH_CIPHER_AUTHFILE, ?SSH_CIPHER_3DES). + +-record(ssh, + { + %%state, %% what it's waiting for + + role, %% client | server + peer, %% string version of peer address + + c_vsn, %% client version {Major,Minor} + s_vsn, %% server version {Major,Minor} + + c_version, %% client version string + s_version, %% server version string + + c_keyinit, %% binary payload of kexinit packet + s_keyinit, %% binary payload of kexinit packet + + algorithms, %% #alg{} + + kex, %% key exchange algorithm + hkey, %% host key algorithm + key_cb, %% Private/Public key callback module + io_cb, %% Interaction callback module + + send_mac = none, %% send MAC algorithm + send_mac_key, %% key used in send MAC algorithm + send_mac_size = 0, + + recv_mac = none, %% recv MAC algorithm + recv_mac_key, %% key used in recv MAC algorithm + recv_mac_size = 0, + + encrypt = none, %% encrypt algorithm + encrypt_keys, %% encrypt keys + encrypt_block_size = 8, + encrypt_ctx, + + decrypt = none, %% decrypt algorithm + decrypt_keys, %% decrypt keys + decrypt_block_size = 8, + decrypt_ctx, %% Decryption context + + compress = none, + compress_ctx, + decompress = none, + decompress_ctx, + + c_lng=none, %% client to server languages + s_lng=none, %% server to client languages + + user_ack = true, %% client + timeout = infinity, + + shared_secret, %% K from key exchange + exchanged_hash, %% H from key exchange + session_id, %% same as FIRST exchanged_hash + + opts = [], + send_sequence = 0, + recv_sequence = 0, + keyex_key, + keyex_info, + + %% User auth + user, + service, + userauth_quiet_mode, % boolean() + userauth_supported_methods , % + userauth_methods, + userauth_preference + }). + +-record(alg, + { + kex, + hkey, + send_mac, + recv_mac, + encrypt, + decrypt, + compress, + decompress, + c_lng, + s_lng + }). + +-record(ssh_key, + { + type, + public, + private, + comment = "" + }). + +-record(ssh_pty, {term = "", % e.g. "xterm" + width = 80, + height = 25, + pixel_width = 1024, + pixel_height = 768, + modes = <<>>}). + +%% assertion macro +-define(ssh_assert(Expr, Reason), + case Expr of + true -> ok; + _ -> exit(Reason) + end). + +-endif. % SSH_HRL defined diff --git a/lib/ssh/src/ssh_acceptor.erl b/lib/ssh/src/ssh_acceptor.erl new file mode 100644 index 0000000000..d19fee14e1 --- /dev/null +++ b/lib/ssh/src/ssh_acceptor.erl @@ -0,0 +1,115 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_acceptor). + +%% Internal application API +-export([start_link/5]). + +%% spawn export +%% TODO: system messages +-export([acceptor_init/6, acceptor_loop/6]). + +-define(SLEEP_TIME, 200). + +%%==================================================================== +%% Internal application API +%%==================================================================== +start_link(Port, Address, SockOpts, Opts, AcceptTimeout) -> + Args = [self(), Port, Address, SockOpts, Opts, AcceptTimeout], + proc_lib:start_link(?MODULE, acceptor_init, Args). + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +acceptor_init(Parent, Port, Address, SockOpts, Opts, AcceptTimeout) -> + {_, Callback, _} = + proplists:get_value(transport, Opts, {tcp, gen_tcp, tcp_closed}), + case (catch do_socket_listen(Callback, Port, SockOpts)) of + {ok, ListenSocket} -> + proc_lib:init_ack(Parent, {ok, self()}), + acceptor_loop(Callback, + Port, Address, Opts, ListenSocket, AcceptTimeout); + Error -> + proc_lib:init_ack(Parent, Error), + error + end. + +do_socket_listen(Callback, Port, Opts) -> + case Callback:listen(Port, Opts) of + {error, eafnosupport} -> + Callback:listen(Port, lists:delete(inet6, Opts)); + Other -> + Other + end. + +acceptor_loop(Callback, Port, Address, Opts, ListenSocket, AcceptTimeout) -> + case (catch Callback:accept(ListenSocket, AcceptTimeout)) of + {ok, Socket} -> + handle_connection(Callback, Address, Port, Opts, Socket), + ?MODULE:acceptor_loop(Callback, Port, Address, Opts, + ListenSocket, AcceptTimeout); + {error, Reason} -> + handle_error(Reason), + ?MODULE:acceptor_loop(Callback, Port, Address, Opts, + ListenSocket, AcceptTimeout); + {'EXIT', Reason} -> + handle_error(Reason), + ?MODULE:acceptor_loop(Callback, Port, Address, Opts, + ListenSocket, AcceptTimeout) + end. + +handle_connection(Callback, Address, Port, Options, Socket) -> + SystemSup = ssh_system_sup:system_supervisor(Address, Port), + ssh_system_sup:start_subsystem(SystemSup, Options), + ConnectionSup = ssh_system_sup:connection_supervisor(SystemSup), + {ok, Pid} = + ssh_connection_controler:start_manager_child(ConnectionSup, + [server, Socket, Options]), + Callback:controlling_process(Socket, Pid), + SshOpts = proplists:get_value(ssh_opts, Options), + Pid ! {start_connection, server, [Address, Port, Socket, SshOpts]}. + +handle_error(timeout) -> + ok; + +handle_error(enfile) -> + %% Out of sockets... + timer:sleep(?SLEEP_TIME); + +handle_error(emfile) -> + %% Too many open files -> Out of sockets... + timer:sleep(?SLEEP_TIME); + +handle_error(closed) -> + error_logger:info_report("The ssh accept socket was closed by " + "a third party. " + "This will not have an impact on ssh " + "that will open a new accept socket and " + "go on as nothing happened. It does however " + "indicate that some other software is behaving " + "badly."), + exit(normal); + +handle_error(Reason) -> + String = lists:flatten(io_lib:format("Accept error: ~p", [Reason])), + error_logger:error_report(String), + exit({accept_failed, String}). diff --git a/lib/ssh/src/ssh_acceptor_sup.erl b/lib/ssh/src/ssh_acceptor_sup.erl new file mode 100644 index 0000000000..707e3d3a5e --- /dev/null +++ b/lib/ssh/src/ssh_acceptor_sup.erl @@ -0,0 +1,95 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The acceptor supervisor for ssh servers hangs under +%% ssh_system_sup. +%%---------------------------------------------------------------------- + +-module(ssh_acceptor_sup). +-behaviour(supervisor). + +-export([start_link/1, start_child/2, stop_child/2]). + +%% Supervisor callback +-export([init/1]). + +-define(DEFAULT_TIMEOUT, 50000). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(Servers) -> + supervisor:start_link(?MODULE, [Servers]). + +start_child(AccSup, ServerOpts) -> + Spec = child_spec(ServerOpts), + case supervisor:start_child(AccSup, Spec) of + {error, already_present} -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + Name = id(Address, Port), + supervisor:delete_child(?MODULE, Name), + supervisor:start_child(AccSup, Spec); + Reply -> + Reply + end. + +stop_child(Address, Port) -> + Name = id(Address, Port), + case supervisor:terminate_child(?MODULE, Name) of + ok -> + supervisor:delete_child(?MODULE, Name); + Error -> + Error + end. + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([ServerOpts]) -> + RestartStrategy = one_for_one, + MaxR = 10, + MaxT = 3600, + Children = [child_spec(ServerOpts)], + {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +child_spec(ServerOpts) -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + Timeout = proplists:get_value(timeout, ServerOpts, ?DEFAULT_TIMEOUT), + Name = id(Address, Port), + SocketOpts = proplists:get_value(socket_opts, ServerOpts), + StartFunc = {ssh_acceptor, start_link, [Port, Address, + [{active, false}, + {reuseaddr, true}] ++ SocketOpts, + ServerOpts, Timeout]}, + Restart = permanent, + Shutdown = 3600, + Modules = [ssh_acceptor], + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +id(Address, Port) -> + {ssh_acceptor_sup, Address, Port}. + diff --git a/lib/ssh/src/ssh_app.erl b/lib/ssh/src/ssh_app.erl new file mode 100644 index 0000000000..5793d3a321 --- /dev/null +++ b/lib/ssh/src/ssh_app.erl @@ -0,0 +1,34 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%% Purpose : Application master for SSH. + +-module(ssh_app). + +-behaviour(application). + +-export([start/2, stop/1]). + +start(_Type, _State) -> + supervisor:start_link({local, ssh_sup}, ssh_sup, []). + +stop(_State) -> + ok. diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl new file mode 100644 index 0000000000..aa74528544 --- /dev/null +++ b/lib/ssh/src/ssh_auth.erl @@ -0,0 +1,423 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_auth). + +-include("ssh.hrl"). + +-include("ssh_auth.hrl"). +-include("ssh_transport.hrl"). + +-export([publickey_msg/1, password_msg/1, keyboard_interactive_msg/1, + service_request_msg/1, init_userauth_request_msg/1, + userauth_request_msg/1, handle_userauth_request/3, + handle_userauth_info_request/3, handle_userauth_info_response/2, + userauth_messages/0 + ]). + +%%-------------------------------------------------------------------- +%%% Internal application API +%%-------------------------------------------------------------------- +publickey_msg([Cb, #ssh{user = User, + session_id = SessionId, + service = Service, + opts = Opts} = Ssh]) -> + ssh_bits:install_messages(userauth_pk_messages()), + Alg = Cb:alg_name(), + case ssh_file:private_identity_key(Alg, Opts) of + {ok, PrivKey} -> + PubKeyBlob = ssh_file:encode_public_key(PrivKey), + SigData = build_sig_data(SessionId, + User, Service, Alg, PubKeyBlob), + Sig = Cb:sign(PrivKey, SigData), + SigBlob = list_to_binary([?string(Alg), ?binary(Sig)]), + ssh_transport:ssh_packet( + #ssh_msg_userauth_request{user = User, + service = Service, + method = "publickey", + data = [?TRUE, + ?string(Alg), + ?binary(PubKeyBlob), + ?binary(SigBlob)]}, + Ssh); + _Error -> + not_ok + end. + +password_msg([#ssh{opts = Opts, io_cb = IoCb, + user = User, service = Service} = Ssh]) -> + ssh_bits:install_messages(userauth_passwd_messages()), + Password = case proplists:get_value(password, Opts) of + undefined -> + IoCb:read_password("ssh password: "); + PW -> + PW + end, + ssh_transport:ssh_packet( + #ssh_msg_userauth_request{user = User, + service = Service, + method = "password", + data = + <>}, + Ssh). + +%% See RFC 4256 for info on keyboard-interactive +keyboard_interactive_msg([#ssh{user = User, + service = Service} = Ssh]) -> + ssh_bits:install_messages(userauth_keyboard_interactive_messages()), + ssh_transport:ssh_packet( + #ssh_msg_userauth_request{user = User, + service = Service, + method = "keyboard-interactive", + data = << ?STRING(<<"">>), + ?STRING(<<>>) >> }, + Ssh). + +service_request_msg(Ssh) -> + ssh_transport:ssh_packet(#ssh_msg_service_request{name = "ssh-userauth"}, + Ssh#ssh{service = "ssh-userauth"}). + +init_userauth_request_msg(#ssh{opts = Opts} = Ssh) -> + case user_name(Opts) of + {ok, User} -> + Msg = #ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "none", + data = <<>>}, + CbFirst = proplists:get_value(public_key_alg, Opts, + ?PREFERRED_PK_ALG), + CbSecond = other_cb(CbFirst), + AllowUserInt = proplists:get_value(allow_user_interaction, Opts, + true), + Prefs = method_preference(CbFirst, CbSecond, AllowUserInt), + ssh_transport:ssh_packet(Msg, Ssh#ssh{user = User, + userauth_preference = Prefs, + userauth_methods = none, + service = "ssh-connection"}); + {error, no_user} -> + ErrStr = "Could not determine the users name", + throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_ILLEGAL_USER_NAME, + description = ErrStr, + language = "en"}) + end. + +userauth_request_msg(#ssh{userauth_preference = []} = Ssh) -> + Msg = #ssh_msg_disconnect{code = + ?SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + description = "Unable to connect using the available" + " authentication methods", + language = "en"}, + {disconnect, Msg, ssh_transport:ssh_packet(Msg, Ssh)}; + +userauth_request_msg(#ssh{userauth_methods = Methods, + userauth_preference = [{Pref, Module, + Function, Args} | Prefs]} + = Ssh0) -> + Ssh = Ssh0#ssh{userauth_preference = Prefs}, + case lists:member(Pref, Methods) of + true -> + case Module:Function(Args ++ [Ssh]) of + not_ok -> + userauth_request_msg(Ssh); + Result -> + Result + end; + false -> + userauth_request_msg(Ssh) + end. + + +handle_userauth_request(#ssh_msg_service_request{name = + Name = "ssh-userauth"}, + _, Ssh) -> + {ok, ssh_transport:ssh_packet(#ssh_msg_service_accept{name = Name}, + Ssh#ssh{service = "ssh-connection"})}; + +handle_userauth_request(#ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "password", + data = Data}, _, + #ssh{opts = Opts} = Ssh) -> + <<_:8, ?UINT32(Sz), BinPwd:Sz/binary>> = Data, + Password = binary_to_list(BinPwd), + + case check_password(User, Password, Opts) of + true -> + {authorized, User, + ssh_transport:ssh_packet(#ssh_msg_userauth_success{}, Ssh)}; + false -> + {not_authorized, {User, {passwd, Password}}, + ssh_transport:ssh_packet(#ssh_msg_userauth_failure{ + authentications = "", + partial_success = false}, Ssh)} + end; + +handle_userauth_request(#ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "none"}, _, + #ssh{userauth_supported_methods = Methods} = Ssh) -> + {not_authorized, {User, undefined}, + ssh_transport:ssh_packet( + #ssh_msg_userauth_failure{authentications = Methods, + partial_success = false}, Ssh)}; + +handle_userauth_request(#ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = "publickey", + data = Data}, + SessionId, #ssh{opts = Opts} = Ssh) -> + <> = Data, + Alg = binary_to_list(BAlg), + case HaveSig of + ?TRUE -> + case verify_sig(SessionId, User, "ssh-connection", Alg, + KeyBlob, SigWLen, Opts) of + ok -> + {authorized, User, + ssh_transport:ssh_packet( + #ssh_msg_userauth_success{}, Ssh)}; + {error, Reason} -> + {not_authorized, {User, {error, Reason}}, + ssh_transport:ssh_packet(#ssh_msg_userauth_failure{ + authentications="publickey,password", + partial_success = false}, Ssh)} + end; + ?FALSE -> + ssh_bits:install_messages(userauth_pk_messages()), + {not_authorized, {User, undefined}, + ssh_transport:ssh_packet( + #ssh_msg_userauth_pk_ok{algorithm_name = Alg, + key_blob = KeyBlob}, Ssh)} + end; + +handle_userauth_request(#ssh_msg_userauth_request{user = User, + service = "ssh-connection", + method = Other}, _, + #ssh{userauth_supported_methods = Methods} = Ssh) -> + {not_authorized, {User, {authmethod, Other}}, + ssh_transport:ssh_packet( + #ssh_msg_userauth_failure{authentications = Methods, + partial_success = false}, Ssh)}. + +handle_userauth_info_request( + #ssh_msg_userauth_info_request{name = Name, + instruction = Instr, + num_prompts = NumPrompts, + data = Data}, IoCb, + #ssh{opts = Opts} = Ssh) -> + PromptInfos = decode_keyboard_interactive_prompts(NumPrompts,Data), + Resps = keyboard_interact_get_responses(IoCb, Opts, + Name, Instr, PromptInfos), + %%?dbg(true, "keyboard_interactive_reply: resps=~n#~p ~n", [Resps]), + RespBin = list_to_binary( + lists:map(fun(S) -> <> end, + Resps)), + {ok, + ssh_transport:ssh_packet( + #ssh_msg_userauth_info_response{num_responses = NumPrompts, + data = RespBin}, Ssh)}. + +handle_userauth_info_response(#ssh_msg_userauth_info_response{}, + _Auth) -> + throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, + description = "Server does not support" + "keyboard-interactive", + language = "en"}). +userauth_messages() -> + [ {ssh_msg_userauth_request, ?SSH_MSG_USERAUTH_REQUEST, + [string, + string, + string, + '...']}, + + {ssh_msg_userauth_failure, ?SSH_MSG_USERAUTH_FAILURE, + [string, + boolean]}, + + {ssh_msg_userauth_success, ?SSH_MSG_USERAUTH_SUCCESS, + []}, + + {ssh_msg_userauth_banner, ?SSH_MSG_USERAUTH_BANNER, + [string, + string]}]. +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +method_preference(Callback1, Callback2, true) -> + [{"publickey", ?MODULE, publickey_msg, [Callback1]}, + {"publickey", ?MODULE, publickey_msg,[Callback2]}, + {"password", ?MODULE, password_msg, []}, + {"keyboard-interactive", ?MODULE, keyboard_interactive_msg, []} + ]; +method_preference(Callback1, Callback2, false) -> + [{"publickey", ?MODULE, publickey_msg, [Callback1]}, + {"publickey", ?MODULE, publickey_msg,[Callback2]}, + {"password", ?MODULE, password_msg, []} + ]. + +user_name(Opts) -> + Env = case os:type() of + {win32, _} -> + "USERNAME"; + {unix, _} -> + "LOGNAME" + end, + case proplists:get_value(user, Opts, os:getenv(Env)) of + false -> + case os:getenv("USER") of + false -> + {error, no_user}; + User -> + {ok, User} + end; + User -> + {ok, User} + end. + +check_password(User, Password, Opts) -> + %%?dbg(true, " ~p ~p ~p ~n", [User, Password, Opts]), + case proplists:get_value(pwdfun, Opts) of + undefined -> + Static = get_password_option(Opts, User), + Password == Static; + Cheker -> + Cheker(User, Password) + end. + +get_password_option(Opts, User) -> + Passwords = proplists:get_value(user_passwords, Opts, []), + case lists:keysearch(User, 1, Passwords) of + {value, {User, Pw}} -> Pw; + false -> proplists:get_value(password, Opts, false) + end. + +verify_sig(SessionId, User, Service, Alg, KeyBlob, SigWLen, Opts) -> + case ssh_file:lookup_user_key(User, Alg, Opts) of + {ok, OurKey} -> + {ok, Key} = ssh_file:decode_public_key_v2(KeyBlob, Alg), + case OurKey of + Key -> + NewSig = build_sig_data(SessionId, + User, Service, Alg, KeyBlob), + <> = SigWLen, + <> = AlgSig, + M = alg_to_module(Alg), + M:verify(OurKey, NewSig, Sig); + _ -> + {error, key_unacceptable} + end; + Error -> Error + end. + +build_sig_data(SessionId, User, Service, Alg, KeyBlob) -> + Sig = [?binary(SessionId), + ?SSH_MSG_USERAUTH_REQUEST, + ?string(User), + ?string(Service), + ?binary(<<"publickey">>), + ?TRUE, + ?string(Alg), + ?binary(KeyBlob)], + list_to_binary(Sig). + +decode_keyboard_interactive_prompts(NumPrompts, Data) -> + Types = lists:append(lists:duplicate(NumPrompts, [string, boolean])), + pairwise_tuplify(ssh_bits:decode(Data, Types)). + +pairwise_tuplify([E1, E2 | Rest]) -> [{E1, E2} | pairwise_tuplify(Rest)]; +pairwise_tuplify([]) -> []. + + +keyboard_interact_get_responses(IoCb, Opts, Name, Instr, PromptInfos) -> + NumPrompts = length(PromptInfos), + case proplists:get_value(keyboard_interact_fun, Opts) of + undefined when NumPrompts == 1 -> + %% Special case/fallback for just one prompt + %% (assumed to be the password prompt) + case proplists:get_value(password, Opts) of + undefined -> keyboard_interact(IoCb, Name, Instr, PromptInfos); + PW -> [PW] + end; + undefined -> + keyboard_interact(IoCb, Name, Instr, PromptInfos); + KbdInteractFun -> + Prompts = lists:map(fun({Prompt, _Echo}) -> Prompt end, + PromptInfos), + case KbdInteractFun(Name, Instr, Prompts) of + Rs when length(Rs) == NumPrompts -> + Rs; + Rs -> + erlang:error({mismatching_number_of_responses, + {got,Rs}, + {expected,NumPrompts}}) + end + end. + +keyboard_interact(IoCb, Name, Instr, Prompts) -> + if Name /= "" -> IoCb:format("~s", [Name]); + true -> ok + end, + if Instr /= "" -> IoCb:format("~s", [Instr]); + true -> ok + end, + lists:map(fun({Prompt, true}) -> IoCb:read_line(Prompt); + ({Prompt, false}) -> IoCb:read_password(Prompt) + end, + Prompts). + +userauth_passwd_messages() -> + [ + {ssh_msg_userauth_passwd_changereq, ?SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, + [string, + string]} + ]. + +userauth_keyboard_interactive_messages() -> + [ {ssh_msg_userauth_info_request, ?SSH_MSG_USERAUTH_INFO_REQUEST, + [string, + string, + string, + uint32, + '...']}, + + {ssh_msg_userauth_info_response, ?SSH_MSG_USERAUTH_INFO_RESPONSE, + [uint32, + '...']} + ]. + +userauth_pk_messages() -> + [ {ssh_msg_userauth_pk_ok, ?SSH_MSG_USERAUTH_PK_OK, + [string, % algorithm name + binary]} % key blob + ]. + +alg_to_module("ssh-dss") -> + ssh_dsa; +alg_to_module("ssh-rsa") -> + ssh_rsa. + +other_cb(ssh_rsa) -> + ssh_dsa; +other_cb(ssh_dsa) -> + ssh_rsa. diff --git a/lib/ssh/src/ssh_auth.hrl b/lib/ssh/src/ssh_auth.hrl new file mode 100644 index 0000000000..80c5a6819b --- /dev/null +++ b/lib/ssh/src/ssh_auth.hrl @@ -0,0 +1,83 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: Ssh User Authentication Protocol + +-define(SUPPORTED_AUTH_METHODS, "publickey,keyboard_interactive,password"). + +-define(PREFERRED_PK_ALG, ssh_rsa). + +-define(SSH_MSG_USERAUTH_REQUEST, 50). +-define(SSH_MSG_USERAUTH_FAILURE, 51). +-define(SSH_MSG_USERAUTH_SUCCESS, 52). +-define(SSH_MSG_USERAUTH_BANNER, 53). +-define(SSH_MSG_USERAUTH_PK_OK, 60). +-define(SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, 60). +-define(SSH_MSG_USERAUTH_INFO_REQUEST, 60). +-define(SSH_MSG_USERAUTH_INFO_RESPONSE, 61). + +-record(ssh_msg_userauth_request, + { + user, %% string + service, %% string + method, %% string "publickey", "password" + data %% opaque + }). + +-record(ssh_msg_userauth_failure, + { + authentications, %% string + partial_success %% boolean + }). + +-record(ssh_msg_userauth_success, + { + }). + +-record(ssh_msg_userauth_banner, + { + message, %% string + language %% string + }). + +-record(ssh_msg_userauth_passwd_changereq, + { + prompt, %% string + languge %% string + }). + +-record(ssh_msg_userauth_pk_ok, + { + algorithm_name, % string + key_blob % string + }). + +-record(ssh_msg_userauth_info_request, + {name, + instruction, + language_tag, + num_prompts, + data}). + +-record(ssh_msg_userauth_info_response, + {num_responses, + data}). + diff --git a/lib/ssh/src/ssh_bits.erl b/lib/ssh/src/ssh_bits.erl new file mode 100755 index 0000000000..21ddc5e8fe --- /dev/null +++ b/lib/ssh/src/ssh_bits.erl @@ -0,0 +1,483 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description : SSH 1/2 pdu elements encode/decode + +-module(ssh_bits). + +-include("ssh.hrl"). + +-export([encode/1, encode/2]). +-export([decode/1, decode/2, decode/3]). +-export([mpint/1, bignum/1, string/1, name_list/1]). +-export([b64_encode/1, b64_decode/1]). +-export([install_messages/1, uninstall_messages/1]). + +%% integer utils +-export([isize/1]). +-export([irandom/1, irandom/3]). +-export([random/1, random/3]). +-export([xor_bits/2, fill_bits/2]). +-export([i2bin/2, bin2i/1]). + +-import(lists, [foreach/2, reverse/1]). + +-define(name_list(X), + (fun(B) -> ?binary(B) end)(list_to_binary(name_concat(X)))). + + +name_concat([Name]) when is_atom(Name) -> atom_to_list(Name); +name_concat([Name]) when is_list(Name) -> Name; +name_concat([Name|Ns]) -> + if is_atom(Name) -> + [atom_to_list(Name),"," | name_concat(Ns)]; + is_list(Name) -> + [Name,"," | name_concat(Ns)] + end; +name_concat([]) -> []. + + +name_list(Ns) -> + ?name_list(Ns). + + +string(Str) -> + ?string(Str). + + +%% MP representaion (SSH2) +mpint(X) when X < 0 -> + if X == -1 -> + <<0,0,0,1,16#ff>>; + true -> + mpint_neg(X,0,[]) + end; +mpint(X) -> + if X == 0 -> + <<0,0,0,0>>; + true -> + mpint_pos(X,0,[]) + end. + +mpint_neg(-1,I,Ds=[MSB|_]) -> + if MSB band 16#80 =/= 16#80 -> + <>; + true -> + (<>) + end; +mpint_neg(X,I,Ds) -> + mpint_neg(X bsr 8,I+1,[(X band 255)|Ds]). + +mpint_pos(0,I,Ds=[MSB|_]) -> + if MSB band 16#80 == 16#80 -> + <>; + true -> + (<>) + end; +mpint_pos(X,I,Ds) -> + mpint_pos(X bsr 8,I+1,[(X band 255)|Ds]). + + +%% BIGNUM representation SSH1 +bignum(X) -> + XSz = isize(X), + Pad = (8 - (XSz rem 8)) rem 8, + <>. + + +install_messages(Codes) -> + foreach(fun({Name, Code, Ts}) -> + %% ?dbg(true, "install msg: ~s = ~w ~w~n", +%% [Name,Code,Ts]), + put({msg_name,Code}, {Name,Ts}), + put({msg_code,Name}, {Code,Ts}) + end, Codes). + +uninstall_messages(Codes) -> + foreach(fun({Name, Code, _Ts}) -> + %% ?dbg(true, "uninstall msg: ~s = ~w ~w~n", +%% [Name,Code,_Ts]), + erase({msg_name,Code}), + erase({msg_code,Name}) + end, Codes). + +%% +%% Encode a record, the type spec is expected to be +%% in process dictionary under the key {msg_code, RecodeName} +%% +encode(Record) -> + case get({msg_code, element(1, Record)}) of + undefined -> + {error, unimplemented}; + {Code, Ts} -> + Data = enc(tl(tuple_to_list(Record)), Ts), + list_to_binary([Code, Data]) + end. + +encode(List, Types) -> + list_to_binary(enc(List, Types)). + +%% +%% Encode record element +%% +enc(Xs, Ts) -> + enc(Xs, Ts, 0). + +enc(Xs, [Type|Ts], Offset) -> + case Type of + boolean -> + X=hd(Xs), + [?boolean(X) | enc(tl(Xs), Ts, Offset+1)]; + byte -> + X=hd(Xs), + [?byte(X) | enc(tl(Xs), Ts,Offset+1)]; + uint16 -> + X=hd(Xs), + [?uint16(X) | enc(tl(Xs), Ts,Offset+2)]; + uint32 -> + X=hd(Xs), + [?uint32(X) | enc(tl(Xs), Ts,Offset+4)]; + uint64 -> + X=hd(Xs), + [?uint64(X) | enc(tl(Xs), Ts,Offset+8)]; + mpint -> + Y=mpint(hd(Xs)), + [Y | enc(tl(Xs), Ts,Offset+size(Y))]; + bignum -> + Y=bignum(hd(Xs)), + [Y | enc(tl(Xs),Ts,Offset+size(Y))]; + string -> + X0=hd(Xs), + Y=?string(X0), + [Y | enc(tl(Xs),Ts,Offset+size(Y))]; + binary -> + X0=hd(Xs), + Y=?binary(X0), + [Y | enc(tl(Xs), Ts,Offset+size(Y))]; + name_list -> + X0=hd(Xs), + Y=?name_list(X0), + [Y | enc(tl(Xs), Ts, Offset+size(Y))]; + cookie -> + [random(16) | enc(tl(Xs), Ts, Offset+16)]; + {pad,N} -> + K = (N - (Offset rem N)) rem N, + [fill_bits(K,0) | enc(Xs, Ts, Offset+K)]; + '...' when Ts==[] -> + X=hd(Xs), + if is_binary(X) -> + [X]; + is_list(X) -> + [list_to_binary(X)]; + X==undefined -> + [] + end + end; +enc([], [],_) -> + []. + + + +%% +%% Decode a SSH record the type is encoded as the first byte +%% and the type spec MUST be installed in {msg_name, ID} +%% + +decode(Binary = <>) -> + case get({msg_name, ID}) of + undefined -> + {unknown, Binary}; + {Name, Ts} -> + {_, Elems} = decode(Binary,1,Ts), + list_to_tuple([Name | Elems]) + end. + +%% +%% Decode a binary form offset 0 +%% + +decode(Binary, Types) when is_binary(Binary) andalso is_list(Types) -> + {_,Elems} = decode(Binary, 0, Types), + Elems. + + +%% +%% Decode a binary from byte offset Offset +%% return {UpdatedOffset, DecodedElements} +%% +decode(Binary, Offset, Types) -> + decode(Binary, Offset, Types, []). + +decode(Binary, Offset, [Type|Ts], Acc) -> + case Type of + boolean -> + <<_:Offset/binary, ?BOOLEAN(X0), _/binary>> = Binary, + X = if X0 == 0 -> false; true -> true end, + decode(Binary, Offset+1, Ts, [X | Acc]); + + byte -> + <<_:Offset/binary, ?BYTE(X), _/binary>> = Binary, + decode(Binary, Offset+1, Ts, [X | Acc]); + + uint16 -> + <<_:Offset/binary, ?UINT16(X), _/binary>> = Binary, + decode(Binary, Offset+2, Ts, [X | Acc]); + + uint32 -> + <<_:Offset/binary, ?UINT32(X), _/binary>> = Binary, + decode(Binary, Offset+4, Ts, [X | Acc]); + + uint64 -> + <<_:Offset/binary, ?UINT64(X), _/binary>> = Binary, + decode(Binary, Offset+8, Ts, [X | Acc]); + + mpint -> + <<_:Offset/binary, ?UINT32(L), X0:L/binary,_/binary>> = Binary, + Sz = L*8, + <> = X0, + decode(Binary, Offset+4+L, Ts, [X | Acc]); + + bignum -> + <<_:Offset/binary, ?UINT16(Bits),_/binary>> = Binary, + L = (Bits+7) div 8, + Pad = (8 - (Bits rem 8)) rem 8, + <<_:Offset/binary, _:16, _:Pad, X:Bits/big-unsigned-integer, + _/binary>> = Binary, + decode(Binary, Offset+2+L, Ts, [X | Acc]); + + string -> + Size = size(Binary), + if Size < Offset + 4 -> + %% empty string at end + {Size, reverse(["" | Acc])}; + true -> + <<_:Offset/binary,?UINT32(L), X:L/binary,_/binary>> = + Binary, + decode(Binary, Offset+4+L, Ts, [binary_to_list(X) | + Acc]) + end; + + binary -> + <<_:Offset/binary,?UINT32(L), X:L/binary,_/binary>> = Binary, + decode(Binary, Offset+4+L, Ts, [X | Acc]); + + name_list -> + <<_:Offset/binary,?UINT32(L), X:L/binary,_/binary>> = Binary, + List = string:tokens(binary_to_list(X), ","), + decode(Binary, Offset+4+L, Ts, [List | Acc]); + + cookie -> + <<_:Offset/binary, X:16/binary, _/binary>> = Binary, + decode(Binary, Offset+16, Ts, [X | Acc]); + + {pad,N} -> %% pad offset to a multiple of N + K = (N - (Offset rem N)) rem N, + decode(Binary, Offset+K, Ts, Acc); + + + '...' when Ts==[] -> + <<_:Offset/binary, X/binary>> = Binary, + {Offset+size(X), reverse([X | Acc])} + end; +decode(_Binary, Offset, [], Acc) -> + {Offset, reverse(Acc)}. + + + +%% HACK WARNING :-) +-define(VERSION_MAGIC, 131). +-define(SMALL_INTEGER_EXT, $a). +-define(INTEGER_EXT, $b). +-define(SMALL_BIG_EXT, $n). +-define(LARGE_BIG_EXT, $o). + +isize(N) when N > 0 -> + case term_to_binary(N) of + <> -> + isize_byte(X); + <> -> + isize_bytes([X3,X2,X1,X0]); + <> -> + K = S - 1, + <<_:K/binary, Top>> = Ds, + isize_byte(Top)+K*8; + <> -> + K = S - 1, + <<_:K/binary, Top>> = Ds, + isize_byte(Top)+K*8 + end; +isize(0) -> 0. + +%% big endian byte list +isize_bytes([0|L]) -> + isize_bytes(L); +isize_bytes([Top|L]) -> + isize_byte(Top) + length(L)*8. + +%% Well could be improved +isize_byte(X) -> + if X >= 2#10000000 -> 8; + X >= 2#1000000 -> 7; + X >= 2#100000 -> 6; + X >= 2#10000 -> 5; + X >= 2#1000 -> 4; + X >= 2#100 -> 3; + X >= 2#10 -> 2; + X >= 2#1 -> 1; + true -> 0 + end. + +%% Convert integer into binary +%% When XLen is the wanted size in octets of the output +i2bin(X, XLen) -> + XSz = isize(X), + Sz = XLen*8, + if Sz < XSz -> + exit(integer_to_large); + true -> + (<>) + end. + +%% Convert a binary into an integer +%% +bin2i(X) -> + Sz = size(X)*8, + <> = X, + Y. + +%% +%% Create a binary with constant bytes +%% +fill_bits(N,C) -> + list_to_binary(fill(N,C)). + +fill(0,_C) -> []; +fill(1,C) -> [C]; +fill(N,C) -> + Cs = fill(N div 2, C), + Cs1 = [Cs,Cs], + if N band 1 == 0 -> + Cs1; + true -> + [C,Cs,Cs] + end. + +%% xor 2 binaries +xor_bits(XBits, YBits) -> + XSz = size(XBits)*8, + YSz = size(YBits)*8, + Sz = if XSz < YSz -> XSz; true -> YSz end, %% min + <> = XBits, + <> = YBits, + <<(X bxor Y):Sz>>. + +%% +%% irandom(N) +%% +%% Generate a N bits size random number +%% note that the top most bit is always set +%% to guarantee that the number is N bits +%% +irandom(Bits) -> + irandom(Bits, 1, 0). + +%% irandom_odd(Bits) -> +%% irandom(Bits, 1, 1). + +%% +%% irandom(N, Top, Bottom) +%% +%% Generate a N bits size random number +%% Where Top = 0 - do not set top bit +%% = 1 - set the most significant bit +%% = 2 - set two most significant bits +%% Bot = 0 - do not set the least signifcant bit +%% Bot = 1 - set the least signifcant bit (i.e always odd) +%% +irandom(0, _Top, _Bottom) -> + 0; +irandom(Bits, Top, Bottom) -> + Bytes = (Bits+7) div 8, + Skip = (8-(Bits rem 8)) rem 8, + TMask = case Top of + 0 -> 0; + 1 -> 16#80; + 2 -> 16#c0 + end, + BMask = case Bottom of + 0 -> 0; + 1 -> (1 bsl Skip) + end, + <> = random(Bytes, TMask, BMask), + X. + +%% +%% random/1 +%% Generate N random bytes +%% +random(N) -> + random(N, 0, 0). + +random(N, TMask, BMask) -> + list_to_binary(rnd(N, TMask, BMask)). + +%% random/3 +%% random(Bytes, TopMask, BotMask) +%% where +%% Bytes is the number of bytes to generate +%% TopMask is bitwised or'ed to the first byte +%% BotMask is bitwised or'ed to the last byte +%% +rnd(0, _TMask, _BMask) -> + []; +rnd(1, TMask, BMask) -> + [(rand8() bor TMask) bor BMask]; +rnd(N, TMask, BMask) -> + [(rand8() bor TMask) | rnd_n(N-1, BMask)]. + +rnd_n(1, BMask) -> + [rand8() bor BMask]; +rnd_n(I, BMask) -> + [rand8() | rnd_n(I-1, BMask)]. + +rand8() -> + (rand32() bsr 8) band 16#ff. + +rand32() -> + random:uniform(16#100000000) -1. + +%% +%% Base 64 encode/decode +%% + +b64_encode(Bs) when is_list(Bs) -> + base64:encode(Bs); +b64_encode(Bin) when is_binary(Bin) -> + base64:encode(Bin). + +b64_decode(Bin) when is_binary(Bin) -> + base64:mime_decode(Bin); +b64_decode(Cs) when is_list(Cs) -> + base64:mime_decode(Cs). + + diff --git a/lib/ssh/src/ssh_channel.erl b/lib/ssh/src/ssh_channel.erl new file mode 100644 index 0000000000..3d67065ee1 --- /dev/null +++ b/lib/ssh/src/ssh_channel.erl @@ -0,0 +1,328 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_channel). + +-include("ssh_connect.hrl"). + +-behaviour(gen_server). + +%%% API +-export([behaviour_info/1, start/4, start/5, start_link/4, start_link/5, call/2, call/3, + cast/2, reply/2, enter_loop/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% Internal application API +-export([cache_create/0, cache_lookup/2, cache_update/2, + cache_delete/1, cache_delete/2, cache_foldl/3, + cache_find/2]). + +-record(state, { + cm, + channel_cb, + channel_state, + channel_id, + close_sent = false + }). + +%%==================================================================== +%% API +%%==================================================================== + +%%% Optionel callbacks handle_call/3, handle_cast/2, handle_msg/2, +%%% code_change/3 +behaviour_info(callbacks) -> + [ + {init, 1}, + {terminate, 2}, + {handle_ssh_msg, 2}, + {handle_msg, 2} + ]. + + +call(ChannelPid, Msg) -> + call(ChannelPid, Msg, infinity). + +call(ChannelPid, Msg, TimeOute) -> + try gen_server:call(ChannelPid, Msg, TimeOute) of + Result -> + Result + catch + exit:{noproc, _} -> + {error, closed}; + exit:{timeout, _} -> + {error, timeout} + end. + + +cast(ChannelPid, Msg) -> + gen_server:cast(ChannelPid, Msg). + + +reply(From, Msg) -> + gen_server:reply(From, Msg). + +%%==================================================================== +%% Internal application API +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start(ConnectionManager, ChannelId, CallBack, CbInitArgs) -> + start(ConnectionManager, ChannelId, CallBack, CbInitArgs, undefined). + +start(ConnectionManager, ChannelId, CallBack, CbInitArgs, Exec) -> + Options = [{channel_cb, CallBack}, + {channel_id, ChannelId}, + {init_args, CbInitArgs}, + {cm, ConnectionManager}, + {exec, Exec}], + gen_server:start(?MODULE, [Options], []). + +start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs) -> + start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs, undefined). + +start_link(ConnectionManager, ChannelId, CallBack, CbInitArgs, Exec) -> + Options = [{channel_cb, CallBack}, + {channel_id, ChannelId}, + {init_args, CbInitArgs}, + {cm, ConnectionManager}, + {exec, Exec}], + gen_server:start_link(?MODULE, [Options], []). + +enter_loop(State) -> + gen_server:enter_loop(?MODULE, [], State). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Options]) -> + Cb = proplists:get_value(channel_cb, Options), + ConnectionManager = proplists:get_value(cm, Options), + ChannelId = proplists:get_value(channel_id, Options), + process_flag(trap_exit, true), + InitArgs = + case proplists:get_value(exec, Options) of + undefined -> + proplists:get_value(init_args, Options); + Exec -> + proplists:get_value(init_args, Options) ++ [Exec] + end, + try Cb:init(InitArgs) of + {ok, ChannelState} -> + State = #state{cm = ConnectionManager, + channel_cb = Cb, + channel_id = ChannelId, + channel_state = ChannelState}, + self() ! {ssh_channel_up, ChannelId, ConnectionManager}, + {ok, State}; + {ok, ChannelState, Timeout} -> + State = #state{cm = ConnectionManager, + channel_cb = Cb, + channel_id = ChannelId, + channel_state = ChannelState}, + self() ! {ssh_channel_up, ChannelId, ConnectionManager}, + {ok, State, Timeout}; + {stop, Why} -> + {stop, Why} + catch + _:Reason -> + {stop, Reason} + end. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(Request, From, #state{channel_cb = Module, + channel_state = ChannelState} = State) -> + try Module:handle_call(Request, From, ChannelState) of + Result -> + handle_cb_result(Result, State) + catch + error:{undef, _} -> + {noreply, State} + end. + + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(Msg, #state{channel_cb = Module, + channel_state = ChannelState} = State) -> + + try Module:handle_cast(Msg, ChannelState) of + Result -> + handle_cb_result(Result, State) + catch + error:{undef, _} -> + {noreply, State} + end. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({ssh_cm, ConnectionManager, {closed, _ChannelId}}, + #state{cm = ConnectionManager, + close_sent = true} = State) -> + {stop, normal, State}; +handle_info({ssh_cm, ConnectionManager, {closed, ChannelId}}, + #state{cm = ConnectionManager, + close_sent = false} = State) -> + %% To be on the safe side, i.e. the manager has already been terminated. + (catch ssh_connection:close(ConnectionManager, ChannelId)), + {stop, normal, State}; + +handle_info({ssh_cm, _, _} = Msg, #state{cm = ConnectionManager, + channel_cb = Module, + channel_state = ChannelState0} = State) -> + case Module:handle_ssh_msg(Msg, ChannelState0) of + {ok, ChannelState} -> + adjust_window(Msg), + {noreply, State#state{channel_state = ChannelState}}; + {ok, ChannelState, Timeout} -> + adjust_window(Msg), + {noreply, State#state{channel_state = ChannelState}, Timeout}; + {stop, ChannelId, ChannelState} -> + ssh_connection:close(ConnectionManager, ChannelId), + {stop, normal, State#state{close_sent = true, + channel_state = ChannelState}} + end; + +handle_info(Msg, #state{cm = ConnectionManager, channel_cb = Module, + channel_state = ChannelState0} = State) -> + case Module:handle_msg(Msg, ChannelState0) of + {ok, ChannelState} -> + {noreply, State#state{channel_state = ChannelState}}; + {ok, ChannelState, Timeout} -> + {noreply, State#state{channel_state = ChannelState}, Timeout}; + {stop, ChannelId, ChannelState} -> + ssh_connection:close(ConnectionManager, ChannelId), + {stop, normal, State#state{close_sent = true, + channel_state = ChannelState}} + end. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(Reason, #state{cm = ConnectionManager, + channel_id = ChannelId, + close_sent = false} = State) -> + ssh_connection:close(ConnectionManager, ChannelId), + terminate(Reason, State#state{close_sent = true}); +terminate(_, #state{channel_cb = Cb, channel_state = ChannelState}) -> + catch Cb:terminate(Cb, ChannelState), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(OldVsn, #state{channel_cb = Module, + channel_state = ChannelState0} = State, Extra) -> + {ok, ChannelState} = Module:code_change(OldVsn, ChannelState0, Extra), + {ok, State#state{channel_state = ChannelState}}. + +%%==================================================================== +%% Internal application API +%%==================================================================== +cache_create() -> + ets:new(cm_tab, [set,{keypos, #channel.local_id}]). + +cache_lookup(Cache, Key) -> + case ets:lookup(Cache, Key) of + [Channel] -> + Channel; + [] -> + undefined + end. + +cache_update(Cache, #channel{local_id = Id} = Entry) when Id =/= undefined -> + ets:insert(Cache, Entry). + +cache_delete(Cache, Key) -> + ets:delete(Cache, Key). + +cache_delete(Cache) -> + ets:delete(Cache). + +cache_foldl(Fun, Acc, Cache) -> + ets:foldl(Fun, Acc, Cache). + +cache_find(ChannelPid, Cache) -> + case ets:match_object(Cache, #channel{user = ChannelPid}) of + [] -> + undefined; + [Channel] -> + Channel + end. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +handle_cb_result({reply, Reply, ChannelState}, State) -> + {reply, Reply, State#state{channel_state = ChannelState}}; +handle_cb_result({reply, Reply, ChannelState, Timeout}, State) -> + {reply, Reply,State#state{channel_state = ChannelState}, Timeout}; +handle_cb_result({noreply, ChannelState}, State) -> + {noreply, State#state{channel_state = ChannelState}}; +handle_cb_result({noreply, ChannelState, Timeout}, State) -> + {noreply, State#state{channel_state = ChannelState}, Timeout}; +handle_cb_result({stop, Reason, Reply, ChannelState}, State) -> + {stop, Reason, Reply, State#state{channel_state = ChannelState}}; +handle_cb_result({stop, Reason, ChannelState}, State) -> + {stop, Reason, State#state{channel_state = ChannelState}}. + +adjust_window({ssh_cm, ConnectionManager, + {data, ChannelId, _, Data}}) -> + ssh_connection:adjust_window(ConnectionManager, ChannelId, size(Data)); +adjust_window(_) -> + ok. + + diff --git a/lib/ssh/src/ssh_channel_sup.erl b/lib/ssh/src/ssh_channel_sup.erl new file mode 100644 index 0000000000..c184fed627 --- /dev/null +++ b/lib/ssh/src/ssh_channel_sup.erl @@ -0,0 +1,54 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: Ssh channel supervisor. +%%---------------------------------------------------------------------- +-module(ssh_channel_sup). + +-behaviour(supervisor). + +-export([start_link/1, start_child/2]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(Args) -> + supervisor:start_link(?MODULE, [Args]). + +start_child(Sup, ChildSpec) -> + supervisor:start_child(Sup, ChildSpec). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init(_Args) -> + RestartStrategy = one_for_one, + MaxR = 10, + MaxT = 3600, + Children = [], + {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= diff --git a/lib/ssh/src/ssh_cli.erl b/lib/ssh/src/ssh_cli.erl new file mode 100644 index 0000000000..964f35121a --- /dev/null +++ b/lib/ssh/src/ssh_cli.erl @@ -0,0 +1,500 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Description: a gen_server implementing a simple +%% terminal (using the group module) for a CLI +%% over SSH + +-module(ssh_cli). + +-behaviour(ssh_channel). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). + +%% ssh_channel callbacks +-export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2]). + +%% backwards compatibility +-export([listen/1, listen/2, listen/3, listen/4, stop/1]). + +%% state +-record(state, { + cm, + channel, + pty, + group, + buf, + shell, + exec + }). + +%%==================================================================== +%% ssh_channel callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} +%% +%% Description: Initiates the CLI +%%-------------------------------------------------------------------- +init([Shell, Exec]) -> + {ok, #state{shell = Shell, exec = Exec}}; +init([Shell]) -> + {ok, #state{shell = Shell}}. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages received on the ssh-connection. +%%-------------------------------------------------------------------- +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, _Type, Data}}, + #state{group = Group} = State) -> + Group ! {self(), {data, binary_to_list(Data)}}, + {ok, State}; + +handle_ssh_msg({ssh_cm, ConnectionManager, + {pty, ChannelId, WantReply, + {TermName, Width, Height, PixWidth, PixHeight, Modes}}}, + State0) -> + State = State0#state{pty = + #ssh_pty{term = TermName, + width = not_zero(Width, 80), + height = not_zero(Height, 24), + pixel_width = PixWidth, + pixel_height = PixHeight, + modes = Modes}}, + set_echo(State), + ssh_connection:reply_request(ConnectionManager, WantReply, + success, ChannelId), + {ok, State}; + +handle_ssh_msg({ssh_cm, ConnectionManager, + {env, ChannelId, WantReply, _Var, _Value}}, State) -> + ssh_connection:reply_request(ConnectionManager, + WantReply, failure, ChannelId), + {ok, State}; + +handle_ssh_msg({ssh_cm, ConnectionManager, + {window_change, ChannelId, Width, Height, PixWidth, PixHeight}}, + #state{buf = Buf, pty = Pty0} = State) -> + Pty = Pty0#ssh_pty{width = Width, height = Height, + pixel_width = PixWidth, + pixel_height = PixHeight}, + {Chars, NewBuf} = io_request({window_change, Pty0}, Buf, Pty), + write_chars(ConnectionManager, ChannelId, Chars), + {ok, State#state{pty = Pty, buf = NewBuf}}; + +handle_ssh_msg({ssh_cm, ConnectionManager, + {shell, ChannelId, WantReply}}, State) -> + NewState = start_shell(ConnectionManager, State), + ssh_connection:reply_request(ConnectionManager, WantReply, + success, ChannelId), + {ok, NewState#state{channel = ChannelId, + cm = ConnectionManager}}; + +handle_ssh_msg({ssh_cm, ConnectionManager, + {exec, ChannelId, WantReply, Cmd}}, #state{exec=undefined} = State) -> + {Reply, Status} = exec(Cmd), + write_chars(ConnectionManager, + ChannelId, io_lib:format("~p\n", [Reply])), + ssh_connection:reply_request(ConnectionManager, WantReply, + success, ChannelId), + ssh_connection:exit_status(ConnectionManager, ChannelId, Status), + ssh_connection:send_eof(ConnectionManager, ChannelId), + {stop, ChannelId, State#state{channel = ChannelId, cm = ConnectionManager}}; +handle_ssh_msg({ssh_cm, ConnectionManager, + {exec, ChannelId, WantReply, Cmd}}, State) -> + NewState = start_shell(ConnectionManager, Cmd, State), + ssh_connection:reply_request(ConnectionManager, WantReply, + success, ChannelId), + {ok, NewState#state{channel = ChannelId, + cm = ConnectionManager}}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, {eof, _ChannelId}}, State) -> + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) -> + Report = io_lib:format("Connection closed by peer ~n Error ~p~n", + [Error]), + error_logger:error_report(Report), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) -> + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) -> + + Report = io_lib:format("Connection closed by peer ~n Status ~p~n", + [Status]), + error_logger:error_report(Report), + {stop, ChannelId, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles other channel messages. +%%-------------------------------------------------------------------- +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, + #state{channel = ChannelId, + cm = ConnectionManager} = State) -> + {ok, State}; + +handle_msg({Group, Req}, #state{group = Group, buf = Buf, pty = Pty, + cm = ConnectionManager, + channel = ChannelId} = State) -> + {Chars, NewBuf} = io_request(Req, Buf, Pty), + write_chars(ConnectionManager, ChannelId, Chars), + {ok, State#state{buf = NewBuf}}; + +handle_msg({'EXIT', Group, _Reason}, #state{group = Group, + channel = ChannelId} = State) -> + {stop, ChannelId, State}; + +handle_msg(_, State) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: Called when the channel process is trminated +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +exec(Cmd) -> + eval(parse(scan(Cmd))). + +scan(Cmd) -> + erl_scan:string(Cmd). + +parse({ok, Tokens, _}) -> + erl_parse:parse_exprs(Tokens); +parse(Error) -> + Error. + +eval({ok, Expr_list}) -> + case (catch erl_eval:exprs(Expr_list, + erl_eval:new_bindings())) of + {value, Value, _NewBindings} -> + {Value, 0}; + {'EXIT', {Error, _}} -> + {Error, -1}; + Error -> + {Error, -1} + end; +eval(Error) -> + {Error, -1}. + +%%% io_request, handle io requests from the user process, +%%% Note, this is not the real I/O-protocol, but the mockup version +%%% used between edlin and a user_driver. The protocol tags are +%%% similar, but the message set is different. +%%% The protocol only exists internally between edlin and a character +%%% displaying device... +%%% We are *not* really unicode aware yet, we just filter away characters +%%% beyond the latin1 range. We however handle the unicode binaries... +io_request({window_change, OldTty}, Buf, Tty) -> + window_change(Tty, OldTty, Buf); +io_request({put_chars, Cs}, Buf, Tty) -> + put_chars(bin_to_list(Cs), Buf, Tty); +io_request({put_chars, unicode, Cs}, Buf, Tty) -> + put_chars([Ch || Ch <- unicode:characters_to_list(Cs,unicode), Ch =< 255], Buf, Tty); +io_request({insert_chars, Cs}, Buf, Tty) -> + insert_chars(bin_to_list(Cs), Buf, Tty); +io_request({insert_chars, unicode, Cs}, Buf, Tty) -> + insert_chars([Ch || Ch <- unicode:characters_to_list(Cs,unicode), Ch =< 255], Buf, Tty); +io_request({move_rel, N}, Buf, Tty) -> + move_rel(N, Buf, Tty); +io_request({delete_chars,N}, Buf, Tty) -> + delete_chars(N, Buf, Tty); +io_request(beep, Buf, _Tty) -> + {[7], Buf}; + +%% New in R12 +io_request({get_geometry,columns},Buf,Tty) -> + {ok, Tty#ssh_pty.width, Buf}; +io_request({get_geometry,rows},Buf,Tty) -> + {ok, Tty#ssh_pty.height, Buf}; +io_request({requests,Rs}, Buf, Tty) -> + io_requests(Rs, Buf, Tty, []); +io_request(tty_geometry, Buf, Tty) -> + io_requests([{move_rel, 0}, {put_chars, unicode, [10]}], Buf, Tty, []); + %{[], Buf}; +io_request(_R, Buf, _Tty) -> + {[], Buf}. + +io_requests([R|Rs], Buf, Tty, Acc) -> + {Chars, NewBuf} = io_request(R, Buf, Tty), + io_requests(Rs, NewBuf, Tty, [Acc|Chars]); +io_requests([], Buf, _Tty, Acc) -> + {Acc, Buf}. + +%%% return commands for cursor navigation, assume everything is ansi +%%% (vt100), add clauses for other terminal types if needed +ansi_tty(N, L) -> + ["\e[", integer_to_list(N), L]. + +get_tty_command(up, N, _TerminalType) -> + ansi_tty(N, $A); +get_tty_command(down, N, _TerminalType) -> + ansi_tty(N, $B); +get_tty_command(right, N, _TerminalType) -> + ansi_tty(N, $C); +get_tty_command(left, N, _TerminalType) -> + ansi_tty(N, $D). + + +-define(PAD, 10). +-define(TABWIDTH, 8). + +%% convert input characters to buffer and to writeout +%% Note that the buf is reversed but the buftail is not +%% (this is handy; the head is always next to the cursor) +conv_buf([], AccBuf, AccBufTail, AccWrite, Col) -> + {AccBuf, AccBufTail, lists:reverse(AccWrite), Col}; +conv_buf([13, 10 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) -> + conv_buf(Rest, [], tl2(AccBufTail), [10, 13 | AccWrite], 0); +conv_buf([13 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) -> + conv_buf(Rest, [], tl1(AccBufTail), [13 | AccWrite], 0); +conv_buf([10 | Rest], _AccBuf, AccBufTail, AccWrite, _Col) -> + conv_buf(Rest, [], tl1(AccBufTail), [10, 13 | AccWrite], 0); +conv_buf([C | Rest], AccBuf, AccBufTail, AccWrite, Col) -> + conv_buf(Rest, [C | AccBuf], tl1(AccBufTail), [C | AccWrite], Col + 1). + + +%%% put characters at current position (possibly overwriting +%%% characters after current position in buffer) +put_chars(Chars, {Buf, BufTail, Col}, _Tty) -> + {NewBuf, NewBufTail, WriteBuf, NewCol} = + conv_buf(Chars, Buf, BufTail, [], Col), + {WriteBuf, {NewBuf, NewBufTail, NewCol}}. + +%%% insert character at current position +insert_chars([], {Buf, BufTail, Col}, _Tty) -> + {[], {Buf, BufTail, Col}}; +insert_chars(Chars, {Buf, BufTail, Col}, Tty) -> + {NewBuf, _NewBufTail, WriteBuf, NewCol} = + conv_buf(Chars, Buf, [], [], Col), + M = move_cursor(NewCol + length(BufTail), NewCol, Tty), + {[WriteBuf, BufTail | M], {NewBuf, BufTail, NewCol}}. + +%%% delete characters at current position, (backwards if negative argument) +delete_chars(0, {Buf, BufTail, Col}, _Tty) -> + {[], {Buf, BufTail, Col}}; +delete_chars(N, {Buf, BufTail, Col}, Tty) when N > 0 -> + NewBufTail = nthtail(N, BufTail), + M = move_cursor(Col + length(NewBufTail) + N, Col, Tty), + {[NewBufTail, lists:duplicate(N, $ ) | M], + {Buf, NewBufTail, Col}}; +delete_chars(N, {Buf, BufTail, Col}, Tty) -> % N < 0 + NewBuf = nthtail(-N, Buf), + NewCol = Col + N, + M1 = move_cursor(Col, NewCol, Tty), + M2 = move_cursor(NewCol + length(BufTail) - N, NewCol, Tty), + {[M1, BufTail, lists:duplicate(-N, $ ) | M2], + {NewBuf, BufTail, NewCol}}. + +%%% Window change, redraw the current line (and clear out after it +%%% if current window is wider than previous) +window_change(Tty, OldTty, Buf) + when OldTty#ssh_pty.width == Tty#ssh_pty.width -> + {[], Buf}; +window_change(Tty, OldTty, {Buf, BufTail, Col}) -> + M1 = move_cursor(Col, 0, OldTty), + N = max(Tty#ssh_pty.width - OldTty#ssh_pty.width, 0) * 2, + S = lists:reverse(Buf, [BufTail | lists:duplicate(N, $ )]), + M2 = move_cursor(length(Buf) + length(BufTail) + N, Col, Tty), + {[M1, S | M2], {Buf, BufTail, Col}}. + +%% move around in buffer, respecting pad characters +step_over(0, Buf, [?PAD | BufTail], Col) -> + {[?PAD | Buf], BufTail, Col+1}; +step_over(0, Buf, BufTail, Col) -> + {Buf, BufTail, Col}; +step_over(N, [C | Buf], BufTail, Col) when N < 0 -> + N1 = ifelse(C == ?PAD, N, N+1), + step_over(N1, Buf, [C | BufTail], Col-1); +step_over(N, Buf, [C | BufTail], Col) when N > 0 -> + N1 = ifelse(C == ?PAD, N, N-1), + step_over(N1, [C | Buf], BufTail, Col+1). + +%%% an empty line buffer +empty_buf() -> {[], [], 0}. + +%%% col and row from position with given width +col(N, W) -> N rem W. +row(N, W) -> N div W. + +%%% move relative N characters +move_rel(N, {Buf, BufTail, Col}, Tty) -> + {NewBuf, NewBufTail, NewCol} = step_over(N, Buf, BufTail, Col), + M = move_cursor(Col, NewCol, Tty), + {M, {NewBuf, NewBufTail, NewCol}}. + +%%% give move command for tty +move_cursor(A, A, _Tty) -> + []; +move_cursor(From, To, #ssh_pty{width=Width, term=Type}) -> + Tcol = case col(To, Width) - col(From, Width) of + 0 -> ""; + I when I < 0 -> get_tty_command(left, -I, Type); + I -> get_tty_command(right, I, Type) + end, + Trow = case row(To, Width) - row(From, Width) of + 0 -> ""; + J when J < 0 -> get_tty_command(up, -J, Type); + J -> get_tty_command(down, J, Type) + end, + [Tcol | Trow]. + +%% %%% write out characters +%% %%% make sure that there is data to send +%% %%% before calling ssh_connection:send +write_chars(ConnectionManager, ChannelId, Chars) -> + case erlang:iolist_size(Chars) of + 0 -> + ok; + _ -> + ssh_connection:send(ConnectionManager, ChannelId, + ?SSH_EXTENDED_DATA_DEFAULT, Chars) + end. + +%%% tail, works with empty lists +tl1([_|A]) -> A; +tl1(_) -> []. + +%%% second tail +tl2([_,_|A]) -> A; +tl2(_) -> []. + +%%% nthtail as in lists, but no badarg if n > the length of list +nthtail(0, A) -> A; +nthtail(N, [_ | A]) when N > 0 -> nthtail(N-1, A); +nthtail(_, _) -> []. + +%%% utils +max(A, B) when A > B -> A; +max(_A, B) -> B. + +ifelse(Cond, A, B) -> + case Cond of + true -> A; + _ -> B + end. + +bin_to_list(B) when is_binary(B) -> + binary_to_list(B); +bin_to_list(L) when is_list(L) -> + lists:flatten([bin_to_list(A) || A <- L]); +bin_to_list(I) when is_integer(I) -> + I. + +start_shell(ConnectionManager, State) -> + Shell = State#state.shell, + ShellFun = case is_function(Shell) of + true -> + case erlang:fun_info(Shell, arity) of + {arity, 1} -> + {ok, User} = + ssh_userreg:lookup_user(ConnectionManager), + fun() -> Shell(User) end; + {arity, 2} -> + {ok, User} = + ssh_userreg:lookup_user(ConnectionManager), + {ok, PeerAddr} = + ssh_cm:get_peer_addr(ConnectionManager), + fun() -> Shell(User, PeerAddr) end; + _ -> + Shell + end; + _ -> + Shell + end, + Echo = get_echo(State#state.pty), + Group = group:start(self(), ShellFun, [{echo, Echo}]), + State#state{group = Group, buf = empty_buf()}. + +start_shell(_ConnectionManager, Cmd, #state{exec={M, F, A}} = State) -> + Group = group:start(self(), {M, F, A++[Cmd]}, [{echo,false}]), + State#state{group = Group, buf = empty_buf()}. + + +% Pty can be undefined if the client never sets any pty options before +% starting the shell. +get_echo(undefined) -> + true; +get_echo(#ssh_pty{modes = Modes}) -> + case proplists:get_value(echo, Modes, 1) of + 0 -> + false; + _ -> + true + end. + +% Group is undefined if the pty options are sent between open and +% shell messages. +set_echo(#state{group = undefined}) -> + ok; +set_echo(#state{group = Group, pty = Pty}) -> + Echo = get_echo(Pty), + Group ! {self(), echo, Echo}. + +not_zero(0, B) -> + B; +not_zero(A, _) -> + A. + +%%% Backwards compatibility + +%%-------------------------------------------------------------------- +%% Function: listen(...) -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts a listening server +%% Note that the pid returned is NOT the pid of this gen_server; +%% this server is started when an SSH connection is made on the +%% listening port +%%-------------------------------------------------------------------- +listen(Shell) -> + listen(Shell, 22). + +listen(Shell, Port) -> + listen(Shell, Port, []). + +listen(Shell, Port, Opts) -> + listen(Shell, any, Port, Opts). + +listen(Shell, HostAddr, Port, Opts) -> + ssh:daemon(HostAddr, Port, [{shell, Shell} | Opts]). + + +%%-------------------------------------------------------------------- +%% Function: stop(Pid) -> ok +%% Description: Stops the listener +%%-------------------------------------------------------------------- +stop(Pid) -> + ssh:stop_listener(Pid). diff --git a/lib/ssh/src/ssh_cm.erl b/lib/ssh/src/ssh_cm.erl new file mode 100755 index 0000000000..c4d535df9a --- /dev/null +++ b/lib/ssh/src/ssh_cm.erl @@ -0,0 +1,237 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description : Backwards compatibility wrapper + +-module(ssh_cm). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). + +%% -define(DEFAULT_PACKET_SIZE, 32768). +%% -define(DEFAULT_WINDOW_SIZE, 2*?DEFAULT_PACKET_SIZE). +%%-define(DEFAULT_TIMEOUT, 5000). + +-export([connect/1, connect/2, connect/3]). +-export([listen/2, listen/3, listen/4, stop_listener/1]). +-export([stop/1]). + +-deprecated({connect, 1, next_major_release}). +-deprecated({connect, 2, next_major_release}). +-deprecated({connect, 3, next_major_release}). +-deprecated({listen, 2, next_major_release}). +-deprecated({listen, 3, next_major_release}). +-deprecated({listen, 4, next_major_release}). +-deprecated({stop_listener, 1, next_major_release}). +-deprecated({stop, 1, next_major_release}). + +-export([adjust_window/3, attach/2, attach/3, detach/2, + tcpip_forward/3, cancel_tcpip_forward/3, direct_tcpip/6, + direct_tcpip/8, close/2, shell/2, exec/4, + send/3, send/4, + send_ack/3, send_ack/4, send_ack/5, send_eof/2, + session_open/2, session_open/4, subsystem/4, + open_pty/3, open_pty/7, open_pty/9, + set_user_ack/4, + setenv/5, signal/3, winch/4]). + +-deprecated({adjust_window, 3, next_major_release}). +-deprecated({attach, 2, next_major_release}). +-deprecated({attach, 3, next_major_release}). +-deprecated({detach, 2, next_major_release}). +-deprecated({tcpip_forward, 3, next_major_release}). +-deprecated({cancel_tcpip_forward, 3, next_major_release}). +-deprecated({direct_tcpip, 6, next_major_release}). +-deprecated({direct_tcpip, 8, next_major_release}). +-deprecated({close, 2, next_major_release}). +-deprecated({shell, 2, next_major_release}). +-deprecated({exec, 4, next_major_release}). +-deprecated({send, 3, next_major_release}). +-deprecated({send, 4, next_major_release}). +-deprecated({send_ack, 3, next_major_release}). +-deprecated({send_ack, 4, next_major_release}). +-deprecated({send_ack, 5, next_major_release}). +-deprecated({send_eof, 2, next_major_release}). +-deprecated({session_open, 2, next_major_release}). +-deprecated({session_open, 4, next_major_release}). +-deprecated({subsystem, 4, next_major_release}). +-deprecated({open_pty, 3, next_major_release}). +-deprecated({open_pty, 7, next_major_release}). +-deprecated({open_pty, 9, next_major_release}). +-deprecated({set_user_ack, 4, next_major_release}). +-deprecated({setenv, 5, next_major_release}). +-deprecated({signal, 3, next_major_release}). +-deprecated({winch, 4, next_major_release}). + +-export([info/1, info/2, recv_window/3, + send_window/3, renegotiate/1, renegotiate/2, + get_peer_addr/1]). + +%%==================================================================== +%% API +%%==================================================================== +connect(Host) -> + connect(Host, []). +connect(Host, Opts) -> + connect(Host, ?SSH_DEFAULT_PORT, Opts). +connect(Host, Port, Opts) -> + ssh:connect(Host, Port, Opts). + +listen(ChannelSpec, Port) -> + listen(ChannelSpec, Port, []). +listen(ChannelSpec, Port, Opts) -> + listen(ChannelSpec, any, Port, Opts). +listen(ChannelSpec, "localhost", Port, Opts) -> + listen(ChannelSpec, any, Port, Opts); +listen(_ChannelSpec, Host, Port, Opts) -> + ssh:daemon(Host, Port, Opts). + +stop_listener(SysSup) -> + ssh_system_sup:stop_listener(SysSup). +stop(Cm) -> + ssh:close(Cm). + +%% CM Client commands +session_open(Cm, Timeout) -> + session_open(Cm, ?DEFAULT_WINDOW_SIZE, ?DEFAULT_PACKET_SIZE, Timeout). + +session_open(Cm, InitialWindowSize, MaxPacketSize, Timeout) -> + ssh_connection:session_channel(Cm, InitialWindowSize, MaxPacketSize, + Timeout). + + +setenv(Cm, Channel, Var, Value, Timeout) -> + ssh_connection:setenv(Cm, Channel, Var, Value, Timeout). + +shell(Cm, Channel) -> + ssh_connection:shell(Cm, Channel). + +exec(Cm, Channel, Command, Timeout) -> + ssh_connection:exec(Cm, Channel, Command, Timeout). + +subsystem(Cm, Channel, SubSystem, Timeout) -> + ssh_connection:subsystem(Cm, Channel, SubSystem, Timeout). + +%% Not needed for backwards compatibility for now +attach(_Cm, _Timeout) -> + ok. + +attach(_Cm, _ChannelPid, _Timeout) -> + ok. + +detach(_Cm, _Timeout) -> + ok. + +%% Not needed, send_ack is now call! Temp backwardcompability +set_user_ack(_, _, _, _) -> + ok. + +adjust_window(Cm, Channel, Bytes) -> + ssh_connection:adjust_window(Cm, Channel, Bytes). + +close(Cm, Channel) -> + ssh_connection:close(Cm, Channel). + +send_eof(Cm, Channel) -> + ssh_connection:send_eof(Cm, Channel). + +send(Cm, Channel, Data) -> + ssh_connection:send(Cm, Channel, 0, Data). + +send(Cm, Channel, Type, Data) -> + ssh_connection:send(Cm, Channel, Type, Data). + +%% Send ack is not needed +send_ack(Cm, Channel, Data) -> + send_ack(Cm, Channel, 0, Data, infinity). + +send_ack(Cm, Channel, Type, Data) -> + send_ack(Cm, Channel, Type, Data, infinity). + +send_ack(Cm, Channel, Type, Data, Timeout) -> + ssh_connection:send(Cm, Channel, Type, Data, Timeout). + +%% ---------------------------------------------------------------------- +%% These functions replacers are not officially supported but proably will be +%% when we had time to test them. +%% ---------------------------------------------------------------------- +direct_tcpip(Cm, RemoteHost, RemotePort, OrigIP, OrigPort, Timeout) -> + direct_tcpip(Cm, RemoteHost, RemotePort, OrigIP, OrigPort, + ?DEFAULT_WINDOW_SIZE, ?DEFAULT_PACKET_SIZE, Timeout). + +direct_tcpip(Cm, RemoteIP, RemotePort, OrigIP, OrigPort, + InitialWindowSize, MaxPacketSize, Timeout) -> + ssh_connection:direct_tcpip(Cm, RemoteIP, RemotePort, + OrigIP, OrigPort, + InitialWindowSize, + MaxPacketSize, Timeout). + +tcpip_forward(Cm, BindIP, BindPort) -> + ssh_connection:tcpip_forward(Cm, BindIP, BindPort). + +cancel_tcpip_forward(Cm, BindIP, Port) -> + ssh_connection:cancel_tcpip_forward(Cm, BindIP, Port). + +open_pty(Cm, Channel, Timeout) -> + open_pty(Cm, Channel, os:getenv("TERM"), 80, 24, [], Timeout). + +open_pty(Cm, Channel, Term, Width, Height, PtyOpts, Timeout) -> + open_pty(Cm, Channel, Term, Width, Height, 0, 0, PtyOpts, Timeout). + +open_pty(Cm, Channel, Term, Width, Height, PixWidth, PixHeight, + PtyOpts, Timeout) -> + ssh_connection:open_pty(Cm, Channel, Term, + Width, Height, PixWidth, + PixHeight, PtyOpts, Timeout). +winch(Cm, Channel, Width, Height) -> + winch(Cm, Channel, Width, Height, 0, 0). +winch(Cm, Channel, Width, Height, PixWidth, PixHeight) -> + ssh_connection:window_change(Cm, Channel, Width, + Height, PixWidth, PixHeight). +signal(Cm, Channel, Sig) -> + ssh_connection:signal(Cm, Channel, Sig). + +%% ---------------------------------------------------------------------- +%% These functions replacers are not officially supported and +%% the format of them will proably change when and +%% if they get supported. +%% ---------------------------------------------------------------------- +info(Cm) -> + info(Cm, all). + +info(Cm, ChannelPid) -> + ssh_connection_manager:info(Cm, ChannelPid). + +send_window(Cm, Channel, Timeout) -> + ssh_connection_manager:send_window(Cm, Channel, Timeout). + +recv_window(Cm, Channel, Timeout) -> + ssh_connection_manager:recv_window(Cm, Channel, Timeout). + +renegotiate(Cm) -> + renegotiate(Cm, []). +renegotiate(Cm, _Opts) -> + %%TODO: How should this work, backwards compat? + ssh_connection_manager:renegotiate(Cm). + +get_peer_addr(Cm) -> + ssh_connection_manager:peer_addr(Cm). + diff --git a/lib/ssh/src/ssh_connect.hrl b/lib/ssh/src/ssh_connect.hrl new file mode 100755 index 0000000000..a5a4e42cd8 --- /dev/null +++ b/lib/ssh/src/ssh_connect.hrl @@ -0,0 +1,264 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description : SSH connection protocol + +-define(DEFAULT_PACKET_SIZE, 32768). +-define(DEFAULT_WINDOW_SIZE, 2*?DEFAULT_PACKET_SIZE). +-define(DEFAULT_TIMEOUT, 5000). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% CONNECT messages +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%---------------------------------------------------------------------- +%%% # SSH_MSG_xxx +%%% Description: Packet types used by the connection protocol. +%%%---------------------------------------------------------------------- +-define(SSH_MSG_GLOBAL_REQUEST, 80). +-define(SSH_MSG_REQUEST_SUCCESS, 81). +-define(SSH_MSG_REQUEST_FAILURE, 82). +-define(SSH_MSG_CHANNEL_OPEN, 90). +-define(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, 91). +-define(SSH_MSG_CHANNEL_OPEN_FAILURE, 92). +-define(SSH_MSG_CHANNEL_WINDOW_ADJUST, 93). +-define(SSH_MSG_CHANNEL_DATA, 94). +-define(SSH_MSG_CHANNEL_EXTENDED_DATA, 95). +-define(SSH_MSG_CHANNEL_EOF, 96). +-define(SSH_MSG_CHANNEL_CLOSE, 97). +-define(SSH_MSG_CHANNEL_REQUEST, 98). +-define(SSH_MSG_CHANNEL_SUCCESS, 99). +-define(SSH_MSG_CHANNEL_FAILURE, 100). + +-record(ssh_msg_global_request, + { + name, + want_reply, + data %% ... + }). + +-record(ssh_msg_request_success, + { + data %% ... + }). + +-record(ssh_msg_request_failure, + { + }). + + +-record(ssh_msg_channel_open, + { + channel_type, + sender_channel, + initial_window_size, + maximum_packet_size, + data %% ... + }). + +-record(ssh_msg_channel_open_confirmation, + { + recipient_channel, + sender_channel, + initial_window_size, + maximum_packet_size, + data %% ... + }). + + +%%%---------------------------------------------------------------------- +%%% # SSH_OPEN_xxx +%%% Description: Reason codes for SSH_MSG_OPEN_FAILURE packages. +%%%---------------------------------------------------------------------- + +-define(SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, 1). +-define(SSH_OPEN_CONNECT_FAILED, 2). +-define(SSH_OPEN_UNKNOWN_CHANNEL_TYPE, 3). +-define(SSH_OPEN_RESOURCE_SHORTAGE, 4). + +-record(ssh_msg_channel_open_failure, + { + recipient_channel, + reason, + description, + lang + }). + + +-record(ssh_msg_channel_window_adjust, + { + recipient_channel, + bytes_to_add + }). + +-record(ssh_msg_channel_data, + { + recipient_channel, + data + }). + +%%%---------------------------------------------------------------------- +%%% # SSH_EXTENDED_DATA_xxx +%%% Description: Type codes for SSH_MSG_CHANNEL_EXTENDED_DATA packages +%%%---------------------------------------------------------------------- +-define(SSH_EXTENDED_DATA_DEFAULT, 0). +-define(SSH_EXTENDED_DATA_STDERR, 1). + +-record(ssh_msg_channel_extended_data, + { + recipient_channel, + data_type_code, + data + }). + +-record(ssh_msg_channel_eof, + { + recipient_channel + }). + +-record(ssh_msg_channel_close, + { + recipient_channel + }). + + +-record(ssh_msg_channel_request, + { + recipient_channel, + request_type, + want_reply, + data %% ... + }). + + +-record(ssh_msg_channel_success, + { + recipient_channel + }). + + +-record(ssh_msg_channel_failure, + { + recipient_channel + }). + +-define(TTY_OP_END,0). %% Indicates end of options. +-define(VINTR,1). %% Interrupt character; 255 if none. Similarly for the + %% other characters. Not all of these characters are + %% supported on all systems. +-define(VQUIT,2). %% The quit character (sends SIGQUIT signal on POSIX + %% systems). +-define(VERASE,3). %% Erase the character to left of the cursor. +-define(VKILL,4). %% Kill the current input line. +-define(VEOF,5). %% End-of-file character (sends EOF from the terminal). +-define(VEOL,6). %% End-of-line character in addition to carriage return + %% or,and). linefeed. +-define(VEOL2,7). %% Additional end-of-line character. +-define(VSTART,8). %% Continues paused output (normally control-Q). +-define(VSTOP,9). %% Pauses output (normally control-S). +-define(VSUSP,10). %% Suspends the current program. +-define(VDSUSP,11). %% Another suspend character. +-define(VREPRINT,12). %% Reprints the current input line. +-define(VWERASE,13). %% Erases a word left of cursor. +-define(VLNEXT,14). %% Enter the next character typed literally, even if it + %% is a special character +-define(VFLUSH,15). %% Character to flush output. +-define(VSWTCH,16). %% Switch to a different shell layer. +-define(VSTATUS,17). %% Prints system status line (load, command, pid etc). +-define(VDISCARD,18). %% Toggles the flushing of terminal output. +-define(IGNPAR,30). %% The ignore parity flag. The parameter SHOULD be 0 if + %% this flag is FALSE set, and 1 if it is TRUE. +-define(PARMRK,31). %% Mark parity and framing errors. +-define(INPCK,32). %% Enable checking of parity errors. +-define(ISTRIP,33). %% Strip 8th bit off characters. +-define(INLCR,34). %% Map NL into CR on input. +-define(IGNCR,35). %% Ignore CR on input. +-define(ICRNL,36). %% Map CR to NL on input. +-define(IUCLC,37). %% Translate uppercase characters to lowercase. +-define(IXON,38). %% Enable output flow control. +-define(IXANY,39). %% Any char will restart after stop. +-define(IXOFF,40). %% Enable input flow control. +-define(IMAXBEL,41). %% Ring bell on input queue full. +-define(ISIG,50). %% Enable signals INTR, QUIT, [D]SUSP. +-define(ICANON,51). %% Canonicalize input lines. +-define(XCASE,52). %% Enable input and output of uppercase characters by + %% preceding their lowercase equivalents with `\'. +-define(ECHO,53). %% Enable echoing. +-define(ECHOE,54). %% Visually erase chars. +-define(ECHOK,55). %% Kill character discards current line. +-define(ECHONL,56). %% Echo NL even if ECHO is off. +-define(NOFLSH,57). %% Don't flush after interrupt. +-define(TOSTOP,58). %% Stop background jobs from output. +-define(IEXTEN,59). %% Enable extensions. +-define(ECHOCTL,60). %% Echo control characters as ^(Char). +-define(ECHOKE,61). %% Visual erase for line kill. +-define(PENDIN,62). %% Retype pending input. +-define(OPOST,70). %% Enable output processing. +-define(OLCUC,71). %% Convert lowercase to uppercase. +-define(ONLCR,72). %% Map NL to CR-NL. +-define(OCRNL,73). %% Translate carriage return to newline (output). +-define(ONOCR,74). %% Translate newline to carriage return-newline + %% (output). +-define(ONLRET,75). %% Newline performs a carriage return (output). +-define(CS7,90). %% 7 bit mode. +-define(CS8,91). %% 8 bit mode. +-define(PARENB,92). %% Parity enable. +-define(PARODD,93). %% Odd parity, else even. + +%% Specifies the input baud rate in bits per second. +-define(TTY_OP_ISPEED,128). +%% Specifies the output baud rate in bits per second. +-define(TTY_OP_OSPEED,129). + +-record(channel, + { + type, %% "session", "x11", "forwarded-tcpip", "direct-tcpip" + sys, %% "none", "shell", "exec" "subsystem" + user, %% "user" process id (default to cm user) + flow_control, + + local_id, %% local channel id + + recv_window_size, + recv_packet_size, + recv_close = false, + + remote_id, %% remote channel id + send_window_size, + send_packet_size, + sent_close = false, + send_buf = [] + }). + +-record(connection, { + requests = [], %% [{ChannelId, Pid}...] awaiting reply on request, + channel_cache, + channel_pids = [], + port_bindings, + channel_id_seed, + cli_spec, + address, + port, + options, + exec + }). diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl new file mode 100644 index 0000000000..0aaf1c18d2 --- /dev/null +++ b/lib/ssh/src/ssh_connection.erl @@ -0,0 +1,1366 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%---------------------------------------------------------------------- +%% Purpose: Details of connection protocol +%%---------------------------------------------------------------------- + +-module(ssh_connection). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). +-include("ssh_transport.hrl"). + +-export([session_channel/2, session_channel/4, + exec/4, shell/2, subsystem/4, send/3, send/4, send/5, + send_eof/2, adjust_window/3, open_pty/3, open_pty/7, + open_pty/9, setenv/5, window_change/4, window_change/6, + direct_tcpip/6, direct_tcpip/8, tcpip_forward/3, + cancel_tcpip_forward/3, signal/3, exit_status/3, encode_ip/1, close/2, + reply_request/4]). + +-export([channel_data/6, handle_msg/4, channel_eof_msg/1, + channel_close_msg/1, channel_success_msg/1, channel_failure_msg/1, + channel_adjust_window_msg/2, channel_data_msg/3, + channel_open_msg/5, channel_open_confirmation_msg/4, + channel_open_failure_msg/4, channel_request_msg/4, + global_request_msg/3, request_failure_msg/0, + request_success_msg/1, bind/4, unbind/3, unbind_channel/2, + bound_channel/3, messages/0]). + +%%-------------------------------------------------------------------- +%%% Internal application API +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Function: session_channel(ConnectionManager +%% [, InitialWindowSize, MaxPacketSize], +%% Timeout) -> {ok, } +%% ConnectionManager = pid() +%% InitialWindowSize = integer() +%% MaxPacketSize = integer() +%% +%% Description: Opens a channel for a ssh session. A session is a +%% remote execution of a program. The program may be a shell, an +%% application, a system command, or some built-in subsystem. +%% -------------------------------------------------------------------- +session_channel(ConnectionManager, Timeout) -> + session_channel(ConnectionManager, + ?DEFAULT_WINDOW_SIZE, ?DEFAULT_PACKET_SIZE, + Timeout). +session_channel(ConnectionManager, InitialWindowSize, + MaxPacketSize, Timeout) -> + ssh_connection_manager:open_channel(ConnectionManager, "session", <<>>, + InitialWindowSize, + MaxPacketSize, Timeout). +%%-------------------------------------------------------------------- +%% Function: exec(ConnectionManager, ChannelId, Command, Timeout) -> +%% +%% ConnectionManager = pid() +%% ChannelId = integer() +%% Cmd = string() +%% Timeout = integer() +%% +%% Description: Will request that the server start the +%% execution of the given command. +%%-------------------------------------------------------------------- +exec(ConnectionManager, ChannelId, Command, TimeOut) -> + ssh_connection_manager:request(ConnectionManager, self(), ChannelId, "exec", + true, [?string(Command)], TimeOut). +%%-------------------------------------------------------------------- +%% Function: shell(ConnectionManager, ChannelId) -> +%% +%% ConnectionManager = pid() +%% ChannelId = integer() +%% +%% Description: Will request that the user's default shell (typically +%% defined in /etc/passwd in UNIX systems) be started at the other +%% end. +%%-------------------------------------------------------------------- +shell(ConnectionManager, ChannelId) -> + ssh_connection_manager:request(ConnectionManager, self(), ChannelId, + "shell", false, <<>>, 0). +%%-------------------------------------------------------------------- +%% Function: subsystem(ConnectionManager, ChannelId, SubSystem, TimeOut) -> +%% +%% ConnectionManager = pid() +%% ChannelId = integer() +%% SubSystem = string() +%% TimeOut = integer() +%% +%% +%% Description: Executes a predefined subsystem. +%%-------------------------------------------------------------------- +subsystem(ConnectionManager, ChannelId, SubSystem, TimeOut) -> + ssh_connection_manager:request(ConnectionManager, self(), + ChannelId, "subsystem", + true, [?string(SubSystem)], TimeOut). +%%-------------------------------------------------------------------- +%% Function: send(ConnectionManager, ChannelId, Type, Data, [TimeOut]) -> +%% +%% +%% Description: Sends channel data. +%%-------------------------------------------------------------------- +send(ConnectionManager, ChannelId, Data) -> + send(ConnectionManager, ChannelId, 0, Data, infinity). +send(ConnectionManager, ChannelId, Data, TimeOut) when is_integer(TimeOut) -> + send(ConnectionManager, ChannelId, 0, Data, TimeOut); +send(ConnectionManager, ChannelId, Type, Data) -> + send(ConnectionManager, ChannelId, Type, Data, infinity). +send(ConnectionManager, ChannelId, Type, Data, TimeOut) -> + ssh_connection_manager:send(ConnectionManager, ChannelId, + Type, Data, TimeOut). +%%-------------------------------------------------------------------- +%% Function: send_eof(ConnectionManager, ChannelId) -> +%% +%% +%% Description: Sends eof on the channel . +%%-------------------------------------------------------------------- +send_eof(ConnectionManager, Channel) -> + ssh_connection_manager:send_eof(ConnectionManager, Channel). + +%%-------------------------------------------------------------------- +%% Function: adjust_window(ConnectionManager, Channel, Bytes) -> +%% +%% +%% Description: Adjusts the ssh flowcontrol window. +%%-------------------------------------------------------------------- +adjust_window(ConnectionManager, Channel, Bytes) -> + ssh_connection_manager:adjust_window(ConnectionManager, Channel, Bytes). + +%%-------------------------------------------------------------------- +%% Function: setenv(ConnectionManager, ChannelId, Var, Value, TimeOut) -> +%% +%% +%% Description: Environment variables may be passed to the shell/command to be +%% started later. +%%-------------------------------------------------------------------- +setenv(ConnectionManager, ChannelId, Var, Value, TimeOut) -> + ssh_connection_manager:request(ConnectionManager, ChannelId, + "env", true, [?string(Var), ?string(Value)], TimeOut). + + +%%-------------------------------------------------------------------- +%% Function: close(ConnectionManager, ChannelId) -> +%% +%% +%% Description: Sends a close message on the channel . +%%-------------------------------------------------------------------- +close(ConnectionManager, ChannelId) -> + ssh_connection_manager:close(ConnectionManager, ChannelId). + + +%%-------------------------------------------------------------------- +%% Function: reply_request(ConnectionManager, WantReply, Status, CannelId) ->_ +%% +%% +%% Description: Send status replies to requests that want such replies. +%%-------------------------------------------------------------------- +reply_request(ConnectionManager, true, Status, ChannelId) -> + ConnectionManager ! {ssh_cm, self(), {Status, ChannelId}}, + ok; +reply_request(_,false, _, _) -> + ok. + + +%%-------------------------------------------------------------------- +%% Function: window_change(ConnectionManager, Channel, Width, Height) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +window_change(ConnectionManager, Channel, Width, Height) -> + window_change(ConnectionManager, Channel, Width, Height, 0, 0). +window_change(ConnectionManager, Channel, Width, Height, + PixWidth, PixHeight) -> + ssh_connection_manager:request(ConnectionManager, Channel, + "window-change", false, + [?uint32(Width), ?uint32(Height), + ?uint32(PixWidth), ?uint32(PixHeight)], 0). +%%-------------------------------------------------------------------- +%% Function: signal(ConnectionManager, Channel, Sig) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +signal(ConnectionManager, Channel, Sig) -> + ssh_connection_manager:request(ConnectionManager, Channel, + "signal", false, [?string(Sig)], 0). + +%%-------------------------------------------------------------------- +%% Function: signal(ConnectionManager, Channel, Status) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +exit_status(ConnectionManager, Channel, Status) -> + ssh_connection_manager:request(ConnectionManager, Channel, + "exit-status", false, [?uint32(Status)], 0). + + +%%-------------------------------------------------------------------- +%% Function: open_pty(ConnectionManager, Channel, TimeOut) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +open_pty(ConnectionManager, Channel, TimeOut) -> + open_pty(ConnectionManager, Channel, + os:getenv("TERM"), 80, 24, [], TimeOut). + +open_pty(ConnectionManager, Channel, Term, Width, Height, PtyOpts, TimeOut) -> + open_pty(ConnectionManager, Channel, Term, Width, + Height, 0, 0, PtyOpts, TimeOut). + +open_pty(ConnectionManager, Channel, Term, Width, Height, + PixWidth, PixHeight, PtyOpts, TimeOut) -> + ssh_connection_manager:request(ConnectionManager, + Channel, "pty-req", true, + [?string(Term), + ?uint32(Width), ?uint32(Height), + ?uint32(PixWidth),?uint32(PixHeight), + encode_pty_opts(PtyOpts)], TimeOut). + + +%%-------------------------------------------------------------------- +%% Function: direct_tcpip(ConnectionManager, RemoteHost, +%% RemotePort, OrigIP, OrigPort, Timeout) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +direct_tcpip(ConnectionManager, RemoteHost, + RemotePort, OrigIP, OrigPort, Timeout) -> + direct_tcpip(ConnectionManager, RemoteHost, RemotePort, OrigIP, OrigPort, + ?DEFAULT_WINDOW_SIZE, ?DEFAULT_PACKET_SIZE, Timeout). + +direct_tcpip(ConnectionManager, RemoteIP, RemotePort, OrigIP, OrigPort, + InitialWindowSize, MaxPacketSize, Timeout) -> + case {encode_ip(RemoteIP), encode_ip(OrigIP)} of + {false, _} -> + {error, einval}; + {_, false} -> + {error, einval}; + {RIP, OIP} -> + ssh_connection_manager:open_channel(ConnectionManager, + "direct-tcpip", + [?string(RIP), + ?uint32(RemotePort), + ?string(OIP), + ?uint32(OrigPort)], + InitialWindowSize, + MaxPacketSize, + Timeout) + end. +%%-------------------------------------------------------------------- +%% Function: tcpip_forward(ConnectionManager, BindIP, BindPort) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +tcpip_forward(ConnectionManager, BindIP, BindPort) -> + case encode_ip(BindIP) of + false -> + {error, einval}; + IPStr -> + ssh_connection_manager:global_request(ConnectionManager, + "tcpip-forward", true, + [?string(IPStr), + ?uint32(BindPort)]) + end. +%%-------------------------------------------------------------------- +%% Function: cancel_tcpip_forward(ConnectionManager, BindIP, Port) -> +%% +%% +%% Description: Not yet officialy supported. +%%-------------------------------------------------------------------- +cancel_tcpip_forward(ConnectionManager, BindIP, Port) -> + case encode_ip(BindIP) of + false -> + {error, einval}; + IPStr -> + ssh_connection_manager:global_request(ConnectionManager, + "cancel-tcpip-forward", true, + [?string(IPStr), + ?uint32(Port)]) + end. + +%%-------------------------------------------------------------------- +%%% Internal API +%%-------------------------------------------------------------------- +channel_data(ChannelId, DataType, Data, Connection, ConnectionPid, From) + when is_list(Data)-> + channel_data(ChannelId, DataType, + list_to_binary(Data), Connection, ConnectionPid, From); + +channel_data(ChannelId, DataType, Data, + #connection{channel_cache = Cache} = Connection, ConnectionPid, + From) -> + + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id} = Channel0 -> + {SendList, Channel} = update_send_window(Channel0, DataType, + Data, Connection), + Replies = + lists:map(fun({SendDataType, SendData}) -> + {connection_reply, ConnectionPid, + channel_data_msg(Id, + SendDataType, + SendData)} + end, SendList), + FlowCtrlMsgs = flow_control(Replies, + Channel#channel{flow_control = From}, + Cache), + {{replies, Replies ++ FlowCtrlMsgs}, Connection}; + undefined -> + {noreply, Connection} + end. + +handle_msg(#ssh_msg_channel_open_confirmation{recipient_channel = ChannelId, + sender_channel = RemoteId, + initial_window_size = WindowSz, + maximum_packet_size = PacketSz}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + + #channel{remote_id = undefined} = Channel = + ssh_channel:cache_lookup(Cache, ChannelId), + + ssh_channel:cache_update(Cache, Channel#channel{ + remote_id = RemoteId, + send_window_size = WindowSz, + send_packet_size = PacketSz}), + {Reply, Connection} = reply_msg(Channel, Connection0, {open, ChannelId}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_open_failure{recipient_channel = ChannelId, + reason = Reason, + description = Descr, + lang = Lang}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + ssh_channel:cache_delete(Cache, ChannelId), + {Reply, Connection} = + reply_msg(Channel, Connection0, {open_error, Reason, Descr, Lang}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_success{recipient_channel = ChannelId}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = reply_msg(Channel, Connection0, success), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_failure{recipient_channel = ChannelId}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = reply_msg(Channel, Connection0, failure), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_eof{recipient_channel = ChannelId}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = reply_msg(Channel, Connection0, {eof, ChannelId}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_close{recipient_channel = ChannelId}, + #connection{channel_cache = Cache} = Connection0, + ConnectionPid, _) -> + + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{sent_close = Closed, remote_id = RemoteId} = Channel -> + ssh_channel:cache_delete(Cache, ChannelId), + {CloseMsg, Connection} = + reply_msg(Channel, Connection0, {closed, ChannelId}), + case Closed of + true -> + {{replies, [CloseMsg]}, Connection}; + false -> + RemoteCloseMsg = channel_close_msg(RemoteId), + {{replies, + [{connection_reply, + ConnectionPid, RemoteCloseMsg}, + CloseMsg]}, Connection} + end; + undefined -> + {{replies, []}, Connection0} + end; + +handle_msg(#ssh_msg_channel_data{recipient_channel = ChannelId, + data = Data}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + + #channel{recv_window_size = Size} = Channel = + ssh_channel:cache_lookup(Cache, ChannelId), + WantedSize = Size - size(Data), + ssh_channel:cache_update(Cache, Channel#channel{ + recv_window_size = WantedSize}), + {Replies, Connection} = + channel_data_reply(Cache, Channel, Connection0, 0, Data), + {{replies, Replies}, Connection}; + +handle_msg(#ssh_msg_channel_extended_data{recipient_channel = ChannelId, + data_type_code = DataType, + data = Data}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + + #channel{recv_window_size = Size} = Channel = + ssh_channel:cache_lookup(Cache, ChannelId), + WantedSize = Size - size(Data), + ssh_channel:cache_update(Cache, Channel#channel{ + recv_window_size = WantedSize}), + {Replies, Connection} = + channel_data_reply(Cache, Channel, Connection0, DataType, Data), + {{replies, Replies}, Connection}; + +handle_msg(#ssh_msg_channel_window_adjust{recipient_channel = ChannelId, + bytes_to_add = Add}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, _) -> + + #channel{send_window_size = Size} = + Channel0 = ssh_channel:cache_lookup(Cache, ChannelId), + + {SendList, Channel} = %% TODO: Datatype 0 ? + update_send_window(Channel0#channel{send_window_size = Size + Add}, + 0, <<>>, Connection), + + Replies = lists:map(fun({Type, Data}) -> + {connection_reply, ConnectionPid, + channel_data_msg(ChannelId, Type, Data)} + end, SendList), + FlowCtrlMsgs = flow_control(Channel, Cache), + {{replies, Replies ++ FlowCtrlMsgs}, Connection}; + +handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type, + sender_channel = ChannelId, + initial_window_size = WindowSz, + maximum_packet_size = PacketSz}, Connection0, + ConnectionPid, server) -> + + try setup_session(Connection0, ConnectionPid, ChannelId, + Type, WindowSz, PacketSz) of + Result -> + Result + catch _:_ -> + FailMsg = channel_open_failure_msg(ChannelId, + ?SSH_OPEN_CONNECT_FAILED, + "Connection refused", "en"), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, + Connection0} + end; + +handle_msg(#ssh_msg_channel_open{channel_type = "session", + sender_channel = RemoteId}, + Connection, ConnectionPid, client) -> + %% Client implementations SHOULD reject any session channel open + %% requests to make it more difficult for a corrupt server to attack the + %% client. See See RFC 4254 6.1. + FailMsg = channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + "Connection refused", "en"), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, + Connection}; + +handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip" = Type, + sender_channel = RemoteId, + initial_window_size = RWindowSz, + maximum_packet_size = RPacketSz, + data = Data}, + #connection{channel_cache = Cache} = Connection0, + ConnectionPid, server) -> + <> = Data, + + case bound_channel(Address, Port, Connection0) of + undefined -> + FailMsg = channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + "Connection refused", "en"), + {{replies, + [{connection_reply, ConnectionPid, FailMsg}]}, Connection0}; + ChannelPid -> + {ChannelId, Connection1} = new_channel_id(Connection0), + LWindowSz = ?DEFAULT_WINDOW_SIZE, + LPacketSz = ?DEFAULT_PACKET_SIZE, + Channel = #channel{type = Type, + sys = "none", + user = ChannelPid, + local_id = ChannelId, + recv_window_size = LWindowSz, + recv_packet_size = LPacketSz, + send_window_size = RWindowSz, + send_packet_size = RPacketSz}, + ssh_channel:cache_update(Cache, Channel), + OpenConfMsg = channel_open_confirmation_msg(RemoteId, ChannelId, + LWindowSz, LPacketSz), + {OpenMsg, Connection} = + reply_msg(Channel, Connection1, + {open, Channel, {forwarded_tcpip, + decode_ip(Address), Port, + decode_ip(Orig), OrigPort}}), + {{replies, [{connection_reply, ConnectionPid, OpenConfMsg}, + OpenMsg]}, Connection} + end; + +handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip", + sender_channel = RemoteId}, + Connection, ConnectionPid, client) -> + %% Client implementations SHOULD reject direct TCP/IP open requests for + %% security reasons. See RFC 4254 7.2. + FailMsg = channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + "Connection refused", "en"), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, Connection}; + + +handle_msg(#ssh_msg_channel_open{sender_channel = ChannelId}, Connection, + ConnectionPid, _) -> + FailMsg = channel_open_failure_msg(ChannelId, + ?SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, + "Not allowed", "en"), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "exit-status", + data = Data}, + #connection{channel_cache = Cache} = Connection, _, _) -> + <> = Data, + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = + reply_msg(Channel, Connection, {exit_status, ChannelId, Status}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "exit-signal", + want_reply = false, + data = Data}, + #connection{channel_cache = Cache} = Connection0, + ConnectionPid, _) -> + <> = Data, + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + RemoteId = Channel#channel.remote_id, + {Reply, Connection} = reply_msg(Channel, Connection0, + {exit_signal, ChannelId, + binary_to_list(SigName), + binary_to_list(Err), + binary_to_list(Lang)}), + CloseMsg = channel_close_msg(RemoteId), + {{replies, [{connection_reply, ConnectionPid, CloseMsg}, Reply]}, + Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "xon-xoff", + want_reply = false, + data = Data}, + #connection{channel_cache = Cache} = Connection, _, _) -> + <> = Data, + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = + reply_msg(Channel, Connection, {xon_xoff, ChannelId, CDo=/= 0}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "window-change", + want_reply = false, + data = Data}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + <> = Data, + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = + reply_msg(Channel, Connection0, {window_change, ChannelId, + Width, Height, + PixWidth, PixHeight}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "signal", + data = Data}, + #connection{channel_cache = Cache} = Connection0, _, _) -> + <> = Data, + + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + {Reply, Connection} = + reply_msg(Channel, Connection0, {signal, ChannelId, + binary_to_list(SigName)}), + {{replies, [Reply]}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "subsystem", + want_reply = WantReply, + data = Data}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, server) -> + <> = Data, + + #channel{remote_id = RemoteId} = Channel0 = + ssh_channel:cache_lookup(Cache, ChannelId), + + ReplyMsg = {subsystem, ChannelId, WantReply, binary_to_list(SsName)}, + + try start_subsytem(SsName, Connection, Channel0, ReplyMsg) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Channel = Channel0#channel{user = Pid}, + ssh_channel:cache_update(Cache, Channel), + Reply = {connection_reply, ConnectionPid, + channel_success_msg(RemoteId)}, + {{replies, [Reply]}, Connection} + catch _:_ -> + Reply = {connection_reply, ConnectionPid, + channel_failure_msg(RemoteId)}, + {{replies, [Reply]}, Connection} + end; + +handle_msg(#ssh_msg_channel_request{request_type = "subsystem"}, + Connection, _, client) -> + %% The client SHOULD ignore subsystem requests. See RFC 4254 6.5. + {{replies, []}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "pty-req", + want_reply = WantReply, + data = Data}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, server) -> + <> = Data, + TermName = binary_to_list(BTermName), + + PtyRequest = {TermName, Width, Height, + PixWidth, PixHeight, decode_pty_opts(Modes)}, + + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + + handle_cli_msg(Connection, ConnectionPid, Channel, + {pty, ChannelId, WantReply, PtyRequest}); + +handle_msg(#ssh_msg_channel_request{request_type = "pty-req"}, + Connection, _, client) -> + %% The client SHOULD ignore pty requests. See RFC 4254 6.2. + {{replies, []}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "shell", + want_reply = WantReply}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, server) -> + + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + + handle_cli_msg(Connection, ConnectionPid, Channel, + {shell, ChannelId, WantReply}); + +handle_msg(#ssh_msg_channel_request{request_type = "shell"}, + Connection, _, client) -> + %% The client SHOULD ignore shell requests. See RFC 4254 6.5. + {{replies, []}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "exec", + want_reply = WantReply, + data = Data}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, server) -> + <> = Data, + + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + + handle_cli_msg(Connection, ConnectionPid, Channel, + {exec, ChannelId, WantReply, binary_to_list(Command)}); + +handle_msg(#ssh_msg_channel_request{request_type = "exec"}, + Connection, _, client) -> + %% The client SHOULD ignore exec requests. See RFC 4254 6.5. + {{replies, []}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = "env", + want_reply = WantReply, + data = Data}, + #connection{channel_cache = Cache} = Connection, + ConnectionPid, server) -> + + <> = Data, + + Channel = ssh_channel:cache_lookup(Cache, ChannelId), + + handle_cli_msg(Connection, ConnectionPid, Channel, + {env, ChannelId, WantReply, Var, Value}); + +handle_msg(#ssh_msg_channel_request{request_type = "env"}, + Connection, _, client) -> + %% The client SHOULD ignore env requests. + {{replies, []}, Connection}; + +handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = _Other, + want_reply = WantReply}, Connection, + ConnectionPid, _) -> + ?dbg(true, "ssh_msg ssh_msg_channel_request: Other=~p\n", + [_Other]), + if WantReply == true -> + FailMsg = channel_failure_msg(ChannelId), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, + Connection}; + true -> + {noreply, Connection} + end; + +handle_msg(#ssh_msg_global_request{name = _Type, + want_reply = WantReply, + data = _Data}, Connection, + ConnectionPid, _) -> + if WantReply == true -> + FailMsg = request_failure_msg(), + {{replies, [{connection_reply, ConnectionPid, FailMsg}]}, + Connection}; + true -> + {noreply, Connection} + end; + +%%% This transport message will also be handled at the connection level +handle_msg(#ssh_msg_disconnect{code = Code, + description = Description, + language = _Lang }, + #connection{channel_cache = Cache} = Connection0, _, _) -> + {Connection, Replies} = + ssh_channel:cache_foldl(fun(Channel, {Connection1, Acc}) -> + {Reply, Connection2} = + reply_msg(Channel, + Connection1, {closed, Channel#channel.local_id}), + {Connection2, [Reply | Acc]} + end, {Connection0, []}, Cache), + + ssh_channel:cache_delete(Cache), + {disconnect, {Code, Description}, {{replies, Replies}, Connection}}. + +handle_cli_msg(#connection{channel_cache = Cache} = Connection0, + ConnectionPid, + #channel{user = undefined, + local_id = ChannelId} = Channel0, Reply0) -> + + case (catch start_cli(Connection0, ChannelId)) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Channel = Channel0#channel{user = Pid}, + ssh_channel:cache_update(Cache, Channel), + {Reply, Connection} = reply_msg(Channel, Connection0, Reply0), + {{replies, [Reply]}, Connection}; + _ -> + Reply = {connection_reply, ConnectionPid, + request_failure_msg()}, + {{replies, [Reply]}, Connection0} + end; + +handle_cli_msg(Connection0, _, Channel, Reply0) -> + {Reply, Connection} = reply_msg(Channel, Connection0, Reply0), + {{replies, [Reply]}, Connection}. + + +channel_eof_msg(ChannelId) -> + #ssh_msg_channel_eof{recipient_channel = ChannelId}. + +channel_close_msg(ChannelId) -> + #ssh_msg_channel_close {recipient_channel = ChannelId}. + +channel_success_msg(ChannelId) -> + #ssh_msg_channel_success{recipient_channel = ChannelId}. + +channel_failure_msg(ChannelId) -> + #ssh_msg_channel_failure{recipient_channel = ChannelId}. + +channel_adjust_window_msg(ChannelId, Bytes) -> + #ssh_msg_channel_window_adjust{recipient_channel = ChannelId, + bytes_to_add = Bytes}. + +channel_data_msg(ChannelId, 0, Data) -> + #ssh_msg_channel_data{recipient_channel = ChannelId, + data = Data}; +channel_data_msg(ChannelId, Type, Data) -> + #ssh_msg_channel_extended_data{recipient_channel = ChannelId, + data_type_code = Type, + data = Data}. + +channel_open_msg(Type, ChannelId, WindowSize, MaxPacketSize, Data) -> + #ssh_msg_channel_open{channel_type = Type, + sender_channel = ChannelId, + initial_window_size = WindowSize, + maximum_packet_size = MaxPacketSize, + data = Data + }. + +channel_open_confirmation_msg(RemoteId, LID, WindowSize, PacketSize) -> + #ssh_msg_channel_open_confirmation{recipient_channel = RemoteId, + sender_channel = LID, + initial_window_size = WindowSize, + maximum_packet_size = PacketSize}. + +channel_open_failure_msg(RemoteId, Reason, Description, Lang) -> + #ssh_msg_channel_open_failure{recipient_channel = RemoteId, + reason = Reason, + description = Description, + lang = Lang}. + +channel_request_msg(ChannelId, Type, WantReply, Data) -> + #ssh_msg_channel_request{recipient_channel = ChannelId, + request_type = Type, + want_reply = WantReply, + data = Data}. + +global_request_msg(Type, WantReply, Data) -> + #ssh_msg_global_request{name = Type, + want_reply = WantReply, + data = Data}. +request_failure_msg() -> + #ssh_msg_request_failure{}. + +request_success_msg(Data) -> + #ssh_msg_request_success{data = Data}. + +bind(IP, Port, ChannelPid, Connection) -> + Binds = [{{IP, Port}, ChannelPid} + | lists:keydelete({IP, Port}, 1, + Connection#connection.port_bindings)], + Connection#connection{port_bindings = Binds}. + +unbind(IP, Port, Connection) -> + Connection#connection{ + port_bindings = + lists:keydelete({IP, Port}, 1, + Connection#connection.port_bindings)}. +unbind_channel(ChannelPid, Connection) -> + Binds = [{Bind, ChannelP} || {Bind, ChannelP} + <- Connection#connection.port_bindings, + ChannelP =/= ChannelPid], + Connection#connection{port_bindings = Binds}. + +bound_channel(IP, Port, Connection) -> + case lists:keysearch({IP, Port}, 1, Connection#connection.port_bindings) of + {value, {{IP, Port}, ChannelPid}} -> ChannelPid; + _ -> undefined + end. + +messages() -> + [ {ssh_msg_global_request, ?SSH_MSG_GLOBAL_REQUEST, + [string, + boolean, + '...']}, + + {ssh_msg_request_success, ?SSH_MSG_REQUEST_SUCCESS, + ['...']}, + + {ssh_msg_request_failure, ?SSH_MSG_REQUEST_FAILURE, + []}, + + {ssh_msg_channel_open, ?SSH_MSG_CHANNEL_OPEN, + [string, + uint32, + uint32, + uint32, + '...']}, + + {ssh_msg_channel_open_confirmation, ?SSH_MSG_CHANNEL_OPEN_CONFIRMATION, + [uint32, + uint32, + uint32, + uint32, + '...']}, + + {ssh_msg_channel_open_failure, ?SSH_MSG_CHANNEL_OPEN_FAILURE, + [uint32, + uint32, + string, + string]}, + + {ssh_msg_channel_window_adjust, ?SSH_MSG_CHANNEL_WINDOW_ADJUST, + [uint32, + uint32]}, + + {ssh_msg_channel_data, ?SSH_MSG_CHANNEL_DATA, + [uint32, + binary]}, + + {ssh_msg_channel_extended_data, ?SSH_MSG_CHANNEL_EXTENDED_DATA, + [uint32, + uint32, + binary]}, + + {ssh_msg_channel_eof, ?SSH_MSG_CHANNEL_EOF, + [uint32]}, + + {ssh_msg_channel_close, ?SSH_MSG_CHANNEL_CLOSE, + [uint32]}, + + {ssh_msg_channel_request, ?SSH_MSG_CHANNEL_REQUEST, + [uint32, + string, + boolean, + '...']}, + + {ssh_msg_channel_success, ?SSH_MSG_CHANNEL_SUCCESS, + [uint32]}, + + {ssh_msg_channel_failure, ?SSH_MSG_CHANNEL_FAILURE, + [uint32]} + ]. + +encode_ip(Addr) when is_tuple(Addr) -> + case catch inet_parse:ntoa(Addr) of + {'EXIT',_} -> false; + A -> A + end; +encode_ip(Addr) when is_list(Addr) -> + case inet_parse:address(Addr) of + {ok, _} -> Addr; + Error -> + case inet:getaddr(Addr, inet) of + {ok, A} -> + inet_parse:ntoa(A); + Error -> false + end + end. + +start_channel(Address, Port, Cb, Id, Args) -> + start_channel(Address, Port, Cb, Id, Args, undefined). + +start_channel(Address, Port, Cb, Id, Args, Exec) -> + ChildSpec = child_spec(Cb, Id, Args, Exec), + SystemSup = ssh_system_sup:system_supervisor(Address, Port), + ChannelSup = ssh_system_sup:channel_supervisor(SystemSup), + ssh_channel_sup:start_child(ChannelSup, ChildSpec). + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +setup_session(#connection{channel_cache = Cache} = Connection0, + ConnectionPid, RemoteId, + Type, WindowSize, PacketSize) -> + {ChannelId, Connection} = new_channel_id(Connection0), + + Channel = #channel{type = Type, + sys = "ssh", + local_id = ChannelId, + recv_window_size = ?DEFAULT_WINDOW_SIZE, + recv_packet_size = ?DEFAULT_PACKET_SIZE, + send_window_size = WindowSize, + send_packet_size = PacketSize, + remote_id = RemoteId + }, + ssh_channel:cache_update(Cache, Channel), + OpenConfMsg = channel_open_confirmation_msg(RemoteId, ChannelId, + ?DEFAULT_WINDOW_SIZE, + ?DEFAULT_PACKET_SIZE), + + {{replies, [{connection_reply, ConnectionPid, OpenConfMsg}]}, Connection}. + + +check_subsystem("sftp"= SsName, Options) -> + case proplists:get_value(subsystems, Options, no_subsys) of + no_subsys -> + {SsName, {Cb, Opts}} = ssh_sftpd:subsystem_spec([]), + {Cb, Opts}; + SubSystems -> + proplists:get_value(SsName, SubSystems, {none, []}) + end; + +check_subsystem(SsName, Options) -> + Subsystems = proplists:get_value(subsystems, Options, []), + case proplists:get_value(SsName, Subsystems, {none, []}) of + Fun when is_function(Fun) -> + {Fun, []}; + {_, _} = Value -> + Value + end. + +child_spec(Callback, Id, Args, Exec) -> + Name = make_ref(), + StartFunc = {ssh_channel, start_link, [self(), Id, Callback, Args, Exec]}, + Restart = temporary, + Shutdown = 3600, + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, [ssh_channel]}. + +%% Backwards compatibility +start_cli(#connection{address = Address, port = Port, cli_spec = {Fun, [Shell]}, + options = Options}, + _ChannelId) when is_function(Fun) -> + case Fun(Shell, Address, Port, Options) of + NewFun when is_function(NewFun) -> + {ok, NewFun()}; + Pid when is_pid(Pid) -> + {ok, Pid} + end; + +start_cli(#connection{address = Address, port = Port, + cli_spec = {CbModule, Args}, exec = Exec}, ChannelId) -> + start_channel(Address, Port, CbModule, ChannelId, Args, Exec). + +start_subsytem(BinName, #connection{address = Address, port = Port, + options = Options}, + #channel{local_id = ChannelId, remote_id = RemoteChannelId}, + ReplyMsg) -> + Name = binary_to_list(BinName), + case check_subsystem(Name, Options) of + {Callback, Opts} when is_atom(Callback), Callback =/= none -> + start_channel(Address, Port, Callback, ChannelId, Opts); + {Other, _} when Other =/= none -> + handle_backwards_compatibility(Other, self(), + ChannelId, RemoteChannelId, + Options, Address, Port, + {ssh_cm, self(), ReplyMsg}) + end. + +channel_data_reply(_, #channel{local_id = ChannelId} = Channel, + Connection0, DataType, Data) -> + {Reply, Connection} = + reply_msg(Channel, Connection0, {data, ChannelId, DataType, Data}), + {[Reply], Connection}. + +new_channel_id(Connection) -> + ID = Connection#connection.channel_id_seed, + {ID, Connection#connection{channel_id_seed = ID + 1}}. + +reply_msg(Channel, Connection, {open, _} = Reply) -> + request_reply_or_data(Channel, Connection, Reply); +reply_msg(Channel, Connection, {open_error, _, _, _} = Reply) -> + request_reply_or_data(Channel, Connection, Reply); +reply_msg(Channel, Connection, success = Reply) -> + request_reply_or_data(Channel, Connection, Reply); +reply_msg(Channel, Connection, failure = Reply) -> + request_reply_or_data(Channel, Connection, Reply); +reply_msg(Channel, Connection, {closed, _} = Reply) -> + request_reply_or_data(Channel, Connection, Reply); +reply_msg(#channel{user = ChannelPid}, Connection, Reply) -> + {{channel_data, ChannelPid, Reply}, Connection}. + +request_reply_or_data(#channel{local_id = ChannelId, user = ChannelPid}, + #connection{requests = Requests} = + Connection, Reply) -> + case lists:keysearch(ChannelId, 1, Requests) of + {value, {ChannelId, From}} -> + {{channel_requst_reply, From, Reply}, + Connection#connection{requests = + lists:keydelete(ChannelId, 1, Requests)}}; + false -> + {{channel_data, ChannelPid, Reply}, Connection} + end. + +update_send_window(Channel0, DataType, Data, + #connection{channel_cache = Cache}) -> + Buf0 = if Data == <<>> -> + Channel0#channel.send_buf; + true -> + Channel0#channel.send_buf ++ [{DataType, Data}] + end, + {Buf1, NewSz, Buf2} = get_window(Buf0, + Channel0#channel.send_packet_size, + Channel0#channel.send_window_size), + + Channel = Channel0#channel{send_window_size = NewSz, send_buf = Buf2}, + ssh_channel:cache_update(Cache, Channel), + {Buf1, Channel}. + +get_window(Bs, PSz, WSz) -> + get_window(Bs, PSz, WSz, []). + +get_window(Bs, _PSz, 0, Acc) -> + {lists:reverse(Acc), 0, Bs}; +get_window([B0 = {DataType, Bin} | Bs], PSz, WSz, Acc) -> + BSz = size(Bin), + if BSz =< WSz -> %% will fit into window + if BSz =< PSz -> %% will fit into a packet + get_window(Bs, PSz, WSz-BSz, [B0|Acc]); + true -> %% split into packet size + <> = Bin, + get_window([setelement(2, B0, Bin2) | Bs], + PSz, WSz-PSz, + [{DataType, Bin1}|Acc]) + end; + WSz =< PSz -> %% use rest of window + <> = Bin, + get_window([setelement(2, B0, Bin2) | Bs], + PSz, WSz-WSz, + [{DataType, Bin1}|Acc]); + true -> %% use packet size + <> = Bin, + get_window([setelement(2, B0, Bin2) | Bs], + PSz, WSz-PSz, + [{DataType, Bin1}|Acc]) + end; +get_window([], _PSz, WSz, Acc) -> + {lists:reverse(Acc), WSz, []}. + +flow_control(Channel, Cache) -> + flow_control([window_adjusted], Channel, Cache). + +flow_control([], Channel, Cache) -> + ssh_channel:cache_update(Cache, Channel), + []; +flow_control([_|_], #channel{flow_control = From} = Channel, Cache) -> + case From of + undefined -> + []; + _ -> + [{flow_control, Cache, Channel, From, ok}] + end. + +encode_pty_opts(Opts) -> + Bin = list_to_binary(encode_pty_opts2(Opts)), + Len = size(Bin), + <>. + +encode_pty_opts2([]) -> + [?TTY_OP_END]; +encode_pty_opts2([{vintr,Value} | Opts]) -> + [?VINTR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vquit,Value} | Opts]) -> + [?VQUIT, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{verase,Value} | Opts]) -> + [?VERASE, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vkill,Value} | Opts]) -> + [?VKILL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{veof,Value} | Opts]) -> + [?VEOF, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{veol,Value} | Opts]) -> + [?VEOL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{veol2,Value} | Opts]) -> + [?VEOL2, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vstart,Value} | Opts]) -> + [?VSTART, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vstop,Value} | Opts]) -> + [?VSTOP, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vsusp,Value} | Opts]) -> + [?VSUSP, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vdsusp,Value} | Opts]) -> + [?VDSUSP, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vreprint,Value} | Opts]) -> + [?VREPRINT, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vwerase,Value} | Opts]) -> + [ ?VWERASE, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vlnext,Value} | Opts]) -> + [?VLNEXT, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vflush,Value} | Opts]) -> + [?VFLUSH, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vswtch,Value} | Opts]) -> + [?VSWTCH, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vstatus,Value} | Opts]) -> + [?VSTATUS, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{vdiscard,Value} | Opts]) -> + [?VDISCARD, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{ignpar,Value} | Opts]) -> + [?IGNPAR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{parmrk,Value} | Opts]) -> + [?PARMRK, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{inpck,Value} | Opts]) -> + [?INPCK, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{istrip,Value} | Opts]) -> + [?ISTRIP, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{inlcr,Value} | Opts]) -> + [?INLCR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{igncr,Value} | Opts]) -> + [?IGNCR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{icrnl,Value} | Opts]) -> + [?ICRNL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{iuclc,Value} | Opts]) -> + [?IUCLC, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{ixon,Value} | Opts]) -> + [?IXON, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{ixany,Value} | Opts]) -> + [?IXANY, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{ixoff,Value} | Opts]) -> + [?IXOFF, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{imaxbel,Value} | Opts]) -> + [?IMAXBEL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{isig,Value} | Opts]) -> + [?ISIG, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{icanon,Value} | Opts]) -> + [?ICANON, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{xcase,Value} | Opts]) -> + [?XCASE, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echo,Value} | Opts]) -> + [?ECHO, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echoe,Value} | Opts]) -> + [?ECHOE, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echok,Value} | Opts]) -> + [?ECHOK, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echonl,Value} | Opts]) -> + [?ECHONL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{noflsh,Value} | Opts]) -> + [?NOFLSH, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{tostop,Value} | Opts]) -> + [?TOSTOP, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{iexten,Value} | Opts]) -> + [?IEXTEN, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echoctl,Value} | Opts]) -> + [?ECHOCTL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{echoke,Value} | Opts]) -> + [?ECHOKE, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{pendin,Value} | Opts]) -> + [?PENDIN, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{opost,Value} | Opts]) -> + [?OPOST, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{olcuc,Value} | Opts]) -> + [?OLCUC, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{onlcr,Value} | Opts]) -> + [?ONLCR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{ocrnl,Value} | Opts]) -> + [?OCRNL, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{onocr,Value} | Opts]) -> + [?ONOCR, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{onlret,Value} | Opts]) -> + [?ONLRET, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{cs7,Value} | Opts]) -> + [?CS7, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{cs8,Value} | Opts]) -> + [?CS8, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{parenb,Value} | Opts]) -> + [?PARENB, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{parodd,Value} | Opts]) -> + [?PARODD, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{tty_op_ispeed,Value} | Opts]) -> + [?TTY_OP_ISPEED, ?uint32(Value) | encode_pty_opts2(Opts)]; +encode_pty_opts2([{tty_op_ospeed,Value} | Opts]) -> + [?TTY_OP_OSPEED, ?uint32(Value) | encode_pty_opts2(Opts)]. + +decode_pty_opts(<<>>) -> + []; +decode_pty_opts(<<0, 0, 0, 0>>) -> + []; +decode_pty_opts(<>) -> + decode_pty_opts2(Modes); +decode_pty_opts(Binary) -> + decode_pty_opts2(Binary). + +decode_pty_opts2(<>) -> + []; +decode_pty_opts2(<>) -> + Op = case Code of + ?VINTR -> vintr; + ?VQUIT -> vquit; + ?VERASE -> verase; + ?VKILL -> vkill; + ?VEOF -> veof; + ?VEOL -> veol; + ?VEOL2 -> veol2; + ?VSTART -> vstart; + ?VSTOP -> vstop; + ?VSUSP -> vsusp; + ?VDSUSP -> vdsusp; + ?VREPRINT -> vreprint; + ?VWERASE -> vwerase; + ?VLNEXT -> vlnext; + ?VFLUSH -> vflush; + ?VSWTCH -> vswtch; + ?VSTATUS -> vstatus; + ?VDISCARD -> vdiscard; + ?IGNPAR -> ignpar; + ?PARMRK -> parmrk; + ?INPCK -> inpck; + ?ISTRIP -> istrip; + ?INLCR -> inlcr; + ?IGNCR -> igncr; + ?ICRNL -> icrnl; + ?IUCLC -> iuclc; + ?IXON -> ixon; + ?IXANY -> ixany; + ?IXOFF -> ixoff; + ?IMAXBEL -> imaxbel; + ?ISIG -> isig; + ?ICANON -> icanon; + ?XCASE -> xcase; + ?ECHO -> echo; + ?ECHOE -> echoe; + ?ECHOK -> echok; + ?ECHONL -> echonl; + ?NOFLSH -> noflsh; + ?TOSTOP -> tostop; + ?IEXTEN -> iexten; + ?ECHOCTL -> echoctl; + ?ECHOKE -> echoke; + ?PENDIN -> pendin; + ?OPOST -> opost; + ?OLCUC -> olcuc; + ?ONLCR -> onlcr; + ?OCRNL -> ocrnl; + ?ONOCR -> onocr; + ?ONLRET -> onlret; + ?CS7 -> cs7; + ?CS8 -> cs8; + ?PARENB -> parenb; + ?PARODD -> parodd; + ?TTY_OP_ISPEED -> tty_op_ispeed; + ?TTY_OP_OSPEED -> tty_op_ospeed; + _ -> Code + end, + [{Op, Value} | decode_pty_opts2(Tail)]. + +decode_ip(Addr) when is_binary(Addr) -> + case inet_parse:address(binary_to_list(Addr)) of + {error,_} -> Addr; + {ok,A} -> A + end. + +%% This is really awful and that is why it is beeing phased out. +handle_backwards_compatibility({_,_,_,_,_,_} = ChildSpec, _, _, _, _, + Address, Port, _) -> + SystemSup = ssh_system_sup:system_supervisor(Address, Port), + ChannelSup = ssh_system_sup:channel_supervisor(SystemSup), + ssh_channel_sup:start_child(ChannelSup, ChildSpec); + +handle_backwards_compatibility(Module, ConnectionManager, ChannelId, + RemoteChannelId, Opts, + _, _, Msg) when is_atom(Module) -> + {ok, SubSystemPid} = gen_server:start_link(Module, [Opts], []), + SubSystemPid ! + {ssh_cm, ConnectionManager, + {open, ChannelId, RemoteChannelId, {session}}}, + SubSystemPid ! Msg, + {ok, SubSystemPid}; + +handle_backwards_compatibility(Fun, ConnectionManager, ChannelId, + RemoteChannelId, + _, _, _, Msg) when is_function(Fun) -> + SubSystemPid = Fun(), + SubSystemPid ! + {ssh_cm, ConnectionManager, + {open, ChannelId, RemoteChannelId, {session}}}, + SubSystemPid ! Msg, + {ok, SubSystemPid}; + +handle_backwards_compatibility(ChildSpec, + ConnectionManager, + ChannelId, RemoteChannelId, _, + Address, Port, Msg) -> + SystemSup = ssh_system_sup:system_supervisor(Address, Port), + ChannelSup = ssh_system_sup:channel_supervisor(SystemSup), + {ok, SubSystemPid} + = ssh_channel_sup:start_child(ChannelSup, ChildSpec), + SubSystemPid ! + {ssh_cm, ConnectionManager, + {open, ChannelId, RemoteChannelId, {session}}}, + SubSystemPid ! Msg, + {ok, SubSystemPid}. diff --git a/lib/ssh/src/ssh_connection_controler.erl b/lib/ssh/src/ssh_connection_controler.erl new file mode 100644 index 0000000000..7960eb11c6 --- /dev/null +++ b/lib/ssh/src/ssh_connection_controler.erl @@ -0,0 +1,137 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%-------------------------------------------------------------------- +%% File : ssh_connection_controler.erl +%% Description : +%% +%%-------------------------------------------------------------------- + +-module(ssh_connection_controler). + +-behaviour(gen_server). + +%%----------------------------------------------------------------- +%% External exports +%%----------------------------------------------------------------- +-export([start_link/1, start_handler_child/2, start_manager_child/2, + connection_manager/1]). + +%%----------------------------------------------------------------- +%% Internal exports +%%----------------------------------------------------------------- +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2, stop/1]). + +-record(state, {role, manager, handler, timeout}). + +%%----------------------------------------------------------------- +%% External interface functions +%%----------------------------------------------------------------- +%%----------------------------------------------------------------- +%% Func: start/0 +%%----------------------------------------------------------------- +start_link(Args) -> + gen_server:start_link(?MODULE, [Args], []). + +%% Will be called from the manager child process +start_handler_child(ServerRef, Args) -> + gen_server:call(ServerRef, {handler, self(), Args}, infinity). + +%% Will be called from the acceptor process +start_manager_child(ServerRef, Args) -> + gen_server:call(ServerRef, {manager, Args}, infinity). + +connection_manager(ServerRef) -> + {ok, gen_server:call(ServerRef, manager, infinity)}. + +%%----------------------------------------------------------------- +%% Internal interface functions +%%----------------------------------------------------------------- +%%----------------------------------------------------------------- +%% Func: stop/1 +%%----------------------------------------------------------------- +stop(Pid) -> + gen_server:cast(Pid, stop). + +%%----------------------------------------------------------------- +%% Server functions +%%----------------------------------------------------------------- +%%----------------------------------------------------------------- +%% Func: init/1 +%%----------------------------------------------------------------- +init([Opts]) -> + process_flag(trap_exit, true), + case proplists:get_value(role, Opts) of + client -> + {ok, Manager} = ssh_connection_manager:start_link([client, Opts]), + {ok, #state{role = client, manager = Manager}}; + _server -> + %% Children started by acceptor process + {ok, #state{role = server}} + end. + + +%%----------------------------------------------------------------- +%% Func: terminate/2 +%%----------------------------------------------------------------- +terminate(_Reason, #state{}) -> + ok. + +%%----------------------------------------------------------------- +%% Func: handle_call/3 +%%----------------------------------------------------------------- +handle_call({handler, Pid, [Role, Socket, Opts]}, _From, State) -> + {ok, Handler} = ssh_connection_handler:start_link(Role, Pid, Socket, Opts), + {reply, {ok, Handler}, State#state{handler = Handler}}; +handle_call({manager, [server = Role, Socket, Opts]}, _From, State) -> + {ok, Manager} = ssh_connection_manager:start_link([Role, Socket, Opts]), + {reply, {ok, Manager}, State#state{manager = Manager}}; +handle_call({manager, [client = Role | Opts]}, _From, State) -> + {ok, Manager} = ssh_connection_manager:start_link([Role, Opts]), + {reply, {ok, Manager}, State#state{manager = Manager}}; +handle_call(manager, _From, State) -> + {reply, State#state.manager, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_, _, State) -> + {noreply, State, State#state.timeout}. + +%%----------------------------------------------------------------- +%% Func: handle_cast/2 +%%----------------------------------------------------------------- +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(_, State) -> + {noreply, State, State#state.timeout}. + +%%----------------------------------------------------------------- +%% Func: handle_info/2 +%%----------------------------------------------------------------- +%% handle_info(ssh_connected, State) -> +%% {stop, normal, State}; +%% Servant termination. +handle_info({'EXIT', _Pid, normal}, State) -> + {stop, normal, State}. + +%%----------------------------------------------------------------- +%% Func: code_change/3 +%%----------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl new file mode 100644 index 0000000000..5240b4b4c5 --- /dev/null +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -0,0 +1,879 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% +%%---------------------------------------------------------------------- +%% Purpose: Handles the setup of an ssh connection, e.i. both the +%% setup SSH Transport Layer Protocol (RFC 4253) and Authentication +%% Protocol (RFC 4252). Details of the different protocols are +%% implemented in ssh_transport.erl, ssh_auth.erl +%% ---------------------------------------------------------------------- + +-module(ssh_connection_handler). + +-behaviour(gen_fsm). + +-include("ssh.hrl"). +-include("ssh_transport.hrl"). +-include("ssh_auth.hrl"). +-include("ssh_connect.hrl"). + +-export([start_link/4, send/2, renegotiate/1, send_event/2, + connection_info/3, + peer_address/1]). + +%% gen_fsm callbacks +-export([hello/2, kexinit/2, key_exchange/2, new_keys/2, + userauth/2, connected/2]). + +-export([init/1, state_name/3, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). + +%% spawn export +-export([ssh_info_handler/3]). + +-record(state, { + transport_protocol, % ex: tcp + transport_cb, + transport_close_tag, + ssh_params, % #ssh{} - from ssh.hrl + socket, % socket() + decoded_data_buffer, % binary() + encoded_data_buffer, % binary() + undecoded_packet_length, % integer() + key_exchange_init_msg, % #ssh_msg_kexinit{} + renegotiate = false, % boolean() + manager, % pid() + connection_queue, + address, + port, + opts + }). + +-define(DBG_MESSAGE, true). + +%%==================================================================== +%% Internal application API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> ok,Pid} | ignore | {error,Error} +%% Description:Creates a gen_fsm process which calls Module:init/1 to +%% initialize. To ensure a synchronized start-up procedure, this function +%% does not return until Module:init/1 has returned. +%%-------------------------------------------------------------------- +start_link(Role, Manager, Socket, Options) -> + gen_fsm:start_link(?MODULE, [Role, Manager, Socket, Options], []). + +send(ConnectionHandler, Data) -> + send_all_state_event(ConnectionHandler, {send, Data}). + +renegotiate(ConnectionHandler) -> + send_all_state_event(ConnectionHandler, renegotiate). + +connection_info(ConnectionHandler, From, Options) -> + send_all_state_event(ConnectionHandler, {info, From, Options}). + +%% Replaced with option to connection_info/3. For now keep +%% for backwards compatibility +peer_address(ConnectionHandler) -> + sync_send_all_state_event(ConnectionHandler, peer_address). + +%%==================================================================== +%% gen_fsm callbacks +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, StateName, State} | +%% {ok, StateName, State, Timeout} | +%% ignore | +%% {stop, StopReason} +%% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or +%% gen_fsm:start_link/3,4, this function is called by the new process to +%% initialize. +%%-------------------------------------------------------------------- +init([Role, Manager, Socket, SshOpts]) -> + {A,B,C} = erlang:now(), + random:seed(A, B, C), + {NumVsn, StrVsn} = ssh_transport:versions(Role, SshOpts), + ssh_bits:install_messages(ssh_transport:transport_messages(NumVsn)), + {Protocol, Callback, CloseTag} = + proplists:get_value(transport, SshOpts, {tcp, gen_tcp, tcp_closed}), + Ssh = init_ssh(Role, NumVsn, StrVsn, SshOpts, Socket), + {ok, hello, #state{ssh_params = + Ssh#ssh{send_sequence = 0, recv_sequence = 0}, + socket = Socket, + decoded_data_buffer = <<>>, + encoded_data_buffer = <<>>, + transport_protocol = Protocol, + transport_cb = Callback, + transport_close_tag = CloseTag, + manager = Manager, + opts = SshOpts + }}. +%%-------------------------------------------------------------------- +%% Function: +%% state_name(Event, State) -> {next_state, NextStateName, NextState}| +%% {next_state, NextStateName, +%% NextState, Timeout} | +%% {stop, Reason, NewState} +%% Description:There should be one instance of this function for each possible +%% state name. Whenever a gen_fsm receives an event sent using +%% gen_fsm:send_event/2, the instance of this function with the same name as +%% the current state name StateName is called to handle the event. It is also +%% called if a timeout occurs. +%%-------------------------------------------------------------------- +hello(socket_control, #state{socket = Socket, ssh_params = Ssh} = State) -> + VsnMsg = ssh_transport:hello_version_msg(string_version(Ssh)), + send_msg(VsnMsg, State), + inet:setopts(Socket, [{packet, line}]), + {next_state, hello, next_packet(State)}; + +hello({info_line, _Line}, State) -> + {next_state, hello, next_packet(State)}; + +hello({version_exchange, Version}, #state{ssh_params = Ssh0, + socket = Socket} = State) -> + {NumVsn, StrVsn} = ssh_transport:handle_hello_version(Version), + case handle_version(NumVsn, StrVsn, Ssh0) of + {ok, Ssh1} -> + inet:setopts(Socket, [{packet,0}, {mode,binary}]), + {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh1), + send_msg(SshPacket, State), + {next_state, kexinit, next_packet(State#state{ssh_params = Ssh, + key_exchange_init_msg = + KeyInitMsg})}; + not_supported -> + DisconnectMsg = + #ssh_msg_disconnect{code = + ?SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, + description = "Protocol version " ++ StrVsn + ++ " not supported", + language = "en"}, + handle_disconnect(DisconnectMsg, State) + end. + +kexinit({#ssh_msg_kexinit{} = Kex, Payload}, + #state{ssh_params = #ssh{role = Role} = Ssh0, + key_exchange_init_msg = OwnKex} = + State) -> + Ssh1 = ssh_transport:key_init(opposite_role(Role), Ssh0, Payload), + try ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1) of + {ok, NextKexMsg, Ssh} when Role == client -> + send_msg(NextKexMsg, State), + {next_state, key_exchange, + next_packet(State#state{ssh_params = Ssh})}; + {ok, Ssh} when Role == server -> + {next_state, key_exchange, + next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end. + +key_exchange(#ssh_msg_kexdh_init{} = Msg, + #state{ssh_params = #ssh{role = server} =Ssh0} = State) -> + try ssh_transport:handle_kexdh_init(Msg, Ssh0) of + {ok, KexdhReply, Ssh1} -> + send_msg(KexdhReply, State), + {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + send_msg(NewKeys, State), + {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +key_exchange(#ssh_msg_kexdh_reply{} = Msg, + #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> + try ssh_transport:handle_kexdh_reply(Msg, Ssh0) of + {ok, NewKeys, Ssh} -> + send_msg(NewKeys, State), + {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +key_exchange(#ssh_msg_kex_dh_gex_group{} = Msg, + #state{ssh_params = #ssh{role = server} = Ssh0} = State) -> + try ssh_transport:handle_kex_dh_gex_group(Msg, Ssh0) of + {ok, NextKexMsg, Ssh1} -> + send_msg(NextKexMsg, State), + {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + send_msg(NewKeys, State), + {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +key_exchange(#ssh_msg_kex_dh_gex_request{} = Msg, + #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> + try ssh_transport:handle_kex_dh_gex_request(Msg, Ssh0) of + {ok, NextKexMsg, Ssh} -> + send_msg(NextKexMsg, State), + {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; +key_exchange(#ssh_msg_kex_dh_gex_reply{} = Msg, + #state{ssh_params = #ssh{role = client} = Ssh0} = State) -> + try ssh_transport:handle_kex_dh_gex_reply(Msg, Ssh0) of + {ok, NewKeys, Ssh} -> + send_msg(NewKeys, State), + {next_state, new_keys, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end. + +new_keys(#ssh_msg_newkeys{} = Msg, #state{ssh_params = Ssh0} = State0) -> + try ssh_transport:handle_new_keys(Msg, Ssh0) of + {ok, Ssh} -> + {NextStateName, State} = + after_new_keys(State0#state{ssh_params = Ssh}), + {next_state, NextStateName, next_packet(State)} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State0), + {stop, normal, State0} + end. + +userauth(#ssh_msg_service_request{name = "ssh-userauth"} = Msg, + #state{ssh_params = #ssh{role = server, + session_id = SessionId} = Ssh0} = State) -> + ssh_bits:install_messages(ssh_auth:userauth_messages()), + try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of + {ok, {Reply, Ssh}} -> + send_msg(Reply, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +userauth(#ssh_msg_service_accept{name = "ssh-userauth"}, + #state{ssh_params = #ssh{role = client, + service = "ssh-userauth"} = Ssh0} = + State) -> + {Msg, Ssh} = ssh_auth:init_userauth_request_msg(Ssh0), + send_msg(Msg, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})}; + +userauth(#ssh_msg_userauth_request{service = "ssh-connection", + method = "none"} = Msg, + #state{ssh_params = #ssh{session_id = SessionId, role = server, + service = "ssh-connection"} = Ssh0 + } = State) -> + try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of + {not_authorized, {_User, _Reason}, {Reply, Ssh}} -> + send_msg(Reply, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +userauth(#ssh_msg_userauth_request{service = "ssh-connection", + method = Method} = Msg, + #state{ssh_params = #ssh{session_id = SessionId, role = server, + service = "ssh-connection", + peer = {_, Address}} = Ssh0, + opts = Opts, manager = Pid} = State) -> + try ssh_auth:handle_userauth_request(Msg, SessionId, Ssh0) of + {authorized, User, {Reply, Ssh}} -> + send_msg(Reply, State), + ssh_userreg:register_user(User, Pid), + Pid ! ssh_connected, + connected_fun(User, Address, Method, Opts), + {next_state, connected, + next_packet(State#state{ssh_params = Ssh})}; + {not_authorized, {User, Reason}, {Reply, Ssh}} -> + retry_fun(User, Reason, Opts), + send_msg(Reply, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +userauth(#ssh_msg_userauth_info_request{} = Msg, + #state{ssh_params = #ssh{role = client, + io_cb = IoCb} = Ssh0} = State) -> + try ssh_auth:handle_userauth_info_request(Msg, IoCb, Ssh0) of + {ok, {Reply, Ssh}} -> + send_msg(Reply, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +userauth(#ssh_msg_userauth_info_response{} = Msg, + #state{ssh_params = #ssh{role = server} = Ssh0} = State) -> + try ssh_auth:handle_userauth_info_response(Msg, Ssh0) of + {ok, {Reply, Ssh}} -> + send_msg(Reply, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + catch + #ssh_msg_disconnect{} = DisconnectMsg -> + handle_disconnect(DisconnectMsg, State) + end; + +userauth(#ssh_msg_userauth_success{}, #state{ssh_params = #ssh{role = client}, + manager = Pid} = State) -> + Pid ! ssh_connected, + {next_state, connected, next_packet(State)}; + +userauth(#ssh_msg_userauth_failure{}, + #state{ssh_params = #ssh{role = client, + userauth_methods = []}} + = State) -> + Msg = #ssh_msg_disconnect{code = + ?SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + description = "Unable to connect using the available" + " authentication methods", + language = "en"}, + handle_disconnect(Msg, State); + +%% Server tells us which authentication methods that are allowed +userauth(#ssh_msg_userauth_failure{authentications = Methodes}, + #state{ssh_params = #ssh{role = client, + userauth_methods = none} = Ssh0} = State) -> + AuthMethods = string:tokens(Methodes, ","), + {Msg, Ssh} = ssh_auth:userauth_request_msg( + Ssh0#ssh{userauth_methods = AuthMethods}), + send_msg(Msg, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})}; + +%% The prefered authentication method failed try next method +userauth(#ssh_msg_userauth_failure{}, + #state{ssh_params = #ssh{role = client} = Ssh0, + manager = Pid} = State) -> + case ssh_auth:userauth_request_msg(Ssh0) of + {disconnect, Event, {Msg, _}} -> + try + send_msg(Msg, State), + ssh_connection_manager:event(Pid, Event) + catch + exit:{noproc, _Reason} -> + Report = io_lib:format("Connection Manager terminated: ~p~n", + [Pid]), + error_logger:info_report(Report); + exit:Exit -> + Report = io_lib:format("Connection Manager returned:~n~p~n~p~n", + [Msg, Exit]), + error_logger:info_report(Report) + end, + {stop, normal, State}; + {Msg, Ssh} -> + send_msg(Msg, State), + {next_state, userauth, next_packet(State#state{ssh_params = Ssh})} + end; + +userauth(#ssh_msg_userauth_banner{}, + #state{ssh_params = #ssh{userauth_quiet_mode = true, + role = client}} = State) -> + {next_state, userauth, next_packet(State)}; +userauth(#ssh_msg_userauth_banner{message = Msg}, + #state{ssh_params = + #ssh{userauth_quiet_mode = false, role = client}} = State) -> + io:format("~s", [Msg]), + {next_state, userauth, next_packet(State)}. + +connected({#ssh_msg_kexinit{}, _Payload} = Event, State) -> + kexinit(Event, State#state{renegotiate = true}). + +%%-------------------------------------------------------------------- +%% Function: +%% state_name(Event, From, State) -> {next_state, NextStateName, NextState} | +%% {next_state, NextStateName, +%% NextState, Timeout} | +%% {reply, Reply, NextStateName, NextState}| +%% {reply, Reply, NextStateName, +%% NextState, Timeout} | +%% {stop, Reason, NewState}| +%% {stop, Reason, Reply, NewState} +%% Description: There should be one instance of this function for each +%% possible state name. Whenever a gen_fsm receives an event sent using +%% gen_fsm:sync_send_event/2,3, the instance of this function with the same +%% name as the current state name StateName is called to handle the event. +%%-------------------------------------------------------------------- +state_name(_Event, _From, State) -> + Reply = ok, + {reply, Reply, state_name, State}. + +%%-------------------------------------------------------------------- +%% Function: +%% handle_event(Event, StateName, State) -> {next_state, NextStateName, +%% NextState} | +%% {next_state, NextStateName, +%% NextState, Timeout} | +%% {stop, Reason, NewState} +%% Description: Whenever a gen_fsm receives an event sent using +%% gen_fsm:send_all_state_event/2, this function is called to handle +%% the event. +%%-------------------------------------------------------------------- +handle_event({send, Data}, StateName, #state{ssh_params = Ssh0} = State) -> + {Packet, Ssh} = ssh_transport:pack(Data, Ssh0), + send_msg(Packet, State), + {next_state, StateName, next_packet(State#state{ssh_params = Ssh})}; + +handle_event(#ssh_msg_disconnect{} = Msg, _StateName, + #state{manager = Pid} = State) -> + (catch ssh_connection_manager:event(Pid, Msg)), + {stop, normal, State}; + +handle_event(#ssh_msg_ignore{}, StateName, State) -> + {next_state, StateName, next_packet(State)}; + +handle_event(#ssh_msg_debug{always_display = true, message = DbgMsg}, + StateName, State) -> + io:format("DEBUG: ~p\n", [DbgMsg]), + {next_state, StateName, next_packet(State)}; + +handle_event(#ssh_msg_debug{}, StateName, State) -> + {next_state, StateName, next_packet(State)}; + +handle_event(#ssh_msg_unimplemented{}, StateName, State) -> + {next_state, StateName, next_packet(State)}; + +handle_event(renegotiate, connected, #state{ssh_params = Ssh0} + = State) -> + {KeyInitMsg, SshPacket, Ssh} = ssh_transport:key_exchange_init_msg(Ssh0), + send_msg(SshPacket, State), + {next_state, connected, + next_packet(State#state{ssh_params = Ssh, + key_exchange_init_msg = KeyInitMsg, + renegotiate = true})}; + +handle_event(renegotiate, StateName, State) -> + %% Allready in keyexcahange so ignore + {next_state, StateName, State}; + +handle_event({info, From, Options}, StateName, #state{ssh_params = Ssh} = State) -> + spawn(?MODULE, ssh_info_handler, [Options, Ssh, From]), + {next_state, StateName, State}; + +handle_event({unknown, Data}, StateName, State) -> + Msg = #ssh_msg_unimplemented{sequence = Data}, + send_msg(Msg, State), + {next_state, StateName, next_packet(State)}. +%%-------------------------------------------------------------------- +%% Function: +%% handle_sync_event(Event, From, StateName, +%% State) -> {next_state, NextStateName, NextState} | +%% {next_state, NextStateName, NextState, +%% Timeout} | +%% {reply, Reply, NextStateName, NextState}| +%% {reply, Reply, NextStateName, NextState, +%% Timeout} | +%% {stop, Reason, NewState} | +%% {stop, Reason, Reply, NewState} +%% Description: Whenever a gen_fsm receives an event sent using +%% gen_fsm:sync_send_all_state_event/2,3, this function is called to handle +%% the event. +%%-------------------------------------------------------------------- + +%% Replaced with option to connection_info/3. For now keep +%% for backwards compatibility +handle_sync_event(peer_address, _From, StateName, + #state{ssh_params = #ssh{peer = {_, Address}}} = State) -> + {reply, {ok, Address}, StateName, State}. + +%%-------------------------------------------------------------------- +%% Function: +%% handle_info(Info,StateName,State)-> {next_state, NextStateName, NextState}| +%% {next_state, NextStateName, NextState, +%% Timeout} | +%% {stop, Reason, NewState} +%% Description: This function is called by a gen_fsm when it receives any +%% other message than a synchronous or asynchronous event +%% (or a system message). +%%-------------------------------------------------------------------- +handle_info({Protocol, Socket, "SSH-" ++ _ = Version}, hello, + #state{socket = Socket, + transport_protocol = Protocol} = State ) -> + event({version_exchange, Version}, hello, State); + +handle_info({Protocol, Socket, Info}, hello, + #state{socket = Socket, + transport_protocol = Protocol} = State) -> + event({info_line, Info}, hello, State); + +handle_info({Protocol, Socket, Data}, Statename, + #state{socket = Socket, + transport_protocol = Protocol, + ssh_params = #ssh{decrypt_block_size = BlockSize, + recv_mac_size = MacSize} = Ssh0, + decoded_data_buffer = <<>>, + encoded_data_buffer = EncData0} = State0) -> + + %% Implementations SHOULD decrypt the length after receiving the + %% first 8 (or cipher block size, whichever is larger) bytes of a + %% packet. (RFC 4253: Section 6 - Binary Packet Protocol) + case size(EncData0) + size(Data) >= max(8, BlockSize) of + true -> + {Ssh, SshPacketLen, DecData, EncData} = + ssh_transport:decrypt_first_block(<>, Ssh0), + case SshPacketLen > ?SSH_MAX_PACKET_SIZE of + true -> + DisconnectMsg = + #ssh_msg_disconnect{code = + ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Bad packet length " + ++ integer_to_list(SshPacketLen), + language = "en"}, + handle_disconnect(DisconnectMsg, State0); + false -> + RemainingSshPacketLen = + (SshPacketLen + ?SSH_LENGHT_INDICATOR_SIZE) - + BlockSize + MacSize, + State = State0#state{ssh_params = Ssh}, + handle_ssh_packet_data(RemainingSshPacketLen, + DecData, EncData, Statename, + State) + end; + false -> + {next_state, Statename, + next_packet(State0#state{encoded_data_buffer = + <>})} + end; + +handle_info({Protocol, Socket, Data}, Statename, + #state{socket = Socket, + transport_protocol = Protocol, + decoded_data_buffer = DecData, + encoded_data_buffer = EncData, + undecoded_packet_length = Len} = + State) when is_integer(Len) -> + handle_ssh_packet_data(Len, DecData, <>, + Statename, State); + +handle_info({CloseTag, _Socket}, _StateName, + #state{transport_close_tag = CloseTag, %%manager = Pid, + ssh_params = #ssh{role = _Role, opts = _Opts}} = State) -> + %%ok = ssh_connection_manager:delivered(Pid), + {stop, normal, State}. +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, StateName, State) -> void() +%% Description:This function is called by a gen_fsm when it is about +%% to terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_fsm terminates with +%% Reason. The return value is ignored. +%%-------------------------------------------------------------------- +terminate(normal, _, #state{transport_cb = Transport, + socket = Socket}) -> + (catch Transport:close(Socket)), + ok; + +terminate(shutdown, _, State) -> + DisconnectMsg = + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, + description = "Application disconnect", + language = "en"}, + handle_disconnect(DisconnectMsg, State); + +terminate(Reason, _, State) -> + Desc = io_lib:format("Erlang ssh connection handler failed with reason: " + "~p , please report this to erlang-bugs@erlang.org \n", + [Reason]), + DisconnectMsg = + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_CONNECTION_LOST, + description = Desc, + language = "en"}, + handle_disconnect(DisconnectMsg, State). + +%%-------------------------------------------------------------------- +%% Function: +%% code_change(OldVsn, StateName, State, Extra) -> {ok, StateName, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +init_ssh(client = Role, Vsn, Version, Options, Socket) -> + IOCb = case proplists:get_value(user_interaction, Options, true) of + true -> + ssh_io; + false -> + ssh_no_io + end, + + AuthMethods = proplists:get_value(auth_methods, Options, + ?SUPPORTED_AUTH_METHODS), + {ok, PeerAddr} = inet:peername(Socket), + + PeerName = proplists:get_value(host, Options), + + #ssh{role = Role, + c_vsn = Vsn, + c_version = Version, + key_cb = proplists:get_value(key_cb, Options, ssh_file), + io_cb = IOCb, + userauth_quiet_mode = proplists:get_value(quiet_mode, Options, false), + opts = Options, + userauth_supported_methods = AuthMethods, + peer = {PeerName, PeerAddr} + }; + +init_ssh(server = Role, Vsn, Version, Options, Socket) -> + + AuthMethods = proplists:get_value(auth_methods, Options, + ?SUPPORTED_AUTH_METHODS), + {ok, PeerAddr} = inet:peername(Socket), + + #ssh{role = Role, + s_vsn = Vsn, + s_version = Version, + key_cb = proplists:get_value(key_cb, Options, ssh_file), + io_cb = proplists:get_value(io_cb, Options, ssh_io), + opts = Options, + userauth_supported_methods = AuthMethods, + peer = {undefined, PeerAddr} + }. + +send_msg(Msg, #state{socket = Socket, transport_cb = Transport}) -> + Transport:send(Socket, Msg). + +handle_version({2, 0} = NumVsn, StrVsn, Ssh0) -> + Ssh = counterpart_versions(NumVsn, StrVsn, Ssh0), + {ok, Ssh}; +handle_version(_,_,_) -> + not_supported. + +string_version(#ssh{role = client, c_version = Vsn}) -> + Vsn; +string_version(#ssh{role = server, s_version = Vsn}) -> + Vsn. + +send_event(FsmPid, Event) -> + gen_fsm:send_event(FsmPid, Event). + +send_all_state_event(FsmPid, Event) -> + gen_fsm:send_all_state_event(FsmPid, Event). + +sync_send_all_state_event(FsmPid, Event) -> + gen_fsm:sync_send_all_state_event(FsmPid, Event). + +%% simulate send_all_state_event(self(), Event) +event(#ssh_msg_disconnect{} = Event, StateName, State) -> + handle_event(Event, StateName, State); +event(#ssh_msg_ignore{} = Event, StateName, State) -> + handle_event(Event, StateName, State); +event(#ssh_msg_debug{} = Event, StateName, State) -> + handle_event(Event, StateName, State); +event(#ssh_msg_unimplemented{} = Event, StateName, State) -> + handle_event(Event, StateName, State); +%% simulate send_event(self(), Event) +event(Event, StateName, State) -> + ?MODULE:StateName(Event, State). + +generate_event(<> = Msg, StateName, + #state{manager = Pid} = State0, EncData) + when Byte == ?SSH_MSG_GLOBAL_REQUEST; + Byte == ?SSH_MSG_REQUEST_SUCCESS; + Byte == ?SSH_MSG_REQUEST_FAILURE; + Byte == ?SSH_MSG_CHANNEL_OPEN; + Byte == ?SSH_MSG_CHANNEL_OPEN_CONFIRMATION; + Byte == ?SSH_MSG_CHANNEL_OPEN_FAILURE; + Byte == ?SSH_MSG_CHANNEL_WINDOW_ADJUST; + Byte == ?SSH_MSG_CHANNEL_DATA; + Byte == ?SSH_MSG_CHANNEL_EXTENDED_DATA; + Byte == ?SSH_MSG_CHANNEL_EOF; + Byte == ?SSH_MSG_CHANNEL_CLOSE; + Byte == ?SSH_MSG_CHANNEL_REQUEST; + Byte == ?SSH_MSG_CHANNEL_SUCCESS; + Byte == ?SSH_MSG_CHANNEL_FAILURE -> + ssh_connection_manager:event(Pid, Msg), + State = generate_event_new_state(State0, EncData), + next_packet(State), + {next_state, StateName, State}; + +generate_event(Msg, StateName, State0, EncData) -> + Event = ssh_bits:decode(Msg), + State = generate_event_new_state(State0, EncData), + case Event of + #ssh_msg_kexinit{} -> + %% We need payload for verification later. + event({Event, Msg}, StateName, State); + _ -> + event(Event, StateName, State) + end. + +generate_event_new_state(#state{ssh_params = + #ssh{recv_sequence = SeqNum0} + = Ssh} = State, EncData) -> + SeqNum = ssh_transport:next_seqnum(SeqNum0), + State#state{ssh_params = Ssh#ssh{recv_sequence = SeqNum}, + decoded_data_buffer = <<>>, + encoded_data_buffer = EncData, + undecoded_packet_length = undefined}. + + +next_packet(#state{decoded_data_buffer = <<>>, + encoded_data_buffer = Buff, + socket = Socket, + transport_protocol = Protocol} = + State) when Buff =/= <<>> andalso size(Buff) >= 8 -> + %% More data from the next packet has been received + %% Fake a socket-recive message so that the data will be processed + self() ! {Protocol, Socket, <<>>} , + State; + +next_packet(#state{socket = Socket} = State) -> + inet:setopts(Socket, [{active, once}]), + State. + +after_new_keys(#state{renegotiate = true} = State) -> + {connected, State#state{renegotiate = false}}; +after_new_keys(#state{renegotiate = false, + ssh_params = #ssh{role = client} = Ssh0} = State) -> + ssh_bits:install_messages(ssh_auth:userauth_messages()), + {Msg, Ssh} = ssh_auth:service_request_msg(Ssh0), + send_msg(Msg, State), + {userauth, State#state{ssh_params = Ssh}}; +after_new_keys(#state{renegotiate = false, + ssh_params = #ssh{role = server}} = State) -> + {userauth, State}. + +max(N, M) when N > M -> + N; +max(_, M) -> + M. + +handle_ssh_packet_data(RemainingSshPacketLen, DecData, EncData, StateName, + State) -> + EncSize = size(EncData), + case RemainingSshPacketLen > EncSize of + true -> + {next_state, StateName, + next_packet(State#state{decoded_data_buffer = DecData, + encoded_data_buffer = EncData, + undecoded_packet_length = + RemainingSshPacketLen})}; + false -> + handle_ssh_packet(RemainingSshPacketLen, StateName, + State#state{decoded_data_buffer = DecData, + encoded_data_buffer = EncData}) + + end. + +handle_ssh_packet(Length, StateName, #state{decoded_data_buffer = DecData0, + encoded_data_buffer = EncData0, + ssh_params = Ssh0, + transport_protocol = _Protocol, + socket = _Socket} = State0) -> + {Ssh1, DecData, EncData, Mac} = + ssh_transport:unpack(EncData0, Length, Ssh0), + SshPacket = <>, + case ssh_transport:is_valid_mac(Mac, SshPacket, Ssh1) of + true -> + PacketData = ssh_transport:msg_data(SshPacket), + {Ssh1, Msg} = ssh_transport:decompress(Ssh1, PacketData), + generate_event(Msg, StateName, + State0#state{ssh_params = Ssh1, + %% Important to be set for + %% next_packet + decoded_data_buffer = <<>>}, EncData); + false -> + DisconnectMsg = + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Bad mac", + language = "en"}, + handle_disconnect(DisconnectMsg, State0) + end. + +handle_disconnect(#ssh_msg_disconnect{} = Msg, + #state{ssh_params = Ssh0, manager = Pid} = State) -> + {SshPacket, Ssh} = ssh_transport:ssh_packet(Msg, Ssh0), + try + send_msg(SshPacket, State), + ssh_connection_manager:event(Pid, Msg) + catch + exit:{noproc, _Reason} -> + Report = io_lib:format("~p Connection Manager terminated: ~p~n", + [self(), Pid]), + error_logger:info_report(Report); + exit:Exit -> + Report = io_lib:format("Connection Manager returned:~n~p~n~p~n", + [Msg, Exit]), + error_logger:info_report(Report) + end, + {stop, normal, State#state{ssh_params = Ssh}}. + +counterpart_versions(NumVsn, StrVsn, #ssh{role = server} = Ssh) -> + Ssh#ssh{c_vsn = NumVsn , c_version = StrVsn}; +counterpart_versions(NumVsn, StrVsn, #ssh{role = client} = Ssh) -> + Ssh#ssh{s_vsn = NumVsn , s_version = StrVsn}. + +opposite_role(client) -> + server; +opposite_role(server) -> + client. +connected_fun(User, PeerAddr, Method, Opts) -> + case proplists:get_value(connectfun, Opts) of + undefined -> + ok; + Fun -> + catch Fun(User, PeerAddr, Method) + end. + +retry_fun(_, undefined, _) -> + ok; + +retry_fun(User, {error, Reason}, Opts) -> + case proplists:get_value(failfun, Opts) of + undefined -> + ok; + Fun -> + catch Fun(User, Reason) + end; + +retry_fun(User, Reason, Opts) -> + case proplists:get_value(infofun, Opts) of + undefined -> + ok; + Fun -> + catch Fun(User, Reason) + end. + +ssh_info_handler(Options, Ssh, From) -> + Info = ssh_info(Options, Ssh, []), + ssh_connection_manager:send_msg({channel_requst_reply, From, Info}). + +ssh_info([], _, Acc) -> + Acc; + +ssh_info([client_version | Rest], #ssh{c_vsn = IntVsn, + c_version = StringVsn} = SshParams, Acc) -> + ssh_info(Rest, SshParams, [{client_version, {IntVsn, StringVsn}} | Acc]); + +ssh_info([server_version | Rest], #ssh{s_vsn = IntVsn, + s_version = StringVsn} = SshParams, Acc) -> + ssh_info(Rest, SshParams, [{server_version, {IntVsn, StringVsn}} | Acc]); + +ssh_info([peer | Rest], #ssh{peer = Peer} = SshParams, Acc) -> + ssh_info(Rest, SshParams, [{peer, Peer} | Acc]); + +ssh_info([ _ | Rest], SshParams, Acc) -> + ssh_info(Rest, SshParams, Acc). diff --git a/lib/ssh/src/ssh_connection_manager.erl b/lib/ssh/src/ssh_connection_manager.erl new file mode 100644 index 0000000000..3863005e74 --- /dev/null +++ b/lib/ssh/src/ssh_connection_manager.erl @@ -0,0 +1,760 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% +%%---------------------------------------------------------------------- +%% Purpose: Handles multiplexing to ssh channels and global connection +%% requests e.i. the SSH Connection Protocol (RFC 4254), that provides +%% interactive login sessions, remote execution of commands, forwarded +%% TCP/IP connections, and forwarded X11 connections. Details of the +%% protocol is implemented in ssh_connection.erl +%% ---------------------------------------------------------------------- +-module(ssh_connection_manager). + +-behaviour(gen_server). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). +-include("ssh_transport.hrl"). + +-export([start_link/1]). + +-export([info/1, info/2, + renegotiate/1, connection_info/2, channel_info/3, + peer_addr/1, send_window/3, recv_window/3, adjust_window/3, + close/2, stop/1, send/5, + send_eof/2]). + +-export([open_channel/6, request/6, request/7, global_request/4, event/2, + cast/2]). + +%% Internal application API and spawn +-export([send_msg/1, ssh_channel_info_handler/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-define(DBG_MESSAGE, true). + +-record(state, + { + role, + client, + starter, + connection, % pid() + connection_state, % #connection{} + latest_channel_id = 0, + opts, + channel_args, + connected + }). + +%%==================================================================== +%% Internal application API +%%==================================================================== + +start_link(Opts) -> + gen_server:start_link(?MODULE, Opts, []). + +open_channel(ConnectionManager, ChannelType, ChannelSpecificData, + InitialWindowSize, MaxPacketSize, Timeout) -> + case (catch call(ConnectionManager, {open, self(), ChannelType, + InitialWindowSize, + MaxPacketSize, ChannelSpecificData}, + Timeout)) of + {open, Channel} -> + {ok, Channel}; + Error -> + %% TODO: Best way? + Error + end. + +request(ConnectionManager, ChannelPid, ChannelId, Type, true, Data, Timeout) -> + call(ConnectionManager, {request, ChannelPid, ChannelId, Type, Data}, Timeout); +request(ConnectionManager, ChannelPid, ChannelId, Type, false, Data, _) -> + cast(ConnectionManager, {request, ChannelPid, ChannelId, Type, Data}). + +request(ConnectionManager, ChannelId, Type, true, Data, Timeout) -> + call(ConnectionManager, {request, ChannelId, Type, Data}, Timeout); +request(ConnectionManager, ChannelId, Type, false, Data, _) -> + cast(ConnectionManager, {request, ChannelId, Type, Data}). + +global_request(ConnectionManager, Type, true = Reply, Data) -> + case call(ConnectionManager, + {global_request, self(), Type, Reply, Data}) of + {ssh_cm, ConnectionManager, {success, _}} -> + ok; + {ssh_cm, ConnectionManager, {failure, _}} -> + error + end; + +global_request(ConnectionManager, Type, false = Reply, Data) -> + cast(ConnectionManager, {global_request, self(), Type, Reply, Data}). + +event(ConnectionManager, BinMsg) -> + call(ConnectionManager, {ssh_msg, self(), BinMsg}). + +info(ConnectionManager) -> + info(ConnectionManager, {info, all}). + +info(ConnectionManager, ChannelProcess) -> + call(ConnectionManager, {info, ChannelProcess}). + +%% TODO: Do we really want this function? Should not +%% renegotiation be triggered by configurable timer +%% or amount of data sent counter! +renegotiate(ConnectionManager) -> + cast(ConnectionManager, renegotiate). + +connection_info(ConnectionManager, Options) -> + call(ConnectionManager, {connection_info, Options}). + +channel_info(ConnectionManager, ChannelId, Options) -> + call(ConnectionManager, {channel_info, ChannelId, Options}). + +%% Replaced by option peer to connection_info/2 keep for now +%% for Backwards compatibility! +peer_addr(ConnectionManager) -> + call(ConnectionManager, {peer_addr, self()}). + +%% Backwards compatibility! +send_window(ConnectionManager, Channel, TimeOut) -> + call(ConnectionManager, {send_window, Channel}, TimeOut). +%% Backwards compatibility! +recv_window(ConnectionManager, Channel, TimeOut) -> + call(ConnectionManager, {recv_window, Channel}, TimeOut). + +adjust_window(ConnectionManager, Channel, Bytes) -> + cast(ConnectionManager, {adjust_window, Channel, Bytes}). + +close(ConnectionManager, ChannelId) -> + try call(ConnectionManager, {close, ChannelId}) of + ok -> + ok + catch + exit:{noproc, _} -> + ok + end. + +stop(ConnectionManager) -> + try call(ConnectionManager, stop) of + ok -> + ok + catch + exit:{noproc, _} -> + ok + end. + +send(ConnectionManager, ChannelId, Type, Data, Timeout) -> + call(ConnectionManager, {data, ChannelId, Type, Data}, Timeout). + +send_eof(ConnectionManager, ChannelId) -> + cast(ConnectionManager, {eof, ChannelId}). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([server, _Socket, Opts]) -> + process_flag(trap_exit, true), + ssh_bits:install_messages(ssh_connection:messages()), + Cache = ssh_channel:cache_create(), + {ok, #state{role = server, + connection_state = #connection{channel_cache = Cache, + channel_id_seed = 0, + port_bindings = [], + requests = [], + channel_pids = []}, + opts = Opts, + connected = false}}; + +init([client, Opts]) -> + process_flag(trap_exit, true), + {links, [Parent]} = process_info(self(), links), + ssh_bits:install_messages(ssh_connection:messages()), + Cache = ssh_channel:cache_create(), + Address = proplists:get_value(address, Opts), + Port = proplists:get_value(port, Opts), + SocketOpts = proplists:get_value(socket_opts, Opts), + Options = proplists:get_value(ssh_opts, Opts), + ChannelPid = proplists:get_value(channel_pid, Opts), + self() ! + {start_connection, client, [Parent, Address, Port, + ChannelPid, SocketOpts, Options]}, + {ok, #state{role = client, + client = ChannelPid, + connection_state = #connection{channel_cache = Cache, + channel_id_seed = 0, + port_bindings = [], + requests = [], + channel_pids = []}, + opts = Opts, + connected = false}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call({request, ChannelPid, ChannelId, Type, Data}, From, State0) -> + {{replies, Replies}, State} = handle_request(ChannelPid, + ChannelId, Type, Data, + true, From, State0), + %% Sends message to the connection handler process, reply to + %% channel is sent later when reply arrives from the connection + %% handler. + lists:foreach(fun send_msg/1, Replies), + {noreply, State}; + +handle_call({request, ChannelId, Type, Data}, From, State0) -> + {{replies, Replies}, State} = handle_request(ChannelId, Type, Data, + true, From, State0), + %% Sends message to the connection handler process, reply to + %% channel is sent later when reply arrives from the connection + %% handler. + lists:foreach(fun send_msg/1, Replies), + {noreply, State}; + +%% Message from ssh_connection_handler +handle_call({ssh_msg, Pid, Msg}, From, + #state{connection_state = Connection0, + role = Role, opts = Opts, connected = IsConnected, + client = ClientPid} + = State) -> + + %% To avoid that not all data sent by the other side is processes before + %% possible crash in ssh_connection_handler takes down the connection. + gen_server:reply(From, ok), + + ConnectionMsg = decode_ssh_msg(Msg), + try ssh_connection:handle_msg(ConnectionMsg, Connection0, Pid, Role) of + {{replies, Replies}, Connection} -> + lists:foreach(fun send_msg/1, Replies), + {noreply, State#state{connection_state = Connection}}; + {noreply, Connection} -> + {noreply, State#state{connection_state = Connection}}; + {disconnect, {_, Reason}, {{replies, Replies}, Connection}} + when Role == client andalso (not IsConnected) -> + lists:foreach(fun send_msg/1, Replies), + ClientPid ! {self(), not_connected, Reason}, + {stop, normal, State#state{connection = Connection}}; + {disconnect, Reason, {{replies, Replies}, Connection}} -> + lists:foreach(fun send_msg/1, Replies), + SSHOpts = proplists:get_value(ssh_opts, Opts), + disconnect_fun(Reason, SSHOpts), + {stop, normal, State#state{connection_state = Connection}} + catch + exit:{noproc, Reason} -> + Report = io_lib:format("Connection probably terminated:~n~p~n~p~n", + [ConnectionMsg, Reason]), + error_logger:info_report(Report), + {noreply, State}; + error:Error -> + Report = io_lib:format("Connection message returned:~n~p~n~p~n", + [ConnectionMsg, Error]), + error_logger:info_report(Report), + {noreply, State}; + exit:Exit -> + Report = io_lib:format("Connection message returned:~n~p~n~p~n", + [ConnectionMsg, Exit]), + error_logger:info_report(Report), + {noreply, State} + end; + +handle_call({global_request, Pid, _, _, _} = Request, From, + #state{connection_state = + #connection{channel_cache = Cache}} = State0) -> + State1 = handle_global_request(Request, State0), + Channel = ssh_channel:cache_find(Pid, Cache), + State = add_request(true, Channel#channel.local_id, From, State1), + {noreply, State}; + +handle_call({data, ChannelId, Type, Data}, From, + #state{connection_state = #connection{channel_cache = _Cache} + = Connection0, + connection = ConnectionPid} = State) -> + channel_data(ChannelId, Type, Data, Connection0, ConnectionPid, From, + State); + +handle_call({connection_info, Options}, From, + #state{connection = Connection} = State) -> + ssh_connection_handler:connection_info(Connection, From, Options), + %% Reply will be sent by the connection handler by calling + %% ssh_connection_handler:send_msg/1. + {noreply, State}; + +handle_call({channel_info, ChannelId, Options}, From, + #state{connection_state = #connection{channel_cache = Cache}} = State) -> + + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{} = Channel -> + spawn(?MODULE, ssh_channel_info_handler, [Options, Channel, From]), + {noreply, State}; + undefined -> + {reply, []} + end; + +handle_call({info, ChannelPid}, _From, + #state{connection_state = + #connection{channel_cache = Cache}} = State) -> + Result = ssh_channel:cache_foldl( + fun(Channel, Acc) when ChannelPid == all; + Channel#channel.user == ChannelPid -> + [Channel | Acc]; + (_, Acc) -> + Acc + end, [], Cache), + {reply, {ok, Result}, State}; + +handle_call({open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data}, + From, #state{connection = Pid, + connection_state = + #connection{channel_cache = Cache}} = State0) -> + {ChannelId, State1} = new_channel_id(State0), + Msg = ssh_connection:channel_open_msg(Type, ChannelId, + InitialWindowSize, + MaxPacketSize, Data), + send_msg({connection_reply, Pid, Msg}), + Channel = #channel{type = Type, + sys = "none", + user = ChannelPid, + local_id = ChannelId, + recv_window_size = InitialWindowSize, + recv_packet_size = MaxPacketSize}, + ssh_channel:cache_update(Cache, Channel), + State = add_request(true, ChannelId, From, State1), + {noreply, State}; + +handle_call({send_window, ChannelId}, _From, + #state{connection_state = + #connection{channel_cache = Cache}} = State) -> + Reply = case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{send_window_size = WinSize, + send_packet_size = Packsize} -> + {ok, {WinSize, Packsize}}; + undefined -> + {error, einval} + end, + {reply, Reply, State}; + +handle_call({recv_window, ChannelId}, _From, + #state{connection_state = #connection{channel_cache = Cache}} + = State) -> + + Reply = case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{recv_window_size = WinSize, + recv_packet_size = Packsize} -> + {ok, {WinSize, Packsize}}; + undefined -> + {error, einval} + end, + {reply, Reply, State}; + +%% Replaced by option peer to connection_info/2 keep for now +%% for Backwards compatibility! +handle_call({peer_addr, _ChannelId}, _From, + #state{connection = Pid} = State) -> + Reply = ssh_connection_handler:peer_address(Pid), + {reply, Reply, State}; + +handle_call(opts, _, #state{opts = Opts} = State) -> + {reply, Opts, State}; + +handle_call({close, ChannelId}, _, + #state{connection = Pid, connection_state = + #connection{channel_cache = Cache}} = State) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id} -> + send_msg({connection_reply, Pid, + ssh_connection:channel_close_msg(Id)}), + {reply, ok, State}; + undefined -> + {reply, ok, State} + end; + +handle_call(stop, _, #state{role = _client, + client = ChannelPid, + connection = Pid} = State) -> + DisconnectMsg = + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, + description = "Application disconnect", + language = "en"}, + (catch gen_fsm:send_all_state_event(Pid, DisconnectMsg)), +% ssh_connection_handler:send(Pid, DisconnectMsg), + {stop, normal, ok, State}; +handle_call(stop, _, State) -> + {stop, normal, ok, State}; + +%% API violation make it the violaters problem +%% by ignoring it. The violating process will get +%% a timeout or hang. +handle_call(_, _, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({request, ChannelPid, ChannelId, Type, Data}, State0) -> + {{replies, Replies}, State} = handle_request(ChannelPid, ChannelId, + Type, Data, + false, none, State0), + lists:foreach(fun send_msg/1, Replies), + {noreply, State}; + +handle_cast({request, ChannelId, Type, Data}, State0) -> + {{replies, Replies}, State} = handle_request(ChannelId, Type, Data, + false, none, State0), + lists:foreach(fun send_msg/1, Replies), + {noreply, State}; + +handle_cast({global_request, _, _, _, _} = Request, State0) -> + State = handle_global_request(Request, State0), + {noreply, State}; + +handle_cast(renegotiate, #state{connection = Pid} = State) -> + ssh_connection_handler:renegotiate(Pid), + {noreply, State}; + +handle_cast({adjust_window, ChannelId, Bytes}, + #state{connection = Pid, connection_state = + #connection{channel_cache = Cache}} = State) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{recv_window_size = WinSize, remote_id = Id} = Channel -> + ssh_channel:cache_update(Cache, Channel#channel{recv_window_size = + WinSize + Bytes}), + Msg = ssh_connection:channel_adjust_window_msg(Id, Bytes), + send_msg({connection_reply, Pid, Msg}); + undefined -> + ignore + end, + {noreply, State}; + +handle_cast({eof, ChannelId}, + #state{connection = Pid, connection_state = + #connection{channel_cache = Cache}} = State) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id} -> + send_msg({connection_reply, Pid, + ssh_connection:channel_eof_msg(Id)}), + {noreply, State}; + undefined -> + {noreply, State} + end; + +handle_cast({success, ChannelId}, #state{connection = Pid} = State) -> + Msg = ssh_connection:channel_success_msg(ChannelId), + send_msg({connection_reply, Pid, Msg}), + {noreply, State}; + +handle_cast({failure, ChannelId}, #state{connection = Pid} = State) -> + Msg = ssh_connection:channel_failure_msg(ChannelId), + send_msg({connection_reply, Pid, Msg}), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({start_connection, server, + [Address, Port, Socket, Options]}, + #state{connection_state = CState} = State) -> + {ok, Connection} = ssh_transport:accept(Address, Port, Socket, Options), + Shell = proplists:get_value(shell, Options), + Exec = proplists:get_value(exec, Options), + CliSpec = proplists:get_value(ssh_cli, Options, {ssh_cli, [Shell]}), + {noreply, State#state{connection = Connection, + connection_state = + CState#connection{address = Address, + port = Port, + cli_spec = CliSpec, + options = Options, + exec = Exec}}}; + +handle_info({start_connection, client, + [Parent, Address, Port, ChannelPid, SocketOpts, Options]}, + State) -> + case (catch ssh_transport:connect(Parent, Address, + Port, SocketOpts, Options)) of + {ok, Connection} -> + erlang:monitor(process, ChannelPid), + {noreply, State#state{connection = Connection}}; + Reason -> + ChannelPid ! {self(), not_connected, Reason}, + {stop, normal, State} + end; + +handle_info({ssh_cm, _Sender, Msg}, State0) -> + %% Backwards compatibility! + State = cm_message(Msg, State0), + {noreply, State}; + +%% Nop backwards compatibility +handle_info({same_user, _}, State) -> + {noreply, State}; + +handle_info(ssh_connected, #state{role = client, client = Pid} + = State) -> + Pid ! {self(), is_connected}, + {noreply, State#state{connected = true}}; + +handle_info(ssh_connected, #state{role = server} = State) -> + {noreply, State#state{connected = true}}; + +handle_info({'DOWN', _Ref, process, ChannelPid, normal}, State0) -> + handle_down(handle_channel_down(ChannelPid, State0)); + +handle_info({'DOWN', _Ref, process, ChannelPid, shutdown}, State0) -> + handle_down(handle_channel_down(ChannelPid, State0)); + +handle_info({'DOWN', _Ref, process, ChannelPid, Reason}, State0) -> + Report = io_lib:format("Pid ~p DOWN ~p\n", [ChannelPid, Reason]), + error_logger:error_report(Report), + handle_down(handle_channel_down(ChannelPid, State0)); + +handle_info({'EXIT', _, _}, State) -> + %% Handled in 'DOWN' + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(Reason, #state{connection_state = + #connection{requests = Requests}, + opts = Opts}) -> + SSHOpts = proplists:get_value(ssh_opts, Opts), + disconnect_fun(Reason, SSHOpts), + (catch lists:foreach(fun({_, From}) -> + gen_server:reply(From, {error, connection_closed}) + end, Requests)), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +channel_data(Id, Type, Data, Connection0, ConnectionPid, From, State) -> + case ssh_connection:channel_data(Id, Type, Data, Connection0, + ConnectionPid, From) of + {{replies, Replies}, Connection} -> + lists:foreach(fun send_msg/1, Replies), + {noreply, State#state{connection_state = Connection}}; + {noreply, Connection} -> + {noreply, State#state{connection_state = Connection}} + end. + +call(Pid, Msg) -> + call(Pid, Msg, infinity). +call(Pid, Msg, Timeout) -> + try gen_server:call(Pid, Msg, Timeout) of + Result -> + Result + catch + exit:{timeout, _} -> + {error, timeout} + end. + +cast(Pid, Msg) -> + gen_server:cast(Pid, Msg). + +decode_ssh_msg(BinMsg) when is_binary(BinMsg)-> + ssh_bits:decode(BinMsg); +decode_ssh_msg(Msg) -> + Msg. + + +send_msg(Msg) -> + case catch do_send_msg(Msg) of + {'EXIT', Reason}-> + Report = io_lib:format("Connection Manager fail to send:~n~p~n" + "Reason why it failed was:~n~p~n", + [Msg, Reason]), + error_logger:info_report(Report); + _ -> + ok + end. + +do_send_msg({channel_data, Pid, Data}) -> + Pid ! {ssh_cm, self(), Data}; +do_send_msg({channel_requst_reply, From, Data}) -> + gen_server:reply(From, Data); +do_send_msg({connection_reply, Pid, Data}) -> + Msg = ssh_bits:encode(Data), + ssh_connection_handler:send(Pid, Msg); +do_send_msg({flow_control, Cache, Channel, From, Msg}) -> + ssh_channel:cache_update(Cache, Channel#channel{flow_control = undefined}), + gen_server:reply(From, Msg). + +handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, + #state{connection = Pid, + connection_state = + #connection{channel_cache = Cache}} = State0) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id} = Channel -> + update_sys(Cache, Channel, Type, ChannelPid), + Msg = ssh_connection:channel_request_msg(Id, Type, + WantReply, Data), + Replies = [{connection_reply, Pid, Msg}], + State = add_request(WantReply, ChannelId, From, State0), + {{replies, Replies}, State}; + undefined -> + {{replies, []}, State0} + end. + +handle_request(ChannelId, Type, Data, WantReply, From, + #state{connection = Pid, + connection_state = + #connection{channel_cache = Cache}} = State0) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id} -> + Msg = ssh_connection:channel_request_msg(Id, Type, + WantReply, Data), + Replies = [{connection_reply, Pid, Msg}], + State = add_request(WantReply, ChannelId, From, State0), + {{replies, Replies}, State}; + undefined -> + {{replies, []}, State0} + end. + +handle_down({{replies, Replies}, State}) -> + lists:foreach(fun send_msg/1, Replies), + {noreply, State}. + +handle_channel_down(ChannelPid, #state{connection_state = + #connection{channel_cache = Cache}} = + State) -> + ssh_channel:cache_foldl( + fun(Channel, Acc) when Channel#channel.user == ChannelPid -> + ssh_channel:cache_delete(Cache, + Channel#channel.local_id), + Acc; + (_,Acc) -> + Acc + end, [], Cache), + {{replies, []}, State}. + +update_sys(Cache, Channel, Type, ChannelPid) -> + ssh_channel:cache_update(Cache, + Channel#channel{sys = Type, user = ChannelPid}). + +add_request(false, _ChannelId, _From, State) -> + State; +add_request(true, ChannelId, From, #state{connection_state = + #connection{requests = Requests0} = + Connection} = State) -> + Requests = [{ChannelId, From} | Requests0], + State#state{connection_state = Connection#connection{requests = Requests}}. + +new_channel_id(#state{connection_state = #connection{channel_id_seed = Id} = + Connection} + = State) -> + {Id, State#state{connection_state = + Connection#connection{channel_id_seed = Id + 1}}}. + +handle_global_request({global_request, ChannelPid, + "tcpip-forward" = Type, WantReply, + <> = Data}, + #state{connection = ConnectionPid, + connection_state = + #connection{channel_cache = Cache} + = Connection0} = State) -> + ssh_channel:cache_update(Cache, #channel{user = ChannelPid, + type = "forwarded-tcpip", + sys = none}), + Connection = ssh_connection:bind(IP, Port, ChannelPid, Connection0), + Msg = ssh_connection:global_request_msg(Type, WantReply, Data), + send_msg({connection_reply, ConnectionPid, Msg}), + State#state{connection_state = Connection}; + +handle_global_request({global_request, _Pid, "cancel-tcpip-forward" = Type, + WantReply, <> = Data}, + #state{connection = Pid, + connection_state = Connection0} = State) -> + Connection = ssh_connection:unbind(IP, Port, Connection0), + Msg = ssh_connection:global_request_msg(Type, WantReply, Data), + send_msg({connection_reply, Pid, Msg}), + State#state{connection_state = Connection}; + +handle_global_request({global_request, _Pid, "cancel-tcpip-forward" = Type, + WantReply, Data}, #state{connection = Pid} = State) -> + Msg = ssh_connection:global_request_msg(Type, WantReply, Data), + send_msg({connection_reply, Pid, Msg}), + State. + +cm_message(Msg, State) -> + {noreply, NewState} = handle_cast(Msg, State), + NewState. + +disconnect_fun(Reason, Opts) -> + case proplists:get_value(disconnectfun, Opts) of + undefined -> + ok; + Fun -> + catch Fun(Reason) + end. + +ssh_channel_info_handler(Options, Channel, From) -> + Info = ssh_channel_info(Options, Channel, []), + send_msg({channel_requst_reply, From, Info}). + +ssh_channel_info([], _, Acc) -> + Acc; + +ssh_channel_info([recv_window | Rest], #channel{recv_window_size = WinSize, + recv_packet_size = Packsize + } = Channel, Acc) -> + ssh_channel_info(Rest, Channel, [{recv_window, {{win_size, WinSize}, + {packet_size, Packsize}}} | Acc]); +ssh_channel_info([send_window | Rest], #channel{send_window_size = WinSize, + send_packet_size = Packsize + } = Channel, Acc) -> + ssh_channel_info(Rest, Channel, [{send_window, {{win_size, WinSize}, + {packet_size, Packsize}}} | Acc]); +ssh_channel_info([ _ | Rest], Channel, Acc) -> + ssh_channel_info(Rest, Channel, Acc). + + + diff --git a/lib/ssh/src/ssh_dsa.erl b/lib/ssh/src/ssh_dsa.erl new file mode 100755 index 0000000000..ec24fbcd01 --- /dev/null +++ b/lib/ssh/src/ssh_dsa.erl @@ -0,0 +1,95 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: dsa public-key sign and verify + +-module(ssh_dsa). + +-export([verify/3]). +-export([sign/2]). +-export([alg_name/0]). + +-include("ssh.hrl"). + +%% start() -> +%% crypto:start(). + +%% sign_file(File, Opts) -> +%% start(), +%% {ok,Bin} = file:read_file(File), +%% {ok,Key} = ssh_file:private_host_dsa_key(user, Opts), +%% sign(Key, Bin). + +%% verify_file(File, Sig) -> +%% start(), +%% {ok,Bin} = file:read_file(File), +%% {ok,Key} = ssh_file:public_host_key(user, dsa), +%% verify(Key, Bin, Sig). + +sign(_Private=#ssh_key { private={P,Q,G,X} },Mb) -> + K = ssh_bits:irandom(160) rem Q, + R = ssh_math:ipow(G, K, P) rem Q, + Ki = ssh_math:invert(K, Q), + <> = crypto:sha(Mb), + S = (Ki * (M + X*R)) rem Q, + <>. + + +%% the paramiko client sends a bad sig sometimes, +%% instead of crashing, we nicely return error, the +%% typcally manifests itself as Sb being 39 bytes +%% instead of 40. + +verify(Public, Mb, Sb) -> + case catch xverify(Public, Mb, Sb) of + {'EXIT', _Reason} -> + %store({Public, Mb, Sb, _Reason}), + {error, inconsistent_key}; + ok -> + %store({Public, Mb, Sb, ok}) + ok + end. + +%% store(Term) -> +%% {ok, Fd} = file:open("/tmp/dsa", [append]), +%% io:format(Fd, "~p~n~n~n", [Term]), +%% file:close(Fd). + + +xverify(_Public=#ssh_key { public={P,Q,G,Y} },Mb,Sb) -> + <> = Sb, + ?ssh_assert(R0 >= 0 andalso R0 < Q andalso + S0 >= 0 andalso S0 < Q, out_of_range), + W = ssh_math:invert(S0,Q), + <> = crypto:sha(Mb), + U1 = (M0*W) rem Q, + U2 = (R0*W) rem Q, + T1 = ssh_math:ipow(G,U1,P), + T2 = ssh_math:ipow(Y,U2,P), + V = ((T1*T2) rem P) rem Q, + if V == R0 -> + ok; + true -> + {error, inconsistent_key} + end. + +alg_name() -> + "ssh-dss". diff --git a/lib/ssh/src/ssh_file.erl b/lib/ssh/src/ssh_file.erl new file mode 100755 index 0000000000..8a3c903e51 --- /dev/null +++ b/lib/ssh/src/ssh_file.erl @@ -0,0 +1,530 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SSH file handling + +-module(ssh_file). + +-include("ssh.hrl"). +-include("PKCS-1.hrl"). +-include("DSS.hrl"). + +-export([public_host_dsa_key/2,private_host_dsa_key/2, + public_host_rsa_key/2,private_host_rsa_key/2, + public_host_key/2,private_host_key/2, + lookup_host_key/3, add_host_key/3, % del_host_key/2, + lookup_user_key/3, ssh_dir/2, file_name/3]). + +-export([private_identity_key/2]). +%% , public_identity_key/2, +%% identity_keys/2]). + +-export([encode_public_key/1, decode_public_key_v2/2]). + +-import(lists, [reverse/1, append/1]). + +-define(DBG_PATHS, true). + +%% API +public_host_dsa_key(Type, Opts) -> + File = file_name(Type, "ssh_host_dsa_key.pub", Opts), + read_public_key_v2(File, "ssh-dss"). + +private_host_dsa_key(Type, Opts) -> + File = file_name(Type, "ssh_host_dsa_key", Opts), + read_private_key_v2(File, "ssh-dss"). + +public_host_rsa_key(Type, Opts) -> + File = file_name(Type, "ssh_host_rsa_key.pub", Opts), + read_public_key_v2(File, "ssh-rsa"). + +private_host_rsa_key(Type, Opts) -> + File = file_name(Type, "ssh_host_rsa_key", Opts), + read_private_key_v2(File, "ssh-rsa"). + +public_host_key(Type, Opts) -> + File = file_name(Type, "ssh_host_key", Opts), + case read_private_key_v1(File,public) of + {error, enoent} -> + read_public_key_v1(File++".pub"); + Result -> + Result + end. + + +private_host_key(Type, Opts) -> + File = file_name(Type, "ssh_host_key", Opts), + read_private_key_v1(File,private). + + + +%% in: "host" out: "host,1.2.3.4. +add_ip(Host) -> + case inet:getaddr(Host, inet) of + {ok, Addr} -> + case ssh_connection:encode_ip(Addr) of + false -> Host; + IPString -> Host ++ "," ++ IPString + end; + _ -> Host + end. + +replace_localhost("localhost") -> + {ok, Hostname} = inet:gethostname(), + Hostname; +replace_localhost(Host) -> + Host. + +%% lookup_host_key +%% return {ok, Key(s)} or {error, not_found} +%% + +lookup_host_key(Host, Alg, Opts) -> + Host1 = replace_localhost(Host), + do_lookup_host_key(Host1, Alg, Opts). + +do_lookup_host_key(Host, Alg, Opts) -> + case file:open(file_name(user, "known_hosts", Opts), [read]) of + {ok, Fd} -> + Res = lookup_host_key_fd(Fd, Host, Alg), + file:close(Fd), + Res; + {error, enoent} -> {error, not_found}; + Error -> Error + end. + +add_host_key(Host, Key, Opts) -> + Host1 = add_ip(replace_localhost(Host)), + case file:open(file_name(user, "known_hosts", Opts),[write,append]) of + {ok, Fd} -> + Res = add_key_fd(Fd, Host1, Key), + file:close(Fd), + Res; + Error -> + Error + end. + +%% del_host_key(Host, Opts) -> +%% Host1 = replace_localhost(Host), +%% case file:open(file_name(user, "known_hosts", Opts),[write,read]) of +%% {ok, Fd} -> +%% Res = del_key_fd(Fd, Host1), +%% file:close(Fd), +%% Res; +%% Error -> +%% Error +%% end. + +identity_key_filename("ssh-dss") -> "id_dsa"; +identity_key_filename("ssh-rsa") -> "id_rsa". + +private_identity_key(Alg, Opts) -> + Path = file_name(user, identity_key_filename(Alg), Opts), + read_private_key_v2(Path, Alg). + +read_public_key_v2(File, Type) -> + case file:read_file(File) of + {ok,Bin} -> + List = binary_to_list(Bin), + case lists:prefix(Type, List) of + true -> + List1 = lists:nthtail(length(Type), List), + K_S = ssh_bits:b64_decode(List1), + decode_public_key_v2(K_S, Type); + false -> + {error, bad_format} + end; + Error -> + Error + end. + +decode_public_key_v2(K_S, "ssh-rsa") -> + case ssh_bits:decode(K_S,[string,mpint,mpint]) of + ["ssh-rsa", E, N] -> + {ok, #ssh_key { type = rsa, + public = {N,E}, + comment=""}}; + _ -> + {error, bad_format} + end; +decode_public_key_v2(K_S, "ssh-dss") -> + case ssh_bits:decode(K_S,[string,mpint,mpint,mpint,mpint]) of + ["ssh-dss",P,Q,G,Y] -> + {ok,#ssh_key { type = dsa, + public = {P,Q,G,Y} + }}; + _A -> + {error, bad_format} + end; +decode_public_key_v2(_, _) -> + {error, bad_format}. + + +read_public_key_v1(File) -> + case file:read_file(File) of + {ok,Bin} -> + List = binary_to_list(Bin), + case io_lib:fread("~d ~d ~d ~s", List) of + {ok,[_Sz,E,N,Comment],_} -> + {ok,#ssh_key { type = rsa, + public ={N,E}, + comment = Comment }}; + _Error -> + {error, bad_format} + end; + Error -> + Error + end. + +%% pem_type("ssh-dss") -> "DSA"; +%% pem_type("ssh-rsa") -> "RSA". + +read_private_key_v2(File, Type) -> + case catch (public_key:pem_to_der(File)) of + {ok, [{_, Bin, not_encrypted}]} -> + decode_private_key_v2(Bin, Type); + Error -> %% Note we do not handle password encrypted keys at the moment + {error, Error} + end. +%% case file:read_file(File) of +%% {ok,Bin} -> +%% case read_pem(binary_to_list(Bin), pem_type(Type)) of +%% {ok,Bin1} -> +%% decode_private_key_v2(Bin1, Type); +%% Error -> +%% Error +%% end; +%% Error -> +%% Error +%% end. + +decode_private_key_v2(Private,"ssh-rsa") -> + case 'PKCS-1':decode( 'RSAPrivateKey', Private) of + {ok,RSA} -> %% FIXME Check for two-prime version + {ok, #ssh_key { type = rsa, + public = {RSA#'RSAPrivateKey'.modulus, + RSA#'RSAPrivateKey'.publicExponent}, + private = {RSA#'RSAPrivateKey'.modulus, + RSA#'RSAPrivateKey'.privateExponent} + }}; + Error -> + Error + end; +decode_private_key_v2(Private, "ssh-dss") -> + case 'DSS':decode('DSAPrivateKey', Private) of + {ok,DSA} -> %% FIXME Check for two-prime version + {ok, #ssh_key { type = dsa, + public = {DSA#'DSAPrivateKey'.p, + DSA#'DSAPrivateKey'.q, + DSA#'DSAPrivateKey'.g, + DSA#'DSAPrivateKey'.y}, + private= {DSA#'DSAPrivateKey'.p, + DSA#'DSAPrivateKey'.q, + DSA#'DSAPrivateKey'.g, + DSA#'DSAPrivateKey'.x} + }}; + _ -> + {error,bad_format} + end. + +%% SSH1 private key format +%% <<"SSH PRIVATE KEY FILE FORMATE 1.1\n" 0:8 +%% CipherNum:8, Reserved:32, +%% NSz/uint32, N/bignum, E/bignum, Comment/string, +%% +%% [ R0:8 R1:8 R0:8 R1:8, D/bignum, IQMP/bignum, Q/bignum, P/bignum, Pad(8)]>> +%% +%% where [ ] is encrypted using des3 (ssh1 version) and +%% a posssibly empty pass phrase using md5(passphase) as key +%% + +read_private_key_v1(File, Type) -> + case file:read_file(File) of + {ok,<<"SSH PRIVATE KEY FILE FORMAT 1.1\n",0, + CipherNum,_Resereved:32,Bin/binary>>} -> + decode_private_key_v1(Bin, CipherNum,Type); + {ok,_} -> + {error, bad_format}; + Error -> + Error + end. + +decode_private_key_v1(Bin, CipherNum, Type) -> + case ssh_bits:decode(Bin,0,[uint32, bignum, bignum, string]) of + {Offset,[_NSz,N,E,Comment]} -> + if Type == public -> + {ok,#ssh_key { type=rsa, + public={N,E}, + comment=Comment}}; + Type == private -> + <<_:Offset/binary, Encrypted/binary>> = Bin, + case ssh_bits:decode(decrypt1(Encrypted, CipherNum),0, + [uint32, bignum, bignum, + bignum, bignum,{pad,8}]) of + {_,[_,D,IQMP,Q,P]} -> + {ok,#ssh_key { type=rsa, + public={N,E}, + private={D,IQMP,Q,P}, + comment=Comment}}; + _ -> + {error,bad_format} + end + end; + _ -> + {error,bad_format} + end. + + +decrypt1(Bin, CipherNum) -> + decrypt1(Bin, CipherNum,""). + +decrypt1(Bin, CipherNum, Phrase) -> + if CipherNum == ?SSH_CIPHER_NONE; Phrase == "" -> + Bin; + CipherNum == ?SSH_CIPHER_3DES -> + <> = erlang:md5(Phrase), + K3 = K1, + IV = <<0,0,0,0,0,0,0,0>>, + Bin1 = crypto:des_cbc_decrypt(K3,IV,Bin), + Bin2 = crypto:des_cbc_encrypt(K2,IV,Bin1), + crypto:des_cbc_decrypt(K1,IV,Bin2) + end. + +%% encrypt1(Bin, CipherNum) -> +%% encrypt1(Bin, CipherNum,""). + +%% encrypt1(Bin, CipherNum, Phrase) -> +%% if CipherNum == ?SSH_CIPHER_NONE; Phrase == "" -> +%% Bin; +%% CipherNum == ?SSH_CIPHER_3DES -> +%% <> = erlang:md5(Phrase), +%% K3 = K1, +%% IV = <<0,0,0,0,0,0,0,0>>, +%% Bin1 = crypto:des_cbc_encrypt(K1,IV,Bin), +%% Bin2 = crypto:des_cbc_decrypt(K2,IV,Bin1), +%% crypto:des_cbc_encrypt(K3,IV,Bin2) +%% end. + +lookup_host_key_fd(Fd, Host, Alg) -> + case io:get_line(Fd, '') of + eof -> + {error, not_found}; + Line -> + case string:tokens(Line, " ") of + [HostList, Alg, KeyData] -> +%% io:format(" ~p lookup_host_key_fd: HostList ~p Alg ~p KeyData ~p\n", +%% [Host, HostList, Alg, KeyData]), + case lists:member(Host, string:tokens(HostList, ",")) of + true -> + decode_public_key_v2(ssh_bits:b64_decode(KeyData), Alg); + false -> + lookup_host_key_fd(Fd, Host, Alg) + end; + _ -> + lookup_host_key_fd(Fd, Host, Alg) + end + end. + + + +%% del_key_fd(Fd, Host) -> +%% del_key_fd(Fd, Host, 0, 0). + +%% del_key_fd(Fd, Host, ReadPos0, WritePos0) -> +%% case io:get_line(Fd, '') of +%% eof -> +%% if ReadPos0 == WritePos0 -> +%% ok; +%% true -> +%% file:truncate(Fd) +%% end; +%% Line -> +%% {ok,ReadPos1} = file:position(Fd, cur), +%% case string:tokens(Line, " ") of +%% [HostList, _Type, _KeyData] -> +%% case lists:member(Host, string:tokens(HostList, ",")) of +%% true -> +%% del_key_fd(Fd, Host, ReadPos1, WritePos0); +%% false -> +%% if ReadPos0 == WritePos0 -> +%% del_key_fd(Fd, Host, ReadPos1, ReadPos1); +%% true -> +%% file:position(Fd, WritePos0), +%% file:write(Fd, Line), +%% {ok,WritePos1} = file:position(Fd,cur), +%% del_key_fd(Fd, Host, ReadPos1, WritePos1) +%% end +%% end; +%% _ -> +%% if ReadPos0 == WritePos0 -> +%% del_key_fd(Fd, Host, ReadPos1, ReadPos1); +%% true -> +%% file:position(Fd, WritePos0), +%% file:write(Fd, Line), +%% {ok,WritePos1} = file:position(Fd,cur), +%% del_key_fd(Fd, Host, ReadPos1, WritePos1) +%% end +%% end +%% end. + + +add_key_fd(Fd, Host, Key) -> + case Key#ssh_key.type of + rsa -> + {N,E} = Key#ssh_key.public, + DK = ssh_bits:b64_encode( + ssh_bits:encode(["ssh-rsa",E,N], + [string,mpint,mpint])), + file:write(Fd, [Host, " ssh-rsa ", DK, "\n"]); + dsa -> + {P,Q,G,Y} = Key#ssh_key.public, + DK = ssh_bits:b64_encode( + ssh_bits:encode(["ssh-dss",P,Q,G,Y], + [string,mpint,mpint,mpint,mpint])), + file:write(Fd, [Host, " ssh-dss ", DK, "\n"]) + end. + + +%% read_pem(Cs, Type) -> +%% case read_line(Cs) of +%% {"-----BEGIN "++Rest,Cs1} -> +%% case string:tokens(Rest, " ") of +%% [Type, "PRIVATE", "KEY-----"] -> +%% read_pem64(Cs1, [], Type); +%% _ -> +%% {error, bad_format} +%% end; +%% {"",Cs1} when Cs1 =/= "" -> +%% read_pem(Cs1,Type); +%% {_,""} -> +%% {error, bad_format} +%% end. + +%% read_pem64(Cs, Acc, Type) -> +%% case read_line(Cs) of +%% {"-----END "++Rest,_Cs1} -> +%% case string:tokens(Rest, " ") of +%% [Type, "PRIVATE", "KEY-----"] -> +%% {ok,ssh_bits:b64_decode(append(reverse(Acc)))}; +%% Toks -> +%% error_logger:format("ssh: TOKENS=~p\n", [Toks]), +%% {error, bad_format} +%% end; +%% {B64, Cs1} when Cs1 =/= "" -> +%% read_pem64(Cs1, [B64|Acc], Type); +%% _What -> +%% {error, bad_format} +%% end. + + +%% read_line(Cs) -> read_line(Cs,[]). +%% read_line([$\r,$\n|T], Acc) -> {reverse(Acc), T}; +%% read_line([$\n|T], Acc) -> {reverse(Acc), T}; +%% read_line([C|T], Acc) -> read_line(T,[C|Acc]); +%% read_line([], Acc) -> {reverse(Acc),[]}. + +lookup_user_key(User, Alg, Opts) -> + SshDir = ssh_dir({remoteuser,User}, Opts), + case lookup_user_key_f(User, SshDir, Alg, "authorized_keys", Opts) of + {ok, Key} -> + {ok, Key}; + _ -> + lookup_user_key_f(User, SshDir, Alg, "authorized_keys2", Opts) + end. + +lookup_user_key_f(_User, [], _Alg, _F, _Opts) -> + {error, nouserdir}; +lookup_user_key_f(_User, nouserdir, _Alg, _F, _Opts) -> + {error, nouserdir}; +lookup_user_key_f(_User, Dir, Alg, F, _Opts) -> + FileName = filename:join(Dir, F), + case file:open(FileName, [read]) of + {ok, Fd} -> + Res = lookup_user_key_fd(Fd, Alg), + file:close(Fd), + Res; + {error, Reason} -> + {error, {{openerr, Reason}, {file, FileName}}} + end. + +lookup_user_key_fd(Fd, Alg) -> + case io:get_line(Fd, '') of + eof -> + {error, not_found}; + Line -> + case string:tokens(Line, " ") of + [Alg, KeyData, _] -> + %% io:format("lookup_user_key_fd: HostList ~p Alg ~p KeyData ~p\n", + %% [HostList, Alg, KeyData]), + decode_public_key_v2(ssh_bits:b64_decode(KeyData), Alg); + _Other -> + %%?dbg(false, "key_fd Other: ~w ~w\n", [Alg, _Other]), + lookup_user_key_fd(Fd, Alg) + end + end. + + +encode_public_key(#ssh_key{type = rsa, public = {N, E}}) -> + ssh_bits:encode(["ssh-rsa",E,N], + [string,mpint,mpint]); +encode_public_key(#ssh_key{type = dsa, public = {P,Q,G,Y}}) -> + ssh_bits:encode(["ssh-dss",P,Q,G,Y], + [string,mpint,mpint,mpint,mpint]). + +%% +%% Utils +%% + +%% server use this to find individual keys for +%% an individual user when user tries to login +%% with publickey +ssh_dir({remoteuser, User}, Opts) -> + case proplists:get_value(user_dir_fun, Opts) of + undefined -> + case proplists:get_value(user_dir, Opts) of + undefined -> + default_user_dir(); + Dir -> + Dir + end; + FUN -> + FUN(User) + end; + +%% client use this to find client ssh keys +ssh_dir(user, Opts) -> + case proplists:get_value(user_dir, Opts, false) of + false -> default_user_dir(); + D -> D + end; + +%% server use this to find server host keys +ssh_dir(system, Opts) -> + proplists:get_value(system_dir, Opts, "/etc/ssh"). + +file_name(Type, Name, Opts) -> + FN = filename:join(ssh_dir(Type, Opts), Name), + %%?dbg(?DBG_PATHS, "file_name: ~p\n", [FN]), + FN. + +default_user_dir()-> + {ok,[[Home|_]]} = init:get_argument(home), + filename:join(Home, ".ssh"). diff --git a/lib/ssh/src/ssh_io.erl b/lib/ssh/src/ssh_io.erl new file mode 100755 index 0000000000..0e343c20b4 --- /dev/null +++ b/lib/ssh/src/ssh_io.erl @@ -0,0 +1,79 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: user interaction for SSH + +-module(ssh_io). + +-export([yes_no/1, read_password/1, read_line/1, format/2]). +-import(lists, [reverse/1]). + + +read_line(Prompt) when is_list(Prompt) -> + io:get_line(list_to_atom(Prompt)); +read_line(Prompt) when is_atom(Prompt) -> + io:get_line(Prompt). + +read_ln(Prompt) -> + trim(read_line(Prompt)). + +yes_no(Prompt) -> + io:format("~s [y/n]?", [Prompt]), + case read_ln('') of + "y" -> yes; + "n" -> no; + "Y" -> yes; + "N" -> no; + _ -> + io:format("please answer y or n\n"), + yes_no(Prompt) + end. + + +read_password(Prompt) -> + format("~s", [listify(Prompt)]), + case io:get_password() of + "" -> + read_password(Prompt); + Pass -> Pass + end. + +listify(A) when is_atom(A) -> + atom_to_list(A); +listify(L) when is_list(L) -> + L. + +format(Fmt, Args) -> + io:format(Fmt, Args). + + +trim(Line) when is_list(Line) -> + reverse(trim1(reverse(trim1(Line)))); +trim(Other) -> Other. + +trim1([$\s|Cs]) -> trim(Cs); +trim1([$\r|Cs]) -> trim(Cs); +trim1([$\n|Cs]) -> trim(Cs); +trim1([$\t|Cs]) -> trim(Cs); +trim1(Cs) -> Cs. + + + diff --git a/lib/ssh/src/ssh_math.erl b/lib/ssh/src/ssh_math.erl new file mode 100755 index 0000000000..efe7f56979 --- /dev/null +++ b/lib/ssh/src/ssh_math.erl @@ -0,0 +1,131 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SSH math utilities + +-module(ssh_math). + +-export([ilog2/1, ipow/3, invert/2, ipow2/3]). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% INTEGER utils +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% number of bits (used) in a integer = isize(N) = |log2(N)|+1 +ilog2(N) -> + ssh_bits:isize(N) - 1. + + +%% calculate A^B mod M +ipow(A, B, M) when M > 0, B >= 0 -> + crypto:mod_exp(A, B, M). + +ipow2(A, B, M) when M > 0, B >= 0 -> + if A == 1 -> + 1; + true -> + ipow2(A, B, M, 1) + end. + +ipow2(A, 1, M, Prod) -> + (A*Prod) rem M; +ipow2(_A, 0, _M, Prod) -> + Prod; +ipow2(A, B, M, Prod) -> + B1 = B bsr 1, + A1 = (A*A) rem M, + if B - B1 == B1 -> + ipow2(A1, B1, M, Prod); + true -> + ipow2(A1, B1, M, (A*Prod) rem M) + end. + +%% %% +%% %% Normal gcd +%% %% +%% gcd(R, Q) when abs(Q) < abs(R) -> gcd1(Q,R); +%% gcd(R, Q) -> gcd1(R,Q). + +%% gcd1(0, Q) -> Q; +%% gcd1(R, Q) -> +%% gcd1(Q rem R, R). + + +%% %% +%% %% Least common multiple of (R,Q) +%% %% +%% lcm(0, _Q) -> 0; +%% lcm(_R, 0) -> 0; +%% lcm(R, Q) -> +%% (Q div gcd(R, Q)) * R. + +%% %% +%% %% Extended gcd gcd(R,Q) -> {G, {A,B}} such that G == R*A + Q*B +%% %% +%% %% Here we could have use for a bif divrem(Q, R) -> {Quote, Remainder} +%% %% +%% egcd(R,Q) when abs(Q) < abs(R) -> egcd1(Q,R,1,0,0,1); +%% egcd(R,Q) -> egcd1(R,Q,0,1,1,0). + +%% egcd1(0,Q,_,_,Q1,Q2) -> {Q, {Q2,Q1}}; +%% egcd1(R,Q,R1,R2,Q1,Q2) -> +%% D = Q div R, +%% egcd1(Q rem R, R, Q1-D*R1, Q2-D*R2, R1, R2). + +%% +%% Invert an element X mod P +%% Calculated as {1, {A,B}} = egcd(X,P), +%% 1 == P*A + X*B == X*B (mod P) i.e B is the inverse element +%% +%% X > 0, P > 0, X < P (P should be prime) +%% +invert(X,P) when X > 0, P > 0, X < P -> + I = inv(X,P,1,0), + if + I < 0 -> P + I; + true -> I + end. + +inv(0,_,_,Q) -> Q; +inv(X,P,R1,Q1) -> + D = P div X, + inv(P rem X, X, Q1 - D*R1, R1). + + +%% %% +%% %% Integer square root +%% %% + +%% isqrt(0) -> 0; +%% isqrt(1) -> 1; +%% isqrt(X) when X >= 0 -> +%% R = X div 2, +%% isqrt(X div R, R, X). + +%% isqrt(Q,R,X) when Q < R -> +%% R1 = (R+Q) div 2, +%% isqrt(X div R1, R1, X); +%% isqrt(_, R, _) -> R. + + diff --git a/lib/ssh/src/ssh_no_io.erl b/lib/ssh/src/ssh_no_io.erl new file mode 100644 index 0000000000..5f363ae6c2 --- /dev/null +++ b/lib/ssh/src/ssh_no_io.erl @@ -0,0 +1,39 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: ssh_io replacement that throws on everything + +-module(ssh_no_io). + +-export([yes_no/1, read_password/1, read_line/1, format/2]). + +yes_no(_Prompt) -> + throw({no_io_allowed, yes_no}). + +read_password(_Prompt) -> + throw({no_io_allowed, read_password}). + +read_line(_Prompt) -> + throw({no_io_allowed, read_line}). + +format(_Fmt, _Args) -> + throw({no_io_allowed, format}). + diff --git a/lib/ssh/src/ssh_rsa.erl b/lib/ssh/src/ssh_rsa.erl new file mode 100755 index 0000000000..7c2bf9a2bf --- /dev/null +++ b/lib/ssh/src/ssh_rsa.erl @@ -0,0 +1,299 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: rsa public-key sign and verify + +-module(ssh_rsa). + +-export([verify/3, sign/2]). +-export([alg_name/0]). + +-include("ssh.hrl"). +-include("PKCS-1.hrl"). + + +-define(MGF(Seed,Len), mgf1((Seed),(Len))). +-define(HASH(X), crypto:sha((X))). +-define(HLen, 20). + +%% start() -> +%% crypto:start(). + +%% sign_file(File) -> +%% start(), +%% {ok,Bin} = file:read_file(File), +%% {ok,Key} = ssh_file:private_host_rsa_key(user), +%% sign(Key, Bin). + +%% verify_file(File, Sig) -> +%% start(), +%% {ok,Bin} = file:read_file(File), +%% {ok,Key} = ssh_file:public_host_rsa_key(user), +%% verify(Key, Bin, Sig). + +sign(Private,Mb) -> + rsassa_pkcs1_v1_5_sign(Private,Mb). + +verify(Public,Mb,Sb) -> + rsassa_pkcs1_v1_5_verify(Public,Mb,Sb). + + + +%% Integer to octet string +i2osp(X, XLen) -> + ssh_bits:i2bin(X, XLen). + +%% Octet string to Integer +os2ip(X) -> + ssh_bits:bin2i(X). + +%% decrypt1, M = message representative +%% rsaep(#ssh_key { public={N,E}}, M) -> +%% ?ssh_assert(M >= 0 andalso M =< N-1, out_of_range), +%% ssh_math:ipow(M, E, N). + +%% encrypt1, C = cipher representative +%% rsadp(#ssh_key { public={N,_}, private={_,D}}, C) -> +%% ?ssh_assert(C >= 0 andalso C =< N-1, out_of_range), +%% ssh_math:ipow(C, D, N). + +%% sign1, M = message representative +rsasp1(#ssh_key { public={N,_}, private={_,D}}, M) -> + ?ssh_assert((M >= 0 andalso M =< N-1), out_of_range), + ssh_math:ipow(M, D, N). + +%% verify1, S =signature representative +rsavp1(#ssh_key { public={N,E}}, S) -> + ?ssh_assert(S >= 0 andalso S =< N-1, out_of_range), + ssh_math:ipow(S, E, N). + + +%% M messaage +%% rsaes_oaep_encrypt(Public, M) -> +%% rsaes_oaep_encrypt(Public, M, <<>>). + +%% rsaes_oaep_encrypt(Public=#ssh_key { public={N,_E}}, M, L) -> +%% ?ssh_assert(size(L) =< 16#ffffffffffffffff, label_to_long), +%% K = (ssh_bits:isize(N)+7) div 8, +%% MLen = size(M), +%% %% LLen = size(L), +%% ?ssh_assert(MLen =< K - 2*?HLen - 2, message_to_long), +%% LHash = ?HASH(L), +%% PS = ssh_bits:fill_bits(K - MLen - 2*?HLen - 2, 0), +%% DB = <>, +%% Seed = ssh_bits:random(?HLen), +%% DbMask = ?MGF(Seed, K - ?HLen - 1), +%% MaskedDB = ssh_bits:xor_bits(DB, DbMask), +%% SeedMask = ?MGF(MaskedDB, ?HLen), +%% MaskedSeed = ssh_bits:xor_bits(Seed, SeedMask), +%% EM = <<16#00, MaskedSeed/binary, MaskedDB/binary>>, +%% Mc = os2ip(EM), +%% C = rsaep(Public, Mc), +%% i2osp(C, K). + +%% rsaes_oaep_decrypt(Key, C) -> +%% rsaes_oaep_decrypt(Key, C, <<>>). + +%% rsaes_oaep_decrypt(Private=#ssh_key { public={N,_},private={_,_}},Cb,L) -> +%% ?ssh_assert(size(L) =< 16#ffffffffffffffff, label_to_long), +%% K = (ssh_bits:isize(N)+7) div 8, +%% ?ssh_assert(K == 2*?HLen + 2, decryption_error), +%% C = os2ip(Cb), +%% M = rsadp(Private, C), +%% EM = i2osp(M, K), +%% LHash = ?HASH(L), +%% MLen = K - ?HLen -1, +%% case EM of +%% <<16#00, MaskedSeed:?HLen/binary, MaskedDB:MLen>> -> +%% SeedMask = ?MGF(MaskedDB, ?HLen), +%% Seed = ssh_bits:xor_bits(MaskedSeed, SeedMask), +%% DbMask = ?MGF(Seed, K - ?HLen - 1), +%% DB = ssh_bits:xor_bits(MaskedDB, DbMask), +%% PSLen = K - MLen - 2*?HLen - 2, +%% case DB of +%% <> -> +%% M; +%% _ -> +%% exit(decryption_error) +%% end; +%% _ -> +%% exit(decryption_error) +%% end. + + +%% rsaes_pkcs1_v1_5_encrypt(Public=#ssh_key { public={N,_}}, M) -> +%% K = (ssh_bits:isize(N)+7) div 8, +%% MLen = size(M), +%% ?ssh_assert(MLen =< K - 11, message_to_long), +%% PS = ssh_bits:random(K - MLen - 3), +%% EM = <<16#00,16#02,PS/binary,16#00,M/binary>>, +%% Mc = os2ip(EM), +%% C = rsaep(Public, Mc), +%% i2osp(C, K). + + +%% rsaes_pkcs1_v1_5_decrypt(Private=#ssh_key { public={N,_},private={_,_}}, +%% Cb) -> +%% K = (ssh_bits:isize(N)+7) div 8, +%% CLen = size(Cb), +%% ?ssh_assert(CLen == K andalso K >= 11, decryption_error), +%% C = os2ip(Cb), +%% M = rsadp(Private, C), +%% EM = i2osp(M, K), +%% PSLen = K - CLen - 3, +%% case EM of +%% <<16#00, 16#02, _PS:PSLen/binary, 16#00, M>> -> +%% M; +%% _ -> +%% exit(decryption_error) +%% end. + +%% rsassa_pss_sign(Private=#ssh_key { public={N,_},private={_,_}},Mb) -> +%% ModBits = ssh_bits:isize(N), +%% K = (ModBits+7) div 8, +%% EM = emsa_pss_encode(Mb, ModBits - 1), +%% M = os2ip(EM), +%% S = rsasp1(Private, M), +%% i2osp(S, K). + +%% rsassa_pss_verify(Public=#ssh_key { public={N,_E}},Mb,Sb) -> +%% ModBits = ssh_bits:isize(N), +%% K = (ModBits+7) div 8, +%% ?ssh_assert(size(Sb) == K, invalid_signature), +%% S = os2ip(Sb), +%% M = rsavp1(Public,S), +%% EMLen = (ModBits-1+7) div 8, +%% EM = i2osp(M, EMLen), +%% emsa_pss_verify(Mb, EM, ModBits-1). + + +rsassa_pkcs1_v1_5_sign(Private=#ssh_key { public={N,_},private={_,_D}},Mb) -> + K = (ssh_bits:isize(N)+7) div 8, + EM = emsa_pkcs1_v1_5_encode(Mb, K), + M = os2ip(EM), + S = rsasp1(Private, M), + i2osp(S, K). + +rsassa_pkcs1_v1_5_verify(Public=#ssh_key { public={N,_E}}, Mb, Sb) -> + K = (ssh_bits:isize(N)+7) div 8, + ?ssh_assert(size(Sb) == K, invalid_signature), + S = os2ip(Sb), + M = rsavp1(Public, S), + EM = i2osp(M, K), + %?dbg(true, "verify K=~p S=~w ~n#M=~w~n#EM=~w~n", [K, S, M, EM]), + case emsa_pkcs1_v1_5_encode(Mb, K) of + EM -> ok; + _S -> + io:format("S: ~p~n", [_S]), + {error, invalid_signature} % exit(invalid_signature) + end. + + +emsa_pkcs1_v1_5_encode(M, EMLen) -> + H = ?HASH(M), + %% Must use speical xxNull types here! + Alg = #'AlgorithmNull' { algorithm = ?'id-sha1', + parameters = <<>> }, + {ok,TCode} = 'PKCS-1':encode('DigestInfoNull', + #'DigestInfoNull'{ digestAlgorithm = Alg, + digest = H }), + T = list_to_binary(TCode), + TLen = size(T), + ?ssh_assert(EMLen >= TLen + 11, message_to_short), + PS = ssh_bits:fill_bits(EMLen - TLen - 3, 16#ff), + <<16#00, 16#01, PS/binary, 16#00, T/binary>>. + + +%% emsa_pss_encode(M, EMBits) -> +%% emsa_pss_encode(M, EMBits, 0). + +%% emsa_pss_encode(M, EMBits, SLen) -> +%% ?ssh_assert(size(M) =< 16#ffffffffffffffff, message_to_long), +%% EMLen = (EMBits + 7) div 8, +%% MHash = ?HASH(M), +%% ?ssh_assert(EMLen >= ?HLen + SLen + 2, encoding_error), +%% Salt = ssh_bits:random(SLen), +%% M1 = [16#00,16#00,16#00,16#00,16#00,16#00,16#00,16#00, +%% MHash, Salt], +%% H = ?HASH(M1), +%% PS = ssh_bits:fill_bits(EMLen-SLen-?HLen-2, 0), +%% DB = <>, +%% DbMask = ?MGF(H, EMLen - ?HLen -1), +%% MaskedDB = ssh_bits:xor_bits(DB, DbMask), +%% ZLen = 8*EMLen - EMBits, +%% NZLen = (8 - (ZLen rem 8)) rem 8, +%% <<_:ZLen, NZ:NZLen, MaskedDB1/binary>> = MaskedDB, +%% MaskedDB2 = <<0:ZLen, NZ:NZLen, MaskedDB1/binary>>, +%% <>. + + +%% emsa_pss_verify(M, EM, EMBits) -> +%% emsa_pss_verify(M, EM, EMBits, 0). + +%% emsa_pss_verify(M, EM, EMBits, SLen) -> +%% ?ssh_assert(size(M) =< 16#ffffffffffffffff, message_to_long), +%% EMLen = (EMBits + 7) div 8, +%% MHash = ?HASH(M), +%% ?ssh_assert(EMLen >= ?HLen + SLen + 2, inconsistent), +%% MaskLen = (EMLen - ?HLen - 1)-1, +%% ZLen = 8*EMLen - EMBits, +%% NZLen = (8 - (ZLen rem 8)) rem 8, +%% case EM of +%% <<0:ZLen,Nz:NZLen,MaskedDB1:MaskLen/binary, H:?HLen/binary, 16#BC>> -> +%% MaskedDB = <<0:ZLen,Nz:NZLen,MaskedDB1/binary>>, +%% DbMask = ?MGF(H, EMLen - ?HLen - 1), +%% DB = ssh_bits:xor_bits(MaskedDB, DbMask), +%% PSLen1 = (EMLen - SLen - ?HLen - 2) - 1, +%% PS = ssh_bits:fill_bits(PSLen1, 0), +%% case DB of +%% <<_:ZLen,0:NZLen,PS:PSLen1/binary,16#01,Salt:SLen/binary>> -> +%% M1 = [16#00,16#00,16#00,16#00,16#00,16#00,16#00,16#00, +%% MHash, Salt], +%% case ?HASH(M1) of +%% H -> ok; +%% _ -> exit(inconsistent) +%% end; +%% _ -> +%% exit(inconsistent) +%% end; +%% _ -> +%% exit(inconsistent) +%% end. + + + +%% Mask generating function MGF1 +%% mgf1(MGFSeed, MaskLen) -> +%% T = mgf1_loop(0, ((MaskLen + ?HLen -1) div ?HLen) - 1, MGFSeed, ""), +%% <> = T, +%% R. + +%% mgf1_loop(Counter, N, _, T) when Counter > N -> +%% list_to_binary(T); +%% mgf1_loop(Counter, N, MGFSeed, T) -> +%% C = i2osp(Counter, 4), +%% mgf1_loop(Counter+1, N, MGFSeed, [T, ?HASH([MGFSeed, C])]). + + + + +alg_name() -> + "ssh-rsa". diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl new file mode 100755 index 0000000000..cbfa208f6f --- /dev/null +++ b/lib/ssh/src/ssh_sftp.erl @@ -0,0 +1,1148 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SFTP protocol front-end + +-module(ssh_sftp). + +-behaviour(ssh_channel). + +-include_lib("kernel/include/file.hrl"). +-include("ssh.hrl"). +-include("ssh_xfer.hrl"). + +%% API + +-export([start_channel/1, start_channel/2, start_channel/3, stop_channel/1]). + +-export([open/3, opendir/2, close/2, readdir/2, pread/4, read/3, + open/4, opendir/3, close/3, readdir/3, pread/5, read/4, + apread/4, aread/3, pwrite/4, write/3, apwrite/4, awrite/3, + pwrite/5, write/4, + position/3, real_path/2, read_file_info/2, get_file_info/2, + position/4, real_path/3, read_file_info/3, get_file_info/3, + write_file_info/3, read_link_info/2, read_link/2, make_symlink/3, + write_file_info/4, read_link_info/3, read_link/3, make_symlink/4, + rename/3, delete/2, make_dir/2, del_dir/2, send_window/1, + rename/4, delete/3, make_dir/3, del_dir/3, send_window/2, + recv_window/1, list_dir/2, read_file/2, write_file/3, + recv_window/2, list_dir/3, read_file/3, write_file/4]). + +%% Deprecated +-export([connect/1, connect/2, connect/3, stop/1]). + +-deprecated({connect, 1, next_major_release}). +-deprecated({connect, 2, next_major_release}). +-deprecated({connect, 3, next_major_release}). +-deprecated({stop, 1, next_major_release}). + +%% ssh_channel callbacks +-export([init/1, handle_call/3, handle_msg/2, handle_ssh_msg/2, terminate/2]). +%% TODO: Should be placed elsewhere ssh_sftpd should not call functions in ssh_sftp! +-export([info_to_attr/1, attr_to_info/1]). + +-record(state, + { + xf, + rep_buf = <<>>, + req_id, + req_list = [], %% {ReqId, Fun} + inf %% list of fileinf + }). + +-record(fileinf, + { + handle, + offset, + size, + mode + }). + +-define(FILEOP_TIMEOUT, infinity). + +-define(NEXT_REQID(S), + S#state { req_id = (S#state.req_id + 1) band 16#ffffffff}). + +-define(XF(S), S#state.xf). +-define(REQID(S), S#state.req_id). + +%%==================================================================== +%% API +%%==================================================================== +start_channel(Cm) when is_pid(Cm) -> + start_channel(Cm, []); +start_channel(Host) when is_list(Host) -> + start_channel(Host, []). +start_channel(Cm, Opts) when is_pid(Cm) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:attach(Cm, []) of + {ok, ChannelId, Cm} -> + case ssh_channel:start(Cm, ChannelId, + ?MODULE, [Cm, ChannelId, Timeout]) of + {ok, Pid} -> + case wait_for_version_negotiation(Pid, Timeout) of + ok -> + {ok, Pid}; + TimeOut -> + TimeOut + end; + {error, Reason} -> + {error, Reason}; + ignore -> + {error, ignore} + end; + Error -> + Error + end; + +start_channel(Host, Opts) -> + start_channel(Host, 22, Opts). +start_channel(Host, Port, Opts) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:connect(Host, Port, proplists:delete(timeout, Opts)) of + {ok, ChannelId, Cm} -> + case ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, + ChannelId, Timeout]) of + {ok, Pid} -> + case wait_for_version_negotiation(Pid, Timeout) of + ok -> + {ok, Pid, Cm}; + TimeOut -> + TimeOut + end; + {error, Reason} -> + {error, Reason}; + ignore -> + {error, ignore} + end; + Error -> + Error + end. + +stop_channel(Pid) -> + case process_info(Pid, [trap_exit]) of + [{trap_exit, Bool}] -> + process_flag(trap_exit, true), + link(Pid), + exit(Pid, ssh_sftp_stop_channel), + receive + {'EXIT', Pid, normal} -> + ok + after 5000 -> + exit(Pid, kill), + receive + {'EXIT', Pid, killed} -> + ok + end + end, + process_flag(trap_exit, Bool), + ok; + undefined -> + ok + end. + +wait_for_version_negotiation(Pid, Timeout) -> + call(Pid, wait_for_version_negotiation, Timeout). + +open(Pid, File, Mode) -> + open(Pid, File, Mode, ?FILEOP_TIMEOUT). +open(Pid, File, Mode, FileOpTimeout) -> + call(Pid, {open, false, File, Mode}, FileOpTimeout). + +opendir(Pid, Path) -> + opendir(Pid, Path, ?FILEOP_TIMEOUT). +opendir(Pid, Path, FileOpTimeout) -> + call(Pid, {opendir, false, Path}, FileOpTimeout). + +close(Pid, Handle) -> + close(Pid, Handle, ?FILEOP_TIMEOUT). +close(Pid, Handle, FileOpTimeout) -> + call(Pid, {close,false,Handle}, FileOpTimeout). + +readdir(Pid,Handle) -> + readdir(Pid,Handle, ?FILEOP_TIMEOUT). +readdir(Pid,Handle, FileOpTimeout) -> + call(Pid, {readdir,false,Handle}, FileOpTimeout). + +pread(Pid, Handle, Offset, Len) -> + pread(Pid, Handle, Offset, Len, ?FILEOP_TIMEOUT). +pread(Pid, Handle, Offset, Len, FileOpTimeout) -> + call(Pid, {pread,false,Handle, Offset, Len}, FileOpTimeout). + +read(Pid, Handle, Len) -> + read(Pid, Handle, Len, ?FILEOP_TIMEOUT). +read(Pid, Handle, Len, FileOpTimeout) -> + call(Pid, {read,false,Handle, Len}, FileOpTimeout). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +apread(Pid, Handle, Offset, Len) -> + call(Pid, {pread,true,Handle, Offset, Len}, infinity). + +%% TODO this ought to be a cast! +aread(Pid, Handle, Len) -> + call(Pid, {read,true,Handle, Len}, infinity). + +pwrite(Pid, Handle, Offset, Data) -> + pwrite(Pid, Handle, Offset, Data, ?FILEOP_TIMEOUT). +pwrite(Pid, Handle, Offset, Data, FileOpTimeout) -> + call(Pid, {pwrite,false,Handle,Offset,Data}, FileOpTimeout). + +write(Pid, Handle, Data) -> + write(Pid, Handle, Data, ?FILEOP_TIMEOUT). +write(Pid, Handle, Data, FileOpTimeout) -> + call(Pid, {write,false,Handle,Data}, FileOpTimeout). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +apwrite(Pid, Handle, Offset, Data) -> + call(Pid, {pwrite,true,Handle,Offset,Data}, infinity). + +%% TODO this ought to be a cast! Is so in all practial meaning +%% even if it is obscure! +awrite(Pid, Handle, Data) -> + call(Pid, {write,true,Handle,Data}, infinity). + +position(Pid, Handle, Pos) -> + position(Pid, Handle, Pos, ?FILEOP_TIMEOUT). +position(Pid, Handle, Pos, FileOpTimeout) -> + call(Pid, {position, Handle, Pos}, FileOpTimeout). + +real_path(Pid, Path) -> + real_path(Pid, Path, ?FILEOP_TIMEOUT). +real_path(Pid, Path, FileOpTimeout) -> + call(Pid, {real_path, false, Path}, FileOpTimeout). + +read_file_info(Pid, Name) -> + read_file_info(Pid, Name, ?FILEOP_TIMEOUT). +read_file_info(Pid, Name, FileOpTimeout) -> + call(Pid, {read_file_info,false,Name}, FileOpTimeout). + +get_file_info(Pid, Handle) -> + get_file_info(Pid, Handle, ?FILEOP_TIMEOUT). +get_file_info(Pid, Handle, FileOpTimeout) -> + call(Pid, {get_file_info,false,Handle}, FileOpTimeout). + +write_file_info(Pid, Name, Info) -> + write_file_info(Pid, Name, Info, ?FILEOP_TIMEOUT). +write_file_info(Pid, Name, Info, FileOpTimeout) -> + call(Pid, {write_file_info,false,Name, Info}, FileOpTimeout). + +read_link_info(Pid, Name) -> + read_link_info(Pid, Name, ?FILEOP_TIMEOUT). +read_link_info(Pid, Name, FileOpTimeout) -> + call(Pid, {read_link_info,false,Name}, FileOpTimeout). + +read_link(Pid, LinkName) -> + read_link(Pid, LinkName, ?FILEOP_TIMEOUT). +read_link(Pid, LinkName, FileOpTimeout) -> + case call(Pid, {read_link,false,LinkName}, FileOpTimeout) of + {ok, [{Name, _Attrs}]} -> + {ok, Name}; + ErrMsg -> + ErrMsg + end. + +make_symlink(Pid, Name, Target) -> + make_symlink(Pid, Name, Target, ?FILEOP_TIMEOUT). +make_symlink(Pid, Name, Target, FileOpTimeout) -> + call(Pid, {make_symlink,false, Name, Target}, FileOpTimeout). + +rename(Pid, FromFile, ToFile) -> + rename(Pid, FromFile, ToFile, ?FILEOP_TIMEOUT). +rename(Pid, FromFile, ToFile, FileOpTimeout) -> + call(Pid, {rename,false,FromFile, ToFile}, FileOpTimeout). + +delete(Pid, Name) -> + delete(Pid, Name, ?FILEOP_TIMEOUT). +delete(Pid, Name, FileOpTimeout) -> + call(Pid, {delete,false,Name}, FileOpTimeout). + +make_dir(Pid, Name) -> + make_dir(Pid, Name, ?FILEOP_TIMEOUT). +make_dir(Pid, Name, FileOpTimeout) -> + call(Pid, {make_dir,false,Name}, FileOpTimeout). + +del_dir(Pid, Name) -> + del_dir(Pid, Name, ?FILEOP_TIMEOUT). +del_dir(Pid, Name, FileOpTimeout) -> + call(Pid, {del_dir,false,Name}, FileOpTimeout). + +%% TODO : send_window and recv_window - Really needed? Not documented! +%% internal use maybe should be handled in other way! +send_window(Pid) -> + send_window(Pid, ?FILEOP_TIMEOUT). +send_window(Pid, FileOpTimeout) -> + call(Pid, send_window, FileOpTimeout). + +recv_window(Pid) -> + recv_window(Pid, ?FILEOP_TIMEOUT). +recv_window(Pid, FileOpTimeout) -> + call(Pid, recv_window, FileOpTimeout). + + +list_dir(Pid, Name) -> + list_dir(Pid, Name, ?FILEOP_TIMEOUT). + +list_dir(Pid, Name, FileOpTimeout) -> + case opendir(Pid, Name, FileOpTimeout) of + {ok,Handle} -> + Res = do_list_dir(Pid, Handle, FileOpTimeout, []), + close(Pid, Handle, FileOpTimeout), + case Res of + {ok, List} -> + NList = lists:foldl(fun({Nm, _Info},Acc) -> + [Nm|Acc] end, + [], List), + {ok,NList}; + Error -> Error + end; + Error -> + Error + end. + +do_list_dir(Pid, Handle, FileOpTimeout, Acc) -> + case readdir(Pid, Handle, FileOpTimeout) of + {ok, []} -> + {ok, Acc}; + {ok, Names} -> + do_list_dir(Pid, Handle, FileOpTimeout, Acc ++ Names); + eof -> + {ok, Acc}; + Error -> + Error + end. + + +read_file(Pid, Name) -> + read_file(Pid, Name, ?FILEOP_TIMEOUT). + +read_file(Pid, Name, FileOpTimeout) -> + case open(Pid, Name, [read, binary], FileOpTimeout) of + {ok, Handle} -> + {ok,{_WindowSz,PacketSz}} = recv_window(Pid, FileOpTimeout), + Res = read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, []), + close(Pid, Handle), + Res; + Error -> + Error + end. + +read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, Acc) -> + case read(Pid, Handle, PacketSz, FileOpTimeout) of + {ok, Data} -> + read_file_loop(Pid, Handle, PacketSz, FileOpTimeout, [Data|Acc]); + eof -> + {ok, list_to_binary(lists:reverse(Acc))}; + Error -> + Error + end. + +write_file(Pid, Name, List) -> + write_file(Pid, Name, List, ?FILEOP_TIMEOUT). + +write_file(Pid, Name, List, FileOpTimeout) when is_list(List) -> + write_file(Pid, Name, list_to_binary(List), FileOpTimeout); +write_file(Pid, Name, Bin, FileOpTimeout) -> + case open(Pid, Name, [write, binary], FileOpTimeout) of + {ok, Handle} -> + {ok,{_Window,Packet}} = send_window(Pid, FileOpTimeout), + Res = write_file_loop(Pid, Handle, 0, Bin, size(Bin), Packet, + FileOpTimeout), + close(Pid, Handle, FileOpTimeout), + Res; + Error -> + Error + end. + +write_file_loop(_Pid, _Handle, _Pos, _Bin, 0, _PacketSz,_FileOpTimeout) -> + ok; +write_file_loop(Pid, Handle, Pos, Bin, Remain, PacketSz, FileOpTimeout) -> + if Remain >= PacketSz -> + <<_:Pos/binary, Data:PacketSz/binary, _/binary>> = Bin, + case write(Pid, Handle, Data, FileOpTimeout) of + ok -> + write_file_loop(Pid, Handle, + Pos+PacketSz, Bin, Remain-PacketSz, + PacketSz, FileOpTimeout); + Error -> + Error + end; + true -> + <<_:Pos/binary, Data/binary>> = Bin, + write(Pid, Handle, Data, FileOpTimeout) + end. + +%%==================================================================== +%% SSh channel callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} +%% +%% Description: +%%-------------------------------------------------------------------- +init([Cm, ChannelId, Timeout]) -> + erlang:monitor(process, Cm), + case ssh_connection:subsystem(Cm, ChannelId, "sftp", Timeout) of + success -> + Xf = #ssh_xfer{cm = Cm, + channel = ChannelId}, + {ok, #state{xf = Xf, + req_id = 0, + rep_buf = <<>>, + inf = new_inf()}}; + failure -> + {stop, {error, "server failed to start sftp subsystem"}}; + Error -> + {stop, Error} + end. + +%%-------------------------------------------------------------------- +%% Function: handle_call/3 +%% Description: Handling call messages +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%-------------------------------------------------------------------- +handle_call({{timeout, infinity}, wait_for_version_negotiation}, From, + #state{xf = #ssh_xfer{vsn = undefined} = Xf} = State) -> + {noreply, State#state{xf = Xf#ssh_xfer{vsn = From}}}; + +handle_call({{timeout, Timeout}, wait_for_version_negotiation}, From, + #state{xf = #ssh_xfer{vsn = undefined} = Xf} = State) -> + timer:send_after(Timeout, {timeout, undefined, From}), + {noreply, State#state{xf = Xf#ssh_xfer{vsn = From}}}; + +handle_call({_, wait_for_version_negotiation}, _, State) -> + {reply, ok, State}; + +handle_call({{timeout, infinity}, Msg}, From, State) -> + do_handle_call(Msg, From, State); +handle_call({{timeout, Timeout}, Msg}, From, #state{req_id = Id} = State) -> + timer:send_after(Timeout, {timeout, Id, From}), + do_handle_call(Msg, From, State). + +do_handle_call({open, Async,FileName,Mode}, From, #state{xf = XF} = State) -> + {Access,Flags,Attrs} = open_mode(XF#ssh_xfer.vsn, Mode), + ReqID = State#state.req_id, + ssh_xfer:open(XF, ReqID, FileName, Access, Flags, Attrs), + case Async of + true -> + {reply, {async,ReqID}, + update_request_info(ReqID, State, + fun({ok,Handle},State1) -> + open2(ReqID,FileName,Handle,Mode,Async, + From,State1); + (Rep,State1) -> + async_reply(ReqID, Rep, From, State1) + end)}; + false -> + {noreply, + update_request_info(ReqID, State, + fun({ok,Handle},State1) -> + open2(ReqID,FileName,Handle,Mode,Async, + From,State1); + (Rep,State1) -> + sync_reply(Rep, From, State1) + end)} + end; + +do_handle_call({opendir,Async,Path}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:opendir(?XF(State), ReqID, Path), + make_reply(ReqID, Async, From, State); + +do_handle_call({readdir,Async,Handle}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:readdir(?XF(State), ReqID, Handle), + make_reply(ReqID, Async, From, State); + +do_handle_call({close,_Async,Handle}, From, State) -> + %% wait until all operations on handle are done + case get_size(Handle, State) of + undefined -> + ReqID = State#state.req_id, + ssh_xfer:close(?XF(State), ReqID, Handle), + make_reply_post(ReqID, false, From, State, + fun(Rep, State1) -> + {Rep, erase_handle(Handle, State1)} + end); + _ -> + case lseek_position(Handle, cur, State) of + {ok,_} -> + ReqID = State#state.req_id, + ssh_xfer:close(?XF(State), ReqID, Handle), + make_reply_post(ReqID, false, From, State, + fun(Rep, State1) -> + {Rep, erase_handle(Handle, State1)} + end); + Error -> + {reply, Error, State} + end + end; + +do_handle_call({pread,Async,Handle,At,Length}, From, State) -> + case lseek_position(Handle, At, State) of + {ok,Offset} -> + ReqID = State#state.req_id, + ssh_xfer:read(?XF(State),ReqID,Handle,Offset,Length), + %% To get multiple async read to work we must update the offset + %% before the operation begins + State1 = update_offset(Handle, Offset+Length, State), + make_reply_post(ReqID,Async,From,State1, + fun({ok,Data}, State2) -> + case get_mode(Handle, State2) of + binary -> {{ok,Data}, State2}; + text -> + {{ok,binary_to_list(Data)}, State2} + end; + (Rep, State2) -> + {Rep, State2} + end); + Error -> + {reply, Error, State} + end; + +do_handle_call({read,Async,Handle,Length}, From, State) -> + case lseek_position(Handle, cur, State) of + {ok,Offset} -> + ReqID = State#state.req_id, + ssh_xfer:read(?XF(State),ReqID,Handle,Offset,Length), + %% To get multiple async read to work we must update the offset + %% before the operation begins + State1 = update_offset(Handle, Offset+Length, State), + make_reply_post(ReqID,Async,From,State1, + fun({ok,Data}, State2) -> + case get_mode(Handle, State2) of + binary -> {{ok,Data}, State2}; + text -> + {{ok,binary_to_list(Data)}, State2} + end; + (Rep, State2) -> {Rep, State2} + end); + Error -> + {reply, Error, State} + end; + +do_handle_call({pwrite,Async,Handle,At,Data0}, From, State) -> + case lseek_position(Handle, At, State) of + {ok,Offset} -> + Data = if + is_binary(Data0) -> + Data0; + is_list(Data0) -> + list_to_binary(Data0) + end, + ReqID = State#state.req_id, + Size = size(Data), + ssh_xfer:write(?XF(State),ReqID,Handle,Offset,Data), + State1 = update_size(Handle, Offset+Size, State), + make_reply(ReqID, Async, From, State1); + Error -> + {reply, Error, State} + end; + +do_handle_call({write,Async,Handle,Data0}, From, State) -> + case lseek_position(Handle, cur, State) of + {ok,Offset} -> + Data = if + is_binary(Data0) -> + Data0; + is_list(Data0) -> + list_to_binary(Data0) + end, + ReqID = State#state.req_id, + Size = size(Data), + ssh_xfer:write(?XF(State),ReqID,Handle,Offset,Data), + State1 = update_offset(Handle, Offset+Size, State), + make_reply(ReqID, Async, From, State1); + Error -> + {reply, Error, State} + end; + +do_handle_call({position,Handle,At}, _From, State) -> + %% We could make this auto sync when all request to Handle is done? + case lseek_position(Handle, At, State) of + {ok,Offset} -> + {reply, {ok, Offset}, update_offset(Handle, Offset, State)}; + Error -> + {reply, Error, State} + end; + +do_handle_call({rename,Async,FromFile,ToFile}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:rename(?XF(State),ReqID,FromFile,ToFile,[overwrite]), + make_reply(ReqID, Async, From, State); + +do_handle_call({delete,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:remove(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({make_dir,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:mkdir(?XF(State), ReqID, Name, + #ssh_xfer_attr{ type = directory }), + make_reply(ReqID, Async, From, State); + +do_handle_call({del_dir,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:rmdir(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({real_path,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:realpath(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_file_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:stat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({get_file_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:fstat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_link_info,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:lstat(?XF(State), ReqID, Name, all), + make_reply(ReqID, Async, From, State); + +do_handle_call({read_link,Async,Name}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:readlink(?XF(State), ReqID, Name), + make_reply(ReqID, Async, From, State); + +do_handle_call({make_symlink, Async, Path, TargetPath}, From, State) -> + ReqID = State#state.req_id, + ssh_xfer:symlink(?XF(State), ReqID, Path, TargetPath), + make_reply(ReqID, Async, From, State); + +do_handle_call({write_file_info,Async,Name,Info}, From, State) -> + ReqID = State#state.req_id, + A = info_to_attr(Info), + ssh_xfer:setstat(?XF(State), ReqID, Name, A), + make_reply(ReqID, Async, From, State); + +%% TODO: Do we really want this format? Function send_window +%% is not documented and seems to be used only inernaly! +%% It is backwards compatible for now. +do_handle_call(send_window, _From, State) -> + XF = State#state.xf, + [{send_window,{{win_size, Size0},{packet_size, Size1}}}] = + ssh:channel_info(XF#ssh_xfer.cm, XF#ssh_xfer.channel, [send_window]), + {reply, {ok, {Size0, Size1}}, State}; + +%% TODO: Do we really want this format? Function recv_window +%% is not documented and seems to be used only inernaly! +%% It is backwards compatible for now. +do_handle_call(recv_window, _From, State) -> + XF = State#state.xf, + [{recv_window,{{win_size, Size0},{packet_size, Size1}}}] = + ssh:channel_info(XF#ssh_xfer.cm, XF#ssh_xfer.channel, [recv_window]), + {reply, {ok, {Size0, Size1}}, State}; + +%% Backwards compatible +do_handle_call(stop, _From, State) -> + {stop, shutdown, ok, State}; + +do_handle_call(Call, _From, State) -> + {reply, {error, bad_call, Call, State}, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages +%%-------------------------------------------------------------------- +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, 0, Data}}, #state{rep_buf = Data0} = + State0) -> + State = handle_reply(State0, <>), + {ok, State}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, 1, Data}}, State) -> + error_logger:format("ssh: STDERR: ~s\n", [binary_to_list(Data)]), + {ok, State}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, {eof, _ChannelId}}, State) -> + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, + State0) -> + State = reply_all(State0, {error, Error}), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State0) -> + State = reply_all(State0, {error, {exit_status, Status}}), + {stop, ChannelId, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages +%%-------------------------------------------------------------------- +handle_msg({ssh_channel_up, _, _}, #state{xf = Xf} = State) -> + ssh_xfer:protocol_version_request(Xf), + {ok, State}; + +%% Version negotiation timed out +handle_msg({timeout, undefined, From}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + ssh_channel:reply(From, {error, timeout}), + {stop, ChannelId, State}; + +handle_msg({timeout, Id, From}, #state{req_list = ReqList0} = State) -> + case lists:keysearch(Id, 1, ReqList0) of + false -> + {ok, State}; + _ -> + ReqList = lists:keydelete(Id, 1, ReqList0), + ssh_channel:reply(From, {error, timeout}), + {ok, State#state{req_list = ReqList}} + end; + +%% Connection manager goes down +handle_msg({'DOWN', _Ref, _Type, _Process, _}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + {stop, ChannelId, State}; + +%% Stopped by user +handle_msg({'EXIT', _, ssh_sftp_stop_channel}, + #state{xf = #ssh_xfer{channel = ChannelId}} = State) -> + {stop, ChannelId, State}; + +handle_msg(_, State) -> + {ok, State}. +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: Called when the channel process is terminated +%%-------------------------------------------------------------------- +%% Backwards compatible +terminate(shutdown, #state{xf = #ssh_xfer{cm = Cm}} = State) -> + reply_all(State, {error, closed}), + ssh:close(Cm); + +terminate(_Reason, State) -> + reply_all(State, {error, closed}). + +%%==================================================================== +%% Internal functions +%%==================================================================== +call(Pid, Msg, TimeOut) -> + ssh_channel:call(Pid, {{timeout, TimeOut}, Msg}, infinity). + +handle_reply(State, <>) -> + do_handle_reply(State, Reply, Rest); +handle_reply(State, Data) -> + State#state{rep_buf = Data}. + +do_handle_reply(#state{xf = Xf} = State, + <>, Rest) -> + Ext = ssh_xfer:decode_ext(BinExt), + case Xf#ssh_xfer.vsn of + undefined -> + ok; + From -> + ssh_channel:reply(From, ok) + end, + State#state{xf = Xf#ssh_xfer{vsn = Version, ext = Ext}, rep_buf = Rest}; + +do_handle_reply(State0, Data, Rest) -> + case catch ssh_xfer:xf_reply(?XF(State0), Data) of + {'EXIT', _Reason} -> + handle_reply(State0, Rest); + XfReply -> + State = handle_req_reply(State0, XfReply), + handle_reply(State, Rest) + end. + +handle_req_reply(State0, {_, ReqID, _} = XfReply) -> + case lists:keysearch(ReqID, 1, State0#state.req_list) of + false -> + State0; + {value,{_,Fun}} -> + List = lists:keydelete(ReqID, 1, State0#state.req_list), + State1 = State0#state { req_list = List }, + case catch Fun(xreply(XfReply),State1) of + {'EXIT', _} -> + State1; + State -> + State + end + end. + +xreply({handle,_,H}) -> {ok, H}; +xreply({data,_,Data}) -> {ok, Data}; +xreply({name,_,Names}) -> {ok, Names}; +xreply({attrs, _, A}) -> {ok, attr_to_info(A)}; +xreply({extended_reply,_,X}) -> {ok, X}; +xreply({status,_,{ok, _Err, _Lang, _Rep}}) -> ok; +xreply({status,_,{eof, _Err, _Lang, _Rep}}) -> eof; +xreply({status,_,{Stat, _Err, _Lang, _Rep}}) -> {error, Stat}; +xreply({Code, _, Reply}) -> {Code, Reply}. + +update_request_info(ReqID, State, Fun) -> + List = [{ReqID,Fun} | State#state.req_list], + ID = (State#state.req_id + 1) band 16#ffffffff, + State#state { req_list = List, req_id = ID }. + +async_reply(ReqID, Reply, _From={To,_}, State) -> + To ! {async_reply, ReqID, Reply}, + State. + +sync_reply(Reply, From, State) -> + catch (ssh_channel:reply(From, Reply)), + State. + +open2(OrigReqID,FileName,Handle,Mode,Async,From,State) -> + I0 = State#state.inf, + FileMode = case lists:member(binary, Mode) orelse lists:member(raw, Mode) of + true -> binary; + false -> text + end, + I1 = add_new_handle(Handle, FileMode, I0), + State0 = State#state{inf = I1}, + ReqID = State0#state.req_id, + ssh_xfer:stat(State0#state.xf, ReqID, FileName, [size]), + case Async of + true -> + update_request_info(ReqID, State0, + fun({ok,FI},State1) -> + Size = FI#file_info.size, + State2 = if is_integer(Size) -> + put_size(Handle, Size, State1); + true -> + State1 + end, + async_reply(OrigReqID, {ok,Handle}, From, State2); + (_, State1) -> + async_reply(OrigReqID, {ok,Handle}, From, State1) + end); + false -> + update_request_info(ReqID, State0, + fun({ok,FI},State1) -> + Size = FI#file_info.size, + State2 = if is_integer(Size) -> + put_size(Handle, Size, State1); + true -> + State1 + end, + sync_reply({ok,Handle}, From, State2); + (_, State1) -> + sync_reply({ok,Handle}, From, State1) + end) + end. + +reply_all(State, Reply) -> + List = State#state.req_list, + lists:foreach(fun({_ReqID,Fun}) -> + catch Fun(Reply,State) + end, List), + State#state {req_list = []}. + +make_reply(ReqID, true, From, State) -> + {reply, {async, ReqID}, + update_request_info(ReqID, State, + fun(Reply,State1) -> + async_reply(ReqID,Reply,From,State1) + end)}; + +make_reply(ReqID, false, From, State) -> + {noreply, + update_request_info(ReqID, State, + fun(Reply,State1) -> + sync_reply(Reply, From, State1) + end)}. + +make_reply_post(ReqID, true, From, State, PostFun) -> + {reply, {async, ReqID}, + update_request_info(ReqID, State, + fun(Reply,State1) -> + case catch PostFun(Reply, State1) of + {'EXIT',_} -> + async_reply(ReqID,Reply, From, State1); + {Reply1, State2} -> + async_reply(ReqID,Reply1, From, State2) + end + end)}; + +make_reply_post(ReqID, false, From, State, PostFun) -> + {noreply, + update_request_info(ReqID, State, + fun(Reply,State1) -> + case catch PostFun(Reply, State1) of + {'EXIT',_} -> + sync_reply(Reply, From, State1); + {Reply1, State2} -> + sync_reply(Reply1, From, State2) + end + end)}. + +%% convert: file_info -> ssh_xfer_attr +info_to_attr(I) when is_record(I, file_info) -> + #ssh_xfer_attr { permissions = I#file_info.mode, + size = I#file_info.size, + type = I#file_info.type, + owner = I#file_info.uid, + group = I#file_info.gid, + atime = datetime_to_unix(I#file_info.atime), + mtime = datetime_to_unix(I#file_info.mtime), + createtime = datetime_to_unix(I#file_info.ctime)}. + +%% convert: ssh_xfer_attr -> file_info +attr_to_info(A) when is_record(A, ssh_xfer_attr) -> + #file_info{ + size = A#ssh_xfer_attr.size, + type = A#ssh_xfer_attr.type, + access = read_write, %% FIXME: read/write/read_write/none + atime = unix_to_datetime(A#ssh_xfer_attr.atime), + mtime = unix_to_datetime(A#ssh_xfer_attr.mtime), + ctime = unix_to_datetime(A#ssh_xfer_attr.createtime), + mode = A#ssh_xfer_attr.permissions, + links = 1, + major_device = 0, + minor_device = 0, + inode = 0, + uid = A#ssh_xfer_attr.owner, + gid = A#ssh_xfer_attr.group}. + + +%% Added workaround for sftp timestam problem. (Timestamps should be +%% in UTC but they where not) . The workaround uses a deprecated +%% function i calandar. This will work as expected most of the time +%% but has problems for the same reason as +%% calendar:local_time_to_universal_time/1. We consider it better that +%% the timestamps work as expected most of the time instead of none of +%% the time. Hopfully the file-api will be updated so that we can +%% solve this problem in a better way in the future. + +unix_to_datetime(undefined) -> + undefined; +unix_to_datetime(UTCSecs) -> + UTCDateTime = + calendar:gregorian_seconds_to_datetime(UTCSecs + 62167219200), + erlang:universaltime_to_localtime(UTCDateTime). + +datetime_to_unix(undefined) -> + undefined; +datetime_to_unix(LocalDateTime) -> + UTCDateTime = erlang:localtime_to_universaltime(LocalDateTime), + calendar:datetime_to_gregorian_seconds(UTCDateTime) - 62167219200. + + +open_mode(Vsn,Modes) when Vsn >= 5 -> + open_mode5(Modes); +open_mode(_Vsn, Modes) -> + open_mode3(Modes). + +open_mode5(Modes) -> + A = #ssh_xfer_attr{type = regular}, + {Fl, Ac} = case {lists:member(write, Modes), + lists:member(read, Modes), + lists:member(append, Modes)} of + {_, _, true} -> + {[append_data], + [read_attributes, + append_data, write_attributes]}; + {true, false, false} -> + {[create_truncate], + [write_data, write_attributes]}; + {true, true, _} -> + {[open_or_create], + [read_data, read_attributes, + write_data, write_attributes]}; + {false, true, _} -> + {[open_existing], + [read_data, read_attributes]} + end, + {Ac, Fl, A}. + +open_mode3(Modes) -> + A = #ssh_xfer_attr{type = regular}, + Fl = case {lists:member(write, Modes), + lists:member(read, Modes), + lists:member(append, Modes)} of + {_, _, true} -> + [append]; + {true, false, false} -> + [write, creat, trunc]; + {true, true, _} -> + [read, write]; + {false, true, _} -> + [read] + end, + {[], Fl, A}. + +%% accessors for inf dict +new_inf() -> dict:new(). + +add_new_handle(Handle, FileMode, Inf) -> + dict:store(Handle, #fileinf{offset=0, size=0, mode=FileMode}, Inf). + +update_size(Handle, NewSize, State) -> + OldSize = get_size(Handle, State), + if NewSize > OldSize -> + put_size(Handle, NewSize, State); + true -> + State + end. + +%% set_offset(Handle, NewOffset) -> +%% put({offset,Handle}, NewOffset). + +update_offset(Handle, NewOffset, State0) -> + State1 = put_offset(Handle, NewOffset, State0), + update_size(Handle, NewOffset, State1). + +%% access size and offset for handle +put_size(Handle, Size, State) -> + Inf0 = State#state.inf, + case dict:find(Handle, Inf0) of + {ok, FI} -> + State#state{inf=dict:store(Handle, FI#fileinf{size=Size}, Inf0)}; + _ -> + State#state{inf=dict:store(Handle, #fileinf{size=Size,offset=0}, + Inf0)} + end. + +put_offset(Handle, Offset, State) -> + Inf0 = State#state.inf, + case dict:find(Handle, Inf0) of + {ok, FI} -> + State#state{inf=dict:store(Handle, FI#fileinf{offset=Offset}, + Inf0)}; + _ -> + State#state{inf=dict:store(Handle, #fileinf{size=Offset, + offset=Offset}, Inf0)} + end. + +get_size(Handle, State) -> + case dict:find(Handle, State#state.inf) of + {ok, FI} -> + FI#fileinf.size; + _ -> + undefined + end. + +%% get_offset(Handle, State) -> +%% {ok, FI} = dict:find(Handle, State#state.inf), +%% FI#fileinf.offset. + +get_mode(Handle, State) -> + case dict:find(Handle, State#state.inf) of + {ok, FI} -> + FI#fileinf.mode; + _ -> + undefined + end. + +erase_handle(Handle, State) -> + FI = dict:erase(Handle, State#state.inf), + State#state{inf = FI}. + +%% +%% Caluclate a integer offset +%% +lseek_position(Handle, Pos, State) -> + case dict:find(Handle, State#state.inf) of + {ok, #fileinf{offset=O, size=S}} -> + lseek_pos(Pos, O, S); + _ -> + {error, einval} + end. + +lseek_pos(_Pos, undefined, _) -> + {error, einval}; +lseek_pos(Pos, _CurOffset, _CurSize) + when is_integer(Pos) andalso 0 =< Pos andalso Pos < ?SSH_FILEXFER_LARGEFILESIZE -> + {ok,Pos}; +lseek_pos(bof, _CurOffset, _CurSize) -> + {ok,0}; +lseek_pos(cur, CurOffset, _CurSize) -> + {ok,CurOffset}; +lseek_pos(eof, _CurOffset, CurSize) -> + {ok,CurSize}; +lseek_pos({bof, Offset}, _CurOffset, _CurSize) + when is_integer(Offset) andalso 0 =< Offset andalso Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + {ok, Offset}; +lseek_pos({cur, Offset}, CurOffset, _CurSize) + when is_integer(Offset) andalso -(?SSH_FILEXFER_LARGEFILESIZE) =< Offset andalso + Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + NewOffset = CurOffset + Offset, + if NewOffset < 0 -> + {ok, 0}; + true -> + {ok, NewOffset} + end; +lseek_pos({eof, Offset}, _CurOffset, CurSize) + when is_integer(Offset) andalso -(?SSH_FILEXFER_LARGEFILESIZE) =< Offset andalso + Offset < ?SSH_FILEXFER_LARGEFILESIZE -> + NewOffset = CurSize + Offset, + if NewOffset < 0 -> + {ok, 0}; + true -> + {ok, NewOffset} + end; +lseek_pos(_, _, _) -> + {error, einval}. + + +%%%%%% Deprecated %%%% +connect(Cm) when is_pid(Cm) -> + connect(Cm, []); +connect(Host) when is_list(Host) -> + connect(Host, []). +connect(Cm, Opts) when is_pid(Cm) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:attach(Cm, []) of + {ok, ChannelId, Cm} -> + ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, ChannelId, + Timeout]); + Error -> + Error + end; +connect(Host, Opts) -> + connect(Host, 22, Opts). +connect(Host, Port, Opts) -> + Timeout = proplists:get_value(timeout, Opts, infinity), + case ssh_xfer:connect(Host, Port, proplists:delete(timeout, Opts)) of + {ok, ChannelId, Cm} -> + ssh_channel:start(Cm, ChannelId, ?MODULE, [Cm, + ChannelId, Timeout]); + Error -> + Error + end. + + +stop(Pid) -> + call(Pid, stop, infinity). + diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl new file mode 100644 index 0000000000..10b8ede1e4 --- /dev/null +++ b/lib/ssh/src/ssh_sftpd.erl @@ -0,0 +1,932 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SFTP server daemon + +-module(ssh_sftpd). + +%%-behaviour(gen_server). +-behaviour(ssh_channel). + +-include_lib("kernel/include/file.hrl"). + +-include("ssh.hrl"). +-include("ssh_xfer.hrl"). + +%%-------------------------------------------------------------------- +%% External exports +-export([subsystem_spec/1, + listen/1, listen/2, listen/3, stop/1]). + +-export([init/1, handle_ssh_msg/2, handle_msg/2, terminate/2, code_change/3]). + +-record(state, { + xf, % [{channel,ssh_xfer states}...] + cwd, % current dir (on first connect) + root, % root dir + remote_channel, % remote channel + pending, % binary() + file_handler, % atom() - callback module + file_state, % state for the file callback module + max_files, % integer >= 0 max no files sent during READDIR + handles % list of open handles + %% handle is either {, directory, {Path, unread|eof}} or + %% {, file, {Path, IoDevice}} + }). + +%%==================================================================== +%% API +%%==================================================================== +subsystem_spec(Options) -> + {"sftp", {?MODULE, Options}}. + +%%% DEPRECATED START %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%-------------------------------------------------------------------- +%% Function: listen() -> Pid | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +listen(Port) -> + listen(any, Port, []). +listen(Port, Options) -> + listen(any, Port, Options). +listen(Addr, Port, Options) -> + SubSystems = [subsystem_spec(Options)], + ssh:daemon(Addr, Port, [{subsystems, SubSystems} |Options]). + +%%-------------------------------------------------------------------- +%% Function: stop(Pid) -> ok +%% Description: Stops the listener +%%-------------------------------------------------------------------- +stop(Pid) -> + ssh_cli:stop(Pid). + + +%%% DEPRECATED END %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%==================================================================== +%% subsystem callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init(Options) -> + {FileMod, FS0} = case proplists:get_value(file_handler, Options, + {ssh_sftpd_file,[]}) of + {F, S} -> + {F, S}; + F -> + {F, []} + end, + + {{ok, Default}, FS1} = FileMod:get_cwd(FS0), + CWD = proplists:get_value(cwd, Options, Default), + + Root0 = proplists:get_value(root, Options, ""), + + %% Get the root of the file system (symlinks must be followed, + %% otherwise the realpath call won't work). But since symbolic links + %% isn't supported on all plattforms we have to use the root property + %% supplied by the user. + {Root, State} = + case resolve_symlinks(Root0, + #state{root = Root0, + file_handler = FileMod, + file_state = FS1}) of + {{ok, Root1}, State0} -> + {Root1, State0}; + {{error, _}, State0} -> + {Root0, State0} + end, + MaxLength = proplists:get_value(max_files, Options, 0), + + Vsn = proplists:get_value(vsn, Options, 5), + + {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength, + handles = [], pending = <<>>, + xf = #ssh_xfer{vsn = Vsn, ext = []}}}. + + +%%-------------------------------------------------------------------- +%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages +%%-------------------------------------------------------------------- +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, Type, Data}}, State) -> + State1 = handle_data(Type, Data, State), + {ok, State1}; + +handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) -> + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) -> + Report = io_lib:format("Connection closed by peer ~n Error ~p~n", + [Error]), + error_logger:error_report(Report), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) -> + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) -> + + Report = io_lib:format("Connection closed by peer ~n Status ~p~n", + [Status]), + error_logger:error_report(Report), + {stop, ChannelId, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles other messages +%%-------------------------------------------------------------------- +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, + #state{xf =Xf} = State) -> + {ok, State#state{xf = Xf#ssh_xfer{cm = ConnectionManager, + channel = ChannelId}}}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_, #state{handles=Handles, file_handler=FileMod, file_state=FS}) -> + CloseFun = fun({_, file, {_, Fd}}, FS0) -> + {_Res, FS1} = FileMod:close(Fd, FS0), + FS1; + (_, FS0) -> + FS0 + end, + lists:foldl(CloseFun, FS, Handles), + ok. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +handle_data(0, <>, + State = #state{pending = <<>>}) -> + <> = Msg, + NewState = handle_op(Op, ReqId, Data, State), + case Rest of + <<>> -> + NewState; + _ -> + handle_data(0, Rest, NewState) + end; + +handle_data(0, Data, State = #state{pending = <<>>}) -> + State#state{pending = Data}; + +handle_data(Type, Data, State = #state{pending = Pending}) -> + handle_data(Type, <>, + State#state{pending = <<>>}). + +handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) -> + XF = State#state.xf, + Vsn = lists:min([XF#ssh_xfer.vsn, Version]), + XF1 = XF#ssh_xfer{vsn = Vsn}, + ssh_xfer:xf_send_reply(XF1, ?SSH_FXP_VERSION, <>), + State#state{xf = XF1}; +handle_op(?SSH_FXP_REALPATH, ReqId, + <>, + State0) -> + RelPath0 = binary_to_list(RPath), + RelPath = relate_file_name(RelPath0, State0, _Canonicalize=false), + {Res, State} = resolve_symlinks(RelPath, State0), + case Res of + {ok, AbsPath} -> + NewAbsPath = chroot_filename(AbsPath, State), + ?dbg(true, "handle_op ?SSH_FXP_REALPATH: RelPath=~p AbsPath=~p\n", + [RelPath, NewAbsPath]), + XF = State#state.xf, + Attr = #ssh_xfer_attr{type=directory}, + ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr), + State; + {error, _} = Error -> + send_status(Error, ReqId, State) + end; +handle_op(?SSH_FXP_OPENDIR, ReqId, + <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + RelPath = binary_to_list(RPath), + AbsPath = relate_file_name(RelPath, State0), + + XF = State0#state.xf, + {IsDir, FS1} = FileMod:is_dir(AbsPath, FS0), + State1 = State0#state{file_state = FS1}, + case IsDir of + false -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY, + "Not a directory"), + State1; + true -> + add_handle(State1, XF, ReqId, directory, {RelPath,unread}) + end; +handle_op(?SSH_FXP_READDIR, ReqId, + <>, + State) -> + XF = State#state.xf, + case get_handle(State#state.handles, BinHandle) of + {_Handle, directory, {_RelPath, eof}} -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_EOF), + State; + {Handle, directory, {RelPath, Status}} -> + read_dir(State, XF, ReqId, Handle, RelPath, Status); + _ -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE), + State + end; +handle_op(?SSH_FXP_CLOSE, ReqId, + <>, + State = #state{handles = Handles, xf = XF, + file_handler = FileMod, file_state = FS0}) -> + case get_handle(Handles, BinHandle) of + {Handle, Type, T} -> + FS1 = case Type of + file -> + close_our_file(T, FileMod, FS0); + _ -> + FS0 + end, + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_OK), + State#state{handles = lists:keydelete(Handle, 1, Handles), + file_state = FS1}; + _ -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE), + State + end; +handle_op(?SSH_FXP_LSTAT, ReqId, Data, State) -> + stat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State, read_link_info); +handle_op(?SSH_FXP_STAT, ReqId, Data, State) -> + stat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State, read_file_info); +handle_op(?SSH_FXP_FSTAT, ReqId, Data, State) -> + fstat((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State); +handle_op(?SSH_FXP_OPEN, ReqId, Data, State) -> + open((State#state.xf)#ssh_xfer.vsn, ReqId, Data, State); +handle_op(?SSH_FXP_READ, ReqId, <>, + State) -> + case get_handle(State#state.handles, BinHandle) of + {_Handle, file, {_Path, IoDevice}} -> + read_file(ReqId, IoDevice, Offset, Len, State); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ?SSH_FX_INVALID_HANDLE), + State + end; +handle_op(?SSH_FXP_WRITE, ReqId, + <>, State) -> + case get_handle(State#state.handles, BinHandle) of + {_Handle, file, {_Path, IoDevice}} -> + write_file(ReqId, IoDevice, Offset, Data, State); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ?SSH_FX_INVALID_HANDLE), + State + end; +handle_op(?SSH_FXP_READLINK, ReqId, <>, + State = #state{file_handler = FileMod, file_state = FS0}) -> + RelPath = binary_to_list(BPath), + AbsPath = relate_file_name(RelPath, State), + {Res, FS1} = FileMod:read_link(AbsPath, FS0), + case Res of + {ok, NewPath} -> + ssh_xfer:xf_send_name(State#state.xf, ReqId, NewPath, + #ssh_xfer_attr{type=regular}); + {error, Error} -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ssh_xfer:encode_erlang_status(Error)) + end, + State#state{file_state = FS1}; +handle_op(?SSH_FXP_SETSTAT, ReqId, <>, State0) -> + Path = relate_file_name(BPath, State0), + {Status, State1} = set_stat(Attr, Path, State0), + send_status(Status, ReqId, State1); +handle_op(?SSH_FXP_MKDIR, ReqId, <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + {Res, FS1} = FileMod:make_dir(Path, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + ok -> + {_, State2} = set_stat(Attr, Path, State1), + send_status(ok, ReqId, State2); + {error, Error} -> + send_status({error, Error}, ReqId, State1) + end; +handle_op(?SSH_FXP_FSETSTAT, ReqId, <>, + State0 = #state{handles = Handles}) -> + + case get_handle(Handles, BinHandle) of + {_Handle, _Type, {Path,_}} -> + {Status, State1} = set_stat(Attr, Path, State0), + send_status(Status, ReqId, State1); + _ -> + ssh_xfer:xf_send_status(State0#state.xf, ReqId, + ?SSH_FX_INVALID_HANDLE), + State0 + end; +handle_op(?SSH_FXP_REMOVE, ReqId, <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + %% case FileMod:is_dir(Path) of %% This version 6 we still have ver 5 + %% true -> + %% ssh_xfer:xf_send_status(State#state.xf, ReqId, + %% ?SSH_FX_FILE_IS_A_DIRECTORY); + %% false -> + {Status, FS1} = FileMod:delete(Path, FS0), + State1 = State0#state{file_state = FS1}, + send_status(Status, ReqId, State1); + %%end; +handle_op(?SSH_FXP_RMDIR, ReqId, <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + {Status, FS1} = FileMod:del_dir(Path, FS0), + State1 = State0#state{file_state = FS1}, + send_status(Status, ReqId, State1); + +handle_op(?SSH_FXP_RENAME, ReqId, + Bin = <>, + State = #state{xf = #ssh_xfer{vsn = Vsn}}) when Vsn==3; Vsn==4 -> + handle_op(?SSH_FXP_RENAME, ReqId, <>, State); + +handle_op(?SSH_FXP_RENAME, ReqId, + <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + Path2 = relate_file_name(BPath2, State0), + case Flags band ?SSH_FXP_RENAME_ATOMIC of + 0 -> + case Flags band ?SSH_FXP_RENAME_OVERWRITE of + 0 -> + {Res, FS1} = FileMod:read_link_info(Path2, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + {ok, _Info} -> + ssh_xfer:xf_send_status( + State1#state.xf, + ReqId, + ?SSH_FX_FILE_ALREADY_EXISTS), + State1; + _ -> + rename(Path, Path2, ReqId, State1) + end; + _ -> + rename(Path, Path2, ReqId, State0) + end; + _ -> + ssh_xfer:xf_send_status(State0#state.xf, ReqId, + ?SSH_FX_OP_UNSUPPORTED), + State0 + end; +handle_op(?SSH_FXP_SYMLINK, ReqId, + <>, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + Path = relate_file_name(BPath, State0), + Path2 = relate_file_name(BPath2, State0), + {Status, FS1} = FileMod:make_symlink(Path2, Path, FS0), + State1 = State0#state{file_state = FS1}, + send_status(Status, ReqId, State1). + +new_handle([], H) -> + H; +new_handle([{N, _} | Rest], H) when N > H -> + new_handle(Rest, N+1); +new_handle([_ | Rest], H) -> + new_handle(Rest, H). + +add_handle(State, XF, ReqId, Type, DirFileTuple) -> + Handles = State#state.handles, + Handle = new_handle(Handles, 0), + ssh_xfer:xf_send_handle(XF, ReqId, integer_to_list(Handle)), + State#state{handles = [{Handle, Type, DirFileTuple} | Handles]}. + +get_handle(Handles, BinHandle) -> + case (catch list_to_integer(binary_to_list(BinHandle))) of + I when is_integer(I) -> + case lists:keysearch(I, 1, Handles) of + {value, T} -> T; + false -> error + end; + _ -> + error + end. + +%%% read_dir/5: read directory, send names, and return new state +read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, + XF, ReqId, Handle, RelPath, {cache, Files}) -> + AbsPath = relate_file_name(RelPath, State0), + ?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]), + if + length(Files) > MaxLength -> + {ToSend, NewCache} = lists:split(MaxLength, Files), + {NamesAndAttrs, FS1} = get_attrs(AbsPath, ToSend, FileMod, FS0), + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, + {Handle, directory, {RelPath,{cache, NewCache}}}), + State0#state{handles = Handles, file_state = FS1}; + true -> + {NamesAndAttrs, FS1} = get_attrs(AbsPath, Files, FileMod, FS0), + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, + {Handle, directory, {RelPath,eof}}), + State0#state{handles = Handles, file_state = FS1} + end; +read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, + XF, ReqId, Handle, RelPath, _Status) -> + AbsPath = relate_file_name(RelPath, State0), + ?dbg(true, "read_dir: AbsPath=~p\n", [AbsPath]), + {Res, FS1} = FileMod:list_dir(AbsPath, FS0), + case Res of + {ok, Files} when MaxLength == 0 orelse MaxLength > length(Files) -> + {NamesAndAttrs, FS2} = get_attrs(AbsPath, Files, FileMod, FS1), + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, + {Handle, directory, {RelPath,eof}}), + State0#state{handles = Handles, file_state = FS2}; + {ok, Files} -> + {ToSend, Cache} = lists:split(MaxLength, Files), + {NamesAndAttrs, FS2} = get_attrs(AbsPath, ToSend, FileMod, FS1), + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, + {Handle, directory, {RelPath,{cache, Cache}}}), + State0#state{handles = Handles, file_state = FS2}; + {error, Error} -> + State1 = State0#state{file_state = FS1}, + send_status({error, Error}, ReqId, State1) + end. + + +%%% get_attrs: get stat of each file and return +get_attrs(RelPath, Files, FileMod, FS) -> + get_attrs(RelPath, Files, FileMod, FS, []). + +get_attrs(_RelPath, [], _FileMod, FS, Acc) -> + {lists:reverse(Acc), FS}; +get_attrs(RelPath, [F | Rest], FileMod, FS0, Acc) -> + Path = filename:absname(F, RelPath), + ?dbg(true, "get_attrs fun: F=~p\n", [F]), + case FileMod:read_link_info(Path, FS0) of + {{ok, Info}, FS1} -> + Attrs = ssh_sftp:info_to_attr(Info), + get_attrs(RelPath, Rest, FileMod, FS1, [{F, Attrs} | Acc]); + {{error, enoent}, FS1} -> + get_attrs(RelPath, Rest, FileMod, FS1, Acc); + {Error, FS1} -> + {Error, FS1} + end. + +close_our_file({_,Fd}, FileMod, FS0) -> + {_Res, FS1} = FileMod:close(Fd, FS0), + FS1. + +%%% stat: do the stat +stat(Vsn, ReqId, Data, State, F) when Vsn =< 3-> + <> = Data, + stat(ReqId, binary_to_list(BPath), State, F); +stat(Vsn, ReqId, Data, State, F) when Vsn >= 4-> + <> = Data, + stat(ReqId, binary_to_list(BPath), State, F). + +fstat(Vsn, ReqId, Data, State) when Vsn =< 3-> + <> = Data, + fstat(ReqId, Handle, State); +fstat(Vsn, ReqId, Data, State) when Vsn >= 4-> + <> = Data, + fstat(ReqId, Handle, State). + +fstat(ReqId, BinHandle, State) -> + case get_handle(State#state.handles, BinHandle) of + {_Handle, _Type, {Path, _}} -> + stat(ReqId, Path, State, read_file_info); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ?SSH_FX_INVALID_HANDLE), + State + end. + +stat(ReqId, RelPath, State0=#state{file_handler=FileMod, + file_state=FS0}, F) -> + AbsPath = relate_file_name(RelPath, State0), + XF = State0#state.xf, + ?dbg(false, "stat: AbsPath=~p\n", [AbsPath]), + {Res, FS1} = FileMod:F(AbsPath, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + {ok, FileInfo} -> + ssh_xfer:xf_send_attr(XF, ReqId, + ssh_sftp:info_to_attr(FileInfo)), + State1; + {error, E} -> + send_status({error, E}, ReqId, State1) + end. + +decode_4_open_flag(create_new) -> + [write]; +decode_4_open_flag(create_truncate) -> + [write]; +decode_4_open_flag(truncate_existing) -> + [write]; +decode_4_open_flag(open_existing) -> + [read]. + +decode_4_flags([OpenFlag | Flags]) -> + decode_4_flags(Flags, decode_4_open_flag(OpenFlag)). + +decode_4_flags([], Flags) -> + Flags; +decode_4_flags([append_data|R], _Flags) -> + decode_4_flags(R, [append]); +decode_4_flags([append_data_atomic|R], _Flags) -> + decode_4_flags(R, [append]); +decode_4_flags([_|R], Flags) -> + decode_4_flags(R, Flags). + +decode_4_access_flag(read_data) -> + [read]; +decode_4_access_flag(list_directory) -> + [read]; +decode_4_access_flag(write_data) -> + [write]; +decode_4_access_flag(add_file) -> + [write]; +decode_4_access_flag(add_subdirectory) -> + [read]; +decode_4_access_flag(append_data) -> + [append]; +decode_4_access_flag(_) -> + [read]. + +decode_4_acess([_ | _] = Flags) -> + lists:map(fun(Flag) -> + [decode_4_access_flag(Flag)] + end, Flags); +decode_4_acess([]) -> + []. + +open(Vsn, ReqId, Data, State) when Vsn =< 3 -> + <> = Data, + Path = binary_to_list(BPath), + Flags = ssh_xfer:decode_open_flags(Vsn, PFlags) -- [creat, excl, trunc], + ?dbg(true, "open: Flags=~p\n", [Flags]), + do_open(ReqId, State, Path, Flags); +open(Vsn, ReqId, Data, State) when Vsn >= 4 -> + <> = Data, + Path = binary_to_list(BPath), + FlagBits = ssh_xfer:decode_open_flags(Vsn, PFlags), + AcessBits = ssh_xfer:decode_ace_mask(Access), + ?dbg(true, "open: Fl=~p\n", [FlagBits]), + %% TODO: This is to make sure the Access flags are not ignored + %% but this should be thought through better. This solution should + %% be considered a hack in order to buy some time. At least + %% it works better than when the Access flags where totally ignored. + %% A better solution may need some code refactoring that we do + %% not have time for right now. + AcessFlags = decode_4_acess(AcessBits), + Flags = lists:append(lists:umerge( + [[decode_4_flags(FlagBits)] | AcessFlags])), + + ?dbg(true, "open: Flags=~p\n", [Flags]), + + do_open(ReqId, State, Path, Flags). + +do_open(ReqId, State0, Path, Flags) -> + #state{file_handler = FileMod, file_state = FS0, root = Root} = State0, + XF = State0#state.xf, + F = [raw, binary | Flags], + %% case FileMod:is_dir(Path) of %% This is version 6 we still have 5 + %% true -> + %% ssh_xfer:xf_send_status(State#state.xf, ReqId, + %% ?SSH_FX_FILE_IS_A_DIRECTORY); + %% false -> + + AbsPath = case Root of + "" -> + Path; + _ -> + relate_file_name(Path, State0) + end, + + {Res, FS1} = FileMod:open(AbsPath, F, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + {ok, IoDevice} -> + add_handle(State1, XF, ReqId, file, {Path,IoDevice}); + {error, Error} -> + ssh_xfer:xf_send_status(State1#state.xf, ReqId, + ssh_xfer:encode_erlang_status(Error)), + State1 + end. + +%% resolve all symlinks in a path +resolve_symlinks(Path, State) -> + resolve_symlinks(Path, _LinkCnt=32, State). + +resolve_symlinks(Path, LinkCnt, State0) -> + resolve_symlinks_2(filename:split(Path), State0, LinkCnt, []). + +resolve_symlinks_2(_Path, State, LinkCnt, _AccPath) when LinkCnt =:= 0 -> + %% Too many links (there might be a symlink loop) + {{error, emlink}, State}; +resolve_symlinks_2(["." | RestPath], State0, LinkCnt, AccPath) -> + resolve_symlinks_2(RestPath, State0, LinkCnt, AccPath); +resolve_symlinks_2([".." | RestPath], State0, LinkCnt, AccPath) -> + %% Remove the last path component + AccPathComps0 = filename:split(AccPath), + Path = case lists:reverse(tl(lists:reverse(AccPathComps0))) of + [] -> + ""; + AccPathComps -> + filename:join(AccPathComps) + end, + resolve_symlinks_2(RestPath, State0, LinkCnt, Path); +resolve_symlinks_2([PathComp | RestPath], State0, LinkCnt, AccPath0) -> + #state{file_handler = FileMod, file_state = FS0} = State0, + AccPath1 = filename:join(AccPath0, PathComp), + {Res, FS1} = FileMod:read_link(AccPath1, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + {ok, Target0} -> % path is a symlink + %% The target may be a relative or an absolute path and + %% may contain symlinks + Target1 = filename:absname(Target0, AccPath0), + {FollowRes, State2} = resolve_symlinks(Target1, LinkCnt-1, State1), + case FollowRes of + {ok, Target} -> + resolve_symlinks_2(RestPath, State2, LinkCnt-1, Target); + {error, _} = Error -> + {Error, State2} + end; + {error, einval} -> % path exists, but is not a symlink + resolve_symlinks_2(RestPath, State1, LinkCnt, AccPath1); + {error, _} = Error -> + {Error, State1} + end; +resolve_symlinks_2([], State, _LinkCnt, AccPath) -> + {{ok, AccPath}, State}. + + +relate_file_name(File, State) -> + relate_file_name(File, State, _Canonicalize=true). + +relate_file_name(File, State, Canonicalize) when is_binary(File) -> + relate_file_name(binary_to_list(File), State, Canonicalize); +relate_file_name(File, #state{cwd = CWD, root = ""}, Canonicalize) -> + relate_filename_to_path(File, CWD, Canonicalize); +relate_file_name(File, #state{root = Root}, Canonicalize) -> + case is_within_root(Root, File) of + true -> + File; + false -> + RelFile = make_relative_filename(File), + NewFile = relate_filename_to_path(RelFile, Root, Canonicalize), + case is_within_root(Root, NewFile) of + true -> + NewFile; + false -> + Root + end + end. + +is_within_root(Root, File) -> + lists:prefix(Root, File). + +%% Remove leading slash (/), if any, in order to make the filename +%% relative (to the root) +make_relative_filename("/") -> "./"; % Make it relative and preserve / +make_relative_filename("/"++File) -> File; +make_relative_filename(File) -> File. + +relate_filename_to_path(File0, Path, Canonicalize) -> + File1 = filename:absname(File0, Path), + File2 = if Canonicalize -> canonicalize_filename(File1); + true -> File1 + end, + ensure_trailing_slash_is_preserved(File0, File2). + +%% It seems as if the openssh client (observed with the +%% openssh-4.2p1-18.30 package on SLED 10), and possibly other clients +%% as well (Maverick?), rely on the fact that a trailing slash (/) is +%% preserved. If trailing slashes aren't preserved, symlinks which +%% point at directories won't be properly identified as directories. +%% +%% A failing example: +%% +%% 1) assume the following directory structure: +%% $ mkdir /tmp/symlink-target +%% $ touch /tmp/symlink-target/foo +%% $ ln -s /tmp/symlink-target /tmp/symlink +%% +%% 2) login using the sftp client in openssh +%% sftp> cd /tmp/ +%% sftp> ls symlink-target +%% symlink-target/foo +%% sftp> ls symlink +%% symlink/ <===== foo should have been visible here +%% sftp> cd symlink-target +%% sftp> ls +%% foo +%% sftp> cd .. +%% sftp> cd symlink +%% sftp> ls +%% <===== foo should have been visible here +%% +%% The symlinks are resolved by file:read_link_info/1 only if the path +%% has a trailing slash, which seems to something that some of the +%% sftp clients utilize: +%% +%% 1> file:read_link_info(".../symlink"). +%% {ok,{file_info,4,symlink,read_write, +%% {{2008,10,20},{10,25,26}}, +%% {{2008,10,17},{16,22,33}}, +%% {{2008,10,17},{16,22,33}}, +%% 41471,1,2053,0,570447,20996,9935}} +%% +%% 2> file:read_link_info(".../symlink/"). +%% {ok,{file_info,8192,directory,read_write, +%% {{2008,10,20},{10,36,2}}, +%% {{2008,10,20},{10,44,35}}, +%% {{2008,10,20},{10,44,35}}, +%% 17407,29,2053,0,521224,0,0}} +ensure_trailing_slash_is_preserved(File0, File1) -> + case {lists:suffix("/", File0), lists:suffix("/", File1)} of + {true, false} -> File1 ++ "/"; + _Other -> File1 + end. + + + +%%% fix file just a little: a/b/.. -> a and a/. -> a +canonicalize_filename(File0) -> + File = filename:join(canonicalize_filename_2(filename:split(File0), [])), + ensure_trailing_slash_is_preserved(File0, File). + +canonicalize_filename_2([".." | Rest], ["/"] = Acc) -> + canonicalize_filename_2(Rest, Acc); +canonicalize_filename_2([".." | Rest], [_Dir | Paths]) -> + canonicalize_filename_2(Rest, Paths); +canonicalize_filename_2(["." | Rest], Acc) -> + canonicalize_filename_2(Rest, Acc); +canonicalize_filename_2([A | Rest], Acc) -> + canonicalize_filename_2(Rest, [A | Acc]); +canonicalize_filename_2([], Acc) -> + lists:reverse(Acc). + +%% return a filename which is relative to the root directory +%% (any filename that's outside the root directory is forced to the root) +chroot_filename(Filename, #state{root = Root}) -> + FilenameComps0 = filename:split(Filename), + RootComps = filename:split(Root), + filename:join(chroot_filename_2(FilenameComps0, RootComps)). + +chroot_filename_2([PathComp | FilenameRest], [PathComp | RootRest]) -> + chroot_filename_2(FilenameRest, RootRest); +chroot_filename_2(FilenameComps, []) when length(FilenameComps) > 0 -> + %% Ensure there's a leading / (filename:join above will take care + %% of any duplicates) + ["/" | FilenameComps]; +chroot_filename_2(_FilenameComps, _RootComps) -> + %% The filename is either outside the root or at the root. In + %% both cases we want to force the filename to the root. + ["/"]. + + +read_file(ReqId, IoDevice, Offset, Len, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + {Res1, FS1} = FileMod:position(IoDevice, {bof, Offset}, FS0), + case Res1 of + {ok, _NewPos} -> + {Res2, FS2} = FileMod:read(IoDevice, Len, FS1), + State1 = State0#state{file_state = FS2}, + case Res2 of + {ok, Data} -> + ssh_xfer:xf_send_data(State1#state.xf, ReqId, Data), + State1; + {error, Error} -> + send_status({error, Error}, ReqId, State1); + eof -> + send_status(eof, ReqId, State1) + end; + {error, Error} -> + State1 = State0#state{file_state = FS1}, + send_status({error, Error}, ReqId, State1) + end. + +write_file(ReqId, IoDevice, Offset, Data, + State0 = #state{file_handler = FileMod, file_state = FS0}) -> + {Res, FS1} = FileMod:position(IoDevice, {bof, Offset}, FS0), + case Res of + {ok, _NewPos} -> + {Status, FS2} = FileMod:write(IoDevice, Data, FS1), + State1 = State0#state{file_state = FS2}, + send_status(Status, ReqId, State1); + {error, Error} -> + State1 = State0#state{file_state = FS1}, + send_status({error, Error}, ReqId, State1) + end. + +get_status(ok) -> + ?SSH_FX_OK; +get_status(eof) -> + ?SSH_FX_EOF; +get_status({error,Error}) -> + ssh_xfer:encode_erlang_status(Error). + +send_status(Status, ReqId, State) -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, get_status(Status)), + State. + +set_stat(<<>>, _Path, State) -> + {ok, State}; +set_stat(Attr, Path, + State0 = #state{file_handler=FileMod, file_state=FS0}) -> + {DecodedAttr, _Rest} = + ssh_xfer:decode_ATTR((State0#state.xf)#ssh_xfer.vsn, Attr), + ?dbg(true, "set_stat DecodedAttr=~p\n", [DecodedAttr]), + Info = ssh_sftp:attr_to_info(DecodedAttr), + {Res1, FS1} = FileMod:read_link_info(Path, FS0), + case Res1 of + {ok, OldInfo} -> + NewInfo = set_file_info(Info, OldInfo), + ?dbg(true, "set_stat Path=~p\nInfo=~p\nOldInfo=~p\nNewInfo=~p\n", + [Path, Info, OldInfo, NewInfo]), + {Res2, FS2} = FileMod:write_file_info(Path, NewInfo, FS1), + State1 = State0#state{file_state = FS2}, + {Res2, State1}; + {error, Error} -> + State1 = State0#state{file_state = FS1}, + {{error, Error}, State1} + end. + + +set_file_info_sel(undefined, F) -> + F; +set_file_info_sel(F, _) -> + F. + +set_file_info(#file_info{atime = Dst_atime, mtime = Dst_mtime, + ctime = Dst_ctime, + mode = Dst_mode, uid = Dst_uid, gid = Dst_gid}, + #file_info{atime = Src_atime, mtime = Src_mtime, + ctime = Src_ctime, + mode = Src_mode, uid = Src_uid, gid = Src_gid}) -> + #file_info{atime = set_file_info_sel(Dst_atime, Src_atime), + mtime = set_file_info_sel(Dst_mtime, Src_mtime), + ctime = set_file_info_sel(Dst_ctime, Src_ctime), + mode = set_file_info_sel(Dst_mode, Src_mode), + uid = set_file_info_sel(Dst_uid, Src_uid), + gid = set_file_info_sel(Dst_gid, Src_gid)}. + +rename(Path, Path2, ReqId, State0) -> + #state{file_handler = FileMod, file_state = FS0} = State0, + {Status, FS1} = FileMod:rename(Path, Path2, FS0), + State1 = State0#state{file_state = FS1}, + send_status(Status, ReqId, State1). diff --git a/lib/ssh/src/ssh_sftpd_file.erl b/lib/ssh/src/ssh_sftpd_file.erl new file mode 100644 index 0000000000..f0b6bb4de5 --- /dev/null +++ b/lib/ssh/src/ssh_sftpd_file.erl @@ -0,0 +1,83 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2006-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: Default Callback module for ssh_sftpd + +-module(ssh_sftpd_file). + +-behaviour(ssh_sftpd_file_api). + +%% API +-export([close/2, delete/2, del_dir/2, get_cwd/1, is_dir/2, list_dir/2, + make_dir/2, make_symlink/3, open/3, position/3, read/3, + read_file_info/2, read_link/2, read_link_info/2, rename/3, + write/3, write_file_info/3]). + +close(IoDevice, State) -> + {file:close(IoDevice), State}. + +delete(Path, State) -> + {file:delete(Path), State}. + +del_dir(Path, State) -> + {file:del_dir(Path), State}. + +get_cwd(State) -> + {file:get_cwd(), State}. + +is_dir(AbsPath, State) -> + {filelib:is_dir(AbsPath), State}. + +list_dir(AbsPath, State) -> + {file:list_dir(AbsPath), State}. + +make_dir(Dir, State) -> + {file:make_dir(Dir), State}. + +make_symlink(Path2, Path, State) -> + {file:make_symlink(Path2, Path), State}. + +open(Path, Flags, State) -> + {file:open(Path, Flags), State}. + +position(IoDevice, Offs, State) -> + {file:position(IoDevice, Offs), State}. + +read(IoDevice, Len, State) -> + {file:read(IoDevice, Len), State}. + +read_link(Path, State) -> + {file:read_link(Path), State}. + +read_link_info(Path, State) -> + {file:read_link_info(Path), State}. + +read_file_info(Path, State) -> + {file:read_file_info(Path), State}. + +rename(Path, Path2, State) -> + {file:rename(Path, Path2), State}. + +write(IoDevice, Data, State) -> + {file:write(IoDevice, Data), State}. + +write_file_info(Path,Info, State) -> + {file:write_file_info(Path, Info), State}. diff --git a/lib/ssh/src/ssh_sftpd_file_api.erl b/lib/ssh/src/ssh_sftpd_file_api.erl new file mode 100644 index 0000000000..8decfb38d9 --- /dev/null +++ b/lib/ssh/src/ssh_sftpd_file_api.erl @@ -0,0 +1,47 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2007-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_sftpd_file_api). + +-export([behaviour_info/1]). + +behaviour_info(callbacks) -> + [ + {close, 2}, + {delete, 2}, + {del_dir, 2}, + {get_cwd, 1}, + {is_dir, 2}, + {list_dir, 2}, + {make_dir, 2}, + {make_symlink, 3}, + {open, 3}, + {position, 3}, + {read, 3}, + {read_file_info, 2}, + {read_link, 2}, + {read_link_info, 2}, + {rename, 3}, + {write, 3}, + {write_file_info, 3} + ]; +behaviour_info(_) -> + undefined. diff --git a/lib/ssh/src/ssh_shell.erl b/lib/ssh/src/ssh_shell.erl new file mode 100644 index 0000000000..f81b949119 --- /dev/null +++ b/lib/ssh/src/ssh_shell.erl @@ -0,0 +1,178 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_shell). + +-include("ssh_connect.hrl"). + +-behaviour(ssh_channel). + +%% ssh_channel callbacks +-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]). + +%% Spawn export +-export([input_loop/2]). + +-record(state, + { + io, %% Io process + channel, %% Id of the ssh channel + cm %% Ssh connection manager + } + ). + +%%==================================================================== +%% ssh_channel callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} +%% +%% Description: Initiates the CLI +%%-------------------------------------------------------------------- +init([ConnectionManager, ChannelId] = Args) -> + + %% Make sure that we are proclib compatible as + %% this client should be runnable from the + %% erlang shell. + case get('$initial_call') of + undefined -> + Me = get_my_name(), + Ancestors = get_ancestors(), + put('$ancestors', [Me | Ancestors]), + put('$initial_call', {?MODULE, init, Args}); + _ -> + ok + end, + + case ssh_connection:shell(ConnectionManager, ChannelId) of + ok -> + {group_leader, GIO} = + process_info(self(), group_leader), + IoPid = spawn_link(?MODULE, input_loop, + [GIO, self()]), + {ok, #state{io = IoPid, + channel = ChannelId, + cm = ConnectionManager}}; + Error -> + {stop, Error} + end. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles channel messages received on the ssh-connection. +%%-------------------------------------------------------------------- +handle_ssh_msg({ssh_cm, _, {data, _ChannelId, 0, Data}}, State) -> + %% TODO: When unicode support is ready + %% should we call this function or perhaps a new + %% function. + io:put_chars(Data), + {ok, State}; + +handle_ssh_msg({ssh_cm, _, + {data, _ChannelId, ?SSH_EXTENDED_DATA_STDERR, Data}}, + State) -> + %% TODO: When unicode support is ready + %% should we call this function or perhaps a new + %% function. + io:put_chars(Data), + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {eof, _ChannelId}}, State) -> + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, Error, _}}, State) -> + io:put_chars("Connection closed by peer"), + %% TODO: When unicode support is ready + %% should we call this function or perhaps a new + %% function. The error is encoded as UTF-8! + io:put_chars(Error), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, 0}}, State) -> + io:put_chars("logout"), + io:put_chars("Connection closed"), + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, Status}}, State) -> + io:put_chars("Connection closed by peer"), + io:put_chars("Status: " ++ integer_to_list(Status)), + {stop, ChannelId, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_ssh_msg(Args) -> {ok, State} | {stop, ChannelId, State} +%% +%% Description: Handles other channel messages +%%-------------------------------------------------------------------- +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, + #state{channel = ChannelId, + cm = ConnectionManager} = State) -> + {ok, State}; + +handle_msg({input, IoPid, eof}, #state{io = IoPid, channel = ChannelId, + cm = ConnectionManager} = State) -> + ssh_connection:send_eof(ConnectionManager, ChannelId), + {ok, State}; + +handle_msg({input, IoPid, Line}, #state{io = IoPid, + channel = ChannelId, + cm = ConnectionManager} = State) -> + ssh_connection:send(ConnectionManager, ChannelId, Line), + {ok, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reasons, State) -> _ +%% +%% Description: Cleanup when shell channel is terminated +%%-------------------------------------------------------------------- +terminate(_Reason, #state{io = IoPid}) -> + exit(IoPid, kill). + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +input_loop(Fd, Pid) -> + case io:get_line(Fd, '>') of + eof -> + Pid ! {input, self(), eof}, + ok; + Line -> + Pid ! {input, self(), Line}, + input_loop (Fd, Pid) + end. + +get_my_name() -> + case process_info(self(),registered_name) of + {registered_name,Name} -> Name; + _ -> self() + end. + +get_ancestors() -> + case get('$ancestors') of + A when is_list(A) -> A; + _ -> [] + end. diff --git a/lib/ssh/src/ssh_ssh.erl b/lib/ssh/src/ssh_ssh.erl new file mode 100644 index 0000000000..6be8bf7a5a --- /dev/null +++ b/lib/ssh/src/ssh_ssh.erl @@ -0,0 +1,65 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: THIS MODULE IS DEPRECATD AND SHOULD BE REMOVED IN R14 + +-module(ssh_ssh). + +-export([connect/1, connect/2, connect/3]). +-deprecated({connect, 1, next_major_release}). +-deprecated({connect, 2, next_major_release}). +-deprecated({connect, 3, next_major_release}). + +-include("ssh.hrl"). +-include("ssh_connect.hrl"). + +-define(default_timeout, 10000). + +%%% Backwards compatibility +connect(A) -> + connect(A, []). + +connect(Host, Opts) when is_list(Host) -> + connect(Host, 22, Opts); +connect(CM, Opts) -> + Timeout = proplists:get_value(connect_timeout, Opts, ?default_timeout), + session(CM, Timeout). + +connect(Host, Port, Opts) -> + case ssh:connect(Host, Port, Opts) of + {ok, CM} -> + session(CM, proplists:get_value(connect_timeout, + Opts, ?default_timeout)); + Error -> + Error + end. + +session(CM, Timeout) -> + case ssh_connection:session_channel(CM, Timeout) of + {ok, ChannelId} -> + Args = [{channel_cb, ssh_shell}, + {init_args,[CM, ChannelId]}, + {cm, CM}, {channel_id, ChannelId}], + {ok, State} = ssh_channel:init([Args]), + ssh_channel:enter_loop(State); + Error -> + Error + end. diff --git a/lib/ssh/src/ssh_sshd.erl b/lib/ssh/src/ssh_sshd.erl new file mode 100644 index 0000000000..4bc0469061 --- /dev/null +++ b/lib/ssh/src/ssh_sshd.erl @@ -0,0 +1,48 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Description: This module uses the erlang shell and +%% ssh_cli to make an erlang sshd + +-module(ssh_sshd). + +%% API +-export([listen/0, listen/1, listen/2, listen/3, stop/1]). + +-deprecated({listen, 0, next_major_release}). +-deprecated({listen, 1, next_major_release}). +-deprecated({listen, 2, next_major_release}). +-deprecated({listen, 3, next_major_release}). +-deprecated({stop, 1, next_major_release}). + +listen() -> + listen(22). + +listen(Port) -> + listen(Port, []). + +listen(Port, Opts) -> + listen(any, Port, Opts). + +listen(Addr, Port, Opts) -> + ssh:daemon(Addr, Port, Opts). + +stop(Pid) -> + ssh:stop_daemon(Pid). diff --git a/lib/ssh/src/ssh_subsystem_sup.erl b/lib/ssh/src/ssh_subsystem_sup.erl new file mode 100644 index 0000000000..17d47a91d5 --- /dev/null +++ b/lib/ssh/src/ssh_subsystem_sup.erl @@ -0,0 +1,109 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% +%%---------------------------------------------------------------------- +%% Purpose: The ssh subsystem supervisor +%%---------------------------------------------------------------------- + +-module(ssh_subsystem_sup). + +-behaviour(supervisor). + +-export([start_link/1, connection_supervisor/1, channel_supervisor/1 + ]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(Opts) -> + supervisor:start_link(?MODULE, [Opts]). + +connection_supervisor(SupPid) -> + Children = supervisor:which_children(SupPid), + ssh_connection_sup(Children). + +channel_supervisor(SupPid) -> + Children = supervisor:which_children(SupPid), + ssh_channel_sup(Children). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([Opts]) -> + RestartStrategy = one_for_all, + MaxR = 0, + MaxT = 3600, + Children = child_specs(Opts), + {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +child_specs(Opts) -> + case proplists:get_value(role, Opts) of + client -> + [ssh_connectinon_child_spec(Opts)]; + server -> + [ssh_connectinon_child_spec(Opts), ssh_channel_child_spec(Opts)] + end. + +ssh_connectinon_child_spec(Opts) -> + Address = proplists:get_value(address, Opts), + Port = proplists:get_value(port, Opts), + Role = proplists:get_value(role, Opts), + Name = id(Role, ssh_connection_controler, Address, Port), + StartFunc = {ssh_connection_controler, start_link, [Opts]}, + Restart = transient, +% Restart = permanent, + Shutdown = 5000, + Modules = [ssh_connection_controler], + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +ssh_channel_child_spec(Opts) -> + Address = proplists:get_value(address, Opts), + Port = proplists:get_value(port, Opts), + Role = proplists:get_value(role, Opts), + Name = id(Role, ssh_channel_sup, Address, Port), + StartFunc = {ssh_channel_sup, start_link, [Opts]}, + Restart = transient, +% Restart = permanent, + Shutdown = infinity, + Modules = [ssh_channel_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +id(Role, Sup, Address, Port) -> + {Role, Sup, Address, Port}. + +ssh_connection_sup([{_, Child, _, [ssh_connection_controler]} | _]) -> + Child; +ssh_connection_sup([_ | Rest]) -> + ssh_connection_sup(Rest). + +ssh_channel_sup([{_, Child, _, [ssh_channel_sup]} | _]) -> + Child; +ssh_channel_sup([_ | Rest]) -> + ssh_channel_sup(Rest). + + + diff --git a/lib/ssh/src/ssh_sup.erl b/lib/ssh/src/ssh_sup.erl new file mode 100644 index 0000000000..4c46b1586b --- /dev/null +++ b/lib/ssh/src/ssh_sup.erl @@ -0,0 +1,101 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The top supervisor for the ssh application. +%%---------------------------------------------------------------------- +-module(ssh_sup). + +-behaviour(supervisor). + +-export([init/1]). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([]) -> + SupFlags = {one_for_one, 10, 3600}, + Children = children(), + {ok, {SupFlags, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +get_services() -> + case (catch application:get_env(ssh, services)) of + {ok, Services} -> + Services; + _ -> + [] + end. + +children() -> + Services = get_services(), + Clients = [Service || Service <- Services, is_client(Service)], + Servers = [Service || Service <- Services, is_server(Service)], + + [server_child_spec(Servers), client_child_spec(Clients), + ssh_userauth_reg_spec()]. + +server_child_spec(Servers) -> + Name = sshd_sup, + StartFunc = {sshd_sup, start_link, [Servers]}, + Restart = permanent, + Shutdown = infinity, + Modules = [sshd_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +client_child_spec(Clients) -> + Name = sshc_sup, + StartFunc = {sshc_sup, start_link, [Clients]}, + Restart = permanent, + Shutdown = infinity, + Modules = [sshc_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +ssh_userauth_reg_spec() -> + Name = ssh_userreg, + StartFunc = {ssh_userreg, start_link, []}, + Restart = transient, + Shutdown = 5000, + Modules = [ssh_userreg], + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + + +is_server({sftpd, _}) -> + true; +is_server({shelld, _}) -> + true; +is_server(_) -> + false. + +is_client({sftpc, _}) -> + true; +is_client({shellc, _}) -> + true; +is_client(_) -> + false. + + + + diff --git a/lib/ssh/src/ssh_system_sup.erl b/lib/ssh/src/ssh_system_sup.erl new file mode 100644 index 0000000000..477f60f993 --- /dev/null +++ b/lib/ssh/src/ssh_system_sup.erl @@ -0,0 +1,160 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The ssh server instance supervisor, an instans of this supervisor +%% exists for every ip-address and port combination, hangs under +%% sshd_sup. +%%---------------------------------------------------------------------- + +-module(ssh_system_sup). + +-behaviour(supervisor). + +-export([start_link/1, stop_listener/1, + stop_listener/2, stop_system/1, + stop_system/2, system_supervisor/2, + subsystem_supervisor/1, channel_supervisor/1, + connection_supervisor/1, + acceptor_supervisor/1, start_subsystem/2, restart_subsystem/2, restart_acceptor/2]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(ServerOpts) -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + Name = make_name(Address, Port), + supervisor:start_link({local, Name}, ?MODULE, [ServerOpts]). + +stop_listener(SysSup) -> + stop_acceptor(SysSup). + +stop_listener(Address, Port) -> + Name = make_name(Address, Port), + stop_acceptor(whereis(Name)). + +stop_system(SysSup) -> + Name = sshd_sup:system_name(SysSup), + sshd_sup:stop_child(Name). + +stop_system(Address, Port) -> + sshd_sup:stop_child(Address, Port). + +system_supervisor(Address, Port) -> + Name = make_name(Address, Port), + whereis(Name). + +subsystem_supervisor(SystemSup) -> + ssh_subsystem_sup(supervisor:which_children(SystemSup)). + +channel_supervisor(SystemSup) -> + SubSysSup = ssh_subsystem_sup(supervisor:which_children(SystemSup)), + ssh_subsystem_sup:channel_supervisor(SubSysSup). + +connection_supervisor(SystemSup) -> + SubSysSup = ssh_subsystem_sup(supervisor:which_children(SystemSup)), + ssh_subsystem_sup:connection_supervisor(SubSysSup). + +acceptor_supervisor(SystemSup) -> + ssh_acceptor_sup(supervisor:which_children(SystemSup)). + +start_subsystem(SystemSup, Options) -> + Spec = ssh_subsystem_child_spec(Options), + supervisor:start_child(SystemSup, Spec). + +restart_subsystem(Address, Port) -> + SysSupName = make_name(Address, Port), + SubSysName = id(ssh_subsystem_sup, Address, Port), + case supervisor:terminate_child(SysSupName, SubSysName) of + ok -> + supervisor:restart_child(SysSupName, SubSysName); + Error -> + Error + end. + +restart_acceptor(Address, Port) -> + SysSupName = make_name(Address, Port), + AcceptorName = id(ssh_acceptor_sup, Address, Port), + supervisor:restart_child(SysSupName, AcceptorName). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([ServerOpts]) -> + RestartStrategy = one_for_one, + MaxR = 10, + MaxT = 3600, + Children = child_specs(ServerOpts), + {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +child_specs(ServerOpts) -> + [ssh_acceptor_child_spec(ServerOpts)]. + +ssh_acceptor_child_spec(ServerOpts) -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + Name = id(ssh_acceptor_sup, Address, Port), + StartFunc = {ssh_acceptor_sup, start_link, [ServerOpts]}, + Restart = permanent, + Shutdown = infinity, + Modules = [ssh_acceptor_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +ssh_subsystem_child_spec(ServerOpts) -> + Name = make_ref(), + StartFunc = {ssh_subsystem_sup, start_link, [ServerOpts]}, + Restart = temporary, + Shutdown = infinity, + Modules = [ssh_subsystem_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + + +id(Sup, Address, Port) -> + {Sup, Address, Port}. + +make_name(Address, Port) -> + list_to_atom(lists:flatten(io_lib:format("ssh_system_~p_~p_sup", + [Address, Port]))). + +ssh_subsystem_sup([{_, Child, _, [ssh_subsystem_sup]} | _]) -> + Child; +ssh_subsystem_sup([_ | Rest]) -> + ssh_subsystem_sup(Rest). + +ssh_acceptor_sup([{_, Child, _, [ssh_acceptor_sup]} | _]) -> + Child; +ssh_acceptor_sup([_ | Rest]) -> + ssh_acceptor_sup(Rest). + +stop_acceptor(Sup) -> + [Name] = + [SupName || {SupName, _, _, [ssh_acceptor_sup]} <- + supervisor:which_children(Sup)], + supervisor:terminate_child(Sup, Name). + diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl new file mode 100644 index 0000000000..5617231c60 --- /dev/null +++ b/lib/ssh/src/ssh_transport.erl @@ -0,0 +1,1161 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2004-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SSH transport protocol + +-module(ssh_transport). + +-include("ssh_transport.hrl"). + +-include("ssh.hrl"). +-include_lib("kernel/include/inet.hrl"). + +-export([connect/5, accept/4]). +-export([versions/2, hello_version_msg/1]). +-export([next_seqnum/1, decrypt_first_block/2, decrypt_blocks/3, + is_valid_mac/3, transport_messages/1, kexdh_messages/0, + kex_dh_gex_messages/0, handle_hello_version/1, + key_exchange_init_msg/1, key_init/3, new_keys_message/1, + handle_kexinit_msg/3, handle_kexdh_init/2, + handle_kex_dh_gex_group/2, handle_kex_dh_gex_reply/2, + handle_new_keys/2, handle_kex_dh_gex_request/2, + handle_kexdh_reply/2, + unpack/3, decompress/2, ssh_packet/2, pack/2, msg_data/1]). + +%% debug flagso +-define(DBG_ALG, true). +-define(DBG_KEX, true). +-define(DBG_CRYPTO, false). +-define(DBG_PACKET, false). +-define(DBG_MESSAGE, true). +-define(DBG_BIN_MESSAGE, true). +-define(DBG_MAC, false). +-define(DBG_ZLIB, true). + +versions(client, Options)-> + Vsn = proplists:get_value(vsn, Options, ?DEFAULT_CLIENT_VERSION), + Version = format_version(Vsn), + {Vsn, Version}; +versions(server, Options) -> + Vsn = proplists:get_value(vsn, Options, ?DEFAULT_SERVER_VERSION), + Version = format_version(Vsn), + {Vsn, Version}. + +hello_version_msg(Data) -> + [Data,"\r\n"]. + +next_seqnum(SeqNum) -> + (SeqNum + 1) band 16#ffffffff. + +decrypt_first_block(Bin, #ssh{decrypt_block_size = BlockSize} = Ssh0) -> + <> = Bin, + {Ssh, <> = DecData} = + decrypt(Ssh0, EncBlock), + {Ssh, PacketLen, DecData, EncData}. + +decrypt_blocks(Bin, Length, Ssh0) -> + <> = Bin, + {Ssh, DecData} = decrypt(Ssh0, EncBlocks), + {Ssh, DecData, EncData}. + +is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) -> + true; +is_valid_mac(Mac, Data, #ssh{recv_mac = Algorithm, + recv_mac_key = Key, recv_sequence = SeqNum}) -> + Mac == mac(Algorithm, Key, SeqNum, Data). + +transport_messages(_) -> + [{ssh_msg_disconnect, ?SSH_MSG_DISCONNECT, + [uint32, string, string]}, + + {ssh_msg_ignore, ?SSH_MSG_IGNORE, + [string]}, + + {ssh_msg_unimplemented, ?SSH_MSG_UNIMPLEMENTED, + [uint32]}, + + {ssh_msg_debug, ?SSH_MSG_DEBUG, + [boolean, string, string]}, + + {ssh_msg_service_request, ?SSH_MSG_SERVICE_REQUEST, + [string]}, + + {ssh_msg_service_accept, ?SSH_MSG_SERVICE_ACCEPT, + [string]}, + + {ssh_msg_kexinit, ?SSH_MSG_KEXINIT, + [cookie, + name_list, name_list, + name_list, name_list, + name_list, name_list, + name_list, name_list, + name_list, name_list, + boolean, + uint32]}, + + {ssh_msg_newkeys, ?SSH_MSG_NEWKEYS, + []} + ]. + +kexdh_messages() -> + [{ssh_msg_kexdh_init, ?SSH_MSG_KEXDH_INIT, + [mpint]}, + + {ssh_msg_kexdh_reply, ?SSH_MSG_KEXDH_REPLY, + [binary, mpint, binary]} + ]. + +kex_dh_gex_messages() -> + [{ssh_msg_kex_dh_gex_request, ?SSH_MSG_KEX_DH_GEX_REQUEST, + [uint32, uint32, uint32]}, + + {ssh_msg_kex_dh_gex_request_old, ?SSH_MSG_KEX_DH_GEX_REQUEST_OLD, + [uint32]}, + + {ssh_msg_kex_dh_gex_group, ?SSH_MSG_KEX_DH_GEX_GROUP, + [mpint, mpint]}, + + {ssh_msg_kex_dh_gex_init, ?SSH_MSG_KEX_DH_GEX_INIT, + [mpint]}, + + {ssh_msg_kex_dh_gex_reply, ?SSH_MSG_KEX_DH_GEX_REPLY, + [binary, mpint, binary]} + ]. + +yes_no(Ssh, Prompt) -> + (Ssh#ssh.io_cb):yes_no(Prompt). + +connect(ConnectionSup, Address, Port, SocketOpts, Opts) -> + Timeout = proplists:get_value(connect_timeout, Opts, infinity), + {_, Callback, _} = + proplists:get_value(transport, Opts, {tcp, gen_tcp, tcp_closed}), + case do_connect(Callback, Address, Port, SocketOpts, Timeout) of + {ok, Socket} -> + {ok, Pid} = + ssh_connection_controler:start_handler_child(ConnectionSup, + [client, Socket, + [{address, Address}, + {port, Port} | + Opts]]), + Callback:controlling_process(Socket, Pid), + ssh_connection_handler:send_event(Pid, socket_control), + {ok, Pid}; + {error, Reason} -> + {error, Reason} + end. + +do_connect(Callback, Address, Port, SocketOpts, Timeout) -> + Opts = [{active, false} | SocketOpts], + case Callback:connect(Address, Port, Opts, Timeout) of + {error, nxdomain} -> + Callback:connect(Address, Port, lists:delete(inet6, Opts), Timeout); + {error, eafnosupport} -> + Callback:connect(Address, Port, lists:delete(inet6, Opts), Timeout); + Other -> + Other + end. + +accept(Address, Port, Socket, Options) -> + {_, Callback, _} = + proplists:get_value(transport, Options, {tcp, gen_tcp, tcp_closed}), + ConnectionSup = + ssh_system_sup:connection_supervisor( + ssh_system_sup:system_supervisor(Address, Port)), + {ok, Pid} = + ssh_connection_controler:start_handler_child(ConnectionSup, + [server, Socket, + [{address, Address}, + {port, Port} | Options]]), + Callback:controlling_process(Socket, Pid), + ssh_connection_handler:send_event(Pid, socket_control), + {ok, Pid}. + +format_version({Major,Minor}) -> + "SSH-" ++ integer_to_list(Major) ++ "." ++ + integer_to_list(Minor) ++ "-Erlang". + +handle_hello_version(Version) -> + StrVersion = trim_tail(Version), + case string:tokens(Version, "-") of + [_, "2.0" | _] -> + {{2,0}, StrVersion}; + [_, "1.99" | _] -> + {{2,0}, StrVersion}; + [_, "1.3" | _] -> + {{1,3}, StrVersion}; + [_, "1.5" | _] -> + {{1,5}, StrVersion} + end. + +key_exchange_init_msg(Ssh0) -> + Msg = kex_init(Ssh0), + {SshPacket, Ssh} = ssh_packet(Msg, Ssh0), + {Msg, SshPacket, Ssh}. + +kex_init(#ssh{role = Role, opts = Opts}) -> + Random = ssh_bits:random(16), + Compression = case proplists:get_value(compression, Opts, none) of + zlib -> ["zlib", "none"]; + none -> ["none", "zlib"] + end, + kexinit_messsage(Role, Random, Compression). + +key_init(client, Ssh, Value) -> + Ssh#ssh{c_keyinit = Value}; +key_init(server, Ssh, Value) -> + Ssh#ssh{s_keyinit = Value}. + +kexinit_messsage(client, Random, Compression) -> + #ssh_msg_kexinit{ + cookie = Random, + kex_algorithms = ["diffie-hellman-group1-sha1"], + server_host_key_algorithms = ["ssh-rsa", "ssh-dss"], + encryption_algorithms_client_to_server = ["aes128-cbc","3des-cbc"], + encryption_algorithms_server_to_client = ["aes128-cbc","3des-cbc"], + mac_algorithms_client_to_server = ["hmac-sha1"], + mac_algorithms_server_to_client = ["hmac-sha1"], + compression_algorithms_client_to_server = Compression, + compression_algorithms_server_to_client = Compression, + languages_client_to_server = [], + languages_server_to_client = [] + }; + +kexinit_messsage(server, Random, Compression) -> + #ssh_msg_kexinit{ + cookie = Random, + kex_algorithms = ["diffie-hellman-group1-sha1"], + server_host_key_algorithms = ["ssh-dss"], + encryption_algorithms_client_to_server = ["aes128-cbc","3des-cbc"], + encryption_algorithms_server_to_client = ["aes128-cbc","3des-cbc"], + mac_algorithms_client_to_server = ["hmac-sha1"], + mac_algorithms_server_to_client = ["hmac-sha1"], + compression_algorithms_client_to_server = Compression, + compression_algorithms_server_to_client = Compression, + languages_client_to_server = [], + languages_server_to_client = [] + }. + +new_keys_message(Ssh0) -> + {SshPacket, Ssh} = + ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {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 -> + install_messages(Algoritms#alg.kex), + key_exchange_first_msg(Algoritms#alg.kex, + Ssh0#ssh{algorithms = Algoritms}); + _ -> + %% TODO: Correct code? + throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Selection of key exchange" + " algorithm failed", + language = "en"}) + end; + +handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, + #ssh{role = server} = Ssh) -> + {ok, Algoritms} = select_algorithm(server, CounterPart, Own), + install_messages(Algoritms#alg.kex), + {ok, Ssh#ssh{algorithms = Algoritms}}. + + +%% TODO: diffie-hellman-group14-sha1 should also be supported. +%% Maybe check more things ... +verify_algorithm(#alg{kex = 'diffie-hellman-group1-sha1'}) -> + true; +verify_algorithm(#alg{kex = 'diffie-hellman-group-exchange-sha1'}) -> + true; +verify_algorithm(_) -> + false. + +install_messages('diffie-hellman-group1-sha1') -> + ssh_bits:install_messages(kexdh_messages()); +install_messages('diffie-hellman-group-exchange-sha1') -> + ssh_bits:install_messages(kex_dh_gex_messages()). + +key_exchange_first_msg('diffie-hellman-group1-sha1', Ssh0) -> + {G, P} = dh_group1(), + {Private, Public} = dh_gen_key(G, P, 1024), + %%?dbg(?DBG_KEX, "public: ~p~n", [Public]), + {SshPacket, Ssh1} = ssh_packet(#ssh_msg_kexdh_init{e = Public}, Ssh0), + {ok, SshPacket, + Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}; + +key_exchange_first_msg('diffie-hellman-group-exchange-sha1', Ssh0) -> + Min = ?DEFAULT_DH_GROUP_MIN, + NBits = ?DEFAULT_DH_GROUP_NBITS, + Max = ?DEFAULT_DH_GROUP_MAX, + {SshPacket, Ssh1} = + ssh_packet(#ssh_msg_kex_dh_gex_request{min = Min, + n = NBits, max = Max}, + Ssh0), + {ok, SshPacket, + Ssh1#ssh{keyex_info = {Min, Max, NBits}}}. + + +handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, Ssh0) -> + {G, P} = dh_group1(), + {Private, Public} = dh_gen_key(G, P, 1024), + %%?dbg(?DBG_KEX, "public: ~p~n", [Public]), + K = ssh_math:ipow(E, Private, P), + {Key, K_S} = get_host_key(Ssh0), + H = kex_h(Ssh0, K_S, E, Public, K), + H_SIG = sign_host_key(Ssh0, Key, H), + {SshPacket, Ssh1} = ssh_packet(#ssh_msg_kexdh_reply{public_host_key = K_S, + f = Public, + h_sig = H_SIG + }, Ssh0), + %%?dbg(?DBG_KEX, "shared_secret: ~s ~n", [fmt_binary(K, 16, 4)]), + %%?dbg(?DBG_KEX, "hash: ~s ~n", [fmt_binary(H, 16, 4)]), + {ok, SshPacket, Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}, + shared_secret = K, + exchanged_hash = H, + session_id = sid(Ssh1, H)}}. + +handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) -> + {Private, Public} = dh_gen_key(G,P,1024), + %%?dbg(?DBG_KEX, "public: ~p ~n", [Public]), + {SshPacket, Ssh1} = + ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), + {ok, SshPacket, + Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}. + +handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> + try install_alg(Ssh0) of + #ssh{} = Ssh -> + {ok, Ssh} + catch + error:_Error -> %% TODO: Throw earlier .... + throw(#ssh_msg_disconnect{code = ?SSH_DISCONNECT_PROTOCOL_ERROR, + description = "Install alg failed", + language = "en"}) + end. + + +%% %% Select algorithms +handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = HostKey, f = F, + h_sig = H_SIG}, + #ssh{keyex_key = {{Private, Public}, {_G, P}}} = Ssh0) -> + K = ssh_math:ipow(F, Private, P), + H = kex_h(Ssh0, HostKey, Public, F, K), + %%?dbg(?DBG_KEX, "shared_secret: ~s ~n", [fmt_binary(K, 16, 4)]), + %%?dbg(?DBG_KEX, "hash: ~s ~n", [fmt_binary(H, 16, 4)]), + case verify_host_key(Ssh0, HostKey, H, H_SIG) of + ok -> + {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {ok, SshPacket, Ssh#ssh{shared_secret = K, + exchanged_hash = H, + session_id = sid(Ssh, H)}}; + _Error -> + Disconnect = #ssh_msg_disconnect{ + code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Key exchange failed", + language = "en"}, + throw(Disconnect) + end. + +handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request{min = _Min, + n = _NBits, + max = _Max}, Ssh0) -> + {G,P} = dh_group1(), %% TODO real imp this seems to be a hack?! + {Private, Public} = dh_gen_key(G, P, 1024), + {SshPacket, Ssh} = + ssh_packet(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0), + {ok, SshPacket, + Ssh#ssh{keyex_key = {{Private, Public}, {G, P}}}}. + +handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = HostKey, + f = F, + h_sig = H_SIG}, + #ssh{keyex_key = {{Private, Public}, {G, P}}, + keyex_info = {Min, Max, NBits}} = + Ssh0) -> + K = ssh_math:ipow(F, Private, P), + H = kex_h(Ssh0, HostKey, Min, NBits, Max, P, G, Public, F, K), + %%?dbg(?DBG_KEX, "shared_secret: ~s ~n", [fmt_binary(K, 16, 4)]), + %%?dbg(?DBG_KEX, "hash: ~s ~n", [fmt_binary(H, 16, 4)]), + case verify_host_key(Ssh0, HostKey, H, H_SIG) of + ok -> + {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {ok, SshPacket, Ssh#ssh{shared_secret = K, + exchanged_hash = H, + session_id = sid(Ssh, H)}}; + _Error -> + Disconnect = #ssh_msg_disconnect{ + code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Key exchange failed", + language = "en"}, + throw(Disconnect) + end. + +%% select session 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 = Mod, opts = Opts, algorithms = ALG} = SSH, + Scope = proplists:get_value(key_scope, Opts, system), + case ALG#alg.hkey of + 'ssh-rsa' -> + case Mod:private_host_rsa_key(Scope, Opts) of + {ok,Key=#ssh_key { public={N,E}} } -> + %%?dbg(true, "x~n", []), + {Key, + ssh_bits:encode(["ssh-rsa",E,N],[string,mpint,mpint])}; + Error -> + %%?dbg(true, "y~n", []), + exit(Error) + end; + 'ssh-dss' -> + case Mod:private_host_dsa_key(Scope, Opts) of + {ok,Key=#ssh_key { public={P,Q,G,Y}}} -> + {Key, ssh_bits:encode(["ssh-dss",P,Q,G,Y], + [string,mpint,mpint,mpint,mpint])}; + Error -> + exit(Error) + end; + _ -> + exit({error, bad_key_type}) + end. + +sign_host_key(Ssh, Private, H) -> + ALG = Ssh#ssh.algorithms, + Module = case ALG#alg.hkey of + 'ssh-rsa' -> + ssh_rsa; + 'ssh-dss' -> + ssh_dsa + end, + case catch Module:sign(Private, H) of + {'EXIT', Reason} -> + error_logger:format("SIGN FAILED: ~p\n", [Reason]), + {error, Reason}; + SIG -> + ssh_bits:encode([Module:alg_name() ,SIG],[string,binary]) + end. + +verify_host_key(SSH, K_S, H, H_SIG) -> + ALG = SSH#ssh.algorithms, + case ALG#alg.hkey of + 'ssh-rsa' -> + case ssh_bits:decode(K_S,[string,mpint,mpint]) of + ["ssh-rsa", E, N] -> + ["ssh-rsa",SIG] = ssh_bits:decode(H_SIG,[string,binary]), + Public = #ssh_key { type=rsa, public={N,E} }, + case catch ssh_rsa:verify(Public, H, SIG) of + {'EXIT', Reason} -> + error_logger:format("VERIFY FAILED: ~p\n", [Reason]), + {error, bad_signature}; + ok -> + known_host_key(SSH, Public, "ssh-rsa") + end; + _ -> + {error, bad_format} + end; + 'ssh-dss' -> + case ssh_bits:decode(K_S,[string,mpint,mpint,mpint,mpint]) of + ["ssh-dss",P,Q,G,Y] -> + ["ssh-dss",SIG] = ssh_bits:decode(H_SIG,[string,binary]), + Public = #ssh_key { type=dsa, public={P,Q,G,Y} }, + case catch ssh_dsa:verify(Public, H, SIG) of + {'EXIT', Reason} -> + error_logger:format("VERIFY FAILED: ~p\n", [Reason]), + {error, bad_signature}; + ok -> + known_host_key(SSH, Public, "ssh-dss") + end; + _ -> + {error, bad_host_key_format} + end; + _ -> + {error, bad_host_key_algorithm} + end. + +accepted_host(Ssh, PeerName, Opts) -> + case proplists:get_value(silently_accept_hosts, Opts, false) of + true -> + yes; + false -> + yes_no(Ssh, "New host " ++ PeerName ++ " accept") + end. + +known_host_key(#ssh{opts = Opts, key_cb = Mod, peer = Peer} = Ssh, + Public, Alg) -> + PeerName = peer_name(Peer), + case Mod:lookup_host_key(PeerName, Alg, Opts) of + {ok, Public} -> + ok; + {ok, BadPublic} -> + error_logger:format("known_host_key: Public ~p BadPublic ~p\n", + [Public, BadPublic]), + {error, bad_public_key}; + {error, not_found} -> + case accepted_host(Ssh, PeerName, Opts) of + yes -> + Mod:add_host_key(PeerName, Public, Opts); + no -> + {error, rejected} + end + end. + + +%% Each of the algorithm strings MUST be a comma-separated list of +%% algorithm names (see ''Algorithm Naming'' in [SSH-ARCH]). Each +%% supported (allowed) algorithm MUST be listed in order of preference. +%% +%% The first algorithm in each list MUST be the preferred (guessed) +%% algorithm. Each string MUST contain at least one algorithm name. +select_algorithm(Role, Client, Server) -> + {Encrypt, Decrypt} = select_encrypt_decrypt(Role, Client, Server), + {SendMac, RecvMac} = select_send_recv_mac(Role, Client, Server), + {Compression, Decompression} = + select_compression_decompression(Role, Client, Server), + + C_Lng = select(Client#ssh_msg_kexinit.languages_client_to_server, + Server#ssh_msg_kexinit.languages_client_to_server), + S_Lng = select(Client#ssh_msg_kexinit.languages_server_to_client, + Server#ssh_msg_kexinit.languages_server_to_client), + HKey = select_all(Client#ssh_msg_kexinit.server_host_key_algorithms, + Server#ssh_msg_kexinit.server_host_key_algorithms), + HK = case HKey of + [] -> undefined; + [HK0|_] -> HK0 + end, + %% Fixme verify Kex against HKey list and algorithms + + 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}. + +select_encrypt_decrypt(client, Client, Server) -> + Encrypt = + select(Client#ssh_msg_kexinit.encryption_algorithms_client_to_server, + Server#ssh_msg_kexinit.encryption_algorithms_client_to_server), + Decrypt = + select(Client#ssh_msg_kexinit.encryption_algorithms_server_to_client, + Server#ssh_msg_kexinit.encryption_algorithms_server_to_client), + {Encrypt, Decrypt}; +select_encrypt_decrypt(server, Client, Server) -> + Decrypt = + select(Client#ssh_msg_kexinit.encryption_algorithms_client_to_server, + Server#ssh_msg_kexinit.encryption_algorithms_client_to_server), + Encrypt = + select(Client#ssh_msg_kexinit.encryption_algorithms_server_to_client, + Server#ssh_msg_kexinit.encryption_algorithms_server_to_client), + {Encrypt, Decrypt}. + +select_send_recv_mac(client, Client, Server) -> + SendMac = select(Client#ssh_msg_kexinit.mac_algorithms_client_to_server, + Server#ssh_msg_kexinit.mac_algorithms_client_to_server), + RecvMac = select(Client#ssh_msg_kexinit.mac_algorithms_server_to_client, + Server#ssh_msg_kexinit.mac_algorithms_server_to_client), + {SendMac, RecvMac}; +select_send_recv_mac(server, Client, Server) -> + RecvMac = select(Client#ssh_msg_kexinit.mac_algorithms_client_to_server, + Server#ssh_msg_kexinit.mac_algorithms_client_to_server), + SendMac = select(Client#ssh_msg_kexinit.mac_algorithms_server_to_client, + Server#ssh_msg_kexinit.mac_algorithms_server_to_client), + {SendMac, RecvMac}. + +select_compression_decompression(client, Client, Server) -> + Compression = + select(Client#ssh_msg_kexinit.compression_algorithms_client_to_server, + Server#ssh_msg_kexinit.compression_algorithms_client_to_server), + Decomprssion = + select(Client#ssh_msg_kexinit.compression_algorithms_server_to_client, + Server#ssh_msg_kexinit.compression_algorithms_server_to_client), + {Compression, Decomprssion}; +select_compression_decompression(server, Client, Server) -> + Decomprssion = + select(Client#ssh_msg_kexinit.compression_algorithms_client_to_server, + Server#ssh_msg_kexinit.compression_algorithms_client_to_server), + Compression = + select(Client#ssh_msg_kexinit.compression_algorithms_server_to_client, + Server#ssh_msg_kexinit.compression_algorithms_server_to_client), + {Compression, Decomprssion}. + +install_alg(SSH) -> + SSH1 = alg_final(SSH), + SSH2 = alg_setup(SSH1), + alg_init(SSH2). + +alg_setup(SSH) -> + ALG = SSH#ssh.algorithms, + %%?dbg(?DBG_ALG, "ALG: setup ~p ~n", [ALG]), + 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), + 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 + }. + +alg_init(SSH0) -> + %%?dbg(?DBG_ALG, "ALG: init~n", []), + {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) -> + %%?dbg(?DBG_ALG, "ALG: final ~n", []), + {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. + +select_all(CL, SL) -> + A = CL -- SL, %% algortihms only used by client + %% algorithms used by client and server (client pref) + lists:map(fun(ALG) -> list_to_atom(ALG) end, (CL -- A)). + +select([], []) -> + none; +select(CL, SL) -> + C = case select_all(CL,SL) of + [] -> undefined; + [ALG|_] -> ALG + end, + %%?dbg(?DBG_ALG, "ALG: select: ~p ~p = ~p~n", [CL, SL, C]), + C. + +ssh_packet(#ssh_msg_kexinit{} = Msg, Ssh0) -> + BinMsg = ssh_bits:encode(Msg), + Ssh = key_init(Ssh0#ssh.role, Ssh0, BinMsg), + %%?dbg(?DBG_MESSAGE, "SEND_MSG: ~p~n", [Msg]), + pack(BinMsg, Ssh); + +ssh_packet(Msg, Ssh) -> + BinMsg = ssh_bits:encode(Msg), + %%?dbg(?DBG_MESSAGE, "SEND_MSG: ~p~n", [Msg]), + %%?dbg(?DBG_BIN_MESSAGE, "Encoded: ~p~n", [BinMsg]), + pack(BinMsg, Ssh). + +pack(Data0, #ssh{encrypt_block_size = BlockSize, + send_sequence = SeqNum, send_mac = MacAlg, + send_mac_key = MacKey} + = Ssh0) when is_binary(Data0) -> + {Ssh1, Data} = compress(Ssh0, Data0), + PL = (BlockSize - ((4 + 1 + size(Data)) rem BlockSize)) rem BlockSize, + PaddingLen = if PL < 4 -> PL + BlockSize; + true -> PL + end, + Padding = ssh_bits:random(PaddingLen), + PacketLen = 1 + PaddingLen + size(Data), + PacketData = <>, + {Ssh2, EncPacket} = encrypt(Ssh1, PacketData), + MAC = mac(MacAlg, MacKey, SeqNum, PacketData), + Packet = [EncPacket, MAC], + Ssh = Ssh2#ssh{send_sequence = (SeqNum+1) band 16#ffffffff}, + {Packet, Ssh}. + +unpack(EncodedSoFar, ReminingLenght, #ssh{recv_mac_size = MacSize} = Ssh0) -> + SshLength = ReminingLenght - MacSize, + {NoMac, Mac, Rest} = case MacSize of + 0 -> + <> = EncodedSoFar, + {NoMac0, <<>>, Rest0}; + _ -> + <> = EncodedSoFar, + {NoMac0, Mac0, Rest0} + end, + {Ssh1, DecData, <<>>} = + case SshLength of + 0 -> + {Ssh0, <<>>, <<>>}; + _ -> + decrypt_blocks(NoMac, SshLength, Ssh0) + end, + {Ssh1, DecData, Rest, Mac}. + +msg_data(PacketData) -> + <> = PacketData, + DataLen = Len - PaddingLen - 1, + <<_:32, _:8, Data:DataLen/binary, + _:PaddingLen/binary>> = PacketData, + Data. + + +%% Send a disconnect message +%% terminate(S, SSH, Code, Message) -> +%% M = #ssh_msg_disconnect{code=Code, +%% description = Message, +%% language = "en"}, +%% send_msg(S, SSH, M), +%% gen_tcp:close(S), +%% {error, M}. + + +%% public key algorithms +%% +%% ssh-dss REQUIRED sign Raw DSS Key +%% ssh-rsa RECOMMENDED sign Raw RSA Key +%% x509v3-sign-rsa OPTIONAL sign X.509 certificates (RSA key) +%% x509v3-sign-dss OPTIONAL sign X.509 certificates (DSS key) +%% spki-sign-rsa OPTIONAL sign SPKI certificates (RSA key) +%% spki-sign-dss OPTIONAL sign SPKI certificates (DSS key) +%% pgp-sign-rsa OPTIONAL sign OpenPGP certificates (RSA key) +%% pgp-sign-dss OPTIONAL sign OpenPGP certificates (DSS key) +%% + +%% key exchange +%% +%% diffie-hellman-group1-sha1 REQUIRED +%% +%% + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Encryption +%% +%% chiphers +%% +%% 3des-cbc REQUIRED +%% three-key 3DES in CBC mode +%% blowfish-cbc OPTIONAL Blowfish in CBC mode +%% twofish256-cbc OPTIONAL Twofish in CBC mode, +%% with 256-bit key +%% twofish-cbc OPTIONAL alias for "twofish256-cbc" (this +%% is being retained for +%% historical reasons) +%% twofish192-cbc OPTIONAL Twofish with 192-bit key +%% twofish128-cbc OPTIONAL Twofish with 128-bit key +%% aes256-cbc OPTIONAL AES in CBC mode, +%% with 256-bit key +%% aes192-cbc OPTIONAL AES with 192-bit key +%% aes128-cbc RECOMMENDED AES with 128-bit key +%% serpent256-cbc OPTIONAL Serpent in CBC mode, with +%% 256-bit key +%% serpent192-cbc OPTIONAL Serpent with 192-bit key +%% serpent128-cbc OPTIONAL Serpent with 128-bit key +%% arcfour OPTIONAL the ARCFOUR stream cipher +%% idea-cbc OPTIONAL IDEA in CBC mode +%% cast128-cbc OPTIONAL CAST-128 in CBC mode +%% none OPTIONAL no encryption; NOT RECOMMENDED +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +encrypt_init(#ssh{encrypt = none} = Ssh) -> + {ok, Ssh}; +encrypt_init(#ssh{encrypt = '3des-cbc', role = client} = Ssh) -> + IV = hash(Ssh, "A", 64), + <> = hash(Ssh, "C", 192), + {ok, Ssh#ssh{encrypt_keys = {K1,K2,K3}, + encrypt_block_size = 8, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = '3des-cbc', role = server} = Ssh) -> + IV = hash(Ssh, "B", 64), + <> = hash(Ssh, "D", 192), + {ok, Ssh#ssh{encrypt_keys = {K1,K2,K3}, + encrypt_block_size = 8, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = 'aes128-cbc', role = client} = Ssh) -> + IV = hash(Ssh, "A", 128), + <> = hash(Ssh, "C", 128), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}; +encrypt_init(#ssh{encrypt = 'aes128-cbc', role = server} = Ssh) -> + IV = hash(Ssh, "B", 128), + <> = hash(Ssh, "D", 128), + {ok, Ssh#ssh{encrypt_keys = K, + encrypt_block_size = 16, + encrypt_ctx = IV}}. + +encrypt_final(Ssh) -> + {ok, Ssh#ssh{encrypt = none, + encrypt_keys = undefined, + encrypt_block_size = 8, + encrypt_ctx = undefined + }}. + +encrypt(#ssh{encrypt = none} = Ssh, Data) -> + {Ssh, Data}; +encrypt(#ssh{encrypt = '3des-cbc', + encrypt_keys = {K1,K2,K3}, + encrypt_ctx = IV0} = Ssh, Data) -> + %%?dbg(?DBG_CRYPTO, "encrypt: IV=~p K1=~p, K2=~p, K3=~p ~n", + %% [IV0,K1,K2,K3]), + Enc = crypto:des3_cbc_encrypt(K1,K2,K3,IV0,Data), + %%?dbg(?DBG_CRYPTO, "encrypt: ~p -> ~p ~n", [Data, Enc]), + IV = crypto:des_cbc_ivec(Enc), + {Ssh#ssh{encrypt_ctx = IV}, Enc}; +encrypt(#ssh{encrypt = 'aes128-cbc', + encrypt_keys = K, + encrypt_ctx = IV0} = Ssh, Data) -> + %%?dbg(?DBG_CRYPTO, "encrypt: IV=~p K=~p ~n", + %% [IV0,K]), + Enc = crypto:aes_cbc_128_encrypt(K,IV0,Data), + %%?dbg(?DBG_CRYPTO, "encrypt: ~p -> ~p ~n", [Data, Enc]), + IV = crypto:aes_cbc_ivec(Enc), + {Ssh#ssh{encrypt_ctx = IV}, Enc}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Decryption +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +decrypt_init(#ssh{decrypt = none} = Ssh) -> + {ok, Ssh}; +decrypt_init(#ssh{decrypt = '3des-cbc', role = client} = Ssh) -> + {IV, KD} = {hash(Ssh, "B", 64), + hash(Ssh, "D", 192)}, + <> = KD, + {ok, Ssh#ssh{decrypt_keys = {K1,K2,K3}, decrypt_ctx = IV, + decrypt_block_size = 8}}; +decrypt_init(#ssh{decrypt = '3des-cbc', role = server} = Ssh) -> + {IV, KD} = {hash(Ssh, "A", 64), + hash(Ssh, "C", 192)}, + <> = KD, + {ok, Ssh#ssh{decrypt_keys = {K1, K2, K3}, decrypt_ctx = IV, + decrypt_block_size = 8}}; +decrypt_init(#ssh{decrypt = 'aes128-cbc', role = client} = Ssh) -> + {IV, KD} = {hash(Ssh, "B", 128), + hash(Ssh, "D", 128)}, + <> = KD, + {ok, Ssh#ssh{decrypt_keys = K, decrypt_ctx = IV, + decrypt_block_size = 16}}; +decrypt_init(#ssh{decrypt = 'aes128-cbc', role = server} = Ssh) -> + {IV, KD} = {hash(Ssh, "A", 128), + hash(Ssh, "C", 128)}, + <> = KD, + {ok, Ssh#ssh{decrypt_keys = K, decrypt_ctx = IV, + decrypt_block_size = 16}}. + + +decrypt_final(Ssh) -> + {ok, Ssh#ssh {decrypt = none, + decrypt_keys = undefined, + decrypt_ctx = undefined, + decrypt_block_size = 8}}. + +decrypt(#ssh{decrypt = none} = Ssh, Data) -> + {Ssh, Data}; +decrypt(#ssh{decrypt = '3des-cbc', decrypt_keys = Keys, + decrypt_ctx = IV0} = Ssh, Data) -> + {K1, K2, K3} = Keys, + %%?dbg(?DBG_CRYPTO, "decrypt: IV=~p K1=~p, K2=~p, K3=~p ~n", + %%[IV0,K1,K2,K3]), + Dec = crypto:des3_cbc_decrypt(K1,K2,K3,IV0,Data), + %%?dbg(?DBG_CRYPTO, "decrypt: ~p -> ~p ~n", [Data, Dec]), + IV = crypto:des_cbc_ivec(Data), + {Ssh#ssh{decrypt_ctx = IV}, Dec}; +decrypt(#ssh{decrypt = 'aes128-cbc', decrypt_keys = Key, + decrypt_ctx = IV0} = Ssh, Data) -> + %%?dbg(?DBG_CRYPTO, "decrypt: IV=~p Key=~p ~n", + %% [IV0,Key]), + Dec = crypto:aes_cbc_128_decrypt(Key,IV0,Data), + %%?dbg(?DBG_CRYPTO, "decrypt: ~p -> ~p ~n", [Data, Dec]), + IV = crypto:aes_cbc_ivec(Data), + {Ssh#ssh{decrypt_ctx = IV}, Dec}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Compression +%% +%% none REQUIRED no compression +%% zlib OPTIONAL ZLIB (LZ77) compression +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +compress_init(SSH) -> + compress_init(SSH, 1). + +compress_init(#ssh{compress = none} = Ssh, _) -> + {ok, Ssh}; +compress_init(#ssh{compress = zlib} = Ssh, Level) -> + Zlib = zlib:open(), + ok = zlib:deflateInit(Zlib, Level), + {ok, Ssh#ssh{compress_ctx = Zlib}}. + + +compress_final(#ssh{compress = none} = Ssh) -> + {ok, Ssh}; +compress_final(#ssh{compress = zlib, compress_ctx = Context} = Ssh) -> + zlib:close(Context), + {ok, Ssh#ssh{compress = none, compress_ctx = undefined}}. + +compress(#ssh{compress = none} = Ssh, Data) -> + {Ssh, Data}; +compress(#ssh{compress = zlib, compress_ctx = Context} = Ssh, Data) -> + Compressed = zlib:deflate(Context, Data, sync), + %%?dbg(?DBG_ZLIB, "deflate: ~p -> ~p ~n", [Data, Compressed]), + {Ssh, list_to_binary(Compressed)}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Decompression +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +decompress_init(#ssh{decompress = none} = Ssh) -> + {ok, Ssh}; +decompress_init(#ssh{decompress = zlib} = Ssh) -> + Zlib = zlib:open(), + ok = zlib:inflateInit(Zlib), + {ok, Ssh#ssh{decompress_ctx = Zlib}}. + +decompress_final(#ssh{decompress = none} = Ssh) -> + {ok, Ssh}; +decompress_final(#ssh{decompress = zlib, decompress_ctx = Context} = Ssh) -> + zlib:close(Context), + {ok, Ssh#ssh{decompress = none, decompress_ctx = undefined}}. + +decompress(#ssh{decompress = none} = Ssh, Data) -> + {Ssh, Data}; +decompress(#ssh{decompress = zlib, decompress_ctx = Context} = Ssh, Data) -> + Decompressed = zlib:inflate(Context, Data), + %%?dbg(?DBG_ZLIB, "inflate: ~p -> ~p ~n", [Data, Decompressed]), + {Ssh, list_to_binary(Decompressed)}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% MAC calculation +%% +%% hmac-sha1 REQUIRED HMAC-SHA1 (digest length = key +%% length = 20) +%% hmac-sha1-96 RECOMMENDED first 96 bits of HMAC-SHA1 (digest +%% length = 12, key length = 20) +%% hmac-md5 OPTIONAL HMAC-MD5 (digest length = key +%% length = 16) +%% hmac-md5-96 OPTIONAL first 96 bits of HMAC-MD5 (digest +%% length = 12, key length = 16) +%% none OPTIONAL no MAC; NOT RECOMMENDED +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +send_mac_init(SSH) -> + case SSH#ssh.role of + client -> + KeySize =mac_key_size(SSH#ssh.send_mac), + Key = hash(SSH, "E", KeySize), + {ok, SSH#ssh { send_mac_key = Key }}; + server -> + KeySize = mac_key_size(SSH#ssh.send_mac), + Key = hash(SSH, "F", KeySize), + {ok, SSH#ssh { send_mac_key = Key }} + end. + +send_mac_final(SSH) -> + {ok, SSH#ssh { send_mac = none, send_mac_key = undefined }}. + +recv_mac_init(SSH) -> + case SSH#ssh.role of + client -> + Key = hash(SSH, "F", mac_key_size(SSH#ssh.recv_mac)), + {ok, SSH#ssh { recv_mac_key = Key }}; + server -> + Key = hash(SSH, "E", mac_key_size(SSH#ssh.recv_mac)), + {ok, SSH#ssh { recv_mac_key = Key }} + end. + +recv_mac_final(SSH) -> + {ok, SSH#ssh { recv_mac = none, recv_mac_key = undefined }}. + +mac(none, _ , _, _) -> + <<>>; +mac('hmac-sha1', Key, SeqNum, Data) -> + crypto:sha_mac(Key, [<>, Data]); +mac('hmac-sha1-96', Key, SeqNum, Data) -> + crypto:sha_mac_96(Key, [<>, Data]); +mac('hmac-md5', Key, SeqNum, Data) -> + crypto:md5_mac(Key, [<>, Data]); +mac('hmac-md5-96', Key, SeqNum, Data) -> + crypto:md5_mac_96(Key, [<>, Data]). + +%% return N hash bytes (HASH) +hash(SSH, Char, Bits) -> + HASH = + case SSH#ssh.kex of + 'diffie-hellman-group1-sha1' -> + fun(Data) -> crypto:sha(Data) end; + 'diffie-hellman-group-exchange-sha1' -> + fun(Data) -> crypto:sha(Data) end; + _ -> + exit({bad_algorithm,SSH#ssh.kex}) + end, + hash(SSH, Char, Bits, HASH). + +hash(_SSH, _Char, 0, _HASH) -> + <<>>; +hash(SSH, Char, N, HASH) -> + K = ssh_bits:mpint(SSH#ssh.shared_secret), + H = SSH#ssh.exchanged_hash, + SessionID = SSH#ssh.session_id, + K1 = HASH([K, H, Char, SessionID]), + Sz = N div 8, + <> = hash(K, H, K1, N-128, HASH), + %%?dbg(?DBG_KEX, "Key ~s: ~s ~n", [Char, fmt_binary(Key, 16, 4)]), + Key. + +hash(_K, _H, Ki, N, _HASH) when N =< 0 -> + Ki; +hash(K, H, Ki, N, HASH) -> + Kj = HASH([K, H, Ki]), + hash(K, H, <>, N-128, HASH). + +kex_h(SSH, K_S, E, F, K) -> + L = ssh_bits:encode([SSH#ssh.c_version, SSH#ssh.s_version, + SSH#ssh.c_keyinit, SSH#ssh.s_keyinit, + K_S, E,F,K], + [string,string,binary,binary,binary, + mpint,mpint,mpint]), + crypto:sha(L). + +kex_h(SSH, K_S, Min, NBits, Max, Prime, Gen, E, F, K) -> + L = if Min==-1; Max==-1 -> + Ts = [string,string,binary,binary,binary, + uint32, + mpint,mpint,mpint,mpint,mpint], + ssh_bits:encode([SSH#ssh.c_version,SSH#ssh.s_version, + SSH#ssh.c_keyinit,SSH#ssh.s_keyinit, + K_S, NBits, Prime, Gen, E,F,K], + Ts); + true -> + Ts = [string,string,binary,binary,binary, + uint32,uint32,uint32, + mpint,mpint,mpint,mpint,mpint], + ssh_bits:encode([SSH#ssh.c_version,SSH#ssh.s_version, + SSH#ssh.c_keyinit,SSH#ssh.s_keyinit, + K_S, Min, NBits, Max, + Prime, Gen, E,F,K], Ts) + end, + crypto:sha(L). + +mac_key_size('hmac-sha1') -> 20*8; +mac_key_size('hmac-sha1-96') -> 20*8; +mac_key_size('hmac-md5') -> 16*8; +mac_key_size('hmac-md5-96') -> 16*8; +mac_key_size(none) -> 0. + +mac_digest_size('hmac-sha1') -> 20; +mac_digest_size('hmac-sha1-96') -> 12; +mac_digest_size('hmac-md5') -> 20; +mac_digest_size('hmac-md5-96') -> 12; +mac_digest_size(none) -> 0. + +peer_name({Host, _}) -> + Host. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Diffie-Hellman utils +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +dh_group1() -> + {2, 16#FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF}. + +dh_gen_key(G, P, _Bits) -> + Private = ssh_bits:irandom(ssh_bits:isize(P)-1, 1, 1), + Public = ssh_math:ipow(G, Private, P), + {Private,Public}. + +%% trim(Str) -> +%% lists:reverse(trim_head(lists:reverse(trim_head(Str)))). + +trim_tail(Str) -> + lists:reverse(trim_head(lists:reverse(Str))). + +trim_head([$\s|Cs]) -> trim_head(Cs); +trim_head([$\t|Cs]) -> trim_head(Cs); +trim_head([$\n|Cs]) -> trim_head(Cs); +trim_head([$\r|Cs]) -> trim_head(Cs); +trim_head(Cs) -> Cs. + +%% Retrieve session_id from ssh, needed by public-key auth +%get_session_id(SSH) -> +% {ok, SessionID} = call(SSH, get_session_id), + +%% DEBUG utils +%% Format integers and binaries as hex blocks +%% +%% -ifdef(debug). +%% fmt_binary(B, BlockSize, GroupSize) -> +%% fmt_block(fmt_bin(B), BlockSize, GroupSize). + +%% fmt_block(Bin, BlockSize, GroupSize) -> +%% fmt_block(Bin, BlockSize, 0, GroupSize). + + +%% fmt_block(Bin, 0, _I, _G) -> +%% binary_to_list(Bin); +%% fmt_block(Bin, Sz, G, G) when G =/= 0 -> +%% ["~n#" | fmt_block(Bin, Sz, 0, G)]; +%% fmt_block(Bin, Sz, I, G) -> +%% case Bin of +%% <> -> +%% if Tail == <<>> -> +%% [binary_to_list(Block)]; +%% true -> +%% [binary_to_list(Block), " " | fmt_block(Tail, Sz, I+1, G)] +%% end; +%% <<>> -> +%% []; +%% _ -> +%% [binary_to_list(Bin)] +%% end. + +%% %% Format integer or binary as hex +%% fmt_bin(X) when integer(X) -> +%% list_to_binary(io_lib:format("~p", [X])); +%% fmt_bin(X) when binary(X) -> +%% Sz = size(X)*8, +%% <> = X, +%% %%Fmt = "~"++integer_to_list(size(X)*2)++"~p", +%% list_to_binary(io_lib:format("~p", [Y])). + +%% -endif. + diff --git a/lib/ssh/src/ssh_transport.hrl b/lib/ssh/src/ssh_transport.hrl new file mode 100644 index 0000000000..18a23f0533 --- /dev/null +++ b/lib/ssh/src/ssh_transport.hrl @@ -0,0 +1,235 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: Record and constant defenitions for the SSH-tansport layer +%% protocol see RFC 4253 +%%---------------------------------------------------------------------- + +-ifndef(ssh_transport). +-define(ssh_transport, true). + +-define(DEFAULT_CLIENT_VERSION, {2, 0}). +-define(DEFAULT_SERVER_VERSION, {2, 0}). +-define(DEFAULT_DH_GROUP_MIN, 512). +-define(DEFAULT_DH_GROUP_NBITS, 1024). +-define(DEFAULT_DH_GROUP_MAX, 4096). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% BASIC transport messages +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-define(SSH_MSG_DISCONNECT, 1). +-define(SSH_MSG_IGNORE, 2). +-define(SSH_MSG_UNIMPLEMENTED, 3). +-define(SSH_MSG_DEBUG, 4). +-define(SSH_MSG_SERVICE_REQUEST, 5). +-define(SSH_MSG_SERVICE_ACCEPT, 6). + +-define(SSH_MSG_KEXINIT, 20). +-define(SSH_MSG_NEWKEYS, 21). + + +-record(ssh_msg_disconnect, + { + code, %% uint32 + description, %% string + language %% string + }). + +-record(ssh_msg_ignore, + { + data %% string + }). + +-record(ssh_msg_unimplemented, + { + sequence %% uint32 + }). + +-record(ssh_msg_debug, + { + always_display, %% boolean + message, %% string + language %% string + }). + + +-record(ssh_msg_service_request, + { + name %% string (service name) + }). + +-record(ssh_msg_service_accept, + { + name %% string + }). + +-record(ssh_msg_kexinit, + { + cookie, %% random(16) + kex_algorithms, %% string + server_host_key_algorithms, %% string + encryption_algorithms_client_to_server, %% string + encryption_algorithms_server_to_client, %% string + mac_algorithms_client_to_server, %% string + mac_algorithms_server_to_client, %% string + compression_algorithms_client_to_server, %% string + compression_algorithms_server_to_client, %% string + languages_client_to_server, %% string + languages_server_to_client, %% string + first_kex_packet_follows=false, %% boolean + %% (reserved for future extension) + reserved=0 %% uint32=0 + }). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% KEY DH messages +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% diffie-hellman-group1-sha1 +-define(SSH_MSG_KEXDH_INIT, 30). +-define(SSH_MSG_KEXDH_REPLY, 31). + +-record(ssh_msg_kexdh_init, + { + e %% mpint + }). + +-record(ssh_msg_kexdh_reply, + { + public_host_key, %% string (K_S) + f, %% mpint + h_sig %% string, signature of H + }). + +-record(ssh_msg_newkeys, + {}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% KEY DH GEX messages +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% diffie-hellman-group-exchange-sha1 +-define(SSH_MSG_KEX_DH_GEX_REQUEST_OLD, 30). +-define(SSH_MSG_KEX_DH_GEX_REQUEST, 34). +-define(SSH_MSG_KEX_DH_GEX_GROUP, 31). +-define(SSH_MSG_KEX_DH_GEX_INIT, 32). +-define(SSH_MSG_KEX_DH_GEX_REPLY, 33). + +-record(ssh_msg_kex_dh_gex_request, + { + min, + n, + max + }). + +-record(ssh_msg_kex_dh_gex_request_old, + { + n + }). + +-record(ssh_msg_kex_dh_gex_group, + { + p, %% prime + g %% generator + }). + +-record(ssh_msg_kex_dh_gex_init, + { + e + }). + +-record(ssh_msg_kex_dh_gex_reply, + { + public_host_key, %% string (K_S) + f, + h_sig + }). + +%% error codes +-define(SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT, 1). +-define(SSH_DISCONNECT_PROTOCOL_ERROR, 2). +-define(SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 3). +-define(SSH_DISCONNECT_RESERVED, 4). +-define(SSH_DISCONNECT_MAC_ERROR, 5). +-define(SSH_DISCONNECT_COMPRESSION_ERROR, 6). +-define(SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, 7). +-define(SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 8). +-define(SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 9). +-define(SSH_DISCONNECT_CONNECTION_LOST, 10). +-define(SSH_DISCONNECT_BY_APPLICATION, 11). +-define(SSH_DISCONNECT_TOO_MANY_CONNECTIONS, 12). +-define(SSH_DISCONNECT_AUTH_CANCELLED_BY_USER, 13). +-define(SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, 14). +-define(SSH_DISCONNECT_ILLEGAL_USER_NAME, 15). + + +%%%---------------------------------------------------------------------- +%%% # DH_14_xxx +%%% Description: Oakley group 14 prime numbers and generator. Used in +%%% diffie-hellman-group1-sha1 key exchange method. +%%%---------------------------------------------------------------------- +%%%---------------------------------------------------------------------- +%%% # DH_14_P +%%% Description: Prime for this group +%%%---------------------------------------------------------------------- + +-define(DH_14_P, + <<000,000,000,129,000,255,255,255,255,255,255,255,255,201,015,218, + 162,033,104,194,052,196,198,098,139,128,220,028,209,041,002,078, + 008,138,103,204,116,002,011,190,166,059,019,155,034,081,074,008, + 121,142,052,004,221,239,149,025,179,205,058,067,027,048,043,010, + 109,242,095,020,055,079,225,053,109,109,081,194,069,228,133,181, + 118,098,094,126,198,244,076,066,233,166,055,237,107,011,255,092, + 182,244,006,183,237,238,056,107,251,090,137,159,165,174,159,036, + 017,124,075,031,230,073,040,102,081,236,230,083,129,255,255,255, + 255,255,255,255,255>>). + +%%%---------------------------------------------------------------------- +%%% # DH_14_G +%%% Description: Generator for DH_14_P. +%%%---------------------------------------------------------------------- + +-define(DH_14_G, <<0,0,0,1,2>>). + +%%%---------------------------------------------------------------------- +%%% # DH_14_Q +%%% Description: Group order (DH_14_P - 1) / 2. +%%%---------------------------------------------------------------------- + +-define(DH_14_Q, + <<000,000,000,128,127,255,255,255,255,255,255,255,228,135,237,081, + 016,180,097,026,098,099,049,069,192,110,014,104,148,129,039,004, + 069,051,230,058,001,005,223,083,029,137,205,145,040,165,004,060, + 199,026,002,110,247,202,140,217,230,157,033,141,152,021,133,054, + 249,047,138,027,167,240,154,182,182,168,225,034,242,066,218,187, + 049,047,063,099,122,038,033,116,211,027,246,181,133,255,174,091, + 122,003,091,246,247,028,053,253,173,068,207,210,215,079,146,008, + 190,037,143,243,036,148,051,040,246,115,041,192,255,255,255,255, + 255,255,255,255>>). + +-endif. % -ifdef(ssh_transport). diff --git a/lib/ssh/src/ssh_userauth.hrl b/lib/ssh/src/ssh_userauth.hrl new file mode 100755 index 0000000000..39cc032ca5 --- /dev/null +++ b/lib/ssh/src/ssh_userauth.hrl @@ -0,0 +1,77 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: user authentication protocol + +-define(SSH_MSG_USERAUTH_REQUEST, 50). +-define(SSH_MSG_USERAUTH_FAILURE, 51). +-define(SSH_MSG_USERAUTH_SUCCESS, 52). +-define(SSH_MSG_USERAUTH_BANNER, 53). +-define(SSH_MSG_USERAUTH_PK_OK, 60). +-define(SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, 60). +-define(SSH_MSG_USERAUTH_INFO_REQUEST, 60). +-define(SSH_MSG_USERAUTH_INFO_RESPONSE, 61). + +-record(ssh_msg_userauth_request, + { + user, %% string + service, %% string + method, %% string "publickey", "password" + data %% opaque + }). + +-record(ssh_msg_userauth_failure, + { + authentications, %% string + partial_success %% boolean + }). + +-record(ssh_msg_userauth_success, + { + }). + +-record(ssh_msg_userauth_banner, + { + message, %% string + language %% string + }). + +-record(ssh_msg_userauth_passwd_changereq, + { + prompt, %% string + languge %% string + }). + +-record(ssh_msg_userauth_pk_ok, + { + algorithm_name, % string + key_blob % string + }). + +-record(ssh_msg_userauth_info_request, + {name, + instruction, + language_tag, + num_prompts, + data}). +-record(ssh_msg_userauth_info_response, + {num_responses, + data}). diff --git a/lib/ssh/src/ssh_userreg.erl b/lib/ssh/src/ssh_userreg.erl new file mode 100644 index 0000000000..06f4076b51 --- /dev/null +++ b/lib/ssh/src/ssh_userreg.erl @@ -0,0 +1,127 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Description: User register for ssh_cli + +-module(ssh_userreg). + +-behaviour(gen_server). + +%% API +-export([start_link/0, register_user/2, lookup_user/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {user_db = []}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +register_user(User, Cm) -> + gen_server:cast(?MODULE, {register, {User, Cm}}). + +lookup_user(Cm) -> + gen_server:call(?MODULE, {get_user, Cm}, infinity). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([]) -> + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call({get_user, Cm}, _From, #state{user_db = Db} = State) -> + User = lookup(Cm, Db), + {reply, {ok, User}, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({register, UserCm}, State0) -> + State = insert(UserCm, State0), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +insert({User, Cm}, #state{user_db = Db} = State) -> + State#state{user_db = [{User, Cm} | Db]}. + +lookup(_, []) -> + undefined; +lookup(Cm, [{User, Cm} | _Rest]) -> + User; +lookup(Cm, [_ | Rest]) -> + lookup(Cm, Rest). + diff --git a/lib/ssh/src/ssh_xfer.erl b/lib/ssh/src/ssh_xfer.erl new file mode 100644 index 0000000000..a347a9c095 --- /dev/null +++ b/lib/ssh/src/ssh_xfer.erl @@ -0,0 +1,925 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SFTP functions + +-module(ssh_xfer). + +-export([attach/2, connect/3]). +-export([open/6, opendir/3, readdir/3, close/3, read/5, write/5, + rename/5, remove/3, mkdir/4, rmdir/3, realpath/3, extended/4, + stat/4, fstat/4, lstat/4, setstat/4, + readlink/3, fsetstat/4, symlink/4, + protocol_version_request/1, + xf_reply/2, + xf_send_reply/3, xf_send_names/3, xf_send_name/4, + xf_send_status/3, xf_send_status/4, xf_send_status/5, + xf_send_handle/3, xf_send_attr/3, xf_send_data/3, + encode_erlang_status/1, + decode_open_flags/2, encode_open_flags/1, + decode_ace_mask/1, decode_ext/1, + decode_ATTR/2, encode_ATTR/2]). + +-include("ssh.hrl"). +-include("ssh_xfer.hrl"). + +-import(lists, [foldl/3, reverse/1]). + +-define(is_set(F, Bits), + ((F) band (Bits)) == (F)). + +-define(XFER_PACKET_SIZE, 32768). +-define(XFER_WINDOW_SIZE, 4*?XFER_PACKET_SIZE). + +attach(CM, Opts) -> + open_xfer(CM, Opts). + +connect(Host, Port, Opts) -> + case ssh:connect(Host, Port, Opts) of + {ok, CM} -> open_xfer(CM, Opts); + Error -> Error + end. + +open_xfer(CM, Opts) -> + TMO = proplists:get_value(timeout, Opts, infinity), + case ssh_connection:session_channel(CM, ?XFER_WINDOW_SIZE, ?XFER_PACKET_SIZE, TMO) of + {ok, ChannelId} -> + {ok, ChannelId, CM}; + Error -> + Error + end. + +protocol_version_request(XF) -> + xf_request(XF, ?SSH_FXP_INIT, <>). + +open(XF, ReqID, FileName, Access, Flags, Attrs) -> + Vsn = XF#ssh_xfer.vsn, + FileName1 = list_to_binary(FileName), + MBits = if Vsn >= 5 -> + M = encode_ace_mask(Access), + ?uint32(M); + true -> + (<<>>) + end, + F = encode_open_flags(Flags), + xf_request(XF,?SSH_FXP_OPEN, + [?uint32(ReqID), + ?binary(FileName1), + MBits, + ?uint32(F), + encode_ATTR(Vsn,Attrs)]). + +opendir(XF, ReqID, DirName) -> + xf_request(XF, ?SSH_FXP_OPENDIR, + [?uint32(ReqID), + ?string(DirName)]). + + +close(XF, ReqID, Handle) -> + xf_request(XF, ?SSH_FXP_CLOSE, + [?uint32(ReqID), + ?binary(Handle)]). + +read(XF, ReqID, Handle, Offset, Length) -> + xf_request(XF, ?SSH_FXP_READ, + [?uint32(ReqID), + ?binary(Handle), + ?uint64(Offset), + ?uint32(Length)]). + +readdir(XF, ReqID, Handle) -> + xf_request(XF, ?SSH_FXP_READDIR, + [?uint32(ReqID), + ?binary(Handle)]). + +write(XF,ReqID, Handle, Offset, Data) -> + Data1 = if + is_binary(Data) -> + Data; + is_list(Data) -> + list_to_binary(Data) + end, + xf_request(XF,?SSH_FXP_WRITE, + [?uint32(ReqID), + ?binary(Handle), + ?uint64(Offset), + ?binary(Data1)]). + +%% Remove a file +remove(XF, ReqID, File) -> + xf_request(XF, ?SSH_FXP_REMOVE, + [?uint32(ReqID), + ?string(File)]). + +%% Rename a file/directory +rename(XF, ReqID, Old, New, Flags) -> + Vsn = XF#ssh_xfer.vsn, + OldPath = list_to_binary(Old), + NewPath = list_to_binary(New), + FlagBits + = if Vsn >= 5 -> + F0 = encode_rename_flags(Flags), + ?uint32(F0); + true -> + (<<>>) + end, + xf_request(XF, ?SSH_FXP_RENAME, + [?uint32(ReqID), + ?binary(OldPath), + ?binary(NewPath), + FlagBits]). + + + +%% Create directory +mkdir(XF, ReqID, Path, Attrs) -> + Path1 = list_to_binary(Path), + xf_request(XF, ?SSH_FXP_MKDIR, + [?uint32(ReqID), + ?binary(Path1), + encode_ATTR(XF#ssh_xfer.vsn, Attrs)]). + +%% Remove a directory +rmdir(XF, ReqID, Dir) -> + Dir1 = list_to_binary(Dir), + xf_request(XF, ?SSH_FXP_RMDIR, + [?uint32(ReqID), + ?binary(Dir1)]). + +%% Stat file +stat(XF, ReqID, Path, Flags) -> + Path1 = list_to_binary(Path), + Vsn = XF#ssh_xfer.vsn, + AttrFlags = if Vsn >= 5 -> + F = encode_attr_flags(Vsn, Flags), + ?uint32(F); + true -> + [] + end, + xf_request(XF, ?SSH_FXP_STAT, + [?uint32(ReqID), + ?binary(Path1), + AttrFlags]). + + +%% Stat file - follow symbolic links +lstat(XF, ReqID, Path, Flags) -> + Path1 = list_to_binary(Path), + Vsn = XF#ssh_xfer.vsn, + AttrFlags = if Vsn >= 5 -> + F = encode_attr_flags(Vsn, Flags), + ?uint32(F); + true -> + [] + end, + xf_request(XF, ?SSH_FXP_LSTAT, + [?uint32(ReqID), + ?binary(Path1), + AttrFlags]). + +%% Stat open file +fstat(XF, ReqID, Handle, Flags) -> + Vsn = XF#ssh_xfer.vsn, + AttrFlags = if Vsn >= 5 -> + F = encode_attr_flags(Vsn, Flags), + ?uint32(F); + true -> + [] + end, + xf_request(XF, ?SSH_FXP_FSTAT, + [?uint32(ReqID), + ?binary(Handle), + AttrFlags]). + +%% Modify file attributes +setstat(XF, ReqID, Path, Attrs) -> + Path1 = list_to_binary(Path), + xf_request(XF, ?SSH_FXP_SETSTAT, + [?uint32(ReqID), + ?binary(Path1), + encode_ATTR(XF#ssh_xfer.vsn, Attrs)]). + + +%% Modify file attributes +fsetstat(XF, ReqID, Handle, Attrs) -> + xf_request(XF, ?SSH_FXP_FSETSTAT, + [?uint32(ReqID), + ?binary(Handle), + encode_ATTR(XF#ssh_xfer.vsn, Attrs)]). + +%% Read a symbolic link +readlink(XF, ReqID, Path) -> + Path1 = list_to_binary(Path), + xf_request(XF, ?SSH_FXP_READLINK, + [?uint32(ReqID), + ?binary(Path1)]). + + +%% Create a symbolic link +symlink(XF, ReqID, LinkPath, TargetPath) -> + LinkPath1 = list_to_binary(LinkPath), + TargetPath1 = list_to_binary(TargetPath), + xf_request(XF, ?SSH_FXP_SYMLINK, + [?uint32(ReqID), + ?binary(LinkPath1), + ?binary(TargetPath1)]). + +%% Convert a path into a 'canonical' form +realpath(XF, ReqID, Path) -> + Path1 = list_to_binary(Path), + xf_request(XF, ?SSH_FXP_REALPATH, + [?uint32(ReqID), + ?binary(Path1)]). + +extended(XF, ReqID, Request, Data) -> + xf_request(XF, ?SSH_FXP_EXTENDED, + [?uint32(ReqID), + ?string(Request), + ?binary(Data)]). + + +%% Send xfer request to connection manager +xf_request(XF, Op, Arg) -> + CM = XF#ssh_xfer.cm, + Channel = XF#ssh_xfer.channel, + Data = if + is_binary(Arg) -> + Arg; + is_list(Arg) -> + list_to_binary(Arg) + end, + Size = 1+size(Data), + ssh_connection:send(CM, Channel, <>). + +xf_send_reply(#ssh_xfer{cm = CM, channel = Channel}, Op, Arg) -> + Data = if + is_binary(Arg) -> + Arg; + is_list(Arg) -> + list_to_binary(Arg) + end, + Size = 1 + size(Data), + ssh_connection:send(CM, Channel, <>). + +xf_send_name(XF, ReqId, Name, Attr) -> + xf_send_names(XF, ReqId, [{Name, Attr}]). + + +xf_send_handle(#ssh_xfer{cm = CM, channel = Channel}, + ReqId, Handle) -> + HLen = length(Handle), + Size = 1 + 4 + 4+HLen, + ToSend = [<>, + Handle], + ssh_connection:send(CM, Channel, ToSend). + +xf_send_names(#ssh_xfer{cm = CM, channel = Channel, vsn = Vsn}, + ReqId, NamesAndAttrs) -> + Count = length(NamesAndAttrs), + {Data, Len} = encode_names(Vsn, NamesAndAttrs), + Size = 1 + 4 + 4 + Len, + ToSend = [<>, + Data], + %%?dbg(true, "xf_send_names: Size=~p size(ToSend)=~p\n", + %% [Size, size(list_to_binary(ToSend))]), + ssh_connection:send(CM, Channel, ToSend). + +xf_send_status(XF, ReqId, ErrorCode) -> + xf_send_status(XF, ReqId, ErrorCode, ""). + +xf_send_status(XF, ReqId, ErrorCode, ErrorMsg) -> + xf_send_status(XF, ReqId, ErrorCode, ErrorMsg, <<>>). + +xf_send_status(#ssh_xfer{cm = CM, channel = Channel}, + ReqId, ErrorCode, ErrorMsg, Data) -> + LangTag = "en", + ELen = length(ErrorMsg), + TLen = 2, %% length(LangTag), + Size = 1 + 4 + 4 + 4+ELen + 4+TLen + size(Data), + ToSend = [<>, + <>, ErrorMsg, + <>, LangTag, + Data], + ssh_connection:send(CM, Channel, ToSend). + +xf_send_attr(#ssh_xfer{cm = CM, channel = Channel, vsn = Vsn}, ReqId, Attr) -> + EncAttr = encode_ATTR(Vsn, Attr), + ALen = size(EncAttr), + Size = 1 + 4 + ALen, + ToSend = [<>, EncAttr], + ssh_connection:send(CM, Channel, ToSend). + +xf_send_data(#ssh_xfer{cm = CM, channel = Channel}, ReqId, Data) -> + DLen = size(Data), + Size = 1 + 4 + 4+DLen, + ToSend = [<>, + Data], + ssh_connection:send(CM, Channel, ToSend). + +xf_reply(_XF, << ?SSH_FXP_STATUS, ?UINT32(ReqID), ?UINT32(Status), + ?UINT32(ELen), Err:ELen/binary, + ?UINT32(LLen), Lang:LLen/binary, + Reply/binary >> ) -> + Stat = decode_status(Status), + {status, ReqID, {Stat,binary_to_list(Err),binary_to_list(Lang), + Reply}}; +xf_reply(_XF, << ?SSH_FXP_STATUS, ?UINT32(ReqID), ?UINT32(Status)>> ) -> + Stat = decode_status(Status), + {status, ReqID, {Stat,"","",<<>>}}; +xf_reply(_XF, <>) -> + {handle, ReqID, Handle}; +xf_reply(_XF, <>) -> + {data, ReqID, Data}; +xf_reply(XF, <>) -> + %%?dbg(true, "xf_reply ?SSH_FXP_NAME: AData=~p\n", [AData]), + {name, ReqID, decode_names(XF#ssh_xfer.vsn, Count, AData)}; +xf_reply(XF, <>) -> + {A, _} = decode_ATTR(XF#ssh_xfer.vsn, AData), + {attrs, ReqID, A}; +xf_reply(_XF, <>) -> + {extended_reply, ReqID, RData}. + + + +decode_status(Status) -> + case Status of + ?SSH_FX_OK -> ok; + ?SSH_FX_EOF -> eof; + ?SSH_FX_NO_SUCH_FILE -> no_such_file; + ?SSH_FX_PERMISSION_DENIED -> permission_denied; + ?SSH_FX_FAILURE -> failure; + ?SSH_FX_BAD_MESSAGE -> bad_message; + ?SSH_FX_NO_CONNECTION -> no_connection; + ?SSH_FX_CONNECTION_LOST -> connection_lost; + ?SSH_FX_OP_UNSUPPORTED -> op_unsupported; + ?SSH_FX_INVALID_HANDLE -> invalid_handle; + ?SSH_FX_NO_SUCH_PATH -> no_such_path; + ?SSH_FX_FILE_ALREADY_EXISTS -> file_already_exists; + ?SSH_FX_WRITE_PROTECT -> write_protect; + ?SSH_FX_NO_MEDIA -> no_media; + ?SSH_FX_NO_SPACE_ON_FILESYSTEM -> no_space_on_filesystem; + ?SSH_FX_QUOTA_EXCEEDED -> quota_exceeded; + ?SSH_FX_UNKNOWN_PRINCIPLE -> unknown_principle; + ?SSH_FX_LOCK_CONFlICT -> lock_conflict; + ?SSH_FX_NOT_A_DIRECTORY -> not_a_directory; + _ -> {error,Status} + end. + +encode_erlang_status(Status) -> + case Status of + ok -> ?SSH_FX_OK; + eof -> ?SSH_FX_EOF; + enoent -> ?SSH_FX_NO_SUCH_FILE; + eacces -> ?SSH_FX_PERMISSION_DENIED; + _ -> ?SSH_FX_FAILURE + end. + +decode_ext(<>) -> + [{binary_to_list(Name), binary_to_list(Data)} + | decode_ext(Tail)]; +decode_ext(<<>>) -> + []. + +%% +%% Encode rename flags +%% +encode_rename_flags(Flags) -> + encode_bits( + fun(overwrite) -> ?SSH_FXP_RENAME_OVERWRITE; + (atomic) -> ?SSH_FXP_RENAME_ATOMIC; + (native) -> ?SSH_FXP_RENAME_NATIVE + end, Flags). + +%% decode_rename_flags(F) -> +%% decode_bits(F, +%% [{?SSH_FXP_RENAME_OVERWRITE, overwrite}, +%% {?SSH_FXP_RENAME_ATOMIC, atomic}, +%% {?SSH_FXP_RENAME_NATIVE, native}]). + + +encode_open_flags(Flags) -> + encode_bits( + fun (read) -> ?SSH_FXF_READ; + (write) -> ?SSH_FXF_WRITE; + (append) -> ?SSH_FXF_APPEND; + (creat) -> ?SSH_FXF_CREAT; + (trunc) -> ?SSH_FXF_TRUNC; + (excl) -> ?SSH_FXF_EXCL; + (create_new) -> ?SSH_FXF_CREATE_NEW; + (create_truncate) -> ?SSH_FXF_CREATE_TRUNCATE; + (open_existing) -> ?SSH_FXF_OPEN_EXISTING; + (open_or_create) -> ?SSH_FXF_OPEN_OR_CREATE; + (truncate_existing) -> ?SSH_FXF_TRUNCATE_EXISTING; + (append_data) -> ?SSH_FXF_ACCESS_APPEND_DATA; + (append_data_atomic) -> ?SSH_FXF_ACCESS_APPEND_DATA_ATOMIC; + (text_mode) -> ?SSH_FXF_ACCESS_TEXT_MODE; + (read_lock) -> ?SSH_FXF_ACCESS_READ_LOCK; + (write_lock) -> ?SSH_FXF_ACCESS_WRITE_LOCK; + (delete_lock) -> ?SSH_FXF_ACCESS_DELETE_LOCK + end, Flags). + +encode_ace_mask(Access) -> + encode_bits( + fun(read_data) -> ?ACE4_READ_DATA; + (list_directory) -> ?ACE4_LIST_DIRECTORY; + (write_data) -> ?ACE4_WRITE_DATA; + (add_file) -> ?ACE4_ADD_FILE; + (append_data) -> ?ACE4_APPEND_DATA; + (add_subdirectory) -> ?ACE4_ADD_SUBDIRECTORY; + (read_named_attrs) -> ?ACE4_READ_NAMED_ATTRS; + (write_named_attrs) -> ?ACE4_WRITE_NAMED_ATTRS; + (execute) -> ?ACE4_EXECUTE; + (delete_child) -> ?ACE4_DELETE_CHILD; + (read_attributes) -> ?ACE4_READ_ATTRIBUTES; + (write_attributes) -> ?ACE4_WRITE_ATTRIBUTES; + (delete) -> ?ACE4_DELETE; + (read_acl) -> ?ACE4_READ_ACL; + (write_acl) -> ?ACE4_WRITE_ACL; + (write_owner) -> ?ACE4_WRITE_OWNER; + (synchronize) -> ?ACE4_SYNCHRONIZE + end, Access). + +decode_ace_mask(F) -> + decode_bits(F, + [ + {?ACE4_READ_DATA, read_data}, + {?ACE4_LIST_DIRECTORY, list_directory}, + {?ACE4_WRITE_DATA, write_data}, + {?ACE4_ADD_FILE, add_file}, + {?ACE4_APPEND_DATA, append_data}, + {?ACE4_ADD_SUBDIRECTORY, add_subdirectory}, + {?ACE4_READ_NAMED_ATTRS, read_named_attrs}, + {?ACE4_WRITE_NAMED_ATTRS, write_named_attrs}, + {?ACE4_EXECUTE, execute}, + {?ACE4_DELETE_CHILD, delete_child}, + {?ACE4_READ_ATTRIBUTES, read_attributes}, + {?ACE4_WRITE_ATTRIBUTES, write_attributes}, + {?ACE4_DELETE, delete}, + {?ACE4_READ_ACL, read_acl}, + {?ACE4_WRITE_ACL, write_acl}, + {?ACE4_WRITE_OWNER, write_owner}, + {?ACE4_SYNCHRONIZE, synchronize} + ]). + +decode_open_flags(Vsn, F) when Vsn =< 3 -> + decode_bits(F, + [ + {?SSH_FXF_READ, read}, + {?SSH_FXF_WRITE, write}, + {?SSH_FXF_APPEND, append}, + {?SSH_FXF_CREAT, creat}, + {?SSH_FXF_TRUNC, trunc}, + {?SSH_FXF_EXCL, excl} + ]); +decode_open_flags(Vsn, F) when Vsn >= 4 -> + R = decode_bits(F, + [ + {?SSH_FXF_ACCESS_APPEND_DATA, append_data}, + {?SSH_FXF_ACCESS_APPEND_DATA_ATOMIC, append_data_atomic}, + {?SSH_FXF_ACCESS_TEXT_MODE, text_mode}, + {?SSH_FXF_ACCESS_READ_LOCK, read_lock}, + {?SSH_FXF_ACCESS_WRITE_LOCK, write_lock}, + {?SSH_FXF_ACCESS_DELETE_LOCK, delete_lock} + ]), + AD = case F band ?SSH_FXF_ACCESS_DISPOSITION of + ?SSH_FXF_CREATE_NEW -> create_new; + ?SSH_FXF_CREATE_TRUNCATE -> create_truncate; + ?SSH_FXF_OPEN_EXISTING -> open_existing; + ?SSH_FXF_OPEN_OR_CREATE -> open_or_create; + ?SSH_FXF_TRUNCATE_EXISTING -> truncate_existing + end, + [AD | R]. + +encode_ace_type(Type) -> + case Type of + access_allowed -> ?ACE4_ACCESS_ALLOWED_ACE_TYPE; + access_denied -> ?ACE4_ACCESS_DENIED_ACE_TYPE; + system_audit -> ?ACE4_SYSTEM_AUDIT_ACE_TYPE; + system_alarm -> ?ACE4_SYSTEM_ALARM_ACE_TYPE + end. + +decode_ace_type(F) -> + case F of + ?ACE4_ACCESS_ALLOWED_ACE_TYPE -> access_allowed; + ?ACE4_ACCESS_DENIED_ACE_TYPE -> access_denied; + ?ACE4_SYSTEM_AUDIT_ACE_TYPE -> system_audit; + ?ACE4_SYSTEM_ALARM_ACE_TYPE -> system_alarm + end. + +encode_ace_flag(Flag) -> + encode_bits( + fun(file_inherit) -> ?ACE4_FILE_INHERIT_ACE; + (directory_inherit) -> ?ACE4_DIRECTORY_INHERIT_ACE; + (no_propagte_inherit) -> ?ACE4_NO_PROPAGATE_INHERIT_ACE; + (inherit_only) -> ?ACE4_INHERIT_ONLY_ACE; + (successful_access) -> ?ACE4_SUCCESSFUL_ACCESS_ACE_FLAG; + (failed_access) -> ?ACE4_FAILED_ACCESS_ACE_FLAG; + (identifier_group) -> ?ACE4_IDENTIFIER_GROUP + end, Flag). + +decode_ace_flag(F) -> + decode_bits(F, + [ + {?ACE4_FILE_INHERIT_ACE, file_inherit}, + {?ACE4_DIRECTORY_INHERIT_ACE, directory_inherit}, + {?ACE4_NO_PROPAGATE_INHERIT_ACE, no_propagte_inherit}, + {?ACE4_INHERIT_ONLY_ACE, inherit_only}, + {?ACE4_SUCCESSFUL_ACCESS_ACE_FLAG, successful_access}, + {?ACE4_FAILED_ACCESS_ACE_FLAG, failed_access}, + {?ACE4_IDENTIFIER_GROUP, identifier_group} + ]). + +encode_attr_flags(Vsn, all) -> + encode_attr_flags(Vsn, + [size, uidgid, permissions, + acmodtime, accesstime, createtime, + modifytime, acl, ownergroup, subsecond_times, + bits, extended]); +encode_attr_flags(Vsn, Flags) -> + encode_bits( + fun(size) -> ?SSH_FILEXFER_ATTR_SIZE; + (uidgid) when Vsn =<3 -> ?SSH_FILEXFER_ATTR_UIDGID; + (permissions) -> ?SSH_FILEXFER_ATTR_PERMISSIONS; + (acmodtime) when Vsn =< 3 -> ?SSH_FILEXFER_ATTR_ACMODTIME; + (accesstime) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_ACCESSTIME; + (createtime) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_CREATETIME; + (modifytime) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_MODIFYTIME; + (acl) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_ACL; + (ownergroup) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_OWNERGROUP; + (subsecond_times) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_SUBSECOND_TIMES; + (bits) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_BITS; + (extended) when Vsn >= 5 -> ?SSH_FILEXFER_ATTR_EXTENDED; + (_) -> 0 + end, Flags). + +encode_file_type(Type) -> + %%?dbg(true, "encode_file_type(~p)\n", [Type]), + case Type of + regular -> ?SSH_FILEXFER_TYPE_REGULAR; + directory -> ?SSH_FILEXFER_TYPE_DIRECTORY; + symlink -> ?SSH_FILEXFER_TYPE_SYMLINK; + special -> ?SSH_FILEXFER_TYPE_SPECIAL; + unknown -> ?SSH_FILEXFER_TYPE_UNKNOWN; + other -> ?SSH_FILEXFER_TYPE_UNKNOWN; + socket -> ?SSH_FILEXFER_TYPE_SOCKET; + char_device -> ?SSH_FILEXFER_TYPE_CHAR_DEVICE; + block_device -> ?SSH_FILEXFER_TYPE_BLOCK_DEVICE; + fifo -> ?SSH_FILEXFER_TYPE_FIFO; + undefined -> ?SSH_FILEXFER_TYPE_UNKNOWN + end. + +decode_file_type(Type) -> + case Type of + ?SSH_FILEXFER_TYPE_REGULAR -> regular; + ?SSH_FILEXFER_TYPE_DIRECTORY -> directory; + ?SSH_FILEXFER_TYPE_SYMLINK -> symlink; + ?SSH_FILEXFER_TYPE_SPECIAL -> special; + ?SSH_FILEXFER_TYPE_UNKNOWN -> other; % unknown + ?SSH_FILEXFER_TYPE_SOCKET -> socket; + ?SSH_FILEXFER_TYPE_CHAR_DEVICE -> char_device; + ?SSH_FILEXFER_TYPE_BLOCK_DEVICE -> block_device; + ?SSH_FILEXFER_TYPE_FIFO -> fifo + end. + +encode_attrib_bits(Bits) -> + encode_bits( + fun(readonly) -> ?SSH_FILEXFER_ATTR_FLAGS_READONLY; + (system) -> ?SSH_FILEXFER_ATTR_FLAGS_SYSTEM; + (hidden) -> ?SSH_FILEXFER_ATTR_FLAGS_HIDDEN; + (case_insensitive) -> ?SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE; + (arcive) -> ?SSH_FILEXFER_ATTR_FLAGS_ARCHIVE; + (encrypted) -> ?SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED; + (compressed) -> ?SSH_FILEXFER_ATTR_FLAGS_COMPRESSED; + (sparse) -> ?SSH_FILEXFER_ATTR_FLAGS_SPARSE; + (append_only) -> ?SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY; + (immutable) -> ?SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE; + (sync) -> ?SSH_FILEXFER_ATTR_FLAGS_SYNC + end, Bits). + +decode_attrib_bits(F) -> + decode_bits(F, + [{?SSH_FILEXFER_ATTR_FLAGS_READONLY, readonly}, + {?SSH_FILEXFER_ATTR_FLAGS_SYSTEM, system}, + {?SSH_FILEXFER_ATTR_FLAGS_HIDDEN, hidden}, + {?SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE, case_insensitive}, + {?SSH_FILEXFER_ATTR_FLAGS_ARCHIVE, arcive}, + {?SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED, encrypted}, + {?SSH_FILEXFER_ATTR_FLAGS_COMPRESSED, compressed}, + {?SSH_FILEXFER_ATTR_FLAGS_SPARSE, sparse}, + {?SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY, append_only}, + {?SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE, immutable}, + {?SSH_FILEXFER_ATTR_FLAGS_SYNC, sync}]). + + +%% +%% Encode file attributes +%% +encode_ATTR(Vsn, A) -> + {Flags,As} = + encode_As(Vsn, + [{size, A#ssh_xfer_attr.size}, + {ownergroup, A#ssh_xfer_attr.owner}, + {ownergroup, A#ssh_xfer_attr.group}, + {permissions, A#ssh_xfer_attr.permissions}, + {acmodtime, A#ssh_xfer_attr.atime}, + {acmodtime, A#ssh_xfer_attr.mtime}, + {accesstime, A#ssh_xfer_attr.atime}, + {subsecond_times, A#ssh_xfer_attr.atime_nseconds}, + {createtime, A#ssh_xfer_attr.createtime}, + {subsecond_times, A#ssh_xfer_attr.createtime_nseconds}, + {modifytime, A#ssh_xfer_attr.mtime}, + {subsecond_times, A#ssh_xfer_attr.mtime_nseconds}, + {acl, A#ssh_xfer_attr.acl}, + {bits, A#ssh_xfer_attr.attrib_bits}, + {extended, A#ssh_xfer_attr.extensions}], + 0, []), + Type = encode_file_type(A#ssh_xfer_attr.type), + %%?dbg(true, "encode_ATTR: Vsn=~p A=~p As=~p Flags=~p Type=~p", + %% [Vsn, A, As, Flags, Type]), + Result = list_to_binary([?uint32(Flags), + if Vsn >= 5 -> + ?byte(Type); + true -> + (<<>>) + end, As]), + %% ?dbg(true, " Result=~p\n", [Result]), + Result. + + +encode_As(Vsn, [{_AName, undefined}|As], Flags, Acc) -> + encode_As(Vsn, As, Flags, Acc); +encode_As(Vsn, [{AName, X}|As], Flags, Acc) -> + case AName of + size -> + encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_SIZE, + [?uint64(X) | Acc]); + ownergroup when Vsn=<4 -> + encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_UIDGID, + [?uint32(X) | Acc]); + ownergroup when Vsn>=5 -> + X1 = list_to_binary(integer_to_list(X)), % TODO: check owner and group + encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_OWNERGROUP, + [?binary(X1) | Acc]); + permissions -> + encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_PERMISSIONS, + [?uint32(X) | Acc]); + acmodtime when Vsn=<3 -> + encode_As(Vsn, As,Flags bor ?SSH_FILEXFER_ATTR_ACMODTIME, + [?uint32(X) | Acc]); + accesstime when Vsn>=5 -> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_ACCESSTIME, + [?uint64(X) | Acc]); + createtime when Vsn>=5-> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_CREATETIME, + [?uint64(X) | Acc]); + modifytime when Vsn>=5 -> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_MODIFYTIME, + [?uint64(X) | Acc]); + subsecond_times when Vsn>=5 -> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_SUBSECOND_TIMES, + [?uint64(X) | Acc]); + acl when Vsn >=5 -> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_ACL, + [encode_acl(X) | Acc]); + bits when Vsn>=5 -> + F = encode_attrib_bits(X), + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_BITS, + [?uint32(F) | Acc]); + extended -> + encode_As(Vsn, As, Flags bor ?SSH_FILEXFER_ATTR_EXTENDED, + [encode_extensions(X) | Acc]); + _ -> + encode_As(Vsn, As, Flags, Acc) + end; +encode_As(_Vsn, [], Flags, Acc) -> + {Flags, reverse(Acc)}. + + +decode_ATTR(Vsn, <>) -> + %%?dbg(true, "decode_ATTR: Vsn=~p Flags=~p Tail=~p\n", [Vsn, Flags, Tail]), + {Type,Tail2} = + if Vsn =< 3 -> + {?SSH_FILEXFER_TYPE_UNKNOWN, Tail}; + Vsn >= 5 -> + <> = Tail, + {T, TL} + end, + decode_As(Vsn, + [{size, #ssh_xfer_attr.size}, + {ownergroup, #ssh_xfer_attr.owner}, + {ownergroup, #ssh_xfer_attr.group}, + {permissions, #ssh_xfer_attr.permissions}, + {acmodtime, #ssh_xfer_attr.atime}, + {acmodtime, #ssh_xfer_attr.mtime}, + {accesstime, #ssh_xfer_attr.atime}, + {subsecond_times, #ssh_xfer_attr.atime_nseconds}, + {createtime, #ssh_xfer_attr.createtime}, + {subsecond_times, #ssh_xfer_attr.createtime_nseconds}, + {modifytime, #ssh_xfer_attr.mtime}, + {subsecond_times, #ssh_xfer_attr.mtime_nseconds}, + {acl, #ssh_xfer_attr.acl}, + {bits, #ssh_xfer_attr.attrib_bits}, + {extended, #ssh_xfer_attr.extensions}], + #ssh_xfer_attr { type = decode_file_type(Type) }, + Flags, + Tail2). + +decode_As(Vsn, [{AName, AField}|As], R, Flags, Tail) -> + %%?dbg(false, "decode_As: Vsn=~p AName=~p AField=~p Flags=~p Tail=~p\n", [Vsn, AName, AField, Flags, Tail]), + case AName of + size when ?is_set(?SSH_FILEXFER_ATTR_SIZE, Flags) -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + ownergroup when ?is_set(?SSH_FILEXFER_ATTR_UIDGID, Flags),Vsn=<3 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + ownergroup when ?is_set(?SSH_FILEXFER_ATTR_OWNERGROUP, Flags),Vsn>=5 -> + <> = Tail, + X = binary_to_list(Bin), + %%?dbg(true, "ownergroup X=~p\n", [X]), + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + + permissions when ?is_set(?SSH_FILEXFER_ATTR_PERMISSIONS,Flags),Vsn>=5-> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + + permissions when ?is_set(?SSH_FILEXFER_ATTR_PERMISSIONS,Flags),Vsn=<3-> + <> = Tail, + R1 = setelement(AField, R, X), + Type = case X band ?S_IFMT of + ?S_IFDIR -> directory; + ?S_IFCHR -> char_device; + ?S_IFBLK -> block_device; + ?S_IFIFO -> fifi; + ?S_IFREG -> regular; + ?S_IFSOCK -> socket; + ?S_IFLNK -> symlink; + _ -> unknown + end, + decode_As(Vsn, As, R1#ssh_xfer_attr { type=Type}, Flags, Tail2); + + acmodtime when ?is_set(?SSH_FILEXFER_ATTR_ACMODTIME,Flags),Vsn=<3 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + accesstime when ?is_set(?SSH_FILEXFER_ATTR_ACCESSTIME,Flags),Vsn>=5 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + modifytime when ?is_set(?SSH_FILEXFER_ATTR_MODIFYTIME,Flags),Vsn>=5 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + createtime when ?is_set(?SSH_FILEXFER_ATTR_CREATETIME,Flags),Vsn>=5 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + subsecond_times when ?is_set(?SSH_FILEXFER_ATTR_SUBSECOND_TIMES,Flags),Vsn>=5 -> + <> = Tail, + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + acl when ?is_set(?SSH_FILEXFER_ATTR_ACL, Flags), Vsn>=5 -> + {X,Tail2} = decode_acl(Tail), + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + bits when ?is_set(?SSH_FILEXFER_ATTR_BITS, Flags), Vsn >=5 -> + <> = Tail, + X = decode_attrib_bits(Y), + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + extended when ?is_set(?SSH_FILEXFER_ATTR_EXTENDED, Flags) -> + {X,Tail2} = decode_extended(Tail), + decode_As(Vsn, As, setelement(AField, R, X), Flags, Tail2); + _ -> + decode_As(Vsn, As, R, Flags, Tail) + end; +decode_As(_Vsn, [], R, _, Tail) -> + {R, Tail}. + + + + +decode_names(_Vsn, 0, _Data) -> + []; +decode_names(Vsn, I, <>) when Vsn =< 3 -> + Name = binary_to_list(FileName), + %%?dbg(true, "decode_names: ~p\n", [Name]), + {A, Tail2} = decode_ATTR(Vsn, Tail), + [{Name, A} | decode_names(Vsn, I-1, Tail2)]; +decode_names(Vsn, I, <>) when Vsn >= 4 -> + Name = binary_to_list(FileName), + %%?dbg(true, "decode_names: ~p\n", [Name]), + {A, Tail2} = decode_ATTR(Vsn, Tail), + [{Name, A} | decode_names(Vsn, I-1, Tail2)]. + +encode_names(Vsn, NamesAndAttrs) -> + lists:mapfoldl(fun(N, L) -> encode_name(Vsn, N, L) end, 0, NamesAndAttrs). + +encode_name(Vsn, {Name,Attr}, Len) when Vsn =< 3 -> + NLen = length(Name), + %%?dbg(true, "encode_name: Vsn=~p Name=~p Attr=~p\n", + %% [Vsn, Name, Attr]), + EncAttr = encode_ATTR(Vsn, Attr), + ALen = size(EncAttr), + NewLen = Len + NLen*2 + 4 + 4 + ALen, + {[<>, Name, <>, Name, EncAttr], NewLen}; +encode_name(Vsn, {Name,Attr}, Len) when Vsn >= 4 -> + NLen = length(Name), + EncAttr = encode_ATTR(Vsn, Attr), + ALen = size(EncAttr), + {[<>, Name, EncAttr], + Len + 4 + NLen + ALen}. + +encode_acl(ACLList) -> + Count = length(ACLList), + [?uint32(Count) | encode_acl_items(ACLList)]. + +encode_acl_items([ACE|As]) -> + Type = encode_ace_type(ACE#ssh_xfer_ace.type), + Flag = encode_ace_flag(ACE#ssh_xfer_ace.flag), + Mask = encode_ace_mask(ACE#ssh_xfer_ace.mask), + Who = list_to_binary(ACE#ssh_xfer_ace.who), + [?uint32(Type), ?uint32(Flag), ?uint32(Mask), + ?binary(Who) | encode_acl_items(As)]; +encode_acl_items([]) -> + []. + + +decode_acl(<>) -> + decode_acl_items(Count, Tail, []). + +decode_acl_items(0, Tail, Acc) -> + {reverse(Acc), Tail}; +decode_acl_items(I, <>, Acc) -> + decode_acl_items(I-1, Tail, + [#ssh_xfer_ace { type = decode_ace_type(Type), + flag = decode_ace_flag(Flag), + mask = decode_ace_mask(Mask), + who = binary_to_list(BWho)} | Acc]). + +encode_extensions(Exts) -> + Count = length(Exts), + [?uint32(Count) | encode_ext(Exts)]. + +encode_ext([{Type, Data} | Exts]) -> + [?string(Type), ?string(Data) | encode_ext(Exts)]; +encode_ext([]) -> + []. + + +decode_extended(<>) -> + decode_ext(Count, Tail, []). + +decode_ext(0, Tail, Acc) -> + {reverse(Acc), Tail}; +decode_ext(I, <>, Acc) -> + decode_ext(I-1, Tail, [{binary_to_list(Type), Data}|Acc]). + + + +%% Encode bit encoded flags +encode_bits(Fun, BitNames) -> + encode_bits(Fun, 0, BitNames). + +encode_bits(Fun, F, [Bit|BitNames]) -> + encode_bits(Fun, Fun(Bit) bor F, BitNames); +encode_bits(_Fun, F, []) -> + F. + +%% Decode bit encoded flags +decode_bits(F, [{Bit,BitName}|Bits]) -> + if F band Bit == Bit -> + [BitName | decode_bits(F, Bits)]; + true -> + decode_bits(F, Bits) + end; +decode_bits(_F, []) -> + []. diff --git a/lib/ssh/src/ssh_xfer.hrl b/lib/ssh/src/ssh_xfer.hrl new file mode 100755 index 0000000000..f32ec5f774 --- /dev/null +++ b/lib/ssh/src/ssh_xfer.hrl @@ -0,0 +1,251 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: SFTP defines +-define(SSH_SFTP_PROTOCOL_VERSION, 6). +%%%---------------------------------------------------------------------- +%%% # SSH_FXP_xxx +%%% Description: Request and initialization packet types for file transfer +%%% protocol. +%%%---------------------------------------------------------------------- +-define(SSH_FXP_INIT, 1). +-define(SSH_FXP_VERSION, 2). +-define(SSH_FXP_OPEN, 3). +-define(SSH_FXP_CLOSE, 4). +-define(SSH_FXP_READ, 5). +-define(SSH_FXP_WRITE, 6). +-define(SSH_FXP_LSTAT, 7). +-define(SSH_FXP_FSTAT, 8). +-define(SSH_FXP_SETSTAT, 9). +-define(SSH_FXP_FSETSTAT, 10). +-define(SSH_FXP_OPENDIR, 11). +-define(SSH_FXP_READDIR, 12). +-define(SSH_FXP_REMOVE, 13). +-define(SSH_FXP_MKDIR, 14). +-define(SSH_FXP_RMDIR, 15). +-define(SSH_FXP_REALPATH, 16). +-define(SSH_FXP_STAT, 17). +-define(SSH_FXP_RENAME, 18). +-define(SSH_FXP_READLINK, 19). +-define(SSH_FXP_SYMLINK, 20). +-define(SSH_FXP_STATUS, 101). +-define(SSH_FXP_HANDLE, 102). +-define(SSH_FXP_DATA, 103). +-define(SSH_FXP_NAME, 104). +-define(SSH_FXP_ATTRS, 105). +-define(SSH_FXP_EXTENDED, 200). +-define(SSH_FXP_EXTENDED_REPLY, 201). + +%%%---------------------------------------------------------------------- +%%% # SSH_FX_xxx +%%% Description: Response packet types for file transfer protocol. +%%%---------------------------------------------------------------------- + +-define(SSH_FX_OK, 0). +-define(SSH_FX_EOF, 1). +-define(SSH_FX_NO_SUCH_FILE, 2). +-define(SSH_FX_PERMISSION_DENIED, 3). +-define(SSH_FX_FAILURE, 4). +-define(SSH_FX_BAD_MESSAGE, 5). +-define(SSH_FX_NO_CONNECTION, 6). +-define(SSH_FX_CONNECTION_LOST, 7). +-define(SSH_FX_OP_UNSUPPORTED, 8). +-define(SSH_FX_INVALID_HANDLE, 9). +-define(SSH_FX_NO_SUCH_PATH, 10). +-define(SSH_FX_FILE_ALREADY_EXISTS, 11). +-define(SSH_FX_WRITE_PROTECT, 12). +-define(SSH_FX_NO_MEDIA, 13). +-define(SSH_FX_NO_SPACE_ON_FILESYSTEM, 14). +-define(SSH_FX_QUOTA_EXCEEDED, 15). +-define(SSH_FX_UNKNOWN_PRINCIPLE, 16). +-define(SSH_FX_LOCK_CONFlICT, 17). +-define(SSH_FX_DIR_NOT_EMPTY, 18). +-define(SSH_FX_NOT_A_DIRECTORY, 19). +-define(SSH_FX_FILE_IS_A_DIRECTORY, 24). + +%%%---------------------------------------------------------------------- +%%% # SSH_FILEXFER_xxx +%%% Description: Bits for file attributes bit mask +%%%---------------------------------------------------------------------- +-define(SSH_FILEXFER_ATTR_SIZE, 16#00000001). %% vsn 3,5 +-define(SSH_FILEXFER_ATTR_UIDGID, 16#00000002). %% vsn 3 +-define(SSH_FILEXFER_ATTR_PERMISSIONS, 16#00000004). %% vsn 3,5 +-define(SSH_FILEXFER_ATTR_ACCESSTIME, 16#00000008). %% vsn 5 +-define(SSH_FILEXFER_ATTR_ACMODTIME, 16#00000008). %% vsn 3 +-define(SSH_FILEXFER_ATTR_CREATETIME, 16#00000010). %% vsn 5 +-define(SSH_FILEXFER_ATTR_MODIFYTIME, 16#00000020) .%% vsn 5 +-define(SSH_FILEXFER_ATTR_ACL, 16#00000040). %% vsn 5 +-define(SSH_FILEXFER_ATTR_OWNERGROUP, 16#00000080). %% vsn 5 +-define(SSH_FILEXFER_ATTR_SUBSECOND_TIMES, 16#00000100). %% vsn 5 +-define(SSH_FILEXFER_ATTR_BITS, 16#00000200). %% vsn 5 +-define(SSH_FILEXFER_ATTR_EXTENDED, 16#80000000). %% vsn 3,5 + +%% File types +-define(SSH_FILEXFER_TYPE_REGULAR, 1). +-define(SSH_FILEXFER_TYPE_DIRECTORY, 2). +-define(SSH_FILEXFER_TYPE_SYMLINK, 3). +-define(SSH_FILEXFER_TYPE_SPECIAL, 4). +-define(SSH_FILEXFER_TYPE_UNKNOWN, 5). +-define(SSH_FILEXFER_TYPE_SOCKET, 6). +-define(SSH_FILEXFER_TYPE_CHAR_DEVICE, 7). +-define(SSH_FILEXFER_TYPE_BLOCK_DEVICE, 8). +-define(SSH_FILEXFER_TYPE_FIFO, 9). + +%% Permissions +-define(S_IRUSR, 8#0000400). +-define(S_IWUSR, 8#0000200). +-define(S_IXUSR, 8#0000100). +-define(S_IRGRP, 8#0000040). +-define(S_IWGRP, 8#0000020). +-define(S_IXGRP, 8#0000010). +-define(S_IROTH, 8#0000004). +-define(S_IWOTH, 8#0000002). +-define(S_IXOTH, 8#0000001). +-define(S_ISUID, 8#0004000). +-define(S_ISGID, 8#0002000). +-define(S_ISVTX, 8#0001000). +%% type bits (version 3 only?) +-define(S_IFMT, 8#0170000). %% file type mask +-define(S_IFDIR, 8#0040000). +-define(S_IFCHR, 8#0020000). +-define(S_IFBLK, 8#0060000). +-define(S_IFIFO, 8#0010000). +-define(S_IFREG, 8#0100000). +-define(S_IFLNK, 8#0120000). +-define(S_IFSOCK, 8#0140000). + +%% ACE-Type +-define(ACE4_ACCESS_ALLOWED_ACE_TYPE, 16#00000000). +-define(ACE4_ACCESS_DENIED_ACE_TYPE, 16#00000001). +-define(ACE4_SYSTEM_AUDIT_ACE_TYPE, 16#00000002). +-define(ACE4_SYSTEM_ALARM_ACE_TYPE, 16#00000003). + +%% ACE-Flag +-define(ACE4_FILE_INHERIT_ACE, 16#00000001). +-define(ACE4_DIRECTORY_INHERIT_ACE, 16#00000002). +-define(ACE4_NO_PROPAGATE_INHERIT_ACE, 16#00000004). +-define(ACE4_INHERIT_ONLY_ACE, 16#00000008). +-define(ACE4_SUCCESSFUL_ACCESS_ACE_FLAG, 16#00000010). +-define(ACE4_FAILED_ACCESS_ACE_FLAG, 16#00000020). +-define(ACE4_IDENTIFIER_GROUP, 16#00000040). + +%% ACE-Mask +-define(ACE4_READ_DATA, 16#00000001). +-define(ACE4_LIST_DIRECTORY, 16#00000001). +-define(ACE4_WRITE_DATA, 16#00000002). +-define(ACE4_ADD_FILE, 16#00000002). +-define(ACE4_APPEND_DATA, 16#00000004). +-define(ACE4_ADD_SUBDIRECTORY, 16#00000004). +-define(ACE4_READ_NAMED_ATTRS, 16#00000008). +-define(ACE4_WRITE_NAMED_ATTRS, 16#00000010). +-define(ACE4_EXECUTE, 16#00000020). +-define(ACE4_DELETE_CHILD, 16#00000040). +-define(ACE4_READ_ATTRIBUTES, 16#00000080). +-define(ACE4_WRITE_ATTRIBUTES, 16#00000100). +-define(ACE4_DELETE, 16#00010000). +-define(ACE4_READ_ACL, 16#00020000). +-define(ACE4_WRITE_ACL, 16#00040000). +-define(ACE4_WRITE_OWNER, 16#00080000). +-define(ACE4_SYNCHRONIZE, 16#00100000). + +%% Attrib-bits +-define(SSH_FILEXFER_ATTR_FLAGS_READONLY, 16#00000001). +-define(SSH_FILEXFER_ATTR_FLAGS_SYSTEM, 16#00000002). +-define(SSH_FILEXFER_ATTR_FLAGS_HIDDEN, 16#00000004). +-define(SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE, 16#00000008). +-define(SSH_FILEXFER_ATTR_FLAGS_ARCHIVE, 16#00000010). +-define(SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED, 16#00000020). +-define(SSH_FILEXFER_ATTR_FLAGS_COMPRESSED, 16#00000040). +-define(SSH_FILEXFER_ATTR_FLAGS_SPARSE, 16#00000080). +-define(SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY, 16#00000100). +-define(SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE, 16#00000200). +-define(SSH_FILEXFER_ATTR_FLAGS_SYNC, 16#00000400). + +%% Open flags (version 3) +-define(SSH_FXF_READ, 16#00000001). +-define(SSH_FXF_WRITE, 16#00000002). +-define(SSH_FXF_APPEND, 16#00000004). +-define(SSH_FXF_CREAT, 16#00000008). +-define(SSH_FXF_TRUNC, 16#00000010). +-define(SSH_FXF_EXCL, 16#00000020). + +%% Open flags (version 5) +-define(SSH_FXF_ACCESS_DISPOSITION, 16#00000007). +-define(SSH_FXF_CREATE_NEW, 16#00000000). +-define(SSH_FXF_CREATE_TRUNCATE, 16#00000001). +-define(SSH_FXF_OPEN_EXISTING, 16#00000002). +-define(SSH_FXF_OPEN_OR_CREATE, 16#00000003). +-define(SSH_FXF_TRUNCATE_EXISTING, 16#00000004). +-define(SSH_FXF_ACCESS_APPEND_DATA, 16#00000008). +-define(SSH_FXF_ACCESS_APPEND_DATA_ATOMIC, 16#00000010). +-define(SSH_FXF_ACCESS_TEXT_MODE, 16#00000020). +-define(SSH_FXF_ACCESS_READ_LOCK, 16#00000040). +-define(SSH_FXF_ACCESS_WRITE_LOCK, 16#00000080). +-define(SSH_FXF_ACCESS_DELETE_LOCK, 16#00000100). + +%% Rename flags +-define(SSH_FXP_RENAME_OVERWRITE, 16#00000001). +-define(SSH_FXP_RENAME_ATOMIC, 16#00000002). +-define(SSH_FXP_RENAME_NATIVE, 16#00000004). + + +-define(SSH_FILEXFER_LARGEFILESIZE, (1 bsl 63)). + +-record(ssh_xfer_attr, + { + type, %% regular, dirctory, symlink, ... + size, + owner, + group, + permissions, + atime, + atime_nseconds, + createtime, + createtime_nseconds, + mtime, + mtime_nseconds, + acl, + attrib_bits, + extensions %% list of [{type,data}] + }). + +-record(ssh_xfer_ace, + { + type, + flag, + mask, + who + }). + +%% connection endpoint and server/client info +-record(ssh_xfer, + { + vsn, %% server version + ext, %% server extensions + cm, %% connection mgr + channel %% SFTP channel + }). + + + + + + diff --git a/lib/ssh/src/sshc_sup.erl b/lib/ssh/src/sshc_sup.erl new file mode 100644 index 0000000000..265d1a1cd6 --- /dev/null +++ b/lib/ssh/src/sshc_sup.erl @@ -0,0 +1,65 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%%---------------------------------------------------------------------- +%% Purpose: The ssh client subsystem supervisor +%%---------------------------------------------------------------------- + +-module(sshc_sup). + +-behaviour(supervisor). + +-export([start_link/1, start_child/1]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(Args) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [Args]). + +start_child(Args) -> + supervisor:start_child(?MODULE, Args). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init(Args) -> + RestartStrategy = simple_one_for_one, + MaxR = 10, + MaxT = 3600, + {ok, {{RestartStrategy, MaxR, MaxT}, [child_spec(Args)]}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +child_spec(_) -> + Name = undefined, % As simple_one_for_one is used. + StartFunc = {ssh_connection_controler, start_link, []}, + Restart = temporary, +% Shutdown = infinity, + Shutdown = 5000, + Modules = [ssh_connection_controler], +% Type = supervisor, + Type = worker, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + diff --git a/lib/ssh/src/sshd_sup.erl b/lib/ssh/src/sshd_sup.erl new file mode 100644 index 0000000000..9c9ba5958c --- /dev/null +++ b/lib/ssh/src/sshd_sup.erl @@ -0,0 +1,111 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2009. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%% +%%---------------------------------------------------------------------- +%% Purpose: The top supervisor for ssh servers hangs under +%% ssh_sup. +%%---------------------------------------------------------------------- + +-module(sshd_sup). + +-behaviour(supervisor). + +-export([start_link/1, start_child/1, stop_child/1, + stop_child/2, system_name/1]). + +%% Supervisor callback +-export([init/1]). + +%%%========================================================================= +%%% API +%%%========================================================================= +start_link(Servers) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [Servers]). + +start_child(ServerOpts) -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + case ssh_system_sup:system_supervisor(Address, Port) of + undefined -> + Spec = child_spec(Address, Port, ServerOpts), + case supervisor:start_child(?MODULE, Spec) of + {error, already_present} -> + Name = id(Address, Port), + supervisor:delete_child(?MODULE, Name), + supervisor:start_child(?MODULE, Spec); + Reply -> + Reply + end; + Pid -> + AccPid = ssh_system_sup:acceptor_supervisor(Pid), + ssh_acceptor_sup:start_child(AccPid, ServerOpts) + end. + +stop_child(Name) -> + case supervisor:terminate_child(?MODULE, Name) of + ok -> + supervisor:delete_child(?MODULE, Name); + Error -> + Error + end. + +stop_child(Address, Port) -> + Name = id(Address, Port), + stop_child(Name). + +system_name(SysSup) -> + Children = supervisor:which_children(sshd_sup), + system_name(SysSup, Children). + +%%%========================================================================= +%%% Supervisor callback +%%%========================================================================= +init([Servers]) -> + RestartStrategy = one_for_one, + MaxR = 10, + MaxT = 3600, + Fun = fun(ServerOpts) -> + Address = proplists:get_value(address, ServerOpts), + Port = proplists:get_value(port, ServerOpts), + child_spec(Address, Port, ServerOpts) + end, + Children = lists:map(Fun, Servers), + {ok, {{RestartStrategy, MaxR, MaxT}, Children}}. + +%%%========================================================================= +%%% Internal functions +%%%========================================================================= +child_spec(Address, Port, ServerOpts) -> + Name = id(Address, Port), + StartFunc = {ssh_system_sup, start_link, [ServerOpts]}, + Restart = transient, + Shutdown = infinity, + Modules = [ssh_system_sup], + Type = supervisor, + {Name, StartFunc, Restart, Shutdown, Type, Modules}. + +id(Address, Port) -> + {server, ssh_system_sup, Address, Port}. + +system_name([], _ ) -> + undefined; +system_name(SysSup, [{Name, SysSup, _, _} | _]) -> + Name; +system_name(SysSup, [_ | Rest]) -> + system_name(SysSup, Rest). -- cgit v1.2.3