From f19b8700da54d62e3d334267128fb2f8e0543101 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 8 Nov 2024 08:04:51 +0100 Subject: [PATCH 01/37] add .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc0e19c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.acton.lock +.build +out +zig-cache +zig-out From 891e802b721fd8752f29805766abff87f6f092a8 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 8 Nov 2024 08:38:19 +0100 Subject: [PATCH 02/37] create initial Channel actor --- src/ssh.act | 23 +++++++++++++++++-- src/ssh.ext.c | 61 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 13 deletions(-) mode change 100644 => 100755 src/ssh.act diff --git a/src/ssh.act b/src/ssh.act old mode 100644 new mode 100755 index a8b0268..7b6d642 --- a/src/ssh.act +++ b/src/ssh.act @@ -1,13 +1,16 @@ import net import testing + def version() -> str: """Get the libssh version""" NotImplemented + def _test_version(): testing.assertEqual("0.11.0", version()) + actor Client(cap: net.TCPConnectCap, host: str, username: str, @@ -22,10 +25,14 @@ actor Client(cap: net.TCPConnectCap, # haha, this is really a pointer :P var _ssh_session: u64 = 0 + def get_ssh_session(): + return _ssh_session + proc def _init() -> None: """Initialize the SSH client""" NotImplemented _init() + print("SSH Client connected") # action def close(on_close: action(TLSConnection) -> None) -> None: @@ -37,7 +44,6 @@ actor Client(cap: net.TCPConnectCap, # # def _connect(c): # NotImplemented -# # TODO: implement support for channels # AFAIK, all things over ssh are done via channels, so need some channel @@ -45,13 +51,23 @@ actor Client(cap: net.TCPConnectCap, # session? Prolly need some higher level wrappers for common things like # starting a shell or running a single command. SFTP / SCP would be nice too, # but for sometime in the future. Custom subsystems need to be supported too. +actor Channel(cap: Client): + """SSH Channel""" + var _ssh_channel: u64 = 0 + var _ssh_session: u64 = cap.get_ssh_session() + proc def _init() -> None: + """Initialize the SSH Channel""" + NotImplemented + _init() + + print("SSH Channel created") actor main(env): def on_connect(client: Client): - print("Connected") + print("Connected on_connect") def on_close(client: Client, error: str): print("Error", error) @@ -66,4 +82,7 @@ actor main(env): password="bar", port=2223, ) + + cc = Channel(c) + env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 2112598..754f538 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -2,6 +2,8 @@ #include // TODO: figure out how to include rts/log so we get access to log_error etc +#define LOG_ERR(msg) printf("ERR\t%s\n", (msg)) + void noop_free(void *ptr) { } @@ -19,13 +21,16 @@ void sshQ___ext_init__() { acton_gc_strdup, acton_gc_strndup); int r = ssh_init(); - printf("SSH extension initialized %d\n", r); + if (r != SSH_OK) + printf("SSH init failed (%d)\n", r); + else + printf("SSH extension successfully initialized (retval: %d)\n", r); } B_str sshQ_version () { - if (LIBSSH_VERSION_MINOR != 11) - return to$str("invalid"); - return to$str("0.11.0"); + if (LIBSSH_VERSION_MAJOR != 0 || LIBSSH_VERSION_MINOR != 11 || LIBSSH_VERSION_MICRO != 0) + return to$str("unsupported version"); + return to$str("libssh 0.11.0 supported\n"); } // TODO: crap function for test, to be replaced with something @@ -47,7 +52,7 @@ int show_remote_processes(ssh_session session) return rc; } - rc = ssh_channel_request_exec(channel, "ps aux"); + rc = ssh_channel_request_exec(channel, "uptime"); if (rc != SSH_OK) { ssh_channel_close(channel); @@ -81,22 +86,56 @@ int show_remote_processes(ssh_session session) return SSH_OK; } +$R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { + int err = 0; + ssh_channel channel = ssh_channel_new(self->_ssh_session); + if (channel == NULL) { + LOG_ERR("Failed to create SSH channel"); + printf("ssh_get_error: %s\n\n" , ssh_get_error(self->_ssh_session)); + return $R_CONT(c$cont, B_None); + } + + printf("\t%s channel: %p\n", __FUNCTION__, channel); + self->_ssh_channel = channel; + printf("\t%s self->_ssh_channel:\t%p\n", __FUNCTION__, self->_ssh_channel); + + err = ssh_channel_open_session(channel); + if (err != SSH_OK) { + printf("\t%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + if (ssh_channel_request_exec(channel, "touch /tmp/bla")) { + printf("\t%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error(self->_ssh_session)); + // ssh_channel_free(channel); + return $R_CONT(c$cont, B_None); + } + ssh_channel_send_eof(channel); + ssh_channel_close(channel); + ssh_channel_free(channel); + + return $R_CONT(c$cont, B_None); +} + $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { ssh_session session = ssh_new(); if (session == NULL) { - //log_error("Failed to create SSH session"); + LOG_ERR("Failed to create SSH session"); return $R_CONT(c$cont, B_None); } - printf("session: %p\n", session); - self->_ssh_session = toB_u64((unsigned long)session); - printf("init self->session: %p\n", self->_ssh_session); + // casting via toB_u64((unsigned long)..) leads to an invalid value in self->_ssh_session + // self->_ssh_session = toB_u64((unsigned long)session); + // instead do direct assignment + self->_ssh_session = session; + + printf("\t%s session:\t\t%p\n", __FUNCTION__, session); + printf("\t%s self->session:\t%p\n", __FUNCTION__, self->_ssh_session); ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); ssh_set_blocking(session, 1); - printf("Connecting to \n"); + printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); int rc = ssh_connect(session); if (rc != SSH_OK) { //log_error("Error connecting to SSH server: %s", ssh_get_error(session)); @@ -107,7 +146,7 @@ int show_remote_processes(ssh_session session) rc = ssh_userauth_password(session, NULL, fromB_str(self->password)); if (rc == SSH_OK) { - printf("Connected\n"); + ($action) self->on_connect; show_remote_processes(session); } else { printf("Error: %s\n", ssh_get_error(session)); From 362deb94ec331f655ad4b42b0c10635da9c71d5b Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:04:07 +0100 Subject: [PATCH 03/37] cleanup --- src/ssh.act | 5 ----- src/ssh.ext.c | 12 ++---------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 7b6d642..d12a1f6 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -6,11 +6,6 @@ def version() -> str: """Get the libssh version""" NotImplemented - -def _test_version(): - testing.assertEqual("0.11.0", version()) - - actor Client(cap: net.TCPConnectCap, host: str, username: str, diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 754f538..15d8f25 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -1,8 +1,5 @@ #include #include -// TODO: figure out how to include rts/log so we get access to log_error etc - -#define LOG_ERR(msg) printf("ERR\t%s\n", (msg)) void noop_free(void *ptr) { } @@ -27,9 +24,9 @@ void sshQ___ext_init__() { printf("SSH extension successfully initialized (retval: %d)\n", r); } -B_str sshQ_version () { +B_str sshQ_version() { if (LIBSSH_VERSION_MAJOR != 0 || LIBSSH_VERSION_MINOR != 11 || LIBSSH_VERSION_MICRO != 0) - return to$str("unsupported version"); + return to$str("unsupported version\n"); return to$str("libssh 0.11.0 supported\n"); } @@ -95,9 +92,7 @@ int show_remote_processes(ssh_session session) return $R_CONT(c$cont, B_None); } - printf("\t%s channel: %p\n", __FUNCTION__, channel); self->_ssh_channel = channel; - printf("\t%s self->_ssh_channel:\t%p\n", __FUNCTION__, self->_ssh_channel); err = ssh_channel_open_session(channel); if (err != SSH_OK) { @@ -127,9 +122,6 @@ int show_remote_processes(ssh_session session) // instead do direct assignment self->_ssh_session = session; - printf("\t%s session:\t\t%p\n", __FUNCTION__, session); - printf("\t%s self->session:\t%p\n", __FUNCTION__, self->_ssh_session); - ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); From 0bcbc9e8f40dd7f745f42452c30b740a05ac44bc Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:06:01 +0100 Subject: [PATCH 04/37] make use of 'fromB_u64()' and code alignment (4 spaces) --- src/ssh.ext.c | 70 ++++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 15d8f25..3e5da7b 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -19,15 +19,13 @@ void sshQ___ext_init__() { acton_gc_strndup); int r = ssh_init(); if (r != SSH_OK) - printf("SSH init failed (%d)\n", r); + printf("SSH init failed (%d)\n", r); else - printf("SSH extension successfully initialized (retval: %d)\n", r); + printf("SSH extension successfully initialized (retval: %d)\n", r); } B_str sshQ_version() { - if (LIBSSH_VERSION_MAJOR != 0 || LIBSSH_VERSION_MINOR != 11 || LIBSSH_VERSION_MICRO != 0) - return to$str("unsupported version\n"); - return to$str("libssh 0.11.0 supported\n"); + return to$str("libssh 0.11.0\n"); } // TODO: crap function for test, to be replaced with something @@ -40,40 +38,40 @@ int show_remote_processes(ssh_session session) channel = ssh_channel_new(session); if (channel == NULL) - return SSH_ERROR; + return SSH_ERROR; rc = ssh_channel_open_session(channel); if (rc != SSH_OK) { - ssh_channel_free(channel); - return rc; + ssh_channel_free(channel); + return rc; } rc = ssh_channel_request_exec(channel, "uptime"); if (rc != SSH_OK) { - ssh_channel_close(channel); - ssh_channel_free(channel); - return rc; + ssh_channel_close(channel); + ssh_channel_free(channel); + return rc; } nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); while (nbytes > 0) { - if (write(1, buffer, nbytes) != (unsigned int) nbytes) - { - ssh_channel_close(channel); - ssh_channel_free(channel); - return SSH_ERROR; - } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); + if (write(1, buffer, nbytes) != (unsigned int) nbytes) + { + ssh_channel_close(channel); + ssh_channel_free(channel); + return SSH_ERROR; + } + nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); } if (nbytes < 0) { - ssh_channel_close(channel); - ssh_channel_free(channel); - return SSH_ERROR; + ssh_channel_close(channel); + ssh_channel_free(channel); + return SSH_ERROR; } ssh_channel_send_eof(channel); @@ -85,10 +83,9 @@ int show_remote_processes(ssh_session session) $R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; - ssh_channel channel = ssh_channel_new(self->_ssh_session); + ssh_channel channel = ssh_channel_new((struct ssh_session_struct *)fromB_u64(self->_ssh_session)); if (channel == NULL) { - LOG_ERR("Failed to create SSH channel"); - printf("ssh_get_error: %s\n\n" , ssh_get_error(self->_ssh_session)); + printf("Failed to create SSH channel. ssh_get_error: %s\n\n" , ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); return $R_CONT(c$cont, B_None); } @@ -100,7 +97,7 @@ int show_remote_processes(ssh_session session) return $R_CONT(c$cont, B_None); } if (ssh_channel_request_exec(channel, "touch /tmp/bla")) { - printf("\t%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error(self->_ssh_session)); + printf("\t%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); // ssh_channel_free(channel); return $R_CONT(c$cont, B_None); } @@ -114,23 +111,28 @@ int show_remote_processes(ssh_session session) $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { ssh_session session = ssh_new(); if (session == NULL) { - LOG_ERR("Failed to create SSH session"); + printf("Failed to create SSH session\n"); return $R_CONT(c$cont, B_None); } - // casting via toB_u64((unsigned long)..) leads to an invalid value in self->_ssh_session - // self->_ssh_session = toB_u64((unsigned long)session); - // instead do direct assignment - self->_ssh_session = session; - ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); - ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); - ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); + self->_ssh_session = toB_u64((unsigned long)session); + + int err = 0; + err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); + if (err < 0) + printf("Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", err); + err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); + if (err < 0) + printf("Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", err); + err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); + if (err < 0) + printf("Error setting SSH option 'SSH_OPTIONS_USER': %d\n", err); ssh_set_blocking(session, 1); printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); int rc = ssh_connect(session); if (rc != SSH_OK) { - //log_error("Error connecting to SSH server: %s", ssh_get_error(session)); + printf("Error connecting to SSH server: %s\n", ssh_get_error(session)); $action2 f = ($action2) self->on_close; f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); return $R_CONT(c$cont, B_None); From a183d4a746de12e5c83506050b6c5e48a7de6378 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:06:22 +0100 Subject: [PATCH 05/37] initialize variables with 0 --- src/ssh.ext.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 3e5da7b..e7b3bc7 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -31,10 +31,10 @@ B_str sshQ_version() { // TODO: crap function for test, to be replaced with something int show_remote_processes(ssh_session session) { - ssh_channel channel; - int rc; - char buffer[256]; - int nbytes; + ssh_channel channel = { 0 }; + char buffer[256] = { 0 }; + int rc = 0; + int nbytes = 0; channel = ssh_channel_new(session); if (channel == NULL) From ab8f27fdf90faee94a7966f2afec3b6b73a78016 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:07:40 +0100 Subject: [PATCH 06/37] avoid using magic numbers --- src/ssh.ext.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index e7b3bc7..6ad4b14 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -58,7 +58,7 @@ int show_remote_processes(ssh_session session) nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); while (nbytes > 0) { - if (write(1, buffer, nbytes) != (unsigned int) nbytes) + if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) { ssh_channel_close(channel); ssh_channel_free(channel); From e41fd92a8ef2604bed05ca6b5ee06f87bbdecd76 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:22:37 +0100 Subject: [PATCH 07/37] further cleanup and added more error handling --- src/ssh.act | 2 +- src/ssh.ext.c | 41 +++++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index d12a1f6..9870d1d 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -3,7 +3,7 @@ import testing def version() -> str: - """Get the libssh version""" + """Get the acton-ssh version""" NotImplemented actor Client(cap: net.TCPConnectCap, diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 6ad4b14..9e7fbd3 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -25,11 +25,11 @@ void sshQ___ext_init__() { } B_str sshQ_version() { - return to$str("libssh 0.11.0\n"); + return to$str("0.1.0"); } // TODO: crap function for test, to be replaced with something -int show_remote_processes(ssh_session session) +int show_remote_load(ssh_session session) { ssh_channel channel = { 0 }; char buffer[256] = { 0 }; @@ -109,6 +109,7 @@ int show_remote_processes(ssh_session session) } $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { + int err = 0; ssh_session session = ssh_new(); if (session == NULL) { printf("Failed to create SSH session\n"); @@ -117,37 +118,45 @@ int show_remote_processes(ssh_session session) self->_ssh_session = toB_u64((unsigned long)session); - int err = 0; err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); - if (err < 0) + if (err < 0) { printf("Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", err); + return $R_CONT(c$cont, B_None); + } err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); - if (err < 0) + if (err < 0) { printf("Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", err); + return $R_CONT(c$cont, B_None); + } err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); - if (err < 0) + if (err < 0) { printf("Error setting SSH option 'SSH_OPTIONS_USER': %d\n", err); + return $R_CONT(c$cont, B_None); + } ssh_set_blocking(session, 1); printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); - int rc = ssh_connect(session); - if (rc != SSH_OK) { + err = ssh_connect(session); + if (err != SSH_OK) { printf("Error connecting to SSH server: %s\n", ssh_get_error(session)); $action2 f = ($action2) self->on_close; f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); return $R_CONT(c$cont, B_None); } - rc = ssh_userauth_password(session, NULL, fromB_str(self->password)); - if (rc == SSH_OK) { - ($action) self->on_connect; - show_remote_processes(session); - } else { + err = ssh_userauth_password(session, NULL, fromB_str(self->password)); + if (err != SSH_OK) { printf("Error: %s\n", ssh_get_error(session)); } -// self->_connected = true; -// $action f = ($action) self->on_connect; -// f->$class->__asyn__(f, self); + $action f = ($action) self->on_connect; + f->$class->__asyn__(f, self); + + err = show_remote_load(session); + if (err != SSH_OK) { + printf("Error setting SSH option 'SSH_OPTIONS_USER': %d\n", err); + return $R_CONT(c$cont, B_None); + } + return $R_CONT(c$cont, B_None); } From dcf7423c4bf37341e581265a81fa4bb04ddf47b2 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:30:04 +0100 Subject: [PATCH 08/37] add wrappers for cleanup functions --- src/ssh.ext.c | 85 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 9e7fbd3..0dff8ae 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -1,5 +1,24 @@ #include #include +#include + +void ssh_close_free(ssh_channel channel) { + int err = ssh_channel_close(channel); + if (err != SSH_OK) + { + printf("%s ssh_channel_close() error (%d)\n", __FUNCTION__, err); + } + ssh_channel_free(channel); +} + +void ssh_close_free_eof(ssh_channel channel) { + int err = ssh_channel_send_eof(channel); + if (err != SSH_OK) + { + printf("%s ssh_channel_send_eof() error (%d)\n", __FUNCTION__, err); + } + ssh_close_free(channel); +} void noop_free(void *ptr) { } @@ -50,8 +69,7 @@ int show_remote_load(ssh_session session) rc = ssh_channel_request_exec(channel, "uptime"); if (rc != SSH_OK) { - ssh_channel_close(channel); - ssh_channel_free(channel); + ssh_close_free(channel); return rc; } @@ -60,8 +78,7 @@ int show_remote_load(ssh_session session) { if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) { - ssh_channel_close(channel); - ssh_channel_free(channel); + ssh_close_free(channel); return SSH_ERROR; } nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); @@ -69,14 +86,11 @@ int show_remote_load(ssh_session session) if (nbytes < 0) { - ssh_channel_close(channel); - ssh_channel_free(channel); + ssh_close_free(channel); return SSH_ERROR; } - ssh_channel_send_eof(channel); - ssh_channel_close(channel); - ssh_channel_free(channel); + ssh_close_free_eof(channel); return SSH_OK; } @@ -84,20 +98,24 @@ int show_remote_load(ssh_session session) $R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; ssh_channel channel = ssh_channel_new((struct ssh_session_struct *)fromB_u64(self->_ssh_session)); - if (channel == NULL) { - printf("Failed to create SSH channel. ssh_get_error: %s\n\n" , ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); + if (channel == NULL) + { + printf("%s Failed to create SSH channel. ssh_get_error: %s\n\n", __FUNCTION__, ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); return $R_CONT(c$cont, B_None); } self->_ssh_channel = channel; err = ssh_channel_open_session(channel); - if (err != SSH_OK) { - printf("\t%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); + if (err != SSH_OK) + { + printf("%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - if (ssh_channel_request_exec(channel, "touch /tmp/bla")) { - printf("\t%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); + + if (ssh_channel_request_exec(channel, "touch /tmp/bla")) + { + printf("%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); // ssh_channel_free(channel); return $R_CONT(c$cont, B_None); } @@ -111,50 +129,59 @@ int show_remote_load(ssh_session session) $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { int err = 0; ssh_session session = ssh_new(); - if (session == NULL) { - printf("Failed to create SSH session\n"); + if (session == NULL) + { + printf("%s Failed to create SSH session\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } self->_ssh_session = toB_u64((unsigned long)session); err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); - if (err < 0) { - printf("Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", err); + if (err < 0) + { + printf("%s Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } + err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); - if (err < 0) { - printf("Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", err); + if (err < 0) + { + printf("%s Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } + err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); - if (err < 0) { - printf("Error setting SSH option 'SSH_OPTIONS_USER': %d\n", err); + if (err < 0) + { + printf("%s Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } ssh_set_blocking(session, 1); printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); err = ssh_connect(session); - if (err != SSH_OK) { - printf("Error connecting to SSH server: %s\n", ssh_get_error(session)); + if (err != SSH_OK) + { + printf("%s Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); $action2 f = ($action2) self->on_close; f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); return $R_CONT(c$cont, B_None); } err = ssh_userauth_password(session, NULL, fromB_str(self->password)); - if (err != SSH_OK) { - printf("Error: %s\n", ssh_get_error(session)); + if (err != SSH_OK) + { + printf("%s Error: %s\n", __FUNCTION__, ssh_get_error(session)); } $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); err = show_remote_load(session); - if (err != SSH_OK) { - printf("Error setting SSH option 'SSH_OPTIONS_USER': %d\n", err); + if (err != SSH_OK) + { + printf("%s Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } From 956d1d10549258500f0ff28e5c4ecb5d800fc76b Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 16 Jan 2025 14:42:36 +0100 Subject: [PATCH 09/37] added more error handling --- src/ssh.act | 4 +++- src/ssh.ext.c | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 9870d1d..ab41ae8 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -6,6 +6,7 @@ def version() -> str: """Get the acton-ssh version""" NotImplemented + actor Client(cap: net.TCPConnectCap, host: str, username: str, @@ -40,6 +41,7 @@ actor Client(cap: net.TCPConnectCap, # def _connect(c): # NotImplemented + # TODO: implement support for channels # AFAIK, all things over ssh are done via channels, so need some channel # primitive, maybe an actor per channel that then multiplexes into the Client @@ -67,7 +69,7 @@ actor main(env): def on_close(client: Client, error: str): print("Error", error) - print(version()) + # print(version()) c = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 0dff8ae..772ed53 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -40,7 +41,7 @@ void sshQ___ext_init__() { if (r != SSH_OK) printf("SSH init failed (%d)\n", r); else - printf("SSH extension successfully initialized (retval: %d)\n", r); + printf("SSH extension successfully initialized\n"); } B_str sshQ_version() { @@ -56,12 +57,15 @@ int show_remote_load(ssh_session session) int nbytes = 0; channel = ssh_channel_new(session); - if (channel == NULL) + if (channel == NULL) { + printf("%s ssh_channel_new error (NULL)", __FUNCTION__); return SSH_ERROR; + } rc = ssh_channel_open_session(channel); if (rc != SSH_OK) { + printf("%s ssh_channel_open_session error (%d)", __FUNCTION__, rc); ssh_channel_free(channel); return rc; } @@ -69,6 +73,7 @@ int show_remote_load(ssh_session session) rc = ssh_channel_request_exec(channel, "uptime"); if (rc != SSH_OK) { + printf("%s ssh_channel_request_exec error (%d)", __FUNCTION__, rc); ssh_close_free(channel); return rc; } @@ -78,6 +83,7 @@ int show_remote_load(ssh_session session) { if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) { + printf("%s write() error (bytes written not matching expectation)", __FUNCTION__); ssh_close_free(channel); return SSH_ERROR; } @@ -86,12 +92,12 @@ int show_remote_load(ssh_session session) if (nbytes < 0) { + printf("%s write() error (%d)", __FUNCTION__, errno); ssh_close_free(channel); return SSH_ERROR; } ssh_close_free_eof(channel); - return SSH_OK; } @@ -116,13 +122,11 @@ int show_remote_load(ssh_session session) if (ssh_channel_request_exec(channel, "touch /tmp/bla")) { printf("%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); - // ssh_channel_free(channel); + ssh_channel_free(channel); return $R_CONT(c$cont, B_None); } - ssh_channel_send_eof(channel); - ssh_channel_close(channel); - ssh_channel_free(channel); + ssh_close_free_eof(channel); return $R_CONT(c$cont, B_None); } @@ -160,6 +164,7 @@ int show_remote_load(ssh_session session) ssh_set_blocking(session, 1); printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); + err = ssh_connect(session); if (err != SSH_OK) { @@ -172,7 +177,8 @@ int show_remote_load(ssh_session session) err = ssh_userauth_password(session, NULL, fromB_str(self->password)); if (err != SSH_OK) { - printf("%s Error: %s\n", __FUNCTION__, ssh_get_error(session)); + printf("%s ssh_userauth_password error: %s\n", __FUNCTION__, ssh_get_error(session)); + return $R_CONT(c$cont, B_None); } $action f = ($action) self->on_connect; @@ -181,7 +187,7 @@ int show_remote_load(ssh_session session) err = show_remote_load(session); if (err != SSH_OK) { - printf("%s Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); + printf("%s show_remote_load error: %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } From 954cf91e37730ad5f39e55adfdc92f4c80b3f506 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 23 Jan 2025 14:28:21 +0100 Subject: [PATCH 10/37] add capability of sending netconf payload to netconf server --- src/ssh.act | 11 +++- src/ssh.ext.c | 178 ++++++++++++++++++++++++++++---------------------- 2 files changed, 109 insertions(+), 80 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index ab41ae8..4ebe56f 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -13,7 +13,7 @@ actor Client(cap: net.TCPConnectCap, on_connect: action(Client) -> None, on_close: action(Client, str) -> None, key: ?str=None, - password: ?str=None, + password: str, port: u16=22, ): """SSH Client""" @@ -24,6 +24,9 @@ actor Client(cap: net.TCPConnectCap, def get_ssh_session(): return _ssh_session + def get_password() -> str: + return password + proc def _init() -> None: """Initialize the SSH client""" NotImplemented @@ -48,11 +51,13 @@ actor Client(cap: net.TCPConnectCap, # session? Prolly need some higher level wrappers for common things like # starting a shell or running a single command. SFTP / SCP would be nice too, # but for sometime in the future. Custom subsystems need to be supported too. -actor Channel(cap: Client): +actor Channel(cap: Client, + nc_payload: str): """SSH Channel""" var _ssh_channel: u64 = 0 var _ssh_session: u64 = cap.get_ssh_session() + var _password: str = cap.get_password() proc def _init() -> None: """Initialize the SSH Channel""" @@ -80,6 +85,6 @@ actor main(env): port=2223, ) - cc = Channel(c) + cc = Channel(c, 'urn:ietf:params:netconf:base:1.0]]>]]>') env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 772ed53..f694c66 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -1,7 +1,10 @@ #include #include -#include #include +#include +#include + +#define USLEEP_100MS 100 void ssh_close_free(ssh_channel channel) { int err = ssh_channel_close(channel); @@ -48,86 +51,47 @@ B_str sshQ_version() { return to$str("0.1.0"); } -// TODO: crap function for test, to be replaced with something -int show_remote_load(ssh_session session) +/** + * @brief Send netconf payload + * + * @param[in] channel ssh_channel + * @param[in] payload The Netconf Payload, for example the hello message + */ +int send_nc_payload(ssh_channel channel, const char *payload) { - ssh_channel channel = { 0 }; - char buffer[256] = { 0 }; - int rc = 0; - int nbytes = 0; - - channel = ssh_channel_new(session); - if (channel == NULL) { - printf("%s ssh_channel_new error (NULL)", __FUNCTION__); - return SSH_ERROR; - } - - rc = ssh_channel_open_session(channel); - if (rc != SSH_OK) - { - printf("%s ssh_channel_open_session error (%d)", __FUNCTION__, rc); - ssh_channel_free(channel); - return rc; - } - - rc = ssh_channel_request_exec(channel, "uptime"); - if (rc != SSH_OK) - { - printf("%s ssh_channel_request_exec error (%d)", __FUNCTION__, rc); - ssh_close_free(channel); - return rc; - } - - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); - while (nbytes > 0) - { - if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) - { - printf("%s write() error (bytes written not matching expectation)", __FUNCTION__); - ssh_close_free(channel); - return SSH_ERROR; - } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); - } - - if (nbytes < 0) - { - printf("%s write() error (%d)", __FUNCTION__, errno); - ssh_close_free(channel); - return SSH_ERROR; - } - - ssh_close_free_eof(channel); - return SSH_OK; -} + char buffer[256] = { 0 }; + int rc = 0; + int nbytes = 0; -$R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { - int err = 0; - ssh_channel channel = ssh_channel_new((struct ssh_session_struct *)fromB_u64(self->_ssh_session)); - if (channel == NULL) + rc = ssh_channel_write(channel, payload, strlen(payload) + 1); + if (rc == SSH_ERROR) { - printf("%s Failed to create SSH channel. ssh_get_error: %s\n\n", __FUNCTION__, ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); - return $R_CONT(c$cont, B_None); + printf("%s ssh_channel_write error (%d)\n", __FUNCTION__, rc); + ssh_close_free(channel); + return rc; } - self->_ssh_channel = channel; - - err = ssh_channel_open_session(channel); - if (err != SSH_OK) + nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); + while (nbytes > 0) { - printf("%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) + { + printf("%s write() error (bytes written not matching expectation)\n", __FUNCTION__); + ssh_close_free(channel); + return SSH_ERROR; + } + nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); } - if (ssh_channel_request_exec(channel, "touch /tmp/bla")) + if (nbytes < 0) { - printf("%s Error executing '%s' : %s\n", __FUNCTION__, "touch /tmp/bla", ssh_get_error((struct ssh_session_struct *)fromB_u64(self->_ssh_session))); - ssh_channel_free(channel); - return $R_CONT(c$cont, B_None); + printf("%s write() error (%d)\n", __FUNCTION__, errno); + ssh_close_free(channel); + return SSH_ERROR; } ssh_close_free_eof(channel); - return $R_CONT(c$cont, B_None); + return SSH_OK; } $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { @@ -162,34 +126,94 @@ int show_remote_load(ssh_session session) return $R_CONT(c$cont, B_None); } + // ssh_set_blocking(session, 1); + // printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); + // + // err = ssh_connect(session); + // if (err != SSH_OK) + // { + // printf("%s Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); + // $action2 f = ($action2) self->on_close; + // f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); + // return $R_CONT(c$cont, B_None); + // } + + // err = ssh_userauth_password(session, NULL, fromB_str(self->password)); + // if (err != SSH_OK) + // { + // printf("%s ssh_userauth_password error: %s\n", __FUNCTION__, ssh_get_error(session)); + // return $R_CONT(c$cont, B_None); + // } + + $action f = ($action) self->on_connect; + f->$class->__asyn__(f, self); + + return $R_CONT(c$cont, B_None); +} + +$R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { + int err = 0; + int timeout = 2000000; // microseconds: 2 seconds + ssh_session session = { 0 }; + ssh_channel channel = { 0 }; + + session = (struct ssh_session_struct *)fromB_u64(self->_ssh_session); + ssh_set_blocking(session, 1); - printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); + printf("Connecting to SSH server\n"); err = ssh_connect(session); if (err != SSH_OK) { printf("%s Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); - $action2 f = ($action2) self->on_close; - f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); return $R_CONT(c$cont, B_None); } - err = ssh_userauth_password(session, NULL, fromB_str(self->password)); + err = ssh_userauth_password(session, NULL, (const char *)fromB_str(self->_password)); if (err != SSH_OK) { printf("%s ssh_userauth_password error: %s\n", __FUNCTION__, ssh_get_error(session)); return $R_CONT(c$cont, B_None); } - $action f = ($action) self->on_connect; - f->$class->__asyn__(f, self); + channel = ssh_channel_new(session); + if (channel == NULL) + { + printf("%s Failed to create SSH channel\n", __FUNCTION__); + return $R_CONT(c$cont, B_None); + } - err = show_remote_load(session); + err = ssh_channel_open_session(channel); if (err != SSH_OK) { - printf("%s show_remote_load error: %d\n", __FUNCTION__, err); + printf("%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } + self->_ssh_channel = toB_u64((unsigned long)channel); + + while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN) + { + err = usleep(100); + if (err) { + printf("usleep() error '%s' (%d)\n", strerror(errno), errno); + return $R_CONT(c$cont, B_None); + } + timeout += USLEEP_100MS; + } + if (err != SSH_OK) + { + printf("%s Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + + err = send_nc_payload(channel, fromB_str(self->nc_payload)); + if (err != SSH_OK) + { + printf("%s send_nc_payload error: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + + ssh_close_free_eof(channel); return $R_CONT(c$cont, B_None); } From 4297dc3243c10849b4c09a6f0ca30c06788155eb Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 23 Jan 2025 14:32:58 +0100 Subject: [PATCH 11/37] make it clear that 'client' capability ref is taken --- src/ssh.act | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 4ebe56f..954bbb8 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -51,13 +51,13 @@ actor Client(cap: net.TCPConnectCap, # session? Prolly need some higher level wrappers for common things like # starting a shell or running a single command. SFTP / SCP would be nice too, # but for sometime in the future. Custom subsystems need to be supported too. -actor Channel(cap: Client, +actor Channel(client: Client, nc_payload: str): """SSH Channel""" var _ssh_channel: u64 = 0 - var _ssh_session: u64 = cap.get_ssh_session() - var _password: str = cap.get_password() + var _ssh_session: u64 = client.get_ssh_session() + var _password: str = client.get_password() proc def _init() -> None: """Initialize the SSH Channel""" From d160abb9fa7557fb31cbd51ff757f1f3ce412441 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 28 Feb 2025 12:59:53 +0100 Subject: [PATCH 12/37] improve sending of data via ssh --- src/ssh.act | 39 ++++++++++++++---- src/ssh.ext.c | 108 ++++++++++++++++++++++++++------------------------ 2 files changed, 87 insertions(+), 60 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 954bbb8..8e4ff1b 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -1,5 +1,4 @@ import net -import testing def version() -> str: @@ -15,6 +14,7 @@ actor Client(cap: net.TCPConnectCap, key: ?str=None, password: str, port: u16=22, + subsystem: str ): """SSH Client""" @@ -27,12 +27,15 @@ actor Client(cap: net.TCPConnectCap, def get_password() -> str: return password + def get_subsystem() -> str: + return subsystem + proc def _init() -> None: """Initialize the SSH client""" NotImplemented _init() - print("SSH Client connected") + # print("SSH Client connected") # action def close(on_close: action(TLSConnection) -> None) -> None: # """Close the connection""" @@ -52,29 +55,40 @@ actor Client(cap: net.TCPConnectCap, # starting a shell or running a single command. SFTP / SCP would be nice too, # but for sometime in the future. Custom subsystems need to be supported too. actor Channel(client: Client, - nc_payload: str): + payload: ?str): """SSH Channel""" var _ssh_channel: u64 = 0 var _ssh_session: u64 = client.get_ssh_session() var _password: str = client.get_password() + var _subsystem: str = client.get_subsystem() proc def _init() -> None: """Initialize the SSH Channel""" NotImplemented _init() - print("SSH Channel created") + # print("SSH Channel created") + + def setPayload(p: str): + payload = p + + def sendPayload() -> None: + """Send payload""" + NotImplemented actor main(env): def on_connect(client: Client): - print("Connected on_connect") + # print("Client connected") + return def on_close(client: Client, error: str): - print("Error", error) + # print("Connection closed", error) + return # print(version()) + c = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", @@ -82,9 +96,18 @@ actor main(env): on_connect, on_close, password="bar", - port=2223, + port=830, + subsystem="netconf", ) - cc = Channel(c, 'urn:ietf:params:netconf:base:1.0]]>]]>') + cc = Channel(c) + + print("setting payload\n") + cc.setPayload('urn:ietf:params:netconf:base:1.0]]>]]>') + print("sending payload...\n") + cc.sendPayload() + + print("sending payload again...\n") + cc.sendPayload() env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index f694c66..e75b50d 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -4,13 +4,18 @@ #include #include -#define USLEEP_100MS 100 +#ifndef DEBUG_MODE +//#define DEBUG_MODE // uncomment for pretty prints +#endif + +#define TIMEOUT 1500000 // microseconds: 1.5 seconds +#define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds void ssh_close_free(ssh_channel channel) { int err = ssh_channel_close(channel); if (err != SSH_OK) { - printf("%s ssh_channel_close() error (%d)\n", __FUNCTION__, err); + printf("%s: ssh_channel_close() error (%d)\n", __FUNCTION__, err); } ssh_channel_free(channel); } @@ -19,7 +24,7 @@ void ssh_close_free_eof(ssh_channel channel) { int err = ssh_channel_send_eof(channel); if (err != SSH_OK) { - printf("%s ssh_channel_send_eof() error (%d)\n", __FUNCTION__, err); + printf("%s: ssh_channel_send_eof() error (%d)\n", __FUNCTION__, err); } ssh_close_free(channel); } @@ -43,8 +48,10 @@ void sshQ___ext_init__() { int r = ssh_init(); if (r != SSH_OK) printf("SSH init failed (%d)\n", r); +#ifdef DEBUG_MODE else printf("SSH extension successfully initialized\n"); +#endif } B_str sshQ_version() { @@ -59,14 +66,14 @@ B_str sshQ_version() { */ int send_nc_payload(ssh_channel channel, const char *payload) { - char buffer[256] = { 0 }; + char buffer[4096] = { 0 }; int rc = 0; int nbytes = 0; rc = ssh_channel_write(channel, payload, strlen(payload) + 1); if (rc == SSH_ERROR) { - printf("%s ssh_channel_write error (%d)\n", __FUNCTION__, rc); + printf("%s: ssh_channel_write() error (%d)\n", __FUNCTION__, rc); ssh_close_free(channel); return rc; } @@ -76,16 +83,20 @@ int send_nc_payload(ssh_channel channel, const char *payload) { if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) { - printf("%s write() error (bytes written not matching expectation)\n", __FUNCTION__); + printf("%s: write() error (bytes written not matching expectation)\n", __FUNCTION__); ssh_close_free(channel); return SSH_ERROR; } + // find end of netconf reply + if (strstr(buffer, "]]>]]>")) { + break; + } nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); } if (nbytes < 0) { - printf("%s write() error (%d)\n", __FUNCTION__, errno); + printf("%s: ssh_channel_read() error (%d)\n", __FUNCTION__, errno); ssh_close_free(channel); return SSH_ERROR; } @@ -99,7 +110,7 @@ int send_nc_payload(ssh_channel channel, const char *payload) ssh_session session = ssh_new(); if (session == NULL) { - printf("%s Failed to create SSH session\n", __FUNCTION__); + printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } @@ -108,43 +119,24 @@ int send_nc_payload(ssh_channel channel, const char *payload) err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); if (err < 0) { - printf("%s Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); + printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); if (err < 0) { - printf("%s Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); + printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); if (err < 0) { - printf("%s Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); + printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - // ssh_set_blocking(session, 1); - // printf("Connecting to SSH server '%s'\n", fromB_str(self->host)); - // - // err = ssh_connect(session); - // if (err != SSH_OK) - // { - // printf("%s Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); - // $action2 f = ($action2) self->on_close; - // f->$class->__asyn__(f, self, to$str(ssh_get_error(session))); - // return $R_CONT(c$cont, B_None); - // } - - // err = ssh_userauth_password(session, NULL, fromB_str(self->password)); - // if (err != SSH_OK) - // { - // printf("%s ssh_userauth_password error: %s\n", __FUNCTION__, ssh_get_error(session)); - // return $R_CONT(c$cont, B_None); - // } - $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); @@ -153,65 +145,77 @@ int send_nc_payload(ssh_channel channel, const char *payload) $R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; - int timeout = 2000000; // microseconds: 2 seconds ssh_session session = { 0 }; ssh_channel channel = { 0 }; session = (struct ssh_session_struct *)fromB_u64(self->_ssh_session); ssh_set_blocking(session, 1); + +#ifdef DEBUG_MODE printf("Connecting to SSH server\n"); +#endif err = ssh_connect(session); if (err != SSH_OK) { - printf("%s Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); + printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); return $R_CONT(c$cont, B_None); } err = ssh_userauth_password(session, NULL, (const char *)fromB_str(self->_password)); if (err != SSH_OK) { - printf("%s ssh_userauth_password error: %s\n", __FUNCTION__, ssh_get_error(session)); + printf("%s: ssh_userauth_password() error: %s\n", __FUNCTION__, ssh_get_error(session)); return $R_CONT(c$cont, B_None); } channel = ssh_channel_new(session); if (channel == NULL) { - printf("%s Failed to create SSH channel\n", __FUNCTION__); + printf("%s: ssh_channel_new() Failed to create SSH channel\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } err = ssh_channel_open_session(channel); if (err != SSH_OK) { - printf("%s ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); + printf("%s: ssh_channel_open_session() ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } self->_ssh_channel = toB_u64((unsigned long)channel); - while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN) - { - err = usleep(100); - if (err) { - printf("usleep() error '%s' (%d)\n", strerror(errno), errno); + return $R_CONT(c$cont, B_None); +} + +$R sshQ_ChannelD_sendPayloadG_local (sshQ_Channel self, $Cont c$cont) { + int err = 0; + int timeout = TIMEOUT; + ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); + + if (self->_subsystem && !strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { + while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN && timeout > 0) + { + err = usleep(USLEEP_INTERVAL); + if (err) { + printf("%s: usleep() error '%s' (%d)\n", __FUNCTION__, strerror(errno), errno); + return $R_CONT(c$cont, B_None); + } + timeout -= USLEEP_INTERVAL; + } + if (err != SSH_OK) + { + printf("%s: ssh_channel_request_subsystem() Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - timeout += USLEEP_100MS; - } - if (err != SSH_OK) - { - printf("%s Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } - err = send_nc_payload(channel, fromB_str(self->nc_payload)); - if (err != SSH_OK) - { - printf("%s send_nc_payload error: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + err = send_nc_payload(channel, (const char *)fromB_str(self->payload)); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } } ssh_close_free_eof(channel); From 27b22581c9426341ec62ab801c8e1c094ca271ce Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 28 Feb 2025 13:00:56 +0100 Subject: [PATCH 13/37] add affiinity and return the retreived string to caller --- src/ssh.act | 29 ++++++++++++----- src/ssh.ext.c | 87 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 8e4ff1b..ae0bb40 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -18,6 +18,10 @@ actor Client(cap: net.TCPConnectCap, ): """SSH Client""" + proc def _pin_affinity() -> None: + NotImplemented + _pin_affinity() + # haha, this is really a pointer :P var _ssh_session: u64 = 0 @@ -47,6 +51,10 @@ actor Client(cap: net.TCPConnectCap, # def _connect(c): # NotImplemented + proc def disconnect() -> None: + """Disconnect the SSH client""" + NotImplemented + # TODO: implement support for channels # AFAIK, all things over ssh are done via channels, so need some channel @@ -54,14 +62,18 @@ actor Client(cap: net.TCPConnectCap, # session? Prolly need some higher level wrappers for common things like # starting a shell or running a single command. SFTP / SCP would be nice too, # but for sometime in the future. Custom subsystems need to be supported too. -actor Channel(client: Client, - payload: ?str): +actor Channel(client: Client): """SSH Channel""" var _ssh_channel: u64 = 0 var _ssh_session: u64 = client.get_ssh_session() var _password: str = client.get_password() var _subsystem: str = client.get_subsystem() + var payload = None + + proc def _pin_affinity() -> None: + NotImplemented + _pin_affinity() proc def _init() -> None: """Initialize the SSH Channel""" @@ -73,7 +85,7 @@ actor Channel(client: Client, def setPayload(p: str): payload = p - def sendPayload() -> None: + def sendPayload() -> str: """Send payload""" NotImplemented @@ -101,13 +113,14 @@ actor main(env): ) cc = Channel(c) + reply = None - print("setting payload\n") + print("setting payload") cc.setPayload('urn:ietf:params:netconf:base:1.0]]>]]>') - print("sending payload...\n") - cc.sendPayload() + print("sending payload") + buf = cc.sendPayload() + print(buf) - print("sending payload again...\n") - cc.sendPayload() + c.disconnect() env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index e75b50d..2a1a7d2 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -3,11 +3,13 @@ #include #include #include +#include #ifndef DEBUG_MODE -//#define DEBUG_MODE // uncomment for pretty prints +#define DEBUG_MODE /* uncomment for pretty prints */ #endif +#define BUF_SIZE 65536 #define TIMEOUT 1500000 // microseconds: 1.5 seconds #define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds @@ -58,17 +60,29 @@ B_str sshQ_version() { return to$str("0.1.0"); } +$R sshQ_ClientD__pin_affinityG_local (sshQ_Client self, $Cont c$cont) { + pin_actor_affinity(); + return $R_CONT(c$cont, B_None); +} + +$R sshQ_ChannelD__pin_affinityG_local (sshQ_Channel self, $Cont c$cont) { + pin_actor_affinity(); + return $R_CONT(c$cont, B_None); +} + /** * @brief Send netconf payload * * @param[in] channel ssh_channel * @param[in] payload The Netconf Payload, for example the hello message */ -int send_nc_payload(ssh_channel channel, const char *payload) +int send_nc_payload(ssh_channel channel, const char *payload, char *buf) { - char buffer[4096] = { 0 }; int rc = 0; int nbytes = 0; + size_t buflen = 0; + size_t len = 0; + char tmp[1024] = {0}; rc = ssh_channel_write(channel, payload, strlen(payload) + 1); if (rc == SSH_ERROR) @@ -78,20 +92,49 @@ int send_nc_payload(ssh_channel channel, const char *payload) return rc; } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); + nbytes = ssh_channel_read(channel, tmp, sizeof(tmp), 0); while (nbytes > 0) { - if (write(STDOUT_FILENO, buffer, nbytes) != (unsigned int) nbytes) +#ifdef DEBUG_MODE + printf("\n\nstrlen buf: %u\n", strlen(buf)); + printf("strlen tmp: %u\n", strlen(tmp)); + printf("sizeof tmp: %u\n", sizeof(tmp)); +#endif + // sometimes the string tmp has invalid characters from the 1024th element + if (strlen(tmp) > sizeof(tmp)) { + tmp[1024] = '\0'; + } + + // append string + len = snprintf(buf + buflen, BUF_SIZE - buflen, "%s", tmp); + buflen = strlen(buf); + +#ifdef DEBUG_MODE + // TODO: sometimes the strlen() of buf is lower than in the previous iteration + // which breaks the appending and the result is invalid + printf("strlen buffer after snprintf: %u\n\n", buflen); +#endif + if (len > BUF_SIZE) + { + printf("%s: snprintf() error %u\n", __FUNCTION__, buflen); + ssh_close_free(channel); + return SSH_ERROR; + } +#ifdef DEBUG_MODE + if (write(STDOUT_FILENO, tmp, nbytes) != (unsigned int) nbytes) { printf("%s: write() error (bytes written not matching expectation)\n", __FUNCTION__); ssh_close_free(channel); return SSH_ERROR; } + fflush(stdout); +#endif // find end of netconf reply - if (strstr(buffer, "]]>]]>")) { - break; + if (strstr(tmp, "]]>]]>")) { + return SSH_OK; } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); + memset(tmp, 0, sizeof(tmp)); + nbytes = ssh_channel_read(channel, tmp, sizeof(tmp), 0); } if (nbytes < 0) @@ -101,11 +144,14 @@ int send_nc_payload(ssh_channel channel, const char *payload) return SSH_ERROR; } - ssh_close_free_eof(channel); return SSH_OK; } +// Client + $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { + pin_actor_affinity(); + int err = 0; ssh_session session = ssh_new(); if (session == NULL) @@ -143,7 +189,16 @@ int send_nc_payload(ssh_channel channel, const char *payload) return $R_CONT(c$cont, B_None); } +$R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { + ssh_disconnect((ssh_session)fromB_u64(self->_ssh_session)); + return $R_CONT(c$cont, B_None); +} + +// Channel + $R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { + pin_actor_affinity(); + int err = 0; ssh_session session = { 0 }; ssh_channel channel = { 0 }; @@ -192,6 +247,7 @@ int send_nc_payload(ssh_channel channel, const char *payload) $R sshQ_ChannelD_sendPayloadG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; int timeout = TIMEOUT; + char *buffer = (char*)acton_calloc(0, BUF_SIZE * sizeof(char)); ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); if (self->_subsystem && !strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { @@ -200,24 +256,27 @@ int send_nc_payload(ssh_channel channel, const char *payload) err = usleep(USLEEP_INTERVAL); if (err) { printf("%s: usleep() error '%s' (%d)\n", __FUNCTION__, strerror(errno), errno); - return $R_CONT(c$cont, B_None); + // return $R_CONT(c$cont, B_None); } timeout -= USLEEP_INTERVAL; } if (err != SSH_OK) { printf("%s: ssh_channel_request_subsystem() Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + // return $R_CONT(c$cont, B_None); } - err = send_nc_payload(channel, (const char *)fromB_str(self->payload)); + err = send_nc_payload(channel, (const char *)fromB_str(self->payload), buffer); if (err != SSH_OK) { printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + // return $R_CONT(c$cont, B_None); } } ssh_close_free_eof(channel); - return $R_CONT(c$cont, B_None); +#ifdef DEBUG_MODE + printf("\n\nbuffer:\n%s\n\n", buffer); +#endif + return $R_CONT(c$cont, to$str(buffer)); } From 8a135ef70a8bb23fcd6314c53bf8bbfc966bb238 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 28 Feb 2025 15:02:57 +0100 Subject: [PATCH 14/37] fix misbehavior by using fixed sized char array --- src/ssh.act | 7 +++---- src/ssh.ext.c | 20 ++++---------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index ae0bb40..1d03cb8 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -115,11 +115,10 @@ actor main(env): cc = Channel(c) reply = None - print("setting payload") + # print("setting payload") cc.setPayload('urn:ietf:params:netconf:base:1.0]]>]]>') - print("sending payload") - buf = cc.sendPayload() - print(buf) + # print("sending payload") + print(cc.sendPayload()) c.disconnect() diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 2a1a7d2..27a81b5 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -3,13 +3,14 @@ #include #include #include +// acton includes #include #ifndef DEBUG_MODE -#define DEBUG_MODE /* uncomment for pretty prints */ +// #define DEBUG_MODE /* uncomment for pretty prints */ #endif -#define BUF_SIZE 65536 +#define BUF_SIZE 65536 // this will definitely not be enough, find a better way. maybe chunks like libyang does? #define TIMEOUT 1500000 // microseconds: 1.5 seconds #define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds @@ -95,11 +96,6 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) nbytes = ssh_channel_read(channel, tmp, sizeof(tmp), 0); while (nbytes > 0) { -#ifdef DEBUG_MODE - printf("\n\nstrlen buf: %u\n", strlen(buf)); - printf("strlen tmp: %u\n", strlen(tmp)); - printf("sizeof tmp: %u\n", sizeof(tmp)); -#endif // sometimes the string tmp has invalid characters from the 1024th element if (strlen(tmp) > sizeof(tmp)) { tmp[1024] = '\0'; @@ -109,11 +105,6 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) len = snprintf(buf + buflen, BUF_SIZE - buflen, "%s", tmp); buflen = strlen(buf); -#ifdef DEBUG_MODE - // TODO: sometimes the strlen() of buf is lower than in the previous iteration - // which breaks the appending and the result is invalid - printf("strlen buffer after snprintf: %u\n\n", buflen); -#endif if (len > BUF_SIZE) { printf("%s: snprintf() error %u\n", __FUNCTION__, buflen); @@ -247,7 +238,7 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) $R sshQ_ChannelD_sendPayloadG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; int timeout = TIMEOUT; - char *buffer = (char*)acton_calloc(0, BUF_SIZE * sizeof(char)); + char buffer[BUF_SIZE] = {0}; ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); if (self->_subsystem && !strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { @@ -275,8 +266,5 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) } ssh_close_free_eof(channel); -#ifdef DEBUG_MODE - printf("\n\nbuffer:\n%s\n\n", buffer); -#endif return $R_CONT(c$cont, to$str(buffer)); } From 830344fcb83f09750e65c5fef6e95079934c104f Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 4 Mar 2025 15:25:37 +0100 Subject: [PATCH 15/37] refactoring and minor fixes --- src/ssh.act | 22 +++++-- src/ssh.ext.c | 179 +++++++++++++++++++++++++++++++------------------- 2 files changed, 128 insertions(+), 73 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 1d03cb8..9647ace 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -85,7 +85,10 @@ actor Channel(client: Client): def setPayload(p: str): payload = p - def sendPayload() -> str: + def getPayload(): + return payload + + def sendNCPayload() -> str: """Send payload""" NotImplemented @@ -113,12 +116,21 @@ actor main(env): ) cc = Channel(c) - reply = None - # print("setting payload") + # send hello message cc.setPayload('urn:ietf:params:netconf:base:1.0]]>]]>') - # print("sending payload") - print(cc.sendPayload()) + print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end") + print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n") + + # get netconf server's capabilities + cc.setPayload(']]>]]>') + print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end") + print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n") + + # gracefully close a channel + cc.setPayload(']]>]]>') + print("\n\npayload 3:\n", cc.getPayload(), "\n\npayload 3 end") + print("\nNC response 3:\n", cc.sendNCPayload(), "\n\nNC response 3 end \n\n") c.disconnect() diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 27a81b5..87f2cb6 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -14,7 +14,7 @@ #define TIMEOUT 1500000 // microseconds: 1.5 seconds #define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds -void ssh_close_free(ssh_channel channel) { +void ssh_channel_close_free(ssh_channel channel) { int err = ssh_channel_close(channel); if (err != SSH_OK) { @@ -23,13 +23,13 @@ void ssh_close_free(ssh_channel channel) { ssh_channel_free(channel); } -void ssh_close_free_eof(ssh_channel channel) { +void ssh_channel_close_free_eof(ssh_channel channel) { int err = ssh_channel_send_eof(channel); if (err != SSH_OK) { printf("%s: ssh_channel_send_eof() error (%d)\n", __FUNCTION__, err); } - ssh_close_free(channel); + ssh_channel_close_free(channel); } void noop_free(void *ptr) { @@ -79,26 +79,26 @@ B_str sshQ_version() { */ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) { - int rc = 0; + int err = 0; int nbytes = 0; + int len = 0; size_t buflen = 0; - size_t len = 0; char tmp[1024] = {0}; - rc = ssh_channel_write(channel, payload, strlen(payload) + 1); - if (rc == SSH_ERROR) + err = ssh_channel_write(channel, payload, (uint32_t)strlen(payload)); + if (err == SSH_ERROR) { - printf("%s: ssh_channel_write() error (%d)\n", __FUNCTION__, rc); - ssh_close_free(channel); - return rc; + printf("%s: ssh_channel_write() error (%d)\n", __FUNCTION__, err); + goto error; } - nbytes = ssh_channel_read(channel, tmp, sizeof(tmp), 0); + nbytes = ssh_channel_read(channel, tmp, sizeof(tmp)-1, 0); while (nbytes > 0) { // sometimes the string tmp has invalid characters from the 1024th element + // and the strlen reports more than what the sizeof() is if (strlen(tmp) > sizeof(tmp)) { - tmp[1024] = '\0'; + tmp[sizeof(tmp)-1] = '\0'; } // append string @@ -107,16 +107,14 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) if (len > BUF_SIZE) { - printf("%s: snprintf() error %u\n", __FUNCTION__, buflen); - ssh_close_free(channel); - return SSH_ERROR; + printf("%s: snprintf() error %lu\n", __FUNCTION__, buflen); + goto error; } #ifdef DEBUG_MODE - if (write(STDOUT_FILENO, tmp, nbytes) != (unsigned int) nbytes) + if (write(STDOUT_FILENO, tmp, (size_t)nbytes) != (ssize_t) nbytes) { printf("%s: write() error (bytes written not matching expectation)\n", __FUNCTION__); - ssh_close_free(channel); - return SSH_ERROR; + goto error; } fflush(stdout); #endif @@ -131,10 +129,47 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) if (nbytes < 0) { printf("%s: ssh_channel_read() error (%d)\n", __FUNCTION__, errno); - ssh_close_free(channel); + goto error; + } + + return SSH_OK; +error: + ssh_channel_close_free(channel); + return SSH_ERROR; +} + +/** + * @brief Set subsystem + * + * @param[in] channel ssh_channel + * @param[in] subsystem The subsystem, for example "netconf" + */ + int set_subsystem (ssh_channel channel, const char *subsystem) { + int err = 0; + int timeout = TIMEOUT; + + if (channel == NULL) { + printf("%s: channel is NULL\n", __FUNCTION__); return SSH_ERROR; } + if (subsystem && !strcmp(subsystem, "netconf")) { + while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN && timeout > 0) + { + err = usleep(USLEEP_INTERVAL); + if (err) { + printf("%s: usleep() error '%s' (%d)\n", __FUNCTION__, strerror(errno), errno); + return SSH_ERROR; + } + timeout -= USLEEP_INTERVAL; + } + if (err != SSH_OK) + { + printf("%s: ssh_channel_request_subsystem() Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); + return SSH_ERROR; + } + } + return SSH_OK; } @@ -153,27 +188,59 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) self->_ssh_session = toB_u64((unsigned long)session); +#ifdef DEBUG_MODE + // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS + err = ssh_set_log_level(SSH_LOG_FUNCTIONS); + if (err != SSH_OK) + { + printf("%s: ssh_set_log_level() Error setting log level: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } +#endif + err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); - if (err < 0) + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); - if (err < 0) + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); - if (err < 0) + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } + // should it auto-parse user config? for example from /home/user/.ssh/ + // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); + // if (err != SSH_OK) + // { + // printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PROCESS_CONFIG': %d\n", __FUNCTION__, err); + // return $R_CONT(c$cont, B_None); + // } + + err = ssh_connect(session); + if (err != SSH_OK) + { + printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); + return $R_CONT(c$cont, B_None); + } + + err = ssh_userauth_password(session, NULL, (const char *)fromB_str(self->password)); + if (err != SSH_OK) + { + printf("%s: ssh_userauth_password() error: %s\n", __FUNCTION__, ssh_get_error(session)); + return $R_CONT(c$cont, B_None); + } + $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); @@ -182,6 +249,11 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) $R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { ssh_disconnect((ssh_session)fromB_u64(self->_ssh_session)); + ssh_free((ssh_session)fromB_u64(self->_ssh_session)); + if (ssh_finalize()) { + printf("%s: ssh_finalize error", __FUNCTION__); + } + return $R_CONT(c$cont, B_None); } @@ -191,31 +263,13 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) pin_actor_affinity(); int err = 0; - ssh_session session = { 0 }; + ssh_session session = (struct ssh_session_struct *)fromB_u64(self->_ssh_session); ssh_channel channel = { 0 }; - session = (struct ssh_session_struct *)fromB_u64(self->_ssh_session); - - ssh_set_blocking(session, 1); - #ifdef DEBUG_MODE printf("Connecting to SSH server\n"); #endif - err = ssh_connect(session); - if (err != SSH_OK) - { - printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); - return $R_CONT(c$cont, B_None); - } - - err = ssh_userauth_password(session, NULL, (const char *)fromB_str(self->_password)); - if (err != SSH_OK) - { - printf("%s: ssh_userauth_password() error: %s\n", __FUNCTION__, ssh_get_error(session)); - return $R_CONT(c$cont, B_None); - } - channel = ssh_channel_new(session); if (channel == NULL) { @@ -223,48 +277,37 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) return $R_CONT(c$cont, B_None); } + self->_ssh_channel = toB_u64((unsigned long)channel); + err = ssh_channel_open_session(channel); if (err != SSH_OK) { - printf("%s: ssh_channel_open_session() ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); + printf("%s: ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - self->_ssh_channel = toB_u64((unsigned long)channel); + if (self->_subsystem) { + err = set_subsystem(channel, (const char *)fromB_str(self->_subsystem)); + if (err != SSH_OK) { + printf("%s: set_subsystem() setting subsystem failed error (%d)\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + } return $R_CONT(c$cont, B_None); } -$R sshQ_ChannelD_sendPayloadG_local (sshQ_Channel self, $Cont c$cont) { +$R sshQ_ChannelD_sendNCPayloadG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; - int timeout = TIMEOUT; char buffer[BUF_SIZE] = {0}; ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); - if (self->_subsystem && !strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { - while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN && timeout > 0) - { - err = usleep(USLEEP_INTERVAL); - if (err) { - printf("%s: usleep() error '%s' (%d)\n", __FUNCTION__, strerror(errno), errno); - // return $R_CONT(c$cont, B_None); - } - timeout -= USLEEP_INTERVAL; - } - if (err != SSH_OK) - { - printf("%s: ssh_channel_request_subsystem() Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); - // return $R_CONT(c$cont, B_None); - } - - err = send_nc_payload(channel, (const char *)fromB_str(self->payload), buffer); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - // return $R_CONT(c$cont, B_None); - } + err = send_nc_payload(channel, (const char *)fromB_str(self->payload), buffer); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); } - ssh_close_free_eof(channel); return $R_CONT(c$cont, to$str(buffer)); } From 4905b472459acde774068445fb9ea065b5a6cfbd Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 4 Mar 2025 15:26:22 +0100 Subject: [PATCH 16/37] comment unused buffer realated functions --- src/ssh.act | 2 -- src/ssh.ext.c | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 9647ace..aaf5d14 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -102,8 +102,6 @@ actor main(env): # print("Connection closed", error) return - # print(version()) - c = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 87f2cb6..9a1397f 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -41,13 +41,13 @@ void sshQ___ext_init__() { // All things related to buffers for receiving data and similarly would have // to be allocated on the GC-heap though since that data is passed outside // of the SSH actor - libssh_replace_allocator( - acton_gc_malloc, - acton_gc_realloc, - acton_gc_calloc, - noop_free, - acton_gc_strdup, - acton_gc_strndup); + // libssh_replace_allocator( + // acton_gc_malloc, + // acton_gc_realloc, + // acton_gc_calloc, + // noop_free, + // acton_gc_strdup, + // acton_gc_strndup); int r = ssh_init(); if (r != SSH_OK) printf("SSH init failed (%d)\n", r); From 7d6b10c421151cdbd451c822832eb45f811b2c1f Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 4 Mar 2025 15:33:48 +0100 Subject: [PATCH 17/37] set custom disconnect message --- src/ssh.ext.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 9a1397f..f841c9d 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -198,6 +198,13 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) } #endif + err = ssh_session_set_disconnect_message(session, "Disconnecting SSH, powered by Acton"); + if (err != SSH_OK) + { + printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); if (err != SSH_OK) { From 95a27011b9f08f3226b0ff53d62677fa67b93397 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 4 Mar 2025 16:15:05 +0100 Subject: [PATCH 18/37] always send hello message --- src/ssh.act | 13 ++++------ src/ssh.ext.c | 66 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index aaf5d14..3b8e6b4 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -115,21 +115,16 @@ actor main(env): cc = Channel(c) - # send hello message - cc.setPayload('urn:ietf:params:netconf:base:1.0]]>]]>') + # get netconf server's capabilities + cc.setPayload(']]>]]>') print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end") print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n") - # get netconf server's capabilities - cc.setPayload(']]>]]>') + # TODO do this in disconnect(): gracefully close a channel + cc.setPayload(']]>]]>') print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end") print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n") - # gracefully close a channel - cc.setPayload(']]>]]>') - print("\n\npayload 3:\n", cc.getPayload(), "\n\npayload 3 end") - print("\nNC response 3:\n", cc.sendNCPayload(), "\n\nNC response 3 end \n\n") - c.disconnect() env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index f841c9d..feeffcf 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -10,6 +10,22 @@ // #define DEBUG_MODE /* uncomment for pretty prints */ #endif +// NETCONF message +#define NETCONF_HELLO_MSG \ + "\n" \ + "\n" \ + " \n" \ + " urn:ietf:params:netconf:base:1.0\n" \ + " \n" \ + "]]>]]>" + +// NETCONF message +#define NETCONF_CLOSE_SESSION_MSG \ + "\n" \ + "\n" \ + " \n" \ + "]]>]]>" + #define BUF_SIZE 65536 // this will definitely not be enough, find a better way. maybe chunks like libyang does? #define TIMEOUT 1500000 // microseconds: 1.5 seconds #define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds @@ -76,8 +92,10 @@ B_str sshQ_version() { * * @param[in] channel ssh_channel * @param[in] payload The Netconf Payload, for example the hello message + * @param[in,out] response response will not be returned if NULL + * @param[in] response_len buffer length */ -int send_nc_payload(ssh_channel channel, const char *payload, char *buf) +int send_nc_payload(ssh_channel channel, const char *payload, char *response, size_t response_len) { int err = 0; int nbytes = 0; @@ -101,14 +119,16 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) tmp[sizeof(tmp)-1] = '\0'; } - // append string - len = snprintf(buf + buflen, BUF_SIZE - buflen, "%s", tmp); - buflen = strlen(buf); + if (response) { + // append string + len = snprintf(response + buflen, response_len - buflen, "%s", tmp); + buflen = strlen(response); - if (len > BUF_SIZE) - { - printf("%s: snprintf() error %lu\n", __FUNCTION__, buflen); - goto error; + if (len > BUF_SIZE) + { + printf("%s: snprintf() error %lu\n", __FUNCTION__, buflen); + goto error; + } } #ifdef DEBUG_MODE if (write(STDOUT_FILENO, tmp, (size_t)nbytes) != (ssize_t) nbytes) @@ -301,20 +321,44 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *buf) } } + // send hello message + err = send_nc_payload(channel, NETCONF_HELLO_MSG, NULL, 0); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + + return $R_CONT(c$cont, B_None); +} + +$R sshQ_ChannelD_disconnectG_local (sshQ_Channel self, $Cont c$cont) { + int err = 0; + ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); + + // send close-session message + err = send_nc_payload(channel, NETCONF_CLOSE_SESSION_MSG, NULL, 0); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + } + + ssh_channel_close_free_eof(channel); + return $R_CONT(c$cont, B_None); } $R sshQ_ChannelD_sendNCPayloadG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; - char buffer[BUF_SIZE] = {0}; + char response[BUF_SIZE] = {0}; ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); - err = send_nc_payload(channel, (const char *)fromB_str(self->payload), buffer); + err = send_nc_payload(channel, (const char *)fromB_str(self->payload), response, sizeof(response)); if (err != SSH_OK) { printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - return $R_CONT(c$cont, to$str(buffer)); + return $R_CONT(c$cont, to$str(response)); } From 47fb5a3e3dc3162b598a6fe4655b42a5025a2af4 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 28 Mar 2025 09:14:42 +0100 Subject: [PATCH 19/37] add libssh + libuv example --- libuv_libssh/CMakeLists.txt | 26 +++ libuv_libssh/async_netconf_client.c | 348 ++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+) create mode 100644 libuv_libssh/CMakeLists.txt create mode 100644 libuv_libssh/async_netconf_client.c diff --git a/libuv_libssh/CMakeLists.txt b/libuv_libssh/CMakeLists.txt new file mode 100644 index 0000000..31e6a8b --- /dev/null +++ b/libuv_libssh/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.10) +project(async_netconf_client C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_executable(ancc async_netconf_client.c) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBSSH REQUIRED libssh) +pkg_check_modules(LIBUV REQUIRED libuv) + +include_directories( + ${LIBSSH_INCLUDE_DIRS} + ${LIBUV_INCLUDE_DIRS} +) + +target_link_libraries(ancc + ${LIBSSH_LIBRARIES} + ${LIBUV_LIBRARIES} +) + +target_compile_options(ancc PRIVATE + ${LIBSSH_CFLAGS_OTHER} + ${LIBUV_CFLAGS_OTHER} +) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c new file mode 100644 index 0000000..7fa0cb2 --- /dev/null +++ b/libuv_libssh/async_netconf_client.c @@ -0,0 +1,348 @@ +#include +#include +#include +#include +#include + +#define NETCONF_PORT 830 +#define BUFFER_SIZE 4096 + +const char *NETCONF_HELLO = "\n" + "\n" + " \n" + " urn:ietf:params:netconf:base:1.0\n" + " \n" + "\n" + "]]>]]>"; + +const char *NETCONF_GET_CONFIG = "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + "]]>]]>"; + +const char *NETCONF_CLOSE_SESSION = "\n" + " \n" + "\n" + "]]>]]>"; + +// Buffer management for handling partial reads +typedef struct { + char data[BUFFER_SIZE * 2]; // Double size to handle partial messages + size_t length; +} message_buffer_t; + +typedef struct { + ssh_session ssh; + ssh_channel channel; + uv_poll_t poll_handle; + char read_buffer[BUFFER_SIZE]; + message_buffer_t message_buffer; + int state; // 0: not connected, 1: hello sent, 2: get-config sent, 3: close-session sent + uv_loop_t *loop; +} netconf_context_t; + +void on_ssh_event(uv_poll_t *handle, int status, int events); +void send_hello(netconf_context_t *context); +void send_get_config(netconf_context_t *context); +void send_close_session(netconf_context_t *context); +void process_reply(netconf_context_t *context, const char *data, size_t len); +void cleanup(netconf_context_t *context); +void close_walk_cb(uv_handle_t* handle, void* arg); + +int init_ssh(netconf_context_t *context, const char *hostname, const char *username, const char *password) { + int rc; + + context->ssh = ssh_new(); + if (context->ssh == NULL) { + fprintf(stderr, "Failed to create SSH session\n"); + return -1; + } + + ssh_options_set(context->ssh, SSH_OPTIONS_HOST, hostname); + ssh_options_set(context->ssh, SSH_OPTIONS_USER, username); + ssh_options_set(context->ssh, SSH_OPTIONS_PORT, &(int){NETCONF_PORT}); + + int verbosity = SSH_LOG_PROTOCOL; + ssh_options_set(context->ssh, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); + + printf("Connecting to SSH server %s:%d...\n", hostname, NETCONF_PORT); + + rc = ssh_connect(context->ssh); + if (rc != SSH_OK) { + fprintf(stderr, "Error connecting to %s: %s\n", hostname, ssh_get_error(context->ssh)); + ssh_free(context->ssh); + return -1; + } + + printf("SSH connection established, authenticating...\n"); + + rc = ssh_userauth_password(context->ssh, NULL, password); + if (rc != SSH_AUTH_SUCCESS) { + fprintf(stderr, "Authentication failed: %s\n", ssh_get_error(context->ssh)); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } + + printf("Authentication successful, opening channel...\n"); + + context->channel = ssh_channel_new(context->ssh); + if (context->channel == NULL) { + fprintf(stderr, "Failed to create channel\n"); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } + + rc = ssh_channel_open_session(context->channel); + if (rc != SSH_OK) { + fprintf(stderr, "Failed to open channel: %s\n", ssh_get_error(context->ssh)); + ssh_channel_free(context->channel); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } + + printf("Channel opened, requesting NETCONF subsystem...\n"); + + // Request subsystem (NETCONF) + rc = ssh_channel_request_subsystem(context->channel, "netconf"); + if (rc != SSH_OK) { + fprintf(stderr, "Failed to request NETCONF subsystem: %s\n", ssh_get_error(context->ssh)); + ssh_channel_close(context->channel); + ssh_channel_free(context->channel); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } + + printf("NETCONF subsystem established\n"); + + printf("Sending NETCONF hello message...\n"); + send_hello(context); + + return 0; +} + +// Setup libuv poll for SSH socket +int setup_poll(netconf_context_t *context) { + int socket_fd = ssh_get_fd(context->ssh); + if (socket_fd < 0) { + fprintf(stderr, "Failed to get SSH socket file descriptor\n"); + return -1; + } + + // Initialize poll handle + uv_poll_init(context->loop, &context->poll_handle, socket_fd); + context->poll_handle.data = context; + + // Start polling for read events + uv_poll_start(&context->poll_handle, UV_READABLE, on_ssh_event); + + return 0; +} + +// Callback for SSH socket events +void on_ssh_event(uv_poll_t *handle, int status, int events) { + netconf_context_t *context = (netconf_context_t *)handle->data; + + if (status < 0) { + fprintf(stderr, "Poll error: %s\n", uv_strerror(status)); + return; + } + + if (events & UV_READABLE) { + int nbytes = ssh_channel_read(context->channel, context->read_buffer, BUFFER_SIZE - 1, 0); + if (nbytes > 0) { + context->read_buffer[nbytes] = '\0'; + printf("DEBUG: Raw data received (%d bytes):\n", nbytes); + printf("----------------------------------------\n"); + printf("%s\n", context->read_buffer); + printf("----------------------------------------\n"); + + // Append to our message buffer + if (context->message_buffer.length + nbytes < sizeof(context->message_buffer.data) - 1) { + memcpy(context->message_buffer.data + context->message_buffer.length, + context->read_buffer, nbytes); + context->message_buffer.length += nbytes; + context->message_buffer.data[context->message_buffer.length] = '\0'; + + // Now process the accumulated buffer + process_reply(context, context->message_buffer.data, context->message_buffer.length); + + // If we found the delimiter, we can clear the buffer for the next message + if (strstr(context->message_buffer.data, "]]>]]>") != NULL) { + context->message_buffer.length = 0; + } + } else { + fprintf(stderr, "Message buffer overflow, resetting\n"); + context->message_buffer.length = 0; + } + } else if (nbytes < 0) { + fprintf(stderr, "Error reading from channel: %s\n", ssh_get_error(context->ssh)); + } else if (ssh_channel_is_eof(context->channel)) { + fprintf(stderr, "Server closed the connection\n"); + uv_poll_stop(&context->poll_handle); + } + } +} + +void send_hello(netconf_context_t *context) { + printf("Sending NETCONF hello message...\n"); + + int rc = ssh_channel_write(context->channel, NETCONF_HELLO, strlen(NETCONF_HELLO)); + if (rc != (int)strlen(NETCONF_HELLO)) { + fprintf(stderr, "Failed to send hello message: %s\n", ssh_get_error(context->ssh)); + return; + } + + context->state = 2; +} + +void send_get_config(netconf_context_t *context) { + printf("Sending NETCONF get-config message...\n"); + + int rc = ssh_channel_write(context->channel, NETCONF_GET_CONFIG, strlen(NETCONF_GET_CONFIG)); + if (rc != (int)strlen(NETCONF_GET_CONFIG)) { + fprintf(stderr, "Failed to send get-config message: %s\n", ssh_get_error(context->ssh)); + return; + } + + context->state = 2; +} + +void send_close_session(netconf_context_t *context) { + printf("Sending NETCONF close-session message...\n"); + + int rc = ssh_channel_write(context->channel, NETCONF_CLOSE_SESSION, strlen(NETCONF_CLOSE_SESSION)); + if (rc != (int)strlen(NETCONF_CLOSE_SESSION)) { + fprintf(stderr, "Failed to send close-session message: %s\n", ssh_get_error(context->ssh)); + return; + } + + context->state = 3; +} + +// Process NETCONF reply +void process_reply(netconf_context_t *context, const char *data, size_t len) { + printf("Processing NETCONF reply (%zu bytes)\n", len); + + // Check for the end of message delimiter + if (strstr(data, "]]>]]>") != NULL) { + printf("Found NETCONF message delimiter\n"); + + if (context->state == 1) { + // Already sent hello, now send get-config + printf("Received server response after our hello, sending get-config\n"); + send_get_config(context); + } else if (context->state == 2) { + // Received get-config reply, now send close-session + printf("Get-config completed successfully, closing session...\n"); + + // TODO so something with retreived data? + + // Send close-session to gracefully terminate the NETCONF session + send_close_session(context); + } else if (context->state == 3) { + // Received close-session reply, we're done + printf("NETCONF session closed gracefully\n"); + + // Stop polling and prepare to exit + uv_poll_stop(&context->poll_handle); + uv_stop(context->loop); + } + } else { + printf("WARNING: No NETCONF message delimiter found in the response\n"); + // It's possible we received a partial message, which is normal in async I/O + // We will accumulate more data on subsequent reads + } +} + +// Cleanup resources +void cleanup(netconf_context_t *context) { + // Stop polling if still active + uv_poll_stop(&context->poll_handle); + + // Close the poll handle (needs to be closed before the loop can be closed properly) + uv_close((uv_handle_t*)&context->poll_handle, NULL); + + // Close SSH channel if it exists + if (context->channel) { + ssh_channel_close(context->channel); + ssh_channel_free(context->channel); + } + + // Close SSH session if it exists + if (context->ssh) { + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + } +} + +int main(int argc, char *argv[]) { + if (argc != 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char *hostname = argv[1]; + const char *username = argv[2]; + const char *password = argv[3]; + + uv_loop_t loop; + uv_loop_init(&loop); + + netconf_context_t context = {0}; + context.loop = &loop; + context.message_buffer.length = 0; + + if (init_ssh(&context, hostname, username, password) < 0) { + uv_loop_close(&loop); + return 1; + } + + // Setup polling for SSH socket + if (setup_poll(&context) < 0) { + cleanup(&context); + uv_loop_close(&loop); + return 1; + } + + printf("Connected to NETCONF server at %s and sent hello message\n", hostname); + printf("Waiting for server response...\n"); + + uv_run(&loop, UV_RUN_DEFAULT); + + cleanup(&context); + + // Walk the loop to close any remaining handles + uv_walk(&loop, (uv_walk_cb)close_walk_cb, NULL); + + // Run the loop one more time to let close callbacks execute + uv_run(&loop, UV_RUN_DEFAULT); + + // Now it's safe to close the loop + int r = uv_loop_close(&loop); + if (r != 0) { + fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(r)); + + // If we still have handles, print debug info + if (r == UV_EBUSY) { + fprintf(stderr, "There are still active handles in the loop. This is a leak.\n"); + } + } + + return 0; +} + +// Walk callback to close remaining handles +void close_walk_cb(uv_handle_t* handle, void* arg) { + if (!uv_is_closing(handle)) { + uv_close(handle, NULL); + } +} From 26d4a9265c451e3076ea125d3b06c515ccda5c5f Mon Sep 17 00:00:00 2001 From: Juraj Vijtiuk Date: Thu, 8 May 2025 14:53:31 +0200 Subject: [PATCH 20/37] enable non-NETCONF SSH subsystems --- libuv_libssh/async_netconf_client.c | 175 +++++++++++++++++----------- 1 file changed, 109 insertions(+), 66 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index 7fa0cb2..e712d8b 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -42,19 +42,24 @@ typedef struct { uv_poll_t poll_handle; char read_buffer[BUFFER_SIZE]; message_buffer_t message_buffer; - int state; // 0: not connected, 1: hello sent, 2: get-config sent, 3: close-session sent + const char *subsystem; + void *subsystem_ctx; uv_loop_t *loop; +} client_context_t; + +typedef struct { + int state; // 0: not connected, 1: hello sent, 2: get-config sent, 3: close-session sent } netconf_context_t; void on_ssh_event(uv_poll_t *handle, int status, int events); -void send_hello(netconf_context_t *context); -void send_get_config(netconf_context_t *context); -void send_close_session(netconf_context_t *context); -void process_reply(netconf_context_t *context, const char *data, size_t len); -void cleanup(netconf_context_t *context); +void send_hello(client_context_t *context); +void send_get_config(client_context_t *context); +void send_close_session(client_context_t *context); +void process_reply(client_context_t *context, const char *data, size_t len); +void cleanup(client_context_t *context); void close_walk_cb(uv_handle_t* handle, void* arg); -int init_ssh(netconf_context_t *context, const char *hostname, const char *username, const char *password) { +int init_ssh(client_context_t *context, const char *hostname, const char *username, const char *password) { int rc; context->ssh = ssh_new(); @@ -65,7 +70,9 @@ int init_ssh(netconf_context_t *context, const char *hostname, const char *usern ssh_options_set(context->ssh, SSH_OPTIONS_HOST, hostname); ssh_options_set(context->ssh, SSH_OPTIONS_USER, username); - ssh_options_set(context->ssh, SSH_OPTIONS_PORT, &(int){NETCONF_PORT}); + if (context->subsystem && !strcmp(context->subsystem, "netconf")) { + ssh_options_set(context->ssh, SSH_OPTIONS_PORT, &(int){NETCONF_PORT}); + } int verbosity = SSH_LOG_PROTOCOL; ssh_options_set(context->ssh, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); @@ -108,29 +115,42 @@ int init_ssh(netconf_context_t *context, const char *hostname, const char *usern return -1; } - printf("Channel opened, requesting NETCONF subsystem...\n"); - - // Request subsystem (NETCONF) - rc = ssh_channel_request_subsystem(context->channel, "netconf"); - if (rc != SSH_OK) { - fprintf(stderr, "Failed to request NETCONF subsystem: %s\n", ssh_get_error(context->ssh)); - ssh_channel_close(context->channel); - ssh_channel_free(context->channel); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } + if (context->subsystem && strlen(context->subsystem) > 0) { + printf("Channel opened, requesting %s subsystem...\n", context->subsystem); + + rc = ssh_channel_request_subsystem(context->channel, context->subsystem); + if (rc != SSH_OK) { + fprintf(stderr, "Failed to request %s subsystem: %s\n", context->subsystem, ssh_get_error(context->ssh)); + ssh_channel_close(context->channel); + ssh_channel_free(context->channel); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } - printf("NETCONF subsystem established\n"); + printf("%s subsystem established\n", context->subsystem); - printf("Sending NETCONF hello message...\n"); - send_hello(context); + if (!strcmp("netconf", context->subsystem)) { + printf("Sending NETCONF hello message...\n"); + send_hello(context); + } + } else { + rc = ssh_channel_request_shell(context->channel); + if (rc != SSH_OK) { + fprintf(stderr, "Failed to request shell: %s\n", ssh_get_error(context->ssh)); + ssh_channel_close(context->channel); + ssh_channel_free(context->channel); + ssh_disconnect(context->ssh); + ssh_free(context->ssh); + return -1; + } + } return 0; } // Setup libuv poll for SSH socket -int setup_poll(netconf_context_t *context) { +int setup_poll(client_context_t *context) { int socket_fd = ssh_get_fd(context->ssh); if (socket_fd < 0) { fprintf(stderr, "Failed to get SSH socket file descriptor\n"); @@ -149,7 +169,7 @@ int setup_poll(netconf_context_t *context) { // Callback for SSH socket events void on_ssh_event(uv_poll_t *handle, int status, int events) { - netconf_context_t *context = (netconf_context_t *)handle->data; + client_context_t *context = (client_context_t *)handle->data; if (status < 0) { fprintf(stderr, "Poll error: %s\n", uv_strerror(status)); @@ -192,7 +212,7 @@ void on_ssh_event(uv_poll_t *handle, int status, int events) { } } -void send_hello(netconf_context_t *context) { +void send_hello(client_context_t *context) { printf("Sending NETCONF hello message...\n"); int rc = ssh_channel_write(context->channel, NETCONF_HELLO, strlen(NETCONF_HELLO)); @@ -201,10 +221,11 @@ void send_hello(netconf_context_t *context) { return; } - context->state = 2; + netconf_context_t *netconf_ctx = context->subsystem_ctx; + netconf_ctx->state = 2; } -void send_get_config(netconf_context_t *context) { +void send_get_config(client_context_t *context) { printf("Sending NETCONF get-config message...\n"); int rc = ssh_channel_write(context->channel, NETCONF_GET_CONFIG, strlen(NETCONF_GET_CONFIG)); @@ -213,10 +234,11 @@ void send_get_config(netconf_context_t *context) { return; } - context->state = 2; + netconf_context_t *netconf_ctx = context->subsystem_ctx; + netconf_ctx->state = 2; } -void send_close_session(netconf_context_t *context) { +void send_close_session(client_context_t *context) { printf("Sending NETCONF close-session message...\n"); int rc = ssh_channel_write(context->channel, NETCONF_CLOSE_SESSION, strlen(NETCONF_CLOSE_SESSION)); @@ -225,46 +247,50 @@ void send_close_session(netconf_context_t *context) { return; } - context->state = 3; + netconf_context_t *netconf_ctx = context->subsystem_ctx; + netconf_ctx->state = 3; } // Process NETCONF reply -void process_reply(netconf_context_t *context, const char *data, size_t len) { - printf("Processing NETCONF reply (%zu bytes)\n", len); - - // Check for the end of message delimiter - if (strstr(data, "]]>]]>") != NULL) { - printf("Found NETCONF message delimiter\n"); - - if (context->state == 1) { - // Already sent hello, now send get-config - printf("Received server response after our hello, sending get-config\n"); - send_get_config(context); - } else if (context->state == 2) { - // Received get-config reply, now send close-session - printf("Get-config completed successfully, closing session...\n"); - - // TODO so something with retreived data? - - // Send close-session to gracefully terminate the NETCONF session - send_close_session(context); - } else if (context->state == 3) { - // Received close-session reply, we're done - printf("NETCONF session closed gracefully\n"); - - // Stop polling and prepare to exit - uv_poll_stop(&context->poll_handle); - uv_stop(context->loop); +void process_reply(client_context_t *context, const char *data, size_t len) { + if (context->subsystem && !strcmp(context->subsystem, "netconf")) { + netconf_context_t *netconf_ctx = context->subsystem_ctx; + printf("Processing NETCONF reply (%zu bytes)\n", len); + + // Check for the end of message delimiter + if (strstr(data, "]]>]]>") != NULL) { + printf("Found NETCONF message delimiter\n"); + + if (netconf_ctx->state == 1) { + // Already sent hello, now send get-config + printf("Received server response after our hello, sending get-config\n"); + send_get_config(context); + } else if (netconf_ctx->state == 2) { + // Received get-config reply, now send close-session + printf("Get-config completed successfully, closing session...\n"); + + // TODO so something with retreived data? + + // Send close-session to gracefully terminate the NETCONF session + send_close_session(context); + } else if (netconf_ctx->state == 3) { + // Received close-session reply, we're done + printf("NETCONF session closed gracefully\n"); + + // Stop polling and prepare to exit + uv_poll_stop(&context->poll_handle); + uv_stop(context->loop); + } + } else { + printf("WARNING: No NETCONF message delimiter found in the response\n"); + // It's possible we received a partial message, which is normal in async I/O + // We will accumulate more data on subsequent reads } - } else { - printf("WARNING: No NETCONF message delimiter found in the response\n"); - // It's possible we received a partial message, which is normal in async I/O - // We will accumulate more data on subsequent reads } } // Cleanup resources -void cleanup(netconf_context_t *context) { +void cleanup(client_context_t *context) { // Stop polling if still active uv_poll_stop(&context->poll_handle); @@ -285,19 +311,28 @@ void cleanup(netconf_context_t *context) { } int main(int argc, char *argv[]) { - if (argc != 4) { - fprintf(stderr, "Usage: %s \n", argv[0]); + if (argc < 4) { + fprintf(stderr, "Usage: %s [subsystem]\n", argv[0]); return 1; } const char *hostname = argv[1]; const char *username = argv[2]; const char *password = argv[3]; + const char *subsystem = NULL; + + if (argc == 5) { + subsystem = argv[4]; + } uv_loop_t loop; uv_loop_init(&loop); - netconf_context_t context = {0}; + client_context_t context = {0}; + context.subsystem = subsystem; + if (subsystem && !strcmp(subsystem, "netconf")) { + context.subsystem_ctx = &((netconf_context_t){0}); + } context.loop = &loop; context.message_buffer.length = 0; @@ -313,7 +348,15 @@ int main(int argc, char *argv[]) { return 1; } - printf("Connected to NETCONF server at %s and sent hello message\n", hostname); + if (subsystem) { + if (!strcmp(subsystem, "netconf")) { + printf("Connected to NETCONF server at %s and sent hello\n", hostname); + } else { + printf("Connected to %s server at %s\n", subsystem, hostname); + } + } else { + printf("Connected to SSH server at %s\n", hostname); + } printf("Waiting for server response...\n"); uv_run(&loop, UV_RUN_DEFAULT); From 72a0fde7dfcacb446d2fff1800152bc4758ab9e4 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 8 Jul 2025 07:23:00 +0200 Subject: [PATCH 21/37] further refactoring --- libuv_libssh/async_netconf_client.c | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index e712d8b..7bdf4dd 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -45,11 +45,8 @@ typedef struct { const char *subsystem; void *subsystem_ctx; uv_loop_t *loop; -} client_context_t; - -typedef struct { int state; // 0: not connected, 1: hello sent, 2: get-config sent, 3: close-session sent -} netconf_context_t; +} client_context_t; void on_ssh_event(uv_poll_t *handle, int status, int events); void send_hello(client_context_t *context); @@ -130,10 +127,10 @@ int init_ssh(client_context_t *context, const char *hostname, const char *userna printf("%s subsystem established\n", context->subsystem); - if (!strcmp("netconf", context->subsystem)) { + if (!strcmp("netconf", context->subsystem)) { printf("Sending NETCONF hello message...\n"); send_hello(context); - } + } } else { rc = ssh_channel_request_shell(context->channel); if (rc != SSH_OK) { @@ -143,7 +140,8 @@ int init_ssh(client_context_t *context, const char *hostname, const char *userna ssh_disconnect(context->ssh); ssh_free(context->ssh); return -1; - } + } + printf("Shell successfully aquired\n"); } return 0; @@ -221,7 +219,7 @@ void send_hello(client_context_t *context) { return; } - netconf_context_t *netconf_ctx = context->subsystem_ctx; + client_context_t *netconf_ctx = context->subsystem_ctx; netconf_ctx->state = 2; } @@ -234,7 +232,7 @@ void send_get_config(client_context_t *context) { return; } - netconf_context_t *netconf_ctx = context->subsystem_ctx; + client_context_t *netconf_ctx = context->subsystem_ctx; netconf_ctx->state = 2; } @@ -247,14 +245,14 @@ void send_close_session(client_context_t *context) { return; } - netconf_context_t *netconf_ctx = context->subsystem_ctx; + client_context_t *netconf_ctx = context->subsystem_ctx; netconf_ctx->state = 3; } // Process NETCONF reply void process_reply(client_context_t *context, const char *data, size_t len) { if (context->subsystem && !strcmp(context->subsystem, "netconf")) { - netconf_context_t *netconf_ctx = context->subsystem_ctx; + client_context_t *netconf_ctx = context->subsystem_ctx; printf("Processing NETCONF reply (%zu bytes)\n", len); // Check for the end of message delimiter @@ -331,7 +329,7 @@ int main(int argc, char *argv[]) { client_context_t context = {0}; context.subsystem = subsystem; if (subsystem && !strcmp(subsystem, "netconf")) { - context.subsystem_ctx = &((netconf_context_t){0}); + context.subsystem_ctx = &((client_context_t){0}); } context.loop = &loop; context.message_buffer.length = 0; @@ -349,11 +347,11 @@ int main(int argc, char *argv[]) { } if (subsystem) { - if (!strcmp(subsystem, "netconf")) { - printf("Connected to NETCONF server at %s and sent hello\n", hostname); - } else { - printf("Connected to %s server at %s\n", subsystem, hostname); - } + if (!strcmp(subsystem, "netconf")) { + printf("Connected to NETCONF server at %s and sent hello\n", hostname); + } else { + printf("Connected to %s server at %s\n", subsystem, hostname); + } } else { printf("Connected to SSH server at %s\n", hostname); } From 64e4c6b0f1102281ff80ce6d5dcadcb52598776c Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 8 Jul 2025 07:23:37 +0200 Subject: [PATCH 22/37] run netconf related code only if netconf subsystem set --- src/ssh.ext.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index feeffcf..7b90f56 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -319,14 +319,17 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si printf("%s: set_subsystem() setting subsystem failed error (%d)\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - } - // send hello message - err = send_nc_payload(channel, NETCONF_HELLO_MSG, NULL, 0); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + // NOTE: add other subsystems here if needed + if (!strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { + // send netconf hello message + err = send_nc_payload(channel, NETCONF_HELLO_MSG, NULL, 0); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } + } } return $R_CONT(c$cont, B_None); @@ -336,11 +339,15 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si int err = 0; ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); - // send close-session message - err = send_nc_payload(channel, NETCONF_CLOSE_SESSION_MSG, NULL, 0); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + if (self->_subsystem) { + // NOTE: add other subsystems here if needed + if (!strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { + err = send_nc_payload(channel, NETCONF_CLOSE_SESSION_MSG, NULL, 0); + if (err != SSH_OK) + { + printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); + } + } } ssh_channel_close_free_eof(channel); From 9e499b1a972b58242e4a2ae86e36d1124640cec6 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Tue, 8 Jul 2025 08:03:59 +0200 Subject: [PATCH 23/37] add input handling for ssh session --- libuv_libssh/async_netconf_client.c | 76 +++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index 7bdf4dd..cb0d36d 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -40,7 +40,9 @@ typedef struct { ssh_session ssh; ssh_channel channel; uv_poll_t poll_handle; + uv_tty_t tty_handle; char read_buffer[BUFFER_SIZE]; + char input_buffer[BUFFER_SIZE]; message_buffer_t message_buffer; const char *subsystem; void *subsystem_ctx; @@ -49,6 +51,8 @@ typedef struct { } client_context_t; void on_ssh_event(uv_poll_t *handle, int status, int events); +void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf); +void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf); void send_hello(client_context_t *context); void send_get_config(client_context_t *context); void send_close_session(client_context_t *context); @@ -140,8 +144,32 @@ int init_ssh(client_context_t *context, const char *hostname, const char *userna ssh_disconnect(context->ssh); ssh_free(context->ssh); return -1; - } - printf("Shell successfully aquired\n"); + } + printf("Shell successfully acquired\n"); + } + + return 0; +} + +// Setup stdin input handling for interactive shell +int setup_stdin(client_context_t *context) { + if (context->subsystem) { + // Skip stdin setup for subsystem sessions + return 0; + } + + int rc = uv_tty_init(context->loop, &context->tty_handle, 0, 1); + if (rc != 0) { + fprintf(stderr, "Failed to initialize TTY: %s\n", uv_strerror(rc)); + return -1; + } + + context->tty_handle.data = context; + + rc = uv_read_start((uv_stream_t*)&context->tty_handle, alloc_buffer, on_stdin_read); + if (rc != 0) { + fprintf(stderr, "Failed to start reading from stdin: %s\n", uv_strerror(rc)); + return -1; } return 0; @@ -267,7 +295,7 @@ void process_reply(client_context_t *context, const char *data, size_t len) { // Received get-config reply, now send close-session printf("Get-config completed successfully, closing session...\n"); - // TODO so something with retreived data? + // TODO do something with retreived data? // Send close-session to gracefully terminate the NETCONF session send_close_session(context); @@ -287,11 +315,46 @@ void process_reply(client_context_t *context, const char *data, size_t len) { } } +// Buffer allocation callback for libuv +void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { + client_context_t *context = (client_context_t *)handle->data; + buf->base = context->input_buffer; + buf->len = sizeof(context->input_buffer); +} + +// Handle stdin input and forward to SSH channel +void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + client_context_t *context = (client_context_t *)stream->data; + + if (nread < 0) { + if (nread == UV_EOF) { + printf("EOF received from stdin\n"); + uv_stop(context->loop); + } else { + fprintf(stderr, "Error reading from stdin: %s\n", uv_strerror(nread)); + } + return; + } + + if (nread > 0) { + int rc = ssh_channel_write(context->channel, buf->base, nread); + if (rc != nread) { + fprintf(stderr, "Failed to write to SSH channel: %s\n", ssh_get_error(context->ssh)); + } + } +} + // Cleanup resources void cleanup(client_context_t *context) { // Stop polling if still active uv_poll_stop(&context->poll_handle); + // Stop stdin reading if active + if (!context->subsystem) { + uv_read_stop((uv_stream_t*)&context->tty_handle); + uv_close((uv_handle_t*)&context->tty_handle, NULL); + } + // Close the poll handle (needs to be closed before the loop can be closed properly) uv_close((uv_handle_t*)&context->poll_handle, NULL); @@ -346,6 +409,13 @@ int main(int argc, char *argv[]) { return 1; } + // Setup stdin input handling for shell sessions + if (setup_stdin(&context) < 0) { + cleanup(&context); + uv_loop_close(&loop); + return 1; + } + if (subsystem) { if (!strcmp(subsystem, "netconf")) { printf("Connected to NETCONF server at %s and sent hello\n", hostname); From b53d87b6ccc7225735eacc737cd510d3d812969f Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Wed, 9 Jul 2025 07:49:53 +0200 Subject: [PATCH 24/37] update affinity related code --- src/ssh.act | 23 +++++++++++++++-------- src/ssh.ext.c | 14 +++++++++++++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 3b8e6b4..f5fe6ed 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -18,13 +18,13 @@ actor Client(cap: net.TCPConnectCap, ): """SSH Client""" + # haha, this is really a pointer :P + var _ssh_session: u64 = 0 + proc def _pin_affinity() -> None: NotImplemented _pin_affinity() - # haha, this is really a pointer :P - var _ssh_session: u64 = 0 - def get_ssh_session(): return _ssh_session @@ -34,13 +34,14 @@ actor Client(cap: net.TCPConnectCap, def get_subsystem() -> str: return subsystem + action def get_affinity() -> u64: + NotImplemented + proc def _init() -> None: """Initialize the SSH client""" NotImplemented _init() - # print("SSH Client connected") - # action def close(on_close: action(TLSConnection) -> None) -> None: # """Close the connection""" # NotImplemented @@ -75,12 +76,18 @@ actor Channel(client: Client): NotImplemented _pin_affinity() - proc def _init() -> None: + proc def _set_affinity(affinity: u64) -> None: + NotImplemented + + proc def _ssh_init() -> None: """Initialize the SSH Channel""" NotImplemented - _init() - # print("SSH Channel created") + proc def _init() -> None: + aff = client.get_affinity() + _set_affinity(aff) + after 0: _ssh_init() + _init() def setPayload(p: str): payload = p diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 7b90f56..802c9b9 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -274,6 +274,11 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si return $R_CONT(c$cont, B_None); } +$R sshQ_ClientD_get_affinityG_local (sshQ_Client self, $Cont c$cont) { + printf("sshQ_ClientD_get_affinityG_local, affinity=%ld\n", self->$affinity); + return $R_CONT(c$cont, B_None); +} + $R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { ssh_disconnect((ssh_session)fromB_u64(self->_ssh_session)); ssh_free((ssh_session)fromB_u64(self->_ssh_session)); @@ -286,7 +291,7 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si // Channel -$R sshQ_ChannelD__initG_local (sshQ_Channel self, $Cont c$cont) { +$R sshQ_ChannelD__ssh_initG_local (sshQ_Channel self, $Cont c$cont) { pin_actor_affinity(); int err = 0; @@ -335,6 +340,13 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si return $R_CONT(c$cont, B_None); } +$R sshQ_ChannelD__set_affinityG_local (sshQ_Channel self, $Cont C_cont, B_u64 affinity) { +#ifdef DEBUG_MODE + printf("sshQ_ChannelD__set_affinityG_local, affinity=%ld\n", self->$affinity); +#endif + return $R_CONT(C_cont, B_None); +} + $R sshQ_ChannelD_disconnectG_local (sshQ_Channel self, $Cont c$cont) { int err = 0; ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); From 632ac0a1f46e68c5acf629529354ef6c71612666 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Mon, 28 Jul 2025 10:05:13 +0200 Subject: [PATCH 25/37] use non-blocking function for reading data on ssh channel --- libuv_libssh/async_netconf_client.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index cb0d36d..d5de46e 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -203,7 +203,7 @@ void on_ssh_event(uv_poll_t *handle, int status, int events) { } if (events & UV_READABLE) { - int nbytes = ssh_channel_read(context->channel, context->read_buffer, BUFFER_SIZE - 1, 0); + int nbytes = ssh_channel_read_nonblocking(context->channel, context->read_buffer, BUFFER_SIZE - 1, 0); if (nbytes > 0) { context->read_buffer[nbytes] = '\0'; printf("DEBUG: Raw data received (%d bytes):\n", nbytes); @@ -229,8 +229,10 @@ void on_ssh_event(uv_poll_t *handle, int status, int events) { fprintf(stderr, "Message buffer overflow, resetting\n"); context->message_buffer.length = 0; } - } else if (nbytes < 0) { + } else if (nbytes == SSH_ERROR) { fprintf(stderr, "Error reading from channel: %s\n", ssh_get_error(context->ssh)); + } else if (nbytes == SSH_AGAIN || nbytes == 0) { + printf("No data on the channel\n"); } else if (ssh_channel_is_eof(context->channel)) { fprintf(stderr, "Server closed the connection\n"); uv_poll_stop(&context->poll_handle); From 33c6989bdea4e6ddf7b67e7a1fb95a14f044f623 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Mon, 28 Jul 2025 10:05:44 +0200 Subject: [PATCH 26/37] improve print output --- libuv_libssh/async_netconf_client.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index d5de46e..c7f91a1 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -206,10 +206,10 @@ void on_ssh_event(uv_poll_t *handle, int status, int events) { int nbytes = ssh_channel_read_nonblocking(context->channel, context->read_buffer, BUFFER_SIZE - 1, 0); if (nbytes > 0) { context->read_buffer[nbytes] = '\0'; - printf("DEBUG: Raw data received (%d bytes):\n", nbytes); + printf("\nDEBUG: Raw data received (%d bytes):\n", nbytes); printf("----------------------------------------\n"); printf("%s\n", context->read_buffer); - printf("----------------------------------------\n"); + printf("----------------------------------------\n\n"); // Append to our message buffer if (context->message_buffer.length + nbytes < sizeof(context->message_buffer.data) - 1) { @@ -310,7 +310,7 @@ void process_reply(client_context_t *context, const char *data, size_t len) { uv_stop(context->loop); } } else { - printf("WARNING: No NETCONF message delimiter found in the response\n"); + printf("INFO: No NETCONF message delimiter found in the response, expecting more data\n"); // It's possible we received a partial message, which is normal in async I/O // We will accumulate more data on subsequent reads } @@ -424,10 +424,11 @@ int main(int argc, char *argv[]) { } else { printf("Connected to %s server at %s\n", subsystem, hostname); } + printf("Waiting for server response...\n"); } else { printf("Connected to SSH server at %s\n", hostname); + printf("Ready for input\n"); } - printf("Waiting for server response...\n"); uv_run(&loop, UV_RUN_DEFAULT); From a285b2a0e4f68e905879718a34a18a68eba52cf7 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Mon, 28 Jul 2025 10:14:17 +0200 Subject: [PATCH 27/37] run until no more active handles --- libuv_libssh/async_netconf_client.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c index c7f91a1..340ce06 100644 --- a/libuv_libssh/async_netconf_client.c +++ b/libuv_libssh/async_netconf_client.c @@ -374,6 +374,8 @@ void cleanup(client_context_t *context) { } int main(int argc, char *argv[]) { + int r = 0; + if (argc < 4) { fprintf(stderr, "Usage: %s [subsystem]\n", argv[0]); return 1; @@ -438,10 +440,12 @@ int main(int argc, char *argv[]) { uv_walk(&loop, (uv_walk_cb)close_walk_cb, NULL); // Run the loop one more time to let close callbacks execute - uv_run(&loop, UV_RUN_DEFAULT); + do { + r = uv_run(&loop, UV_RUN_DEFAULT); + } while (r != 0); // Now it's safe to close the loop - int r = uv_loop_close(&loop); + r = uv_loop_close(&loop); if (r != 0) { fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(r)); From d927e8845549a213d5789be8d9cd2580a0532218 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 21 Aug 2025 13:59:33 +0200 Subject: [PATCH 28/37] added improved libuv+libssh example --- libuv_libssh/CMakeLists.txt | 2 +- libuv_libssh/async_netconf_client_2.c | 558 ++++++++++++++++++++++++++ libuv_libssh/make.sh | 1 + 3 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 libuv_libssh/async_netconf_client_2.c create mode 100755 libuv_libssh/make.sh diff --git a/libuv_libssh/CMakeLists.txt b/libuv_libssh/CMakeLists.txt index 31e6a8b..ac58414 100644 --- a/libuv_libssh/CMakeLists.txt +++ b/libuv_libssh/CMakeLists.txt @@ -4,7 +4,7 @@ project(async_netconf_client C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -add_executable(ancc async_netconf_client.c) +add_executable(ancc async_netconf_client_2.c) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBSSH REQUIRED libssh) diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c new file mode 100644 index 0000000..954e8f3 --- /dev/null +++ b/libuv_libssh/async_netconf_client_2.c @@ -0,0 +1,558 @@ +/* + * async_netconf_client_2.c + * + * Non-blocking NETCONF hello exchange using libssh (non-blocking) + libuv (uv_poll) + * + * Build: + * gcc -o ancc2 async_netconf_client_2.c -lssh -luv + * + * Usage: + * ./ancc2 + */ + +#include +#include +#include +#include + +#include + +#include + +#define BUF_SIZE 512 + +const char *NETCONF_HELLO = "\n" + "\n" + " \n" + " urn:ietf:params:netconf:base:1.0\n" + " \n" + "\n" + "]]>]]>"; + +const char *NETCONF_GET_CONFIG = "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + "]]>]]>"; + +const char *NETCONF_CLOSE_SESSION = "\n" + " \n" + "\n" + "]]>]]>"; + +enum client_state { + S_CONNECT, + S_AUTH, + S_CHANNEL_OPEN, + S_SUBSYSTEM, + S_SEND_HELLO, + S_RECV_HELLO, + S_SEND_GET_CONFIG, + S_RECV_GET_CONFIG, + S_CLOSE, + S_RECV_CLOSE_REPLY, + S_CLEANUP, + S_DONE, + S_ERROR +}; + +typedef struct { + uv_loop_t *loop; + + ssh_session session; + ssh_channel channel; + uv_poll_t poll; + int fd; + enum client_state state; + const char *host; + int port; + const char *user; + const char *password; + + /* writing hello */ + const char *hello; + size_t hello_len; + size_t hello_sent; + + /* writing get-config */ + const char *get_config; + size_t get_config_len; + size_t get_config_sent; + + /* writing close */ + const char *close; + size_t close_len; + size_t close_sent; + + /* read buffer */ + char *reply; + size_t reply_len; + size_t reply_cap; +} client_t; + +/* Simple helper to append to reply buffer */ +static int append_reply(client_t *c, const char *buf, size_t n) { + if (n == 0) return 0; + if (c->reply_len + n + 1 > c->reply_cap) { + size_t newcap = (c->reply_cap == 0) ? BUF_SIZE : c->reply_cap * 2; + while (newcap < c->reply_len + n + 1) + newcap *= 2; + char *p = realloc(c->reply, newcap); + if (!p) { + fprintf(stderr, "ERROR: realloc failed\n"); + return -1; + } + c->reply = p; + c->reply_cap = newcap; + } + memcpy(c->reply + c->reply_len, buf, n); + c->reply_len += n; + c->reply[c->reply_len] = '\0'; + return 0; +} + +/* Print libssh error and transition to error */ +static void set_error(client_t *c, const char *msg) { + fprintf(stderr, "ERROR: %s: %s\n", msg, ssh_get_error(c->session)); + c->state = S_ERROR; +} + +/* Called whenever the polled fd has activity (readable/writable) */ +static void poll_cb(uv_poll_t *handle, int status, int events) { + (void)status; + client_t *c = (client_t*)handle->data; + int rc = 0; + + if (c->state == S_DONE || c->state == S_ERROR) { + printf("uv_poll_stop\n"); + uv_poll_stop(&c->poll); + return; + } + + /* Drive a simple state machine */ + switch (c->state) { + case S_CONNECT: + // ssh_connect() has to be called multiple times if the session is in non blocking mode + rc = ssh_connect(c->session); + if (rc == SSH_OK) { + fprintf(stderr, "Connected (SSH_OK)\n"); + c->state = S_AUTH; + } else if (rc == SSH_AGAIN) { + /* Not ready yet: wait for next poll events */ + return; + } else { + set_error(c, "ssh_connect failed"); + return; + } + /* fallthrough */ + case S_AUTH: + /* Authenticate with password (non-blocking)*/ + rc = ssh_userauth_password(c->session, NULL, c->password); + if (rc == SSH_AUTH_SUCCESS) { + fprintf(stderr, "Authenticated (password)\n"); + c->state = S_CHANNEL_OPEN; + } else if (rc == SSH_AUTH_AGAIN) { + return; + } else { + set_error(c, "ssh_userauth_password failed"); + return; + } + /* fallthrough */ + case S_CHANNEL_OPEN: + if (!c->channel) + c->channel = ssh_channel_new(c->session); + if (!c->channel) { + set_error(c, "ssh_channel_new failed"); + return; + } + rc = ssh_channel_open_session(c->channel); + if (rc == SSH_OK) { + fprintf(stderr, "Channel opened\n"); + c->state = S_SUBSYSTEM; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_open_session failed"); + return; + } + /* fallthrough */ + case S_SUBSYSTEM: + rc = ssh_channel_request_subsystem(c->channel, "netconf"); + if (rc == SSH_OK) { + fprintf(stderr, "Requested subsystem: netconf\n"); + c->state = S_SEND_HELLO; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_request_subsystem failed"); + return; + } + /* fallthrough */ + case S_SEND_HELLO: { + if (c->hello_sent >= c->hello_len) { + c->state = S_RECV_HELLO; + return; + } + /* write portion remaining */ + int wrote = ssh_channel_write(c->channel, + c->hello + c->hello_sent, + (uint32_t)(c->hello_len - c->hello_sent)); + if (wrote > 0) { + c->hello_sent += (size_t)wrote; + /* if not fully written, we will be called again when socket is writable */ + if (c->hello_sent >= c->hello_len) { + fprintf(stderr, "Hello fully sent\n"); + c->state = S_RECV_HELLO; + } else { + return; + } + } else if (wrote == SSH_ERROR) { + set_error(c, "ssh_channel_write failed"); + return; + } else if (wrote == SSH_AGAIN || wrote == 0) { + /* Not writable now; wait for next poll callback */ + return; + } else { + /* unexpected */ + set_error(c, "ssh_channel_write returned unexpected value"); + return; + } + break; + } + case S_RECV_HELLO: { + char buf[BUF_SIZE]; + int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); + if (n > 0) { + /* append and check for end marker "]]>]]>" */ + if (append_reply(c, buf, (size_t)n) != 0) { + set_error(c, "realloc failed"); + return; + } + fprintf(stderr, "Read %d bytes (total %zu)\n", n, c->reply_len); + /* If we see RFC6242 end-of-message token, we can process hello reply */ + if (strstr(c->reply, "]]>]]>") != NULL) { + fprintf(stdout, "=== NETCONF hello reply ===\n%s\n=== end ===\n", c->reply); + /* Reset reply buffer for next message */ + c->reply_len = 0; + c->reply[0] = '\0'; + c->state = S_SEND_GET_CONFIG; + return; + } + /* Keep waiting for more data */ + return; + } else if (n == 0) { + /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ + if (ssh_channel_is_eof(c->channel)) { + fprintf(stderr, "Channel EOF\n"); + if (c->reply_len > 0) { + fprintf(stdout, "=== NETCONF hello reply ===\n%s\n=== end ===\n", c->reply); + } + c->state = S_CLOSE; + return; + } else { + /* no data now, wait */ + return; + } + } else { /* n < 0 => SSH_ERROR */ + set_error(c, "ssh_channel_read_nonblocking failed"); + return; + } + break; + } + case S_SEND_GET_CONFIG: { + if (c->get_config_sent >= c->get_config_len) { + c->state = S_RECV_GET_CONFIG; + return; + } + /* write portion remaining */ + int wrote = ssh_channel_write(c->channel, + c->get_config + c->get_config_sent, + (uint32_t)(c->get_config_len - c->get_config_sent)); + if (wrote > 0) { + c->get_config_sent += (size_t)wrote; + /* if not fully written, we will be called again when socket is writable */ + if (c->get_config_sent >= c->get_config_len) { + fprintf(stderr, "GET_CONFIG fully sent\n"); + c->state = S_RECV_GET_CONFIG; + } else { + return; + } + } else if (wrote == SSH_ERROR) { + set_error(c, "ssh_channel_write failed for GET_CONFIG"); + return; + } else if (wrote == SSH_AGAIN || wrote == 0) { + /* Not writable now; wait for next poll callback */ + return; + } else { + /* unexpected */ + set_error(c, "ssh_channel_write returned unexpected value for GET_CONFIG"); + return; + } + break; + } + case S_RECV_GET_CONFIG: { + char buf[BUF_SIZE]; + int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); + if (n > 0) { + /* append and check for end marker "]]>]]>" */ + if (append_reply(c, buf, (size_t)n) != 0) { + set_error(c, "realloc failed"); + return; + } + fprintf(stderr, "Read GET_CONFIG reply %d bytes (total %zu)\n", n, c->reply_len); + /* If we see RFC6242 end-of-message token, we can proceed to close */ + if (strstr(c->reply, "]]>]]>") != NULL) { + fprintf(stdout, "=== NETCONF GET_CONFIG reply ===\n%s\n=== end ===\n", c->reply); + /* Reset reply buffer for next message */ + c->reply_len = 0; + c->reply[0] = '\0'; + c->state = S_CLOSE; + return; + } + /* Keep waiting for more data */ + return; + } else if (n == 0) { + /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ + if (ssh_channel_is_eof(c->channel)) { + fprintf(stderr, "Channel EOF during GET_CONFIG reply\n"); + if (c->reply_len > 0) { + fprintf(stdout, "=== NETCONF GET_CONFIG reply (partial) ===\n%s\n=== end ===\n", c->reply); + } + c->state = S_CLOSE; + return; + } else { + /* no data now, wait */ + return; + } + } else { /* n < 0 => SSH_ERROR */ + set_error(c, "ssh_channel_read_nonblocking failed during GET_CONFIG reply"); + return; + } + break; + } + case S_CLOSE: { + /* gracefully close the NETCONF session */ + if (c->close_sent >= c->close_len) { + c->state = S_RECV_CLOSE_REPLY; + return; + } + /* write portion remaining */ + int wrote = ssh_channel_write(c->channel, + c->close + c->close_sent, + (uint32_t)(c->close_len - c->close_sent)); + if (wrote > 0) { + c->close_sent += (size_t)wrote; + /* if not fully written, we will be called again when socket is writable */ + if (c->close_sent >= c->close_len) { + fprintf(stderr, "Close message fully sent, waiting for reply\n"); + c->state = S_RECV_CLOSE_REPLY; + } else { + return; + } + } else if (wrote == SSH_ERROR) { + set_error(c, "ssh_channel_write failed for NETCONF close message"); + return; + } else if (wrote == SSH_AGAIN || wrote == 0) { + /* Not writable now; wait for next poll callback */ + return; + } else { + /* unexpected */ + set_error(c, "ssh_channel_write returned unexpected value"); + return; + } + break; + } + case S_RECV_CLOSE_REPLY: { + char buf[BUF_SIZE]; + int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); + if (n > 0) { + /* append and check for end marker "]]>]]>" */ + if (append_reply(c, buf, (size_t)n) != 0) { + set_error(c, "realloc failed"); + return; + } + fprintf(stderr, "Read close reply %d bytes (total %zu)\n", n, c->reply_len); + /* If we see RFC6242 end-of-message token, we can stop */ + if (strstr(c->reply, "]]>]]>") != NULL) { + fprintf(stdout, "=== NETCONF close reply ===\n%s\n=== end ===\n", c->reply); + c->state = S_CLEANUP; + return; + } + /* Keep waiting for more data */ + return; + } else if (n == 0) { + /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ + if (ssh_channel_is_eof(c->channel)) { + fprintf(stderr, "Channel EOF during close reply\n"); + if (c->reply_len > 0) { + fprintf(stdout, "=== NETCONF close reply (partial) ===\n%s\n=== end ===\n", c->reply); + } + c->state = S_CLEANUP; + return; + } else { + /* no data now, wait */ + return; + } + } else { /* n < 0 => SSH_ERROR */ + set_error(c, "ssh_channel_read_nonblocking failed during close reply"); + return; + } + break; + } + case S_CLEANUP: + c->state = S_DONE; + fprintf(stderr, "Disconnected and cleaned up\n"); + /* stop the poll loop in a safe way */ + uv_poll_stop(&c->poll); + uv_stop(c->loop); + return; + + default: + return; + } +} + +/* Helper: initialize session and uv poll */ +static int client_init(client_t *c) { + c->session = ssh_new(); + if (!c->session) { + fprintf(stderr, "ERROR: ssh_new error"); + return -1; + } + ssh_options_set(c->session, SSH_OPTIONS_HOST, c->host); + ssh_options_set(c->session, SSH_OPTIONS_PORT, &c->port); + ssh_options_set(c->session, SSH_OPTIONS_USER, c->user); + + /* Non-blocking mode */ + ssh_set_blocking(c->session, 0); + + c->channel = NULL; + c->reply = NULL; + c->reply_len = 0; + c->reply_cap = 0; + c->hello_sent = 0; + c->state = S_CONNECT; + + /* prepare hello xml (NETCONF 1.0/1.1 capabilities + chunked framing marker) */ + c->hello = NETCONF_HELLO; + c->hello_len = strlen(c->hello); + + c->get_config = NETCONF_GET_CONFIG; + c->get_config_len = strlen(c->get_config); + c->get_config_sent = 0; + + c->close = NETCONF_CLOSE_SESSION; + c->close_len = strlen(c->close); + c->close_sent = 0; + + return 0; +} + +// Walk callback to close remaining handles +static void close_walk_cb(uv_handle_t* handle, void* arg) { + if (!uv_is_closing(handle)) { + uv_close(handle, NULL); + } +} + +int main(int argc, char **argv) { + if (argc < 5) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + client_t client = { 0 }; + + client.host = argv[1]; + client.port = atoi(argv[2]); + client.user = argv[3]; + client.password = argv[4]; + + if (client_init(&client) != 0) { + fprintf(stderr, "Failed to init client\n"); + return 2; + } + + /* create the uv loop and poll handle*/ + client.loop = uv_default_loop(); + + /* We need to ensure we have a valid fd to initialize uv_poll */ + int err = ssh_connect(client.session); + if (err == SSH_ERROR) { + fprintf(stderr, "Initial ssh_connect failed: %s\n", ssh_get_error(client.session)); + ssh_free(client.session); + return 3; + } + /* At this point ssh_get_fd should return a valid fd for uv_poll */ + client.fd = ssh_get_fd(client.session); + if (client.fd < 0) { + fprintf(stderr, "Could not get SSH session fd\n"); + ssh_disconnect(client.session); + ssh_free(client.session); + return 4; + } + + err = uv_poll_init(client.loop, &client.poll, client.fd); + if (err < 0) { + fprintf(stderr, "uv_poll_init failed: %s\n", uv_strerror(err)); + ssh_disconnect(client.session); + ssh_free(client.session); + return 5; + } + client.poll.data = &client; + + /* Watch for read/write events; libssh manages what it needs depending on state */ + err = uv_poll_start(&client.poll, UV_READABLE | UV_WRITABLE, poll_cb); + if (err < 0) { + fprintf(stderr, "uv_poll_start failed: %s\n", uv_strerror(err)); + uv_close((uv_handle_t*)&client.poll, NULL); + ssh_disconnect(client.session); + ssh_free(client.session); + return 6; + } + + /* If the preliminary ssh_connect returned SSH_AGAIN, the state should still be CONNECT. + Otherwise, poll_cb will drive the next steps. */ + fprintf(stderr, "Starting libuv loop\n"); + uv_run(client.loop, UV_RUN_DEFAULT); + + /* cleanup */ + if (client.reply) + free(client.reply); + + /* graceful close of ssh channel & session */ + if (client.channel) { + ssh_channel_send_eof(client.channel); + ssh_channel_close(client.channel); + ssh_channel_free(client.channel); + client.channel = NULL; + } + if (client.session) { + ssh_disconnect(client.session); + ssh_free(client.session); + } + + // see MAKE_VALGRIND_HAPPY in libuv/test/task.h + // walk the loop to close any remaining handles + uv_walk(client.loop, close_walk_cb, NULL); + // run the loop one more time to let close callbacks execute + uv_run(client.loop, UV_RUN_DEFAULT); + + // now it's safe to close the loop + err = uv_loop_close(client.loop); + if (err != 0) { + fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(err)); + + // If we still have handles, print debug info + if (err == UV_EBUSY) { + fprintf(stderr, "There are still active handles in the loop. This is a leak.\n"); + } + } + uv_library_shutdown(); + + fprintf(stderr, "Exited\n"); + return (client.state == S_DONE) ? 0 : 7; +} diff --git a/libuv_libssh/make.sh b/libuv_libssh/make.sh new file mode 100755 index 0000000..9dcdd3c --- /dev/null +++ b/libuv_libssh/make.sh @@ -0,0 +1 @@ +gcc -Wall -g -o ancc2 async_netconf_client_2.c -lssh -luv From 1384e0197dd1cc7d9646051b93bb98db97515788 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Thu, 21 Aug 2025 17:03:13 +0200 Subject: [PATCH 29/37] refactor --- libuv_libssh/async_netconf_client_2.c | 359 ++++++++++---------------- 1 file changed, 130 insertions(+), 229 deletions(-) diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c index 954e8f3..935cc31 100644 --- a/libuv_libssh/async_netconf_client_2.c +++ b/libuv_libssh/async_netconf_client_2.c @@ -52,14 +52,22 @@ enum client_state { S_SEND_HELLO, S_RECV_HELLO, S_SEND_GET_CONFIG, + S_SEND_GET_CONFIG_WRITING, S_RECV_GET_CONFIG, S_CLOSE, + S_CLOSE_WRITING, S_RECV_CLOSE_REPLY, S_CLEANUP, S_DONE, S_ERROR }; +typedef struct { + const char *data; + size_t len; + size_t sent; +} write_buffer_t; + typedef struct { uv_loop_t *loop; @@ -73,20 +81,8 @@ typedef struct { const char *user; const char *password; - /* writing hello */ - const char *hello; - size_t hello_len; - size_t hello_sent; - - /* writing get-config */ - const char *get_config; - size_t get_config_len; - size_t get_config_sent; - - /* writing close */ - const char *close; - size_t close_len; - size_t close_sent; + /* current write buffer */ + write_buffer_t write_buf; /* read buffer */ char *reply; @@ -121,6 +117,85 @@ static void set_error(client_t *c, const char *msg) { c->state = S_ERROR; } +/* Initialize write buffer for sending data */ +static void init_write_buffer(client_t *c, const char *data) { + c->write_buf.data = data; + c->write_buf.len = strlen(data); + c->write_buf.sent = 0; +} + +/* Generic write operation - returns 1 if complete, 0 if more needed, -1 if error */ +static int do_write(client_t *c, const char *operation) { + if (c->write_buf.sent >= c->write_buf.len) { + return 1; /* complete */ + } + + int wrote = ssh_channel_write(c->channel, + c->write_buf.data + c->write_buf.sent, + (uint32_t)(c->write_buf.len - c->write_buf.sent)); + + if (wrote > 0) { + c->write_buf.sent += (size_t)wrote; + if (c->write_buf.sent >= c->write_buf.len) { + fprintf(stderr, "%s fully sent\n", operation); + return 1; /* complete */ + } + return 0; /* more data to send */ + } else if (wrote == SSH_ERROR) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } else if (wrote == SSH_AGAIN || wrote == 0) { + return 0; /* not writable now */ + } else { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } +} + +/* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ +static int do_read(client_t *c, const char *operation, enum client_state next_state) { + char buf[BUF_SIZE]; + int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); + + if (n > 0) { + if (append_reply(c, buf, (size_t)n) != 0) { + set_error(c, "realloc failed"); + return -1; + } + fprintf(stderr, "Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); + + /* Check for RFC6242 end-of-message token */ + if (strstr(c->reply, "]]>]]>") != NULL) { + fprintf(stdout, "=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); + /* Reset reply buffer for next message */ + c->reply_len = 0; + c->reply[0] = '\0'; + c->state = next_state; + return 1; /* complete */ + } + return 0; /* more data expected */ + } else if (n == 0) { + if (ssh_channel_is_eof(c->channel)) { + fprintf(stderr, "Channel EOF during %s\n", operation); + if (c->reply_len > 0) { + fprintf(stdout, "=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); + } + c->state = S_CLEANUP; + return 1; /* complete via EOF */ + } + return 0; /* no data now, wait */ + } else { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } +} + /* Called whenever the polled fd has activity (readable/writable) */ static void poll_cb(uv_poll_t *handle, int status, int events) { (void)status; @@ -136,13 +211,11 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { /* Drive a simple state machine */ switch (c->state) { case S_CONNECT: - // ssh_connect() has to be called multiple times if the session is in non blocking mode rc = ssh_connect(c->session); if (rc == SSH_OK) { fprintf(stderr, "Connected (SSH_OK)\n"); c->state = S_AUTH; } else if (rc == SSH_AGAIN) { - /* Not ready yet: wait for next poll events */ return; } else { set_error(c, "ssh_connect failed"); @@ -150,7 +223,6 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_AUTH: - /* Authenticate with password (non-blocking)*/ rc = ssh_userauth_password(c->session, NULL, c->password); if (rc == SSH_AUTH_SUCCESS) { fprintf(stderr, "Authenticated (password)\n"); @@ -163,11 +235,12 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_CHANNEL_OPEN: - if (!c->channel) - c->channel = ssh_channel_new(c->session); if (!c->channel) { - set_error(c, "ssh_channel_new failed"); - return; + c->channel = ssh_channel_new(c->session); + if (!c->channel) { + set_error(c, "ssh_channel_new failed"); + return; + } } rc = ssh_channel_open_session(c->channel); if (rc == SSH_OK) { @@ -184,6 +257,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { rc = ssh_channel_request_subsystem(c->channel, "netconf"); if (rc == SSH_OK) { fprintf(stderr, "Requested subsystem: netconf\n"); + init_write_buffer(c, NETCONF_HELLO); c->state = S_SEND_HELLO; } else if (rc == SSH_AGAIN) { return; @@ -192,225 +266,60 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { return; } /* fallthrough */ - case S_SEND_HELLO: { - if (c->hello_sent >= c->hello_len) { + case S_SEND_HELLO: + rc = do_write(c, "Hello"); + if (rc == 1) { c->state = S_RECV_HELLO; - return; - } - /* write portion remaining */ - int wrote = ssh_channel_write(c->channel, - c->hello + c->hello_sent, - (uint32_t)(c->hello_len - c->hello_sent)); - if (wrote > 0) { - c->hello_sent += (size_t)wrote; - /* if not fully written, we will be called again when socket is writable */ - if (c->hello_sent >= c->hello_len) { - fprintf(stderr, "Hello fully sent\n"); - c->state = S_RECV_HELLO; - } else { - return; - } - } else if (wrote == SSH_ERROR) { - set_error(c, "ssh_channel_write failed"); - return; - } else if (wrote == SSH_AGAIN || wrote == 0) { - /* Not writable now; wait for next poll callback */ - return; - } else { - /* unexpected */ - set_error(c, "ssh_channel_write returned unexpected value"); + } else if (rc == -1) { return; } break; - } - case S_RECV_HELLO: { - char buf[BUF_SIZE]; - int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - if (n > 0) { - /* append and check for end marker "]]>]]>" */ - if (append_reply(c, buf, (size_t)n) != 0) { - set_error(c, "realloc failed"); - return; - } - fprintf(stderr, "Read %d bytes (total %zu)\n", n, c->reply_len); - /* If we see RFC6242 end-of-message token, we can process hello reply */ - if (strstr(c->reply, "]]>]]>") != NULL) { - fprintf(stdout, "=== NETCONF hello reply ===\n%s\n=== end ===\n", c->reply); - /* Reset reply buffer for next message */ - c->reply_len = 0; - c->reply[0] = '\0'; - c->state = S_SEND_GET_CONFIG; - return; - } - /* Keep waiting for more data */ - return; - } else if (n == 0) { - /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ - if (ssh_channel_is_eof(c->channel)) { - fprintf(stderr, "Channel EOF\n"); - if (c->reply_len > 0) { - fprintf(stdout, "=== NETCONF hello reply ===\n%s\n=== end ===\n", c->reply); - } - c->state = S_CLOSE; - return; - } else { - /* no data now, wait */ - return; - } - } else { /* n < 0 => SSH_ERROR */ - set_error(c, "ssh_channel_read_nonblocking failed"); + case S_RECV_HELLO: + rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); + if (rc == -1) return; - } break; - } - case S_SEND_GET_CONFIG: { - if (c->get_config_sent >= c->get_config_len) { + case S_SEND_GET_CONFIG: + init_write_buffer(c, NETCONF_GET_CONFIG); + c->state = S_SEND_GET_CONFIG_WRITING; + /* fallthrough */ + case S_SEND_GET_CONFIG_WRITING: + rc = do_write(c, "GET_CONFIG"); + if (rc == 1) { c->state = S_RECV_GET_CONFIG; - return; - } - /* write portion remaining */ - int wrote = ssh_channel_write(c->channel, - c->get_config + c->get_config_sent, - (uint32_t)(c->get_config_len - c->get_config_sent)); - if (wrote > 0) { - c->get_config_sent += (size_t)wrote; - /* if not fully written, we will be called again when socket is writable */ - if (c->get_config_sent >= c->get_config_len) { - fprintf(stderr, "GET_CONFIG fully sent\n"); - c->state = S_RECV_GET_CONFIG; - } else { - return; - } - } else if (wrote == SSH_ERROR) { - set_error(c, "ssh_channel_write failed for GET_CONFIG"); - return; - } else if (wrote == SSH_AGAIN || wrote == 0) { - /* Not writable now; wait for next poll callback */ - return; - } else { - /* unexpected */ - set_error(c, "ssh_channel_write returned unexpected value for GET_CONFIG"); + } else if (rc == -1) { return; } break; - } - case S_RECV_GET_CONFIG: { - char buf[BUF_SIZE]; - int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - if (n > 0) { - /* append and check for end marker "]]>]]>" */ - if (append_reply(c, buf, (size_t)n) != 0) { - set_error(c, "realloc failed"); - return; - } - fprintf(stderr, "Read GET_CONFIG reply %d bytes (total %zu)\n", n, c->reply_len); - /* If we see RFC6242 end-of-message token, we can proceed to close */ - if (strstr(c->reply, "]]>]]>") != NULL) { - fprintf(stdout, "=== NETCONF GET_CONFIG reply ===\n%s\n=== end ===\n", c->reply); - /* Reset reply buffer for next message */ - c->reply_len = 0; - c->reply[0] = '\0'; - c->state = S_CLOSE; - return; - } - /* Keep waiting for more data */ - return; - } else if (n == 0) { - /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ - if (ssh_channel_is_eof(c->channel)) { - fprintf(stderr, "Channel EOF during GET_CONFIG reply\n"); - if (c->reply_len > 0) { - fprintf(stdout, "=== NETCONF GET_CONFIG reply (partial) ===\n%s\n=== end ===\n", c->reply); - } - c->state = S_CLOSE; - return; - } else { - /* no data now, wait */ - return; - } - } else { /* n < 0 => SSH_ERROR */ - set_error(c, "ssh_channel_read_nonblocking failed during GET_CONFIG reply"); + case S_RECV_GET_CONFIG: + rc = do_read(c, "GET_CONFIG reply", S_CLOSE); + if (rc == -1) return; - } break; - } - case S_CLOSE: { - /* gracefully close the NETCONF session */ - if (c->close_sent >= c->close_len) { + case S_CLOSE: + init_write_buffer(c, NETCONF_CLOSE_SESSION); + c->state = S_CLOSE_WRITING; + /* fallthrough */ + case S_CLOSE_WRITING: + rc = do_write(c, "Close message"); + if (rc == 1) { + fprintf(stderr, "Close message fully sent, waiting for reply\n"); c->state = S_RECV_CLOSE_REPLY; - return; - } - /* write portion remaining */ - int wrote = ssh_channel_write(c->channel, - c->close + c->close_sent, - (uint32_t)(c->close_len - c->close_sent)); - if (wrote > 0) { - c->close_sent += (size_t)wrote; - /* if not fully written, we will be called again when socket is writable */ - if (c->close_sent >= c->close_len) { - fprintf(stderr, "Close message fully sent, waiting for reply\n"); - c->state = S_RECV_CLOSE_REPLY; - } else { - return; - } - } else if (wrote == SSH_ERROR) { - set_error(c, "ssh_channel_write failed for NETCONF close message"); - return; - } else if (wrote == SSH_AGAIN || wrote == 0) { - /* Not writable now; wait for next poll callback */ - return; - } else { - /* unexpected */ - set_error(c, "ssh_channel_write returned unexpected value"); + } else if (rc == -1) { return; } break; - } - case S_RECV_CLOSE_REPLY: { - char buf[BUF_SIZE]; - int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - if (n > 0) { - /* append and check for end marker "]]>]]>" */ - if (append_reply(c, buf, (size_t)n) != 0) { - set_error(c, "realloc failed"); - return; - } - fprintf(stderr, "Read close reply %d bytes (total %zu)\n", n, c->reply_len); - /* If we see RFC6242 end-of-message token, we can stop */ - if (strstr(c->reply, "]]>]]>") != NULL) { - fprintf(stdout, "=== NETCONF close reply ===\n%s\n=== end ===\n", c->reply); - c->state = S_CLEANUP; - return; - } - /* Keep waiting for more data */ + case S_RECV_CLOSE_REPLY: + rc = do_read(c, "close reply", S_CLEANUP); + if (rc == -1) return; - } else if (n == 0) { - /* 0: no data available in nonblocking mode or EOF (depends). Check channel EOF */ - if (ssh_channel_is_eof(c->channel)) { - fprintf(stderr, "Channel EOF during close reply\n"); - if (c->reply_len > 0) { - fprintf(stdout, "=== NETCONF close reply (partial) ===\n%s\n=== end ===\n", c->reply); - } - c->state = S_CLEANUP; - return; - } else { - /* no data now, wait */ - return; - } - } else { /* n < 0 => SSH_ERROR */ - set_error(c, "ssh_channel_read_nonblocking failed during close reply"); - return; - } break; - } case S_CLEANUP: c->state = S_DONE; fprintf(stderr, "Disconnected and cleaned up\n"); - /* stop the poll loop in a safe way */ uv_poll_stop(&c->poll); uv_stop(c->loop); return; - default: return; } @@ -434,20 +343,12 @@ static int client_init(client_t *c) { c->reply = NULL; c->reply_len = 0; c->reply_cap = 0; - c->hello_sent = 0; c->state = S_CONNECT; - - /* prepare hello xml (NETCONF 1.0/1.1 capabilities + chunked framing marker) */ - c->hello = NETCONF_HELLO; - c->hello_len = strlen(c->hello); - - c->get_config = NETCONF_GET_CONFIG; - c->get_config_len = strlen(c->get_config); - c->get_config_sent = 0; - - c->close = NETCONF_CLOSE_SESSION; - c->close_len = strlen(c->close); - c->close_sent = 0; + + /* Initialize write buffer to empty */ + c->write_buf.data = NULL; + c->write_buf.len = 0; + c->write_buf.sent = 0; return 0; } From 6097e8beca888c1e7f7874d66af160078577a06e Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 22 Aug 2025 08:34:52 +0200 Subject: [PATCH 30/37] import new libuv+libssh solution into acton --- libuv_libssh/async_netconf_client_2.c | 41 ++- src/netconf.h | 26 ++ src/ssh.act | 23 +- src/ssh.ext.c | 471 ++++++++++++++++++++------ 4 files changed, 435 insertions(+), 126 deletions(-) create mode 100644 src/netconf.h diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c index 935cc31..eb4a527 100644 --- a/libuv_libssh/async_netconf_client_2.c +++ b/libuv_libssh/async_netconf_client_2.c @@ -73,7 +73,7 @@ typedef struct { ssh_session session; ssh_channel channel; - uv_poll_t poll; + uv_poll_t *poll; int fd; enum client_state state; const char *host; @@ -204,7 +204,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { if (c->state == S_DONE || c->state == S_ERROR) { printf("uv_poll_stop\n"); - uv_poll_stop(&c->poll); + uv_poll_stop(c->poll); return; } @@ -317,7 +317,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { case S_CLEANUP: c->state = S_DONE; fprintf(stderr, "Disconnected and cleaned up\n"); - uv_poll_stop(&c->poll); + uv_poll_stop(c->poll); uv_stop(c->loop); return; default: @@ -340,6 +340,7 @@ static int client_init(client_t *c) { ssh_set_blocking(c->session, 0); c->channel = NULL; + c->poll = NULL; c->reply = NULL; c->reply_len = 0; c->reply_cap = 0; @@ -396,34 +397,44 @@ int main(int argc, char **argv) { return 4; } - err = uv_poll_init(client.loop, &client.poll, client.fd); + client.poll = malloc(sizeof(uv_poll_t)); + if (!client.poll) { + fprintf(stderr, "Failed to allocate poll handle\n"); + ssh_disconnect(client.session); + ssh_free(client.session); + return 5; + } + + err = uv_poll_init(client.loop, client.poll, client.fd); if (err < 0) { fprintf(stderr, "uv_poll_init failed: %s\n", uv_strerror(err)); + free(client.poll); + client.poll = NULL; ssh_disconnect(client.session); ssh_free(client.session); - return 5; + return 6; } - client.poll.data = &client; + client.poll->data = &client; /* Watch for read/write events; libssh manages what it needs depending on state */ - err = uv_poll_start(&client.poll, UV_READABLE | UV_WRITABLE, poll_cb); + err = uv_poll_start(client.poll, UV_READABLE | UV_WRITABLE, poll_cb); if (err < 0) { fprintf(stderr, "uv_poll_start failed: %s\n", uv_strerror(err)); - uv_close((uv_handle_t*)&client.poll, NULL); + uv_close((uv_handle_t*)client.poll, NULL); + free(client.poll); + client.poll = NULL; ssh_disconnect(client.session); ssh_free(client.session); - return 6; + return 7; } /* If the preliminary ssh_connect returned SSH_AGAIN, the state should still be CONNECT. Otherwise, poll_cb will drive the next steps. */ fprintf(stderr, "Starting libuv loop\n"); uv_run(client.loop, UV_RUN_DEFAULT); + fprintf(stderr, "Stopped libuv loop\n"); /* cleanup */ - if (client.reply) - free(client.reply); - /* graceful close of ssh channel & session */ if (client.channel) { ssh_channel_send_eof(client.channel); @@ -454,6 +465,12 @@ int main(int argc, char **argv) { } uv_library_shutdown(); + if (client.reply) + free(client.reply); + + if (client.poll) + free(client.poll); + fprintf(stderr, "Exited\n"); return (client.state == S_DONE) ? 0 : 7; } diff --git a/src/netconf.h b/src/netconf.h new file mode 100644 index 0000000..abccdc6 --- /dev/null +++ b/src/netconf.h @@ -0,0 +1,26 @@ +// NETCONF message +#define NETCONF_HELLO_MSG \ + "\n" \ + "\n" \ + " \n" \ + " urn:ietf:params:netconf:base:1.0\n" \ + " \n" \ + "]]>]]>" + +#define NETCONF_GET_CONFIG_MSG \ + "\n" \ + "\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\n" \ + "]]>]]>" + +// NETCONF message +#define NETCONF_CLOSE_SESSION_MSG \ + "\n" \ + "\n" \ + " \n" \ + "]]>]]>" diff --git a/src/ssh.act b/src/ssh.act index f5fe6ed..f13647b 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -20,6 +20,9 @@ actor Client(cap: net.TCPConnectCap, # haha, this is really a pointer :P var _ssh_session: u64 = 0 + var _uv_loop: u64 = 0 + var _poll: u64 = 0 + var _fd: int = 0 proc def _pin_affinity() -> None: NotImplemented @@ -28,6 +31,9 @@ actor Client(cap: net.TCPConnectCap, def get_ssh_session(): return _ssh_session + def get_uv_loop(): + return _uv_loop + def get_password() -> str: return password @@ -68,6 +74,7 @@ actor Channel(client: Client): var _ssh_channel: u64 = 0 var _ssh_session: u64 = client.get_ssh_session() + var _uv_loop: u64 = client.get_uv_loop() var _password: str = client.get_password() var _subsystem: str = client.get_subsystem() var payload = None @@ -122,15 +129,15 @@ actor main(env): cc = Channel(c) - # get netconf server's capabilities - cc.setPayload(']]>]]>') - print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end") - print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n") + # # get netconf server's capabilities + # cc.setPayload(']]>]]>') + # print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end") + # print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n") - # TODO do this in disconnect(): gracefully close a channel - cc.setPayload(']]>]]>') - print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end") - print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n") + # # TODO do this in disconnect(): gracefully close a channel + # cc.setPayload(']]>]]>') + # print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end") + # print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n") c.disconnect() diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 802c9b9..ac082a4 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -6,29 +6,295 @@ // acton includes #include +#include "netconf.h" + #ifndef DEBUG_MODE // #define DEBUG_MODE /* uncomment for pretty prints */ #endif -// NETCONF message -#define NETCONF_HELLO_MSG \ - "\n" \ - "\n" \ - " \n" \ - " urn:ietf:params:netconf:base:1.0\n" \ - " \n" \ - "]]>]]>" - -// NETCONF message -#define NETCONF_CLOSE_SESSION_MSG \ - "\n" \ - "\n" \ - " \n" \ - "]]>]]>" - -#define BUF_SIZE 65536 // this will definitely not be enough, find a better way. maybe chunks like libyang does? -#define TIMEOUT 1500000 // microseconds: 1.5 seconds -#define USLEEP_INTERVAL 5000 // microseconds: 0.005 seconds +#define BUF_SIZE 512 + +enum client_state { + S_CONNECT, + S_AUTH, + S_CHANNEL_OPEN, + S_SUBSYSTEM, + S_SEND_HELLO, + S_RECV_HELLO, + S_SEND_GET_CONFIG, + S_SEND_GET_CONFIG_WRITING, + S_RECV_GET_CONFIG, + S_CLOSE, + S_CLOSE_WRITING, + S_RECV_CLOSE_REPLY, + S_CLEANUP, + S_DONE, + S_ERROR +}; + +typedef struct { + const char *data; + size_t len; + size_t sent; +} write_buffer_t; + +typedef struct { + uv_loop_t *loop; + + ssh_session session; + ssh_channel channel; + uv_poll_t *poll; + int fd; + enum client_state state; + const char *host; + int port; + const char *user; + const char *password; + + /* current write buffer */ + write_buffer_t write_buf; + + /* read buffer */ + char *reply; + size_t reply_len; + size_t reply_cap; +} client_t; + +/* Simple helper to append to reply buffer */ +static int append_reply(client_t *c, const char *buf, size_t n) { + if (n == 0) return 0; + if (c->reply_len + n + 1 > c->reply_cap) { + size_t newcap = (c->reply_cap == 0) ? BUF_SIZE : c->reply_cap * 2; + while (newcap < c->reply_len + n + 1) + newcap *= 2; + char *p = realloc(c->reply, newcap); + if (!p) { + printf("ERROR: realloc failed\n"); + return -1; + } + c->reply = p; + c->reply_cap = newcap; + } + memcpy(c->reply + c->reply_len, buf, n); + c->reply_len += n; + c->reply[c->reply_len] = '\0'; + return 0; +} + +/* Print libssh error and transition to error */ +static void set_error(client_t *c, const char *msg) { + printf("ERROR: %s: %s\n", msg, ssh_get_error(c->session)); + c->state = S_ERROR; +} + +/* Initialize write buffer for sending data */ +static void init_write_buffer(client_t *c, const char *data) { + c->write_buf.data = data; + c->write_buf.len = strlen(data); + c->write_buf.sent = 0; +} + +/* Generic write operation - returns 1 if complete, 0 if more needed, -1 if error */ +static int do_write(client_t *c, const char *operation) { + if (c->write_buf.sent >= c->write_buf.len) { + return 1; /* complete */ + } + + int wrote = ssh_channel_write(c->channel, + c->write_buf.data + c->write_buf.sent, + (uint32_t)(c->write_buf.len - c->write_buf.sent)); + + if (wrote > 0) { + c->write_buf.sent += (size_t)wrote; + if (c->write_buf.sent >= c->write_buf.len) { + fprintf(stderr, "%s fully sent\n", operation); + return 1; /* complete */ + } + return 0; /* more data to send */ + } else if (wrote == SSH_ERROR) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } else if (wrote == SSH_AGAIN || wrote == 0) { + return 0; /* not writable now */ + } else { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } +} + +/* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ +static int do_read(client_t *c, const char *operation, enum client_state next_state) { + char buf[BUF_SIZE]; + int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); + + if (n > 0) { + if (append_reply(c, buf, (size_t)n) != 0) { + set_error(c, "realloc failed"); + return -1; + } + fprintf(stderr, "Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); + + /* Check for RFC6242 end-of-message token */ + if (strstr(c->reply, "]]>]]>") != NULL) { + fprintf(stdout, "=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); + /* Reset reply buffer for next message */ + c->reply_len = 0; + c->reply[0] = '\0'; + c->state = next_state; + return 1; /* complete */ + } + return 0; /* more data expected */ + } else if (n == 0) { + if (ssh_channel_is_eof(c->channel)) { + fprintf(stderr, "Channel EOF during %s\n", operation); + if (c->reply_len > 0) { + fprintf(stdout, "=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); + } + c->state = S_CLEANUP; + return 1; /* complete via EOF */ + } + return 0; /* no data now, wait */ + } else { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", operation); + set_error(c, error_msg); + return -1; /* error */ + } +} + +/* Called whenever the polled fd has activity (readable/writable) */ +static void poll_cb(uv_poll_t *handle, int status, int events) { + printf("poll_cb\n"); + (void)status; + int rc = 0; + client_t *c = (client_t*)handle->data; + + if (c->state == S_DONE || c->state == S_ERROR) { + printf("uv_poll_stop\n"); + uv_poll_stop(c->poll); + return; + } + + /* Drive a simple state machine */ + switch (c->state) { + case S_CONNECT: + rc = ssh_connect(c->session); + if (rc == SSH_OK) { + printf("Connected (SSH_OK)\n"); + c->state = S_AUTH; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_connect failed"); + return; + } + /* fallthrough */ + case S_AUTH: + rc = ssh_userauth_password(c->session, NULL, c->password); + if (rc == SSH_AUTH_SUCCESS) { + printf("Authenticated (password)\n"); + c->state = S_CHANNEL_OPEN; + } else if (rc == SSH_AUTH_AGAIN) { + return; + } else { + set_error(c, "ssh_userauth_password failed"); + return; + } + /* fallthrough */ + case S_CHANNEL_OPEN: + if (!c->channel) { + c->channel = ssh_channel_new(c->session); + if (!c->channel) { + set_error(c, "ssh_channel_new failed"); + return; + } + } + rc = ssh_channel_open_session(c->channel); + if (rc == SSH_OK) { + printf("Channel opened\n"); + c->state = S_SUBSYSTEM; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_open_session failed"); + return; + } + /* fallthrough */ + case S_SUBSYSTEM: + rc = ssh_channel_request_subsystem(c->channel, "netconf"); + if (rc == SSH_OK) { + printf("Requested subsystem: netconf\n"); + init_write_buffer(c, NETCONF_HELLO_MSG); + c->state = S_SEND_HELLO; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_request_subsystem failed"); + return; + } + /* fallthrough */ + case S_SEND_HELLO: + rc = do_write(c, "Hello"); + if (rc == 1) { + c->state = S_RECV_HELLO; + } else if (rc == -1) { + return; + } + break; + case S_RECV_HELLO: + rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); + if (rc == -1) + return; + break; + case S_SEND_GET_CONFIG: + init_write_buffer(c, NETCONF_GET_CONFIG_MSG); + c->state = S_SEND_GET_CONFIG_WRITING; + /* fallthrough */ + case S_SEND_GET_CONFIG_WRITING: + rc = do_write(c, "GET_CONFIG"); + if (rc == 1) { + c->state = S_RECV_GET_CONFIG; + } else if (rc == -1) { + return; + } + break; + case S_RECV_GET_CONFIG: + rc = do_read(c, "GET_CONFIG reply", S_CLOSE); + if (rc == -1) + return; + break; + case S_CLOSE: + init_write_buffer(c, NETCONF_CLOSE_SESSION_MSG); + c->state = S_CLOSE_WRITING; + /* fallthrough */ + case S_CLOSE_WRITING: + rc = do_write(c, "Close message"); + if (rc == 1) { + printf("Close message fully sent, waiting for reply\n"); + c->state = S_RECV_CLOSE_REPLY; + } else if (rc == -1) { + return; + } + break; + case S_RECV_CLOSE_REPLY: + rc = do_read(c, "close reply", S_CLEANUP); + if (rc == -1) + return; + break; + case S_CLEANUP: + c->state = S_DONE; + printf("Disconnected and cleaned up\n"); + uv_poll_stop(c->poll); + uv_stop(c->loop); + return; + default: + return; + } +} void ssh_channel_close_free(ssh_channel channel) { int err = ssh_channel_close(channel); @@ -158,55 +424,41 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si return SSH_ERROR; } -/** - * @brief Set subsystem - * - * @param[in] channel ssh_channel - * @param[in] subsystem The subsystem, for example "netconf" - */ - int set_subsystem (ssh_channel channel, const char *subsystem) { - int err = 0; - int timeout = TIMEOUT; - - if (channel == NULL) { - printf("%s: channel is NULL\n", __FUNCTION__); - return SSH_ERROR; - } - - if (subsystem && !strcmp(subsystem, "netconf")) { - while ((err = ssh_channel_request_subsystem(channel, "netconf")) == SSH_AGAIN && timeout > 0) - { - err = usleep(USLEEP_INTERVAL); - if (err) { - printf("%s: usleep() error '%s' (%d)\n", __FUNCTION__, strerror(errno), errno); - return SSH_ERROR; - } - timeout -= USLEEP_INTERVAL; - } - if (err != SSH_OK) - { - printf("%s: ssh_channel_request_subsystem() Error setting SSH subsystem 'netconf': %d\n", __FUNCTION__, err); - return SSH_ERROR; - } - } - - return SSH_OK; -} - // Client $R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { pin_actor_affinity(); int err = 0; - ssh_session session = ssh_new(); - if (session == NULL) + + client_t client = { 0 }; + client.poll = NULL; + client.loop = uv_default_loop(); + client.session = ssh_new(); + if (client.session == NULL) { printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } - self->_ssh_session = toB_u64((unsigned long)session); + client.channel = NULL; + client.reply = NULL; + client.reply_len = 0; + client.reply_cap = 0; + client.state = S_CONNECT; + + /* Initialize write buffer to empty */ + client.write_buf.data = NULL; + client.write_buf.len = 0; + client.write_buf.sent = 0; + + client.host = (const char *)fromB_str(self->host); + client.port = self->port->val; + client.user = (const char *)fromB_str(self->username); + client.password = (const char *)fromB_str(self->password); + + self->_ssh_session = toB_u64((unsigned long)client.session); + self->_uv_loop = toB_u64((unsigned long)client.loop); #ifdef DEBUG_MODE // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS @@ -218,34 +470,36 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si } #endif - err = ssh_session_set_disconnect_message(session, "Disconnecting SSH, powered by Acton"); + err = ssh_session_set_disconnect_message(client.session, "Disconnecting SSH, powered by Acton"); if (err != SSH_OK) { printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(session, SSH_OPTIONS_HOST, fromB_str(self->host)); + err = ssh_options_set(client.session, SSH_OPTIONS_HOST, client.host); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(session, SSH_OPTIONS_PORT, &self->port->val); + err = ssh_options_set(client.session, SSH_OPTIONS_PORT, &client.port); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(session, SSH_OPTIONS_USER, fromB_str(self->username)); + err = ssh_options_set(client.session, SSH_OPTIONS_USER, client.user); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } + ssh_set_blocking(client.session, 0); + // should it auto-parse user config? for example from /home/user/.ssh/ // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); // if (err != SSH_OK) @@ -254,17 +508,56 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si // return $R_CONT(c$cont, B_None); // } - err = ssh_connect(session); + err = ssh_connect(client.session); if (err != SSH_OK) { - printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(session)); + printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(client.session)); return $R_CONT(c$cont, B_None); } - err = ssh_userauth_password(session, NULL, (const char *)fromB_str(self->password)); - if (err != SSH_OK) + // At this point ssh_get_fd should return a valid fd for uv_poll + int fd = ssh_get_fd(client.session); + self->_fd = to$int(fd); + if (self->_fd < 0) { - printf("%s: ssh_userauth_password() error: %s\n", __FUNCTION__, ssh_get_error(session)); + printf("Could not get SSH session fd\n"); + ssh_disconnect(client.session); + ssh_free(client.session); + return $R_CONT(c$cont, B_None); + } + + client.poll = malloc(sizeof(uv_poll_t)); + if (!client.poll) + { + printf("Failed to allocate poll handle\n"); + ssh_disconnect(client.session); + ssh_free(client.session); + return $R_CONT(c$cont, B_None); + } + + err = uv_poll_init(client.loop, client.poll, fd); + if (err < 0) + { + printf("uv_poll_init failed: %s\n", uv_strerror(err)); + free(client.poll); + client.poll = NULL; + ssh_disconnect(client.session); + ssh_free(client.session); + return $R_CONT(c$cont, B_None); + } + self->_poll = toB_u64((unsigned long)client.poll); + client.poll->data = &client; + + /* Watch for read/write events; libssh manages what it needs depending on state */ + err = uv_poll_start(client.poll, UV_READABLE | UV_WRITABLE, poll_cb); + if (err < 0) + { + printf("uv_poll_start failed: %s\n", uv_strerror(err)); + uv_close((uv_handle_t*)client.poll, NULL); + free(client.poll); + client.poll = NULL; + ssh_disconnect(client.session); + ssh_free(client.session); return $R_CONT(c$cont, B_None); } @@ -294,56 +587,22 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si $R sshQ_ChannelD__ssh_initG_local (sshQ_Channel self, $Cont c$cont) { pin_actor_affinity(); - int err = 0; - ssh_session session = (struct ssh_session_struct *)fromB_u64(self->_ssh_session); - ssh_channel channel = { 0 }; - #ifdef DEBUG_MODE printf("Connecting to SSH server\n"); #endif - channel = ssh_channel_new(session); - if (channel == NULL) - { - printf("%s: ssh_channel_new() Failed to create SSH channel\n", __FUNCTION__); - return $R_CONT(c$cont, B_None); - } - - self->_ssh_channel = toB_u64((unsigned long)channel); - - err = ssh_channel_open_session(channel); - if (err != SSH_OK) - { - printf("%s: ssh_channel_open_session() error (%d)\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } - - if (self->_subsystem) { - err = set_subsystem(channel, (const char *)fromB_str(self->_subsystem)); - if (err != SSH_OK) { - printf("%s: set_subsystem() setting subsystem failed error (%d)\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } - - // NOTE: add other subsystems here if needed - if (!strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { - // send netconf hello message - err = send_nc_payload(channel, NETCONF_HELLO_MSG, NULL, 0); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } - } - } + printf("Starting libuv loop\n"); + uv_run(self->_uv_loop, UV_RUN_DEFAULT); + printf("Stopped libuv loop\n"); return $R_CONT(c$cont, B_None); } $R sshQ_ChannelD__set_affinityG_local (sshQ_Channel self, $Cont C_cont, B_u64 affinity) { -#ifdef DEBUG_MODE +// #ifdef DEBUG_MODE printf("sshQ_ChannelD__set_affinityG_local, affinity=%ld\n", self->$affinity); -#endif +// #endif + // TODO set affinity return $R_CONT(C_cont, B_None); } From fae50255f9718e803efe6e630f13fad10cbba62f Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 22 Aug 2025 13:13:50 +0200 Subject: [PATCH 31/37] adapt and fix new libuv+libssh poll solution in the acton code --- libuv_libssh/async_netconf_client_2.c | 123 +++++---- libuv_libssh/make.sh | 2 +- src/netconf.h | 13 +- src/ssh.act | 85 ++---- src/ssh.ext.c | 356 +++++++++++--------------- 5 files changed, 248 insertions(+), 331 deletions(-) diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c index eb4a527..b873207 100644 --- a/libuv_libssh/async_netconf_client_2.c +++ b/libuv_libssh/async_netconf_client_2.c @@ -51,9 +51,9 @@ enum client_state { S_SUBSYSTEM, S_SEND_HELLO, S_RECV_HELLO, - S_SEND_GET_CONFIG, - S_SEND_GET_CONFIG_WRITING, - S_RECV_GET_CONFIG, + S_SEND_GET, + S_SEND_GET_WRITING, + S_RECV_GET, S_CLOSE, S_CLOSE_WRITING, S_RECV_CLOSE_REPLY, @@ -275,23 +275,23 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_HELLO: - rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); + rc = do_read(c, "hello reply", S_SEND_GET); if (rc == -1) return; break; - case S_SEND_GET_CONFIG: + case S_SEND_GET: init_write_buffer(c, NETCONF_GET_CONFIG); - c->state = S_SEND_GET_CONFIG_WRITING; + c->state = S_SEND_GET_WRITING; /* fallthrough */ - case S_SEND_GET_CONFIG_WRITING: + case S_SEND_GET_WRITING: rc = do_write(c, "GET_CONFIG"); if (rc == 1) { - c->state = S_RECV_GET_CONFIG; + c->state = S_RECV_GET; } else if (rc == -1) { return; } break; - case S_RECV_GET_CONFIG: + case S_RECV_GET: rc = do_read(c, "GET_CONFIG reply", S_CLOSE); if (rc == -1) return; @@ -366,95 +366,104 @@ int main(int argc, char **argv) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } - client_t client = { 0 }; + client_t *client = calloc(1, sizeof(client_t)); + if (!client) { + fprintf(stderr, "Failed to allocate client\n"); + return 1; + } - client.host = argv[1]; - client.port = atoi(argv[2]); - client.user = argv[3]; - client.password = argv[4]; + client->host = argv[1]; + client->port = atoi(argv[2]); + client->user = argv[3]; + client->password = argv[4]; - if (client_init(&client) != 0) { + if (client_init(client) != 0) { fprintf(stderr, "Failed to init client\n"); return 2; } /* create the uv loop and poll handle*/ - client.loop = uv_default_loop(); + client->loop = uv_default_loop(); /* We need to ensure we have a valid fd to initialize uv_poll */ - int err = ssh_connect(client.session); + int err = ssh_connect(client->session); if (err == SSH_ERROR) { - fprintf(stderr, "Initial ssh_connect failed: %s\n", ssh_get_error(client.session)); - ssh_free(client.session); + fprintf(stderr, "Initial ssh_connect failed: %s\n", ssh_get_error(client->session)); + ssh_free(client->session); + free(client); return 3; } /* At this point ssh_get_fd should return a valid fd for uv_poll */ - client.fd = ssh_get_fd(client.session); - if (client.fd < 0) { + client->fd = ssh_get_fd(client->session); + if (client->fd < 0) { fprintf(stderr, "Could not get SSH session fd\n"); - ssh_disconnect(client.session); - ssh_free(client.session); + ssh_disconnect(client->session); + ssh_free(client->session); + free(client); return 4; } - client.poll = malloc(sizeof(uv_poll_t)); - if (!client.poll) { + client->poll = calloc(1, sizeof(uv_poll_t)); + if (!client->poll) { fprintf(stderr, "Failed to allocate poll handle\n"); - ssh_disconnect(client.session); - ssh_free(client.session); + ssh_disconnect(client->session); + ssh_free(client->session); + free(client); return 5; } - err = uv_poll_init(client.loop, client.poll, client.fd); + err = uv_poll_init(client->loop, client->poll, client->fd); if (err < 0) { fprintf(stderr, "uv_poll_init failed: %s\n", uv_strerror(err)); - free(client.poll); - client.poll = NULL; - ssh_disconnect(client.session); - ssh_free(client.session); + free(client->poll); + client->poll = NULL; + ssh_disconnect(client->session); + ssh_free(client->session); + free(client); return 6; } - client.poll->data = &client; + client->poll->data = client; /* Watch for read/write events; libssh manages what it needs depending on state */ - err = uv_poll_start(client.poll, UV_READABLE | UV_WRITABLE, poll_cb); + err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); if (err < 0) { fprintf(stderr, "uv_poll_start failed: %s\n", uv_strerror(err)); - uv_close((uv_handle_t*)client.poll, NULL); - free(client.poll); - client.poll = NULL; - ssh_disconnect(client.session); - ssh_free(client.session); + uv_close((uv_handle_t*)client->poll, NULL); + free(client->poll); + client->poll = NULL; + ssh_disconnect(client->session); + ssh_free(client->session); + free(client); return 7; } /* If the preliminary ssh_connect returned SSH_AGAIN, the state should still be CONNECT. Otherwise, poll_cb will drive the next steps. */ fprintf(stderr, "Starting libuv loop\n"); - uv_run(client.loop, UV_RUN_DEFAULT); + uv_run(client->loop, UV_RUN_DEFAULT); fprintf(stderr, "Stopped libuv loop\n"); /* cleanup */ /* graceful close of ssh channel & session */ - if (client.channel) { - ssh_channel_send_eof(client.channel); - ssh_channel_close(client.channel); - ssh_channel_free(client.channel); - client.channel = NULL; + if (client->channel) { + ssh_channel_send_eof(client->channel); + ssh_channel_close(client->channel); + ssh_channel_free(client->channel); + client->channel = NULL; } - if (client.session) { - ssh_disconnect(client.session); - ssh_free(client.session); + if (client->session) { + ssh_disconnect(client->session); + ssh_free(client->session); } // see MAKE_VALGRIND_HAPPY in libuv/test/task.h // walk the loop to close any remaining handles - uv_walk(client.loop, close_walk_cb, NULL); + uv_walk(client->loop, close_walk_cb, NULL); // run the loop one more time to let close callbacks execute - uv_run(client.loop, UV_RUN_DEFAULT); + uv_run(client->loop, UV_RUN_DEFAULT); // now it's safe to close the loop - err = uv_loop_close(client.loop); + err = uv_loop_close(client->loop); if (err != 0) { fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(err)); @@ -465,12 +474,14 @@ int main(int argc, char **argv) { } uv_library_shutdown(); - if (client.reply) - free(client.reply); + if (client->reply) + free(client->reply); - if (client.poll) - free(client.poll); + if (client->poll) + free(client->poll); + + free(client); fprintf(stderr, "Exited\n"); - return (client.state == S_DONE) ? 0 : 7; + return 0; } diff --git a/libuv_libssh/make.sh b/libuv_libssh/make.sh index 9dcdd3c..0dd4b58 100755 --- a/libuv_libssh/make.sh +++ b/libuv_libssh/make.sh @@ -1 +1 @@ -gcc -Wall -g -o ancc2 async_netconf_client_2.c -lssh -luv +gcc -Wall -g -o ancc async_netconf_client_2.c -lssh -luv diff --git a/src/netconf.h b/src/netconf.h index abccdc6..e373b5a 100644 --- a/src/netconf.h +++ b/src/netconf.h @@ -15,8 +15,17 @@ " \n" \ " \n" \ " \n" \ - "\n" \ - "]]>]]>" + "]]>]]>" + +#define NETCONF_GET_STATE \ + "\n" \ + "\n" \ + " " \ + " " \ + " " \ + " " \ + " " \ + "]]>]]>" // NETCONF message #define NETCONF_CLOSE_SESSION_MSG \ diff --git a/src/ssh.act b/src/ssh.act index f13647b..26d9d15 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -19,30 +19,26 @@ actor Client(cap: net.TCPConnectCap, """SSH Client""" # haha, this is really a pointer :P - var _ssh_session: u64 = 0 - var _uv_loop: u64 = 0 - var _poll: u64 = 0 - var _fd: int = 0 + var _client: u64 = 0 + + var _payload: str = "" proc def _pin_affinity() -> None: NotImplemented _pin_affinity() - def get_ssh_session(): - return _ssh_session - - def get_uv_loop(): - return _uv_loop - - def get_password() -> str: - return password - - def get_subsystem() -> str: - return subsystem + def get_client(): + return _client action def get_affinity() -> u64: NotImplemented + def set_payload(p: str): + _payload = p + + def get_payload(): + return _payload + proc def _init() -> None: """Initialize the SSH client""" NotImplemented @@ -62,47 +58,7 @@ actor Client(cap: net.TCPConnectCap, """Disconnect the SSH client""" NotImplemented - -# TODO: implement support for channels -# AFAIK, all things over ssh are done via channels, so need some channel -# primitive, maybe an actor per channel that then multiplexes into the Client -# session? Prolly need some higher level wrappers for common things like -# starting a shell or running a single command. SFTP / SCP would be nice too, -# but for sometime in the future. Custom subsystems need to be supported too. -actor Channel(client: Client): - """SSH Channel""" - - var _ssh_channel: u64 = 0 - var _ssh_session: u64 = client.get_ssh_session() - var _uv_loop: u64 = client.get_uv_loop() - var _password: str = client.get_password() - var _subsystem: str = client.get_subsystem() - var payload = None - - proc def _pin_affinity() -> None: - NotImplemented - _pin_affinity() - - proc def _set_affinity(affinity: u64) -> None: - NotImplemented - - proc def _ssh_init() -> None: - """Initialize the SSH Channel""" - NotImplemented - - proc def _init() -> None: - aff = client.get_affinity() - _set_affinity(aff) - after 0: _ssh_init() - _init() - - def setPayload(p: str): - payload = p - - def getPayload(): - return payload - - def sendNCPayload() -> str: + def send_nc_payload() -> str: """Send payload""" NotImplemented @@ -127,17 +83,16 @@ actor main(env): subsystem="netconf", ) - cc = Channel(c) - - # # get netconf server's capabilities - # cc.setPayload(']]>]]>') - # print("\n\npayload 1:\n", cc.getPayload(), "\n\npayload 1 end") - # print("\nNC response 1:\n", cc.sendNCPayload(), "\n\nNC response 1 end \n\n") + # get netconf server's capabilities + c.set_payload(']]>]]>') + # c.set_payload(']]>]]>') + print("\npayload 1:\n", c.get_payload(), "\npayload 1 end") + print("\nNC response 1:\n", c.send_nc_payload(), "\nNC response 1 end\n") # # TODO do this in disconnect(): gracefully close a channel - # cc.setPayload(']]>]]>') - # print("\n\npayload 2:\n", cc.getPayload(), "\n\npayload 2 end") - # print("\nNC response 2:\n", cc.sendNCPayload(), "\n\nNC response 2 end \n\n") + # cc.set_payload(']]>]]>') + # print("\npayload 2:\n", cc.get_payload(), "\n\npayload 2 end") + # print("\nNC response 2:\n", cc.send_nc_payload(), "\n\nNC response 2 end \n\n") c.disconnect() diff --git a/src/ssh.ext.c b/src/ssh.ext.c index ac082a4..ef2fa27 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -21,9 +21,9 @@ enum client_state { S_SUBSYSTEM, S_SEND_HELLO, S_RECV_HELLO, - S_SEND_GET_CONFIG, - S_SEND_GET_CONFIG_WRITING, - S_RECV_GET_CONFIG, + S_SEND_GET, + S_SEND_GET_WRITING, + S_RECV_GET, S_CLOSE, S_CLOSE_WRITING, S_RECV_CLOSE_REPLY, @@ -54,6 +54,9 @@ typedef struct { /* current write buffer */ write_buffer_t write_buf; + char *payload; + char *response; + /* read buffer */ char *reply; size_t reply_len; @@ -107,7 +110,7 @@ static int do_write(client_t *c, const char *operation) { if (wrote > 0) { c->write_buf.sent += (size_t)wrote; if (c->write_buf.sent >= c->write_buf.len) { - fprintf(stderr, "%s fully sent\n", operation); + printf("%s fully sent\n", operation); return 1; /* complete */ } return 0; /* more data to send */ @@ -136,11 +139,15 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st set_error(c, "realloc failed"); return -1; } - fprintf(stderr, "Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); + printf("Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); /* Check for RFC6242 end-of-message token */ if (strstr(c->reply, "]]>]]>") != NULL) { - fprintf(stdout, "=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); + printf("=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); + // save response so it can be returned + if (!strcmp(operation, "GET_CONFIG reply")) { + c->response = acton_strdup(c->reply); + } /* Reset reply buffer for next message */ c->reply_len = 0; c->reply[0] = '\0'; @@ -150,9 +157,9 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st return 0; /* more data expected */ } else if (n == 0) { if (ssh_channel_is_eof(c->channel)) { - fprintf(stderr, "Channel EOF during %s\n", operation); + printf("Channel EOF during %s\n", operation); if (c->reply_len > 0) { - fprintf(stdout, "=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); + printf("=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); } c->state = S_CLEANUP; return 1; /* complete via EOF */ @@ -168,7 +175,6 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st /* Called whenever the polled fd has activity (readable/writable) */ static void poll_cb(uv_poll_t *handle, int status, int events) { - printf("poll_cb\n"); (void)status; int rc = 0; client_t *c = (client_t*)handle->data; @@ -246,23 +252,23 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_HELLO: - rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); + rc = do_read(c, "hello reply", S_SEND_GET); if (rc == -1) return; break; - case S_SEND_GET_CONFIG: - init_write_buffer(c, NETCONF_GET_CONFIG_MSG); - c->state = S_SEND_GET_CONFIG_WRITING; + case S_SEND_GET: + init_write_buffer(c, c->payload); + c->state = S_SEND_GET_WRITING; /* fallthrough */ - case S_SEND_GET_CONFIG_WRITING: + case S_SEND_GET_WRITING: rc = do_write(c, "GET_CONFIG"); if (rc == 1) { - c->state = S_RECV_GET_CONFIG; + c->state = S_RECV_GET; } else if (rc == -1) { return; } break; - case S_RECV_GET_CONFIG: + case S_RECV_GET: rc = do_read(c, "GET_CONFIG reply", S_CLOSE); if (rc == -1) return; @@ -296,22 +302,11 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } } -void ssh_channel_close_free(ssh_channel channel) { - int err = ssh_channel_close(channel); - if (err != SSH_OK) - { - printf("%s: ssh_channel_close() error (%d)\n", __FUNCTION__, err); +// Walk callback to close remaining handles +static void close_walk_cb(uv_handle_t* handle, void* arg) { + if (!uv_is_closing(handle)) { + uv_close(handle, NULL); } - ssh_channel_free(channel); -} - -void ssh_channel_close_free_eof(ssh_channel channel) { - int err = ssh_channel_send_eof(channel); - if (err != SSH_OK) - { - printf("%s: ssh_channel_send_eof() error (%d)\n", __FUNCTION__, err); - } - ssh_channel_close_free(channel); } void noop_free(void *ptr) { @@ -343,122 +338,49 @@ B_str sshQ_version() { return to$str("0.1.0"); } +// Client + $R sshQ_ClientD__pin_affinityG_local (sshQ_Client self, $Cont c$cont) { pin_actor_affinity(); return $R_CONT(c$cont, B_None); } -$R sshQ_ChannelD__pin_affinityG_local (sshQ_Channel self, $Cont c$cont) { +$R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { pin_actor_affinity(); - return $R_CONT(c$cont, B_None); -} -/** - * @brief Send netconf payload - * - * @param[in] channel ssh_channel - * @param[in] payload The Netconf Payload, for example the hello message - * @param[in,out] response response will not be returned if NULL - * @param[in] response_len buffer length - */ -int send_nc_payload(ssh_channel channel, const char *payload, char *response, size_t response_len) -{ int err = 0; - int nbytes = 0; - int len = 0; - size_t buflen = 0; - char tmp[1024] = {0}; - - err = ssh_channel_write(channel, payload, (uint32_t)strlen(payload)); - if (err == SSH_ERROR) - { - printf("%s: ssh_channel_write() error (%d)\n", __FUNCTION__, err); - goto error; - } - - nbytes = ssh_channel_read(channel, tmp, sizeof(tmp)-1, 0); - while (nbytes > 0) - { - // sometimes the string tmp has invalid characters from the 1024th element - // and the strlen reports more than what the sizeof() is - if (strlen(tmp) > sizeof(tmp)) { - tmp[sizeof(tmp)-1] = '\0'; - } - if (response) { - // append string - len = snprintf(response + buflen, response_len - buflen, "%s", tmp); - buflen = strlen(response); - - if (len > BUF_SIZE) - { - printf("%s: snprintf() error %lu\n", __FUNCTION__, buflen); - goto error; - } - } -#ifdef DEBUG_MODE - if (write(STDOUT_FILENO, tmp, (size_t)nbytes) != (ssize_t) nbytes) - { - printf("%s: write() error (bytes written not matching expectation)\n", __FUNCTION__); - goto error; - } - fflush(stdout); -#endif - // find end of netconf reply - if (strstr(tmp, "]]>]]>")) { - return SSH_OK; - } - memset(tmp, 0, sizeof(tmp)); - nbytes = ssh_channel_read(channel, tmp, sizeof(tmp), 0); + client_t *client = calloc(1, sizeof(client_t)); + if (client == NULL) { + printf("error allocating client_t\n"); + return $R_CONT(c$cont, B_None); } - if (nbytes < 0) - { - printf("%s: ssh_channel_read() error (%d)\n", __FUNCTION__, errno); - goto error; - } + client->loop = uv_default_loop(); - return SSH_OK; -error: - ssh_channel_close_free(channel); - return SSH_ERROR; -} - -// Client - -$R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { - pin_actor_affinity(); - - int err = 0; - - client_t client = { 0 }; - client.poll = NULL; - client.loop = uv_default_loop(); - client.session = ssh_new(); - if (client.session == NULL) + client->session = ssh_new(); + if (client->session == NULL) { printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } - client.channel = NULL; - client.reply = NULL; - client.reply_len = 0; - client.reply_cap = 0; - client.state = S_CONNECT; + client->poll = NULL; + client->channel = NULL; + client->reply = NULL; + client->reply_len = 0; + client->reply_cap = 0; + client->state = S_CONNECT; /* Initialize write buffer to empty */ - client.write_buf.data = NULL; - client.write_buf.len = 0; - client.write_buf.sent = 0; + client->write_buf.data = NULL; + client->write_buf.len = 0; + client->write_buf.sent = 0; - client.host = (const char *)fromB_str(self->host); - client.port = self->port->val; - client.user = (const char *)fromB_str(self->username); - client.password = (const char *)fromB_str(self->password); - - self->_ssh_session = toB_u64((unsigned long)client.session); - self->_uv_loop = toB_u64((unsigned long)client.loop); + client->host = (const char *)fromB_str(self->host); + client->port = self->port->val; + client->user = (const char *)fromB_str(self->username); + client->password = (const char *)fromB_str(self->password); #ifdef DEBUG_MODE // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS @@ -470,35 +392,35 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si } #endif - err = ssh_session_set_disconnect_message(client.session, "Disconnecting SSH, powered by Acton"); + err = ssh_session_set_disconnect_message(client->session, "Disconnecting SSH, powered by Acton"); if (err != SSH_OK) { printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(client.session, SSH_OPTIONS_HOST, client.host); + err = ssh_options_set(client->session, SSH_OPTIONS_HOST, client->host); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(client.session, SSH_OPTIONS_PORT, &client.port); + err = ssh_options_set(client->session, SSH_OPTIONS_PORT, &client->port); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - err = ssh_options_set(client.session, SSH_OPTIONS_USER, client.user); + err = ssh_options_set(client->session, SSH_OPTIONS_USER, client->user); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } - ssh_set_blocking(client.session, 0); + ssh_set_blocking(client->session, 0); // should it auto-parse user config? for example from /home/user/.ssh/ // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); @@ -508,59 +430,63 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si // return $R_CONT(c$cont, B_None); // } - err = ssh_connect(client.session); - if (err != SSH_OK) + err = ssh_connect(client->session); + if (err == SSH_ERROR) { - printf("%s: ssh_connect() Error connecting to SSH server: %s\n", __FUNCTION__, ssh_get_error(client.session)); + printf("%s: ssh_connect() Error connecting to SSH server: '%s' (%d)\n", __FUNCTION__, ssh_get_error(client->session), err); return $R_CONT(c$cont, B_None); } // At this point ssh_get_fd should return a valid fd for uv_poll - int fd = ssh_get_fd(client.session); - self->_fd = to$int(fd); - if (self->_fd < 0) + client->fd = ssh_get_fd(client->session); + if (client->fd < 0) { printf("Could not get SSH session fd\n"); - ssh_disconnect(client.session); - ssh_free(client.session); + ssh_disconnect(client->session); + ssh_free(client->session); return $R_CONT(c$cont, B_None); } - client.poll = malloc(sizeof(uv_poll_t)); - if (!client.poll) + client->poll = calloc(1, sizeof(uv_poll_t)); + if (!client->poll) { printf("Failed to allocate poll handle\n"); - ssh_disconnect(client.session); - ssh_free(client.session); + ssh_disconnect(client->session); + ssh_free(client->session); return $R_CONT(c$cont, B_None); } - err = uv_poll_init(client.loop, client.poll, fd); + err = uv_poll_init(client->loop, client->poll, client->fd); if (err < 0) { printf("uv_poll_init failed: %s\n", uv_strerror(err)); - free(client.poll); - client.poll = NULL; - ssh_disconnect(client.session); - ssh_free(client.session); + free(client->poll); + client->poll = NULL; + ssh_disconnect(client->session); + ssh_free(client->session); return $R_CONT(c$cont, B_None); } - self->_poll = toB_u64((unsigned long)client.poll); - client.poll->data = &client; + client->poll->data = client; /* Watch for read/write events; libssh manages what it needs depending on state */ - err = uv_poll_start(client.poll, UV_READABLE | UV_WRITABLE, poll_cb); + err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); if (err < 0) { printf("uv_poll_start failed: %s\n", uv_strerror(err)); - uv_close((uv_handle_t*)client.poll, NULL); - free(client.poll); - client.poll = NULL; - ssh_disconnect(client.session); - ssh_free(client.session); + uv_close((uv_handle_t*)client->poll, NULL); + free(client->poll); + client->poll = NULL; + ssh_disconnect(client->session); + ssh_free(client->session); return $R_CONT(c$cont, B_None); } + self->_client = toB_u64((unsigned long)client); + + // printf("Starting libuv loop\n"); + // uv_run(client->loop, UV_RUN_DEFAULT); + // printf("Stopped libuv loop\n"); + $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); @@ -572,71 +498,87 @@ int send_nc_payload(ssh_channel channel, const char *payload, char *response, si return $R_CONT(c$cont, B_None); } -$R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { - ssh_disconnect((ssh_session)fromB_u64(self->_ssh_session)); - ssh_free((ssh_session)fromB_u64(self->_ssh_session)); - if (ssh_finalize()) { - printf("%s: ssh_finalize error", __FUNCTION__); - } - - return $R_CONT(c$cont, B_None); -} +$R sshQ_ClientD_send_nc_payloadG_local (sshQ_Client self, $Cont c$cont) { + int err = 0; -// Channel + client_t *client = (client_t*)fromB_u64(self->_client); + if (client == NULL) { + printf("ERROR: client == NULL\n"); + goto err_out; + } -$R sshQ_ChannelD__ssh_initG_local (sshQ_Channel self, $Cont c$cont) { - pin_actor_affinity(); + client->payload = (const char *)fromB_str(self->_payload); #ifdef DEBUG_MODE - printf("Connecting to SSH server\n"); + printf("Starting libuv loop client\n"); +#endif + uv_run(client->loop, UV_RUN_DEFAULT); +#ifdef DEBUG_MODE + printf("Stopped libuv loop client\n"); #endif - printf("Starting libuv loop\n"); - uv_run(self->_uv_loop, UV_RUN_DEFAULT); - printf("Stopped libuv loop\n"); - + return $R_CONT(c$cont, to$str(client->response)); +err_out: return $R_CONT(c$cont, B_None); } -$R sshQ_ChannelD__set_affinityG_local (sshQ_Channel self, $Cont C_cont, B_u64 affinity) { -// #ifdef DEBUG_MODE - printf("sshQ_ChannelD__set_affinityG_local, affinity=%ld\n", self->$affinity); -// #endif - // TODO set affinity - return $R_CONT(C_cont, B_None); -} - -$R sshQ_ChannelD_disconnectG_local (sshQ_Channel self, $Cont c$cont) { +$R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { int err = 0; - ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); - - if (self->_subsystem) { - // NOTE: add other subsystems here if needed - if (!strcmp((const char *)fromB_str(self->_subsystem), "netconf")) { - err = send_nc_payload(channel, NETCONF_CLOSE_SESSION_MSG, NULL, 0); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - } - } + + client_t *client = (client_t*)fromB_u64(self->_client); + if (client == NULL) { + printf("client == NULL, nothing to cleanup\n"); + goto out; } - ssh_channel_close_free_eof(channel); + /* cleanup */ + /* graceful close of ssh channel & session */ + if (client->channel) { + ssh_channel_send_eof(client->channel); + ssh_channel_close(client->channel); + ssh_channel_free(client->channel); + client->channel = NULL; + } + if (client->session) { + ssh_disconnect(client->session); + ssh_free(client->session); + } - return $R_CONT(c$cont, B_None); -} + // see MAKE_VALGRIND_HAPPY in libuv/test/task.h + // walk the loop to close any remaining handles + uv_walk(client->loop, close_walk_cb, NULL); + // run the loop one more time to let close callbacks execute + uv_run(client->loop, UV_RUN_DEFAULT); -$R sshQ_ChannelD_sendNCPayloadG_local (sshQ_Channel self, $Cont c$cont) { - int err = 0; - char response[BUF_SIZE] = {0}; - ssh_channel channel = (ssh_channel)fromB_u64(self->_ssh_channel); + // now it's safe to close the loop + err = uv_loop_close(client->loop); + if (err != 0) { + printf("WARNING: Loop close failed: %s\n", uv_strerror(err)); - err = send_nc_payload(channel, (const char *)fromB_str(self->payload), response, sizeof(response)); - if (err != SSH_OK) - { - printf("%s: send_nc_payload() error: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + // If we still have handles, print debug info + if (err == UV_EBUSY) { + printf("There are still active handles in the loop. This is a leak.\n"); + } } - return $R_CONT(c$cont, to$str(response)); + if (client->reply) { + free(client->reply); + client->reply = NULL; + } + + if (client->poll) + free(client->poll); + + if (client) { + free(client); + client = NULL; + } + + uv_library_shutdown(); + + if (ssh_finalize()) { + printf("%s: ssh_finalize error", __FUNCTION__); + } +out: + return $R_CONT(c$cont, B_None); } From 61ecd1adfda50fd0cca7ecf6ee7d1e557fb0464d Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Fri, 22 Aug 2025 13:51:21 +0200 Subject: [PATCH 32/37] further improvements and fixes --- libuv_libssh/async_netconf_client_2.c | 33 ++--- src/ssh.act | 5 - src/ssh.ext.c | 166 +++++++++++++++----------- 3 files changed, 116 insertions(+), 88 deletions(-) diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c index b873207..a9f7122 100644 --- a/libuv_libssh/async_netconf_client_2.c +++ b/libuv_libssh/async_netconf_client_2.c @@ -51,8 +51,8 @@ enum client_state { S_SUBSYSTEM, S_SEND_HELLO, S_RECV_HELLO, - S_SEND_GET, - S_SEND_GET_WRITING, + S_SEND_GET_CONFIG, + S_SEND_GET_CONFIG_WRITING, S_RECV_GET, S_CLOSE, S_CLOSE_WRITING, @@ -80,6 +80,7 @@ typedef struct { int port; const char *user; const char *password; + const char *subsystem; /* current write buffer */ write_buffer_t write_buf; @@ -130,8 +131,7 @@ static int do_write(client_t *c, const char *operation) { return 1; /* complete */ } - int wrote = ssh_channel_write(c->channel, - c->write_buf.data + c->write_buf.sent, + int wrote = ssh_channel_write(c->channel, c->write_buf.data + c->write_buf.sent, (uint32_t)(c->write_buf.len - c->write_buf.sent)); if (wrote > 0) { @@ -203,7 +203,10 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { int rc = 0; if (c->state == S_DONE || c->state == S_ERROR) { - printf("uv_poll_stop\n"); + if (c->state == S_DONE) + printf("loop is done. stopping it\n"); + else if (c->state == S_ERROR) + printf("loop encountered and error. stopping it\n"); uv_poll_stop(c->poll); return; } @@ -254,10 +257,11 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_SUBSYSTEM: - rc = ssh_channel_request_subsystem(c->channel, "netconf"); + rc = ssh_channel_request_subsystem(c->channel, c->subsystem); if (rc == SSH_OK) { - fprintf(stderr, "Requested subsystem: netconf\n"); - init_write_buffer(c, NETCONF_HELLO); + fprintf(stderr, "Requested subsystem: %s\n", c->subsystem); + if (!strcmp(c->subsystem, "netconf")) + init_write_buffer(c, NETCONF_HELLO); c->state = S_SEND_HELLO; } else if (rc == SSH_AGAIN) { return; @@ -275,15 +279,15 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_HELLO: - rc = do_read(c, "hello reply", S_SEND_GET); + rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); if (rc == -1) return; break; - case S_SEND_GET: + case S_SEND_GET_CONFIG: init_write_buffer(c, NETCONF_GET_CONFIG); - c->state = S_SEND_GET_WRITING; + c->state = S_SEND_GET_CONFIG_WRITING; /* fallthrough */ - case S_SEND_GET_WRITING: + case S_SEND_GET_CONFIG_WRITING: rc = do_write(c, "GET_CONFIG"); if (rc == 1) { c->state = S_RECV_GET; @@ -362,8 +366,8 @@ static void close_walk_cb(uv_handle_t* handle, void* arg) { } int main(int argc, char **argv) { - if (argc < 5) { - fprintf(stderr, "Usage: %s \n", argv[0]); + if (argc < 6) { + fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } client_t *client = calloc(1, sizeof(client_t)); @@ -376,6 +380,7 @@ int main(int argc, char **argv) { client->port = atoi(argv[2]); client->user = argv[3]; client->password = argv[4]; + client->subsystem = argv[5]; if (client_init(client) != 0) { fprintf(stderr, "Failed to init client\n"); diff --git a/src/ssh.act b/src/ssh.act index 26d9d15..2fd4a6d 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -89,11 +89,6 @@ actor main(env): print("\npayload 1:\n", c.get_payload(), "\npayload 1 end") print("\nNC response 1:\n", c.send_nc_payload(), "\nNC response 1 end\n") - # # TODO do this in disconnect(): gracefully close a channel - # cc.set_payload(']]>]]>') - # print("\npayload 2:\n", cc.get_payload(), "\n\npayload 2 end") - # print("\nNC response 2:\n", cc.send_nc_payload(), "\n\nNC response 2 end \n\n") - c.disconnect() env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index ef2fa27..6a44ed4 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -32,6 +32,15 @@ enum client_state { S_ERROR }; +enum client_operation { + OP_HELLO, + OP_HELLO_REPLY, + OP_GET, + OP_GET_REPLY, + OP_CLOSE, + OP_CLOSE_REPLY +}; + typedef struct { const char *data; size_t len; @@ -44,6 +53,8 @@ typedef struct { ssh_session session; ssh_channel channel; uv_poll_t *poll; + const char *subsystem; + int fd; enum client_state state; const char *host; @@ -63,6 +74,26 @@ typedef struct { size_t reply_cap; } client_t; +static char *client_operation_to_str(enum client_operation op) { + switch (op) { + case OP_HELLO: + return "OP_HELLO"; + case OP_HELLO_REPLY: + return "OP_HELLO_REPLY"; + case OP_GET: + return "OP_GET"; + case OP_GET_REPLY: + return "OP_GET_REPLY"; + case OP_CLOSE: + return "OP_CLOSE"; + case OP_CLOSE_REPLY: + return "OP_CLOSE_REPLY"; + default: + return ""; + } + return ""; +} + /* Simple helper to append to reply buffer */ static int append_reply(client_t *c, const char *buf, size_t n) { if (n == 0) return 0; @@ -98,39 +129,38 @@ static void init_write_buffer(client_t *c, const char *data) { } /* Generic write operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_write(client_t *c, const char *operation) { +static int do_write(client_t *c, enum client_operation operation) { if (c->write_buf.sent >= c->write_buf.len) { return 1; /* complete */ } - int wrote = ssh_channel_write(c->channel, - c->write_buf.data + c->write_buf.sent, + int wrote = ssh_channel_write(c->channel, c->write_buf.data + c->write_buf.sent, (uint32_t)(c->write_buf.len - c->write_buf.sent)); if (wrote > 0) { c->write_buf.sent += (size_t)wrote; if (c->write_buf.sent >= c->write_buf.len) { - printf("%s fully sent\n", operation); + printf("%s fully sent\n", client_operation_to_str(operation)); return 1; /* complete */ } return 0; /* more data to send */ } else if (wrote == SSH_ERROR) { char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", operation); + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", client_operation_to_str(operation)); set_error(c, error_msg); return -1; /* error */ } else if (wrote == SSH_AGAIN || wrote == 0) { return 0; /* not writable now */ } else { char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", operation); + snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", client_operation_to_str(operation)); set_error(c, error_msg); return -1; /* error */ } } /* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_read(client_t *c, const char *operation, enum client_state next_state) { +static int do_read(client_t *c, enum client_operation operation, enum client_state next_state) { char buf[BUF_SIZE]; int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); @@ -139,14 +169,14 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st set_error(c, "realloc failed"); return -1; } - printf("Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); + printf("Read %s %d bytes (total %zu)\n", client_operation_to_str(operation), n, c->reply_len); /* Check for RFC6242 end-of-message token */ if (strstr(c->reply, "]]>]]>") != NULL) { - printf("=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); + printf("=== NETCONF %s ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); // save response so it can be returned - if (!strcmp(operation, "GET_CONFIG reply")) { - c->response = acton_strdup(c->reply); + if (operation == OP_GET_REPLY) { + c->response = strdup(c->reply); } /* Reset reply buffer for next message */ c->reply_len = 0; @@ -157,9 +187,9 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st return 0; /* more data expected */ } else if (n == 0) { if (ssh_channel_is_eof(c->channel)) { - printf("Channel EOF during %s\n", operation); + printf("Channel EOF during %s\n", client_operation_to_str(operation)); if (c->reply_len > 0) { - printf("=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); + printf("=== NETCONF %s (partial) ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); } c->state = S_CLEANUP; return 1; /* complete via EOF */ @@ -167,7 +197,7 @@ static int do_read(client_t *c, const char *operation, enum client_state next_st return 0; /* no data now, wait */ } else { char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", operation); + snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", client_operation_to_str(operation)); set_error(c, error_msg); return -1; /* error */ } @@ -180,7 +210,10 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { client_t *c = (client_t*)handle->data; if (c->state == S_DONE || c->state == S_ERROR) { - printf("uv_poll_stop\n"); + if (c->state == S_DONE) + printf("uv loop is done. stopping it\n"); + else if (c->state == S_ERROR) + printf("uv loop encountered and error. stopping it\n"); uv_poll_stop(c->poll); return; } @@ -231,9 +264,9 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_SUBSYSTEM: - rc = ssh_channel_request_subsystem(c->channel, "netconf"); + rc = ssh_channel_request_subsystem(c->channel, c->subsystem); if (rc == SSH_OK) { - printf("Requested subsystem: netconf\n"); + printf("Requested subsystem: '%s'\n", c->subsystem); init_write_buffer(c, NETCONF_HELLO_MSG); c->state = S_SEND_HELLO; } else if (rc == SSH_AGAIN) { @@ -244,7 +277,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_SEND_HELLO: - rc = do_write(c, "Hello"); + rc = do_write(c, OP_HELLO); if (rc == 1) { c->state = S_RECV_HELLO; } else if (rc == -1) { @@ -252,7 +285,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_HELLO: - rc = do_read(c, "hello reply", S_SEND_GET); + rc = do_read(c, OP_HELLO_REPLY, S_SEND_GET); if (rc == -1) return; break; @@ -261,7 +294,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { c->state = S_SEND_GET_WRITING; /* fallthrough */ case S_SEND_GET_WRITING: - rc = do_write(c, "GET_CONFIG"); + rc = do_write(c, OP_GET); if (rc == 1) { c->state = S_RECV_GET; } else if (rc == -1) { @@ -269,7 +302,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_GET: - rc = do_read(c, "GET_CONFIG reply", S_CLOSE); + rc = do_read(c, OP_GET_REPLY, S_CLOSE); if (rc == -1) return; break; @@ -278,7 +311,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { c->state = S_CLOSE_WRITING; /* fallthrough */ case S_CLOSE_WRITING: - rc = do_write(c, "Close message"); + rc = do_write(c, OP_CLOSE); if (rc == 1) { printf("Close message fully sent, waiting for reply\n"); c->state = S_RECV_CLOSE_REPLY; @@ -287,7 +320,7 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } break; case S_RECV_CLOSE_REPLY: - rc = do_read(c, "close reply", S_CLEANUP); + rc = do_read(c, OP_CLOSE_REPLY, S_CLEANUP); if (rc == -1) return; break; @@ -359,8 +392,7 @@ B_str sshQ_version() { client->loop = uv_default_loop(); client->session = ssh_new(); - if (client->session == NULL) - { + if (client->session == NULL) { printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); return $R_CONT(c$cont, B_None); } @@ -381,6 +413,7 @@ B_str sshQ_version() { client->port = self->port->val; client->user = (const char *)fromB_str(self->username); client->password = (const char *)fromB_str(self->password); + client->subsystem = (const char *)fromB_str(self->subsystem); #ifdef DEBUG_MODE // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS @@ -393,29 +426,25 @@ B_str sshQ_version() { #endif err = ssh_session_set_disconnect_message(client->session, "Disconnecting SSH, powered by Acton"); - if (err != SSH_OK) - { + if (err != SSH_OK) { printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(client->session, SSH_OPTIONS_HOST, client->host); - if (err != SSH_OK) - { + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(client->session, SSH_OPTIONS_PORT, &client->port); - if (err != SSH_OK) - { + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } err = ssh_options_set(client->session, SSH_OPTIONS_USER, client->user); - if (err != SSH_OK) - { + if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); return $R_CONT(c$cont, B_None); } @@ -424,23 +453,20 @@ B_str sshQ_version() { // should it auto-parse user config? for example from /home/user/.ssh/ // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); - // if (err != SSH_OK) - // { + // if (err != SSH_OK) { // printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PROCESS_CONFIG': %d\n", __FUNCTION__, err); // return $R_CONT(c$cont, B_None); // } err = ssh_connect(client->session); - if (err == SSH_ERROR) - { + if (err == SSH_ERROR) { printf("%s: ssh_connect() Error connecting to SSH server: '%s' (%d)\n", __FUNCTION__, ssh_get_error(client->session), err); return $R_CONT(c$cont, B_None); } // At this point ssh_get_fd should return a valid fd for uv_poll client->fd = ssh_get_fd(client->session); - if (client->fd < 0) - { + if (client->fd < 0) { printf("Could not get SSH session fd\n"); ssh_disconnect(client->session); ssh_free(client->session); @@ -448,8 +474,7 @@ B_str sshQ_version() { } client->poll = calloc(1, sizeof(uv_poll_t)); - if (!client->poll) - { + if (!client->poll) { printf("Failed to allocate poll handle\n"); ssh_disconnect(client->session); ssh_free(client->session); @@ -457,8 +482,7 @@ B_str sshQ_version() { } err = uv_poll_init(client->loop, client->poll, client->fd); - if (err < 0) - { + if (err < 0) { printf("uv_poll_init failed: %s\n", uv_strerror(err)); free(client->poll); client->poll = NULL; @@ -470,8 +494,7 @@ B_str sshQ_version() { /* Watch for read/write events; libssh manages what it needs depending on state */ err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); - if (err < 0) - { + if (err < 0) { printf("uv_poll_start failed: %s\n", uv_strerror(err)); uv_close((uv_handle_t*)client->poll, NULL); free(client->poll); @@ -483,10 +506,6 @@ B_str sshQ_version() { self->_client = toB_u64((unsigned long)client); - // printf("Starting libuv loop\n"); - // uv_run(client->loop, UV_RUN_DEFAULT); - // printf("Stopped libuv loop\n"); - $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); @@ -517,7 +536,7 @@ B_str sshQ_version() { printf("Stopped libuv loop client\n"); #endif - return $R_CONT(c$cont, to$str(client->response)); + return $R_CONT(c$cont, client->response ? to$str(client->response) : B_None); err_out: return $R_CONT(c$cont, B_None); } @@ -525,6 +544,10 @@ B_str sshQ_version() { $R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { int err = 0; + if (self == NULL || self->_client == NULL) { + printf("self->_client == NULL\n"); + goto out; + } client_t *client = (client_t*)fromB_u64(self->_client); if (client == NULL) { printf("client == NULL, nothing to cleanup\n"); @@ -544,38 +567,43 @@ B_str sshQ_version() { ssh_free(client->session); } - // see MAKE_VALGRIND_HAPPY in libuv/test/task.h - // walk the loop to close any remaining handles - uv_walk(client->loop, close_walk_cb, NULL); - // run the loop one more time to let close callbacks execute - uv_run(client->loop, UV_RUN_DEFAULT); - - // now it's safe to close the loop - err = uv_loop_close(client->loop); - if (err != 0) { - printf("WARNING: Loop close failed: %s\n", uv_strerror(err)); - - // If we still have handles, print debug info - if (err == UV_EBUSY) { - printf("There are still active handles in the loop. This is a leak.\n"); + if (client->loop) { + // see MAKE_VALGRIND_HAPPY in libuv/test/task.h + // walk the loop to close any remaining handles + uv_walk(client->loop, close_walk_cb, NULL); + // run the loop one more time to let close callbacks execute + uv_run(client->loop, UV_RUN_DEFAULT); + + // now it's safe to close the loop + err = uv_loop_close(client->loop); + if (err != 0) { + printf("WARNING: Loop close failed: %s\n", uv_strerror(err)); + + // If we still have handles, print debug info + if (err == UV_EBUSY) { + printf("There are still active handles in the loop. This is a leak.\n"); + } } + uv_library_shutdown(); } if (client->reply) { free(client->reply); client->reply = NULL; } - - if (client->poll) + if (client->response) { + free(client->response); + client->response = NULL; + } + if (client->poll) { free(client->poll); - + client->poll = NULL; + } if (client) { free(client); client = NULL; } - uv_library_shutdown(); - if (ssh_finalize()) { printf("%s: ssh_finalize error", __FUNCTION__); } From e4ff85b85ad5fdfa8490db93db5b67bc1b6fea2c Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Mon, 25 Aug 2025 07:56:06 +0200 Subject: [PATCH 33/37] add ssh subsystem support --- libuv_libssh/CMakeLists.txt | 26 -- libuv_libssh/async_netconf_client.c | 466 ------------------------ libuv_libssh/async_netconf_client_2.c | 492 -------------------------- libuv_libssh/make.sh | 1 - src/ssh.act | 24 +- src/ssh.ext.c | 304 ++++++++++------ 6 files changed, 214 insertions(+), 1099 deletions(-) delete mode 100644 libuv_libssh/CMakeLists.txt delete mode 100644 libuv_libssh/async_netconf_client.c delete mode 100644 libuv_libssh/async_netconf_client_2.c delete mode 100755 libuv_libssh/make.sh diff --git a/libuv_libssh/CMakeLists.txt b/libuv_libssh/CMakeLists.txt deleted file mode 100644 index ac58414..0000000 --- a/libuv_libssh/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(async_netconf_client C) - -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED ON) - -add_executable(ancc async_netconf_client_2.c) - -find_package(PkgConfig REQUIRED) -pkg_check_modules(LIBSSH REQUIRED libssh) -pkg_check_modules(LIBUV REQUIRED libuv) - -include_directories( - ${LIBSSH_INCLUDE_DIRS} - ${LIBUV_INCLUDE_DIRS} -) - -target_link_libraries(ancc - ${LIBSSH_LIBRARIES} - ${LIBUV_LIBRARIES} -) - -target_compile_options(ancc PRIVATE - ${LIBSSH_CFLAGS_OTHER} - ${LIBUV_CFLAGS_OTHER} -) diff --git a/libuv_libssh/async_netconf_client.c b/libuv_libssh/async_netconf_client.c deleted file mode 100644 index 340ce06..0000000 --- a/libuv_libssh/async_netconf_client.c +++ /dev/null @@ -1,466 +0,0 @@ -#include -#include -#include -#include -#include - -#define NETCONF_PORT 830 -#define BUFFER_SIZE 4096 - -const char *NETCONF_HELLO = "\n" - "\n" - " \n" - " urn:ietf:params:netconf:base:1.0\n" - " \n" - "\n" - "]]>]]>"; - -const char *NETCONF_GET_CONFIG = "\n" - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - "\n" - "]]>]]>"; - -const char *NETCONF_CLOSE_SESSION = "\n" - " \n" - "\n" - "]]>]]>"; - -// Buffer management for handling partial reads -typedef struct { - char data[BUFFER_SIZE * 2]; // Double size to handle partial messages - size_t length; -} message_buffer_t; - -typedef struct { - ssh_session ssh; - ssh_channel channel; - uv_poll_t poll_handle; - uv_tty_t tty_handle; - char read_buffer[BUFFER_SIZE]; - char input_buffer[BUFFER_SIZE]; - message_buffer_t message_buffer; - const char *subsystem; - void *subsystem_ctx; - uv_loop_t *loop; - int state; // 0: not connected, 1: hello sent, 2: get-config sent, 3: close-session sent -} client_context_t; - -void on_ssh_event(uv_poll_t *handle, int status, int events); -void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf); -void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf); -void send_hello(client_context_t *context); -void send_get_config(client_context_t *context); -void send_close_session(client_context_t *context); -void process_reply(client_context_t *context, const char *data, size_t len); -void cleanup(client_context_t *context); -void close_walk_cb(uv_handle_t* handle, void* arg); - -int init_ssh(client_context_t *context, const char *hostname, const char *username, const char *password) { - int rc; - - context->ssh = ssh_new(); - if (context->ssh == NULL) { - fprintf(stderr, "Failed to create SSH session\n"); - return -1; - } - - ssh_options_set(context->ssh, SSH_OPTIONS_HOST, hostname); - ssh_options_set(context->ssh, SSH_OPTIONS_USER, username); - if (context->subsystem && !strcmp(context->subsystem, "netconf")) { - ssh_options_set(context->ssh, SSH_OPTIONS_PORT, &(int){NETCONF_PORT}); - } - - int verbosity = SSH_LOG_PROTOCOL; - ssh_options_set(context->ssh, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); - - printf("Connecting to SSH server %s:%d...\n", hostname, NETCONF_PORT); - - rc = ssh_connect(context->ssh); - if (rc != SSH_OK) { - fprintf(stderr, "Error connecting to %s: %s\n", hostname, ssh_get_error(context->ssh)); - ssh_free(context->ssh); - return -1; - } - - printf("SSH connection established, authenticating...\n"); - - rc = ssh_userauth_password(context->ssh, NULL, password); - if (rc != SSH_AUTH_SUCCESS) { - fprintf(stderr, "Authentication failed: %s\n", ssh_get_error(context->ssh)); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } - - printf("Authentication successful, opening channel...\n"); - - context->channel = ssh_channel_new(context->ssh); - if (context->channel == NULL) { - fprintf(stderr, "Failed to create channel\n"); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } - - rc = ssh_channel_open_session(context->channel); - if (rc != SSH_OK) { - fprintf(stderr, "Failed to open channel: %s\n", ssh_get_error(context->ssh)); - ssh_channel_free(context->channel); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } - - if (context->subsystem && strlen(context->subsystem) > 0) { - printf("Channel opened, requesting %s subsystem...\n", context->subsystem); - - rc = ssh_channel_request_subsystem(context->channel, context->subsystem); - if (rc != SSH_OK) { - fprintf(stderr, "Failed to request %s subsystem: %s\n", context->subsystem, ssh_get_error(context->ssh)); - ssh_channel_close(context->channel); - ssh_channel_free(context->channel); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } - - printf("%s subsystem established\n", context->subsystem); - - if (!strcmp("netconf", context->subsystem)) { - printf("Sending NETCONF hello message...\n"); - send_hello(context); - } - } else { - rc = ssh_channel_request_shell(context->channel); - if (rc != SSH_OK) { - fprintf(stderr, "Failed to request shell: %s\n", ssh_get_error(context->ssh)); - ssh_channel_close(context->channel); - ssh_channel_free(context->channel); - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - return -1; - } - printf("Shell successfully acquired\n"); - } - - return 0; -} - -// Setup stdin input handling for interactive shell -int setup_stdin(client_context_t *context) { - if (context->subsystem) { - // Skip stdin setup for subsystem sessions - return 0; - } - - int rc = uv_tty_init(context->loop, &context->tty_handle, 0, 1); - if (rc != 0) { - fprintf(stderr, "Failed to initialize TTY: %s\n", uv_strerror(rc)); - return -1; - } - - context->tty_handle.data = context; - - rc = uv_read_start((uv_stream_t*)&context->tty_handle, alloc_buffer, on_stdin_read); - if (rc != 0) { - fprintf(stderr, "Failed to start reading from stdin: %s\n", uv_strerror(rc)); - return -1; - } - - return 0; -} - -// Setup libuv poll for SSH socket -int setup_poll(client_context_t *context) { - int socket_fd = ssh_get_fd(context->ssh); - if (socket_fd < 0) { - fprintf(stderr, "Failed to get SSH socket file descriptor\n"); - return -1; - } - - // Initialize poll handle - uv_poll_init(context->loop, &context->poll_handle, socket_fd); - context->poll_handle.data = context; - - // Start polling for read events - uv_poll_start(&context->poll_handle, UV_READABLE, on_ssh_event); - - return 0; -} - -// Callback for SSH socket events -void on_ssh_event(uv_poll_t *handle, int status, int events) { - client_context_t *context = (client_context_t *)handle->data; - - if (status < 0) { - fprintf(stderr, "Poll error: %s\n", uv_strerror(status)); - return; - } - - if (events & UV_READABLE) { - int nbytes = ssh_channel_read_nonblocking(context->channel, context->read_buffer, BUFFER_SIZE - 1, 0); - if (nbytes > 0) { - context->read_buffer[nbytes] = '\0'; - printf("\nDEBUG: Raw data received (%d bytes):\n", nbytes); - printf("----------------------------------------\n"); - printf("%s\n", context->read_buffer); - printf("----------------------------------------\n\n"); - - // Append to our message buffer - if (context->message_buffer.length + nbytes < sizeof(context->message_buffer.data) - 1) { - memcpy(context->message_buffer.data + context->message_buffer.length, - context->read_buffer, nbytes); - context->message_buffer.length += nbytes; - context->message_buffer.data[context->message_buffer.length] = '\0'; - - // Now process the accumulated buffer - process_reply(context, context->message_buffer.data, context->message_buffer.length); - - // If we found the delimiter, we can clear the buffer for the next message - if (strstr(context->message_buffer.data, "]]>]]>") != NULL) { - context->message_buffer.length = 0; - } - } else { - fprintf(stderr, "Message buffer overflow, resetting\n"); - context->message_buffer.length = 0; - } - } else if (nbytes == SSH_ERROR) { - fprintf(stderr, "Error reading from channel: %s\n", ssh_get_error(context->ssh)); - } else if (nbytes == SSH_AGAIN || nbytes == 0) { - printf("No data on the channel\n"); - } else if (ssh_channel_is_eof(context->channel)) { - fprintf(stderr, "Server closed the connection\n"); - uv_poll_stop(&context->poll_handle); - } - } -} - -void send_hello(client_context_t *context) { - printf("Sending NETCONF hello message...\n"); - - int rc = ssh_channel_write(context->channel, NETCONF_HELLO, strlen(NETCONF_HELLO)); - if (rc != (int)strlen(NETCONF_HELLO)) { - fprintf(stderr, "Failed to send hello message: %s\n", ssh_get_error(context->ssh)); - return; - } - - client_context_t *netconf_ctx = context->subsystem_ctx; - netconf_ctx->state = 2; -} - -void send_get_config(client_context_t *context) { - printf("Sending NETCONF get-config message...\n"); - - int rc = ssh_channel_write(context->channel, NETCONF_GET_CONFIG, strlen(NETCONF_GET_CONFIG)); - if (rc != (int)strlen(NETCONF_GET_CONFIG)) { - fprintf(stderr, "Failed to send get-config message: %s\n", ssh_get_error(context->ssh)); - return; - } - - client_context_t *netconf_ctx = context->subsystem_ctx; - netconf_ctx->state = 2; -} - -void send_close_session(client_context_t *context) { - printf("Sending NETCONF close-session message...\n"); - - int rc = ssh_channel_write(context->channel, NETCONF_CLOSE_SESSION, strlen(NETCONF_CLOSE_SESSION)); - if (rc != (int)strlen(NETCONF_CLOSE_SESSION)) { - fprintf(stderr, "Failed to send close-session message: %s\n", ssh_get_error(context->ssh)); - return; - } - - client_context_t *netconf_ctx = context->subsystem_ctx; - netconf_ctx->state = 3; -} - -// Process NETCONF reply -void process_reply(client_context_t *context, const char *data, size_t len) { - if (context->subsystem && !strcmp(context->subsystem, "netconf")) { - client_context_t *netconf_ctx = context->subsystem_ctx; - printf("Processing NETCONF reply (%zu bytes)\n", len); - - // Check for the end of message delimiter - if (strstr(data, "]]>]]>") != NULL) { - printf("Found NETCONF message delimiter\n"); - - if (netconf_ctx->state == 1) { - // Already sent hello, now send get-config - printf("Received server response after our hello, sending get-config\n"); - send_get_config(context); - } else if (netconf_ctx->state == 2) { - // Received get-config reply, now send close-session - printf("Get-config completed successfully, closing session...\n"); - - // TODO do something with retreived data? - - // Send close-session to gracefully terminate the NETCONF session - send_close_session(context); - } else if (netconf_ctx->state == 3) { - // Received close-session reply, we're done - printf("NETCONF session closed gracefully\n"); - - // Stop polling and prepare to exit - uv_poll_stop(&context->poll_handle); - uv_stop(context->loop); - } - } else { - printf("INFO: No NETCONF message delimiter found in the response, expecting more data\n"); - // It's possible we received a partial message, which is normal in async I/O - // We will accumulate more data on subsequent reads - } - } -} - -// Buffer allocation callback for libuv -void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { - client_context_t *context = (client_context_t *)handle->data; - buf->base = context->input_buffer; - buf->len = sizeof(context->input_buffer); -} - -// Handle stdin input and forward to SSH channel -void on_stdin_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { - client_context_t *context = (client_context_t *)stream->data; - - if (nread < 0) { - if (nread == UV_EOF) { - printf("EOF received from stdin\n"); - uv_stop(context->loop); - } else { - fprintf(stderr, "Error reading from stdin: %s\n", uv_strerror(nread)); - } - return; - } - - if (nread > 0) { - int rc = ssh_channel_write(context->channel, buf->base, nread); - if (rc != nread) { - fprintf(stderr, "Failed to write to SSH channel: %s\n", ssh_get_error(context->ssh)); - } - } -} - -// Cleanup resources -void cleanup(client_context_t *context) { - // Stop polling if still active - uv_poll_stop(&context->poll_handle); - - // Stop stdin reading if active - if (!context->subsystem) { - uv_read_stop((uv_stream_t*)&context->tty_handle); - uv_close((uv_handle_t*)&context->tty_handle, NULL); - } - - // Close the poll handle (needs to be closed before the loop can be closed properly) - uv_close((uv_handle_t*)&context->poll_handle, NULL); - - // Close SSH channel if it exists - if (context->channel) { - ssh_channel_close(context->channel); - ssh_channel_free(context->channel); - } - - // Close SSH session if it exists - if (context->ssh) { - ssh_disconnect(context->ssh); - ssh_free(context->ssh); - } -} - -int main(int argc, char *argv[]) { - int r = 0; - - if (argc < 4) { - fprintf(stderr, "Usage: %s [subsystem]\n", argv[0]); - return 1; - } - - const char *hostname = argv[1]; - const char *username = argv[2]; - const char *password = argv[3]; - const char *subsystem = NULL; - - if (argc == 5) { - subsystem = argv[4]; - } - - uv_loop_t loop; - uv_loop_init(&loop); - - client_context_t context = {0}; - context.subsystem = subsystem; - if (subsystem && !strcmp(subsystem, "netconf")) { - context.subsystem_ctx = &((client_context_t){0}); - } - context.loop = &loop; - context.message_buffer.length = 0; - - if (init_ssh(&context, hostname, username, password) < 0) { - uv_loop_close(&loop); - return 1; - } - - // Setup polling for SSH socket - if (setup_poll(&context) < 0) { - cleanup(&context); - uv_loop_close(&loop); - return 1; - } - - // Setup stdin input handling for shell sessions - if (setup_stdin(&context) < 0) { - cleanup(&context); - uv_loop_close(&loop); - return 1; - } - - if (subsystem) { - if (!strcmp(subsystem, "netconf")) { - printf("Connected to NETCONF server at %s and sent hello\n", hostname); - } else { - printf("Connected to %s server at %s\n", subsystem, hostname); - } - printf("Waiting for server response...\n"); - } else { - printf("Connected to SSH server at %s\n", hostname); - printf("Ready for input\n"); - } - - uv_run(&loop, UV_RUN_DEFAULT); - - cleanup(&context); - - // Walk the loop to close any remaining handles - uv_walk(&loop, (uv_walk_cb)close_walk_cb, NULL); - - // Run the loop one more time to let close callbacks execute - do { - r = uv_run(&loop, UV_RUN_DEFAULT); - } while (r != 0); - - // Now it's safe to close the loop - r = uv_loop_close(&loop); - if (r != 0) { - fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(r)); - - // If we still have handles, print debug info - if (r == UV_EBUSY) { - fprintf(stderr, "There are still active handles in the loop. This is a leak.\n"); - } - } - - return 0; -} - -// Walk callback to close remaining handles -void close_walk_cb(uv_handle_t* handle, void* arg) { - if (!uv_is_closing(handle)) { - uv_close(handle, NULL); - } -} diff --git a/libuv_libssh/async_netconf_client_2.c b/libuv_libssh/async_netconf_client_2.c deleted file mode 100644 index a9f7122..0000000 --- a/libuv_libssh/async_netconf_client_2.c +++ /dev/null @@ -1,492 +0,0 @@ -/* - * async_netconf_client_2.c - * - * Non-blocking NETCONF hello exchange using libssh (non-blocking) + libuv (uv_poll) - * - * Build: - * gcc -o ancc2 async_netconf_client_2.c -lssh -luv - * - * Usage: - * ./ancc2 - */ - -#include -#include -#include -#include - -#include - -#include - -#define BUF_SIZE 512 - -const char *NETCONF_HELLO = "\n" - "\n" - " \n" - " urn:ietf:params:netconf:base:1.0\n" - " \n" - "\n" - "]]>]]>"; - -const char *NETCONF_GET_CONFIG = "\n" - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - "\n" - "]]>]]>"; - -const char *NETCONF_CLOSE_SESSION = "\n" - " \n" - "\n" - "]]>]]>"; - -enum client_state { - S_CONNECT, - S_AUTH, - S_CHANNEL_OPEN, - S_SUBSYSTEM, - S_SEND_HELLO, - S_RECV_HELLO, - S_SEND_GET_CONFIG, - S_SEND_GET_CONFIG_WRITING, - S_RECV_GET, - S_CLOSE, - S_CLOSE_WRITING, - S_RECV_CLOSE_REPLY, - S_CLEANUP, - S_DONE, - S_ERROR -}; - -typedef struct { - const char *data; - size_t len; - size_t sent; -} write_buffer_t; - -typedef struct { - uv_loop_t *loop; - - ssh_session session; - ssh_channel channel; - uv_poll_t *poll; - int fd; - enum client_state state; - const char *host; - int port; - const char *user; - const char *password; - const char *subsystem; - - /* current write buffer */ - write_buffer_t write_buf; - - /* read buffer */ - char *reply; - size_t reply_len; - size_t reply_cap; -} client_t; - -/* Simple helper to append to reply buffer */ -static int append_reply(client_t *c, const char *buf, size_t n) { - if (n == 0) return 0; - if (c->reply_len + n + 1 > c->reply_cap) { - size_t newcap = (c->reply_cap == 0) ? BUF_SIZE : c->reply_cap * 2; - while (newcap < c->reply_len + n + 1) - newcap *= 2; - char *p = realloc(c->reply, newcap); - if (!p) { - fprintf(stderr, "ERROR: realloc failed\n"); - return -1; - } - c->reply = p; - c->reply_cap = newcap; - } - memcpy(c->reply + c->reply_len, buf, n); - c->reply_len += n; - c->reply[c->reply_len] = '\0'; - return 0; -} - -/* Print libssh error and transition to error */ -static void set_error(client_t *c, const char *msg) { - fprintf(stderr, "ERROR: %s: %s\n", msg, ssh_get_error(c->session)); - c->state = S_ERROR; -} - -/* Initialize write buffer for sending data */ -static void init_write_buffer(client_t *c, const char *data) { - c->write_buf.data = data; - c->write_buf.len = strlen(data); - c->write_buf.sent = 0; -} - -/* Generic write operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_write(client_t *c, const char *operation) { - if (c->write_buf.sent >= c->write_buf.len) { - return 1; /* complete */ - } - - int wrote = ssh_channel_write(c->channel, c->write_buf.data + c->write_buf.sent, - (uint32_t)(c->write_buf.len - c->write_buf.sent)); - - if (wrote > 0) { - c->write_buf.sent += (size_t)wrote; - if (c->write_buf.sent >= c->write_buf.len) { - fprintf(stderr, "%s fully sent\n", operation); - return 1; /* complete */ - } - return 0; /* more data to send */ - } else if (wrote == SSH_ERROR) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", operation); - set_error(c, error_msg); - return -1; /* error */ - } else if (wrote == SSH_AGAIN || wrote == 0) { - return 0; /* not writable now */ - } else { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", operation); - set_error(c, error_msg); - return -1; /* error */ - } -} - -/* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_read(client_t *c, const char *operation, enum client_state next_state) { - char buf[BUF_SIZE]; - int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - - if (n > 0) { - if (append_reply(c, buf, (size_t)n) != 0) { - set_error(c, "realloc failed"); - return -1; - } - fprintf(stderr, "Read %s %d bytes (total %zu)\n", operation, n, c->reply_len); - - /* Check for RFC6242 end-of-message token */ - if (strstr(c->reply, "]]>]]>") != NULL) { - fprintf(stdout, "=== NETCONF %s ===\n%s\n=== end ===\n", operation, c->reply); - /* Reset reply buffer for next message */ - c->reply_len = 0; - c->reply[0] = '\0'; - c->state = next_state; - return 1; /* complete */ - } - return 0; /* more data expected */ - } else if (n == 0) { - if (ssh_channel_is_eof(c->channel)) { - fprintf(stderr, "Channel EOF during %s\n", operation); - if (c->reply_len > 0) { - fprintf(stdout, "=== NETCONF %s (partial) ===\n%s\n=== end ===\n", operation, c->reply); - } - c->state = S_CLEANUP; - return 1; /* complete via EOF */ - } - return 0; /* no data now, wait */ - } else { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", operation); - set_error(c, error_msg); - return -1; /* error */ - } -} - -/* Called whenever the polled fd has activity (readable/writable) */ -static void poll_cb(uv_poll_t *handle, int status, int events) { - (void)status; - client_t *c = (client_t*)handle->data; - int rc = 0; - - if (c->state == S_DONE || c->state == S_ERROR) { - if (c->state == S_DONE) - printf("loop is done. stopping it\n"); - else if (c->state == S_ERROR) - printf("loop encountered and error. stopping it\n"); - uv_poll_stop(c->poll); - return; - } - - /* Drive a simple state machine */ - switch (c->state) { - case S_CONNECT: - rc = ssh_connect(c->session); - if (rc == SSH_OK) { - fprintf(stderr, "Connected (SSH_OK)\n"); - c->state = S_AUTH; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_connect failed"); - return; - } - /* fallthrough */ - case S_AUTH: - rc = ssh_userauth_password(c->session, NULL, c->password); - if (rc == SSH_AUTH_SUCCESS) { - fprintf(stderr, "Authenticated (password)\n"); - c->state = S_CHANNEL_OPEN; - } else if (rc == SSH_AUTH_AGAIN) { - return; - } else { - set_error(c, "ssh_userauth_password failed"); - return; - } - /* fallthrough */ - case S_CHANNEL_OPEN: - if (!c->channel) { - c->channel = ssh_channel_new(c->session); - if (!c->channel) { - set_error(c, "ssh_channel_new failed"); - return; - } - } - rc = ssh_channel_open_session(c->channel); - if (rc == SSH_OK) { - fprintf(stderr, "Channel opened\n"); - c->state = S_SUBSYSTEM; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_channel_open_session failed"); - return; - } - /* fallthrough */ - case S_SUBSYSTEM: - rc = ssh_channel_request_subsystem(c->channel, c->subsystem); - if (rc == SSH_OK) { - fprintf(stderr, "Requested subsystem: %s\n", c->subsystem); - if (!strcmp(c->subsystem, "netconf")) - init_write_buffer(c, NETCONF_HELLO); - c->state = S_SEND_HELLO; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_channel_request_subsystem failed"); - return; - } - /* fallthrough */ - case S_SEND_HELLO: - rc = do_write(c, "Hello"); - if (rc == 1) { - c->state = S_RECV_HELLO; - } else if (rc == -1) { - return; - } - break; - case S_RECV_HELLO: - rc = do_read(c, "hello reply", S_SEND_GET_CONFIG); - if (rc == -1) - return; - break; - case S_SEND_GET_CONFIG: - init_write_buffer(c, NETCONF_GET_CONFIG); - c->state = S_SEND_GET_CONFIG_WRITING; - /* fallthrough */ - case S_SEND_GET_CONFIG_WRITING: - rc = do_write(c, "GET_CONFIG"); - if (rc == 1) { - c->state = S_RECV_GET; - } else if (rc == -1) { - return; - } - break; - case S_RECV_GET: - rc = do_read(c, "GET_CONFIG reply", S_CLOSE); - if (rc == -1) - return; - break; - case S_CLOSE: - init_write_buffer(c, NETCONF_CLOSE_SESSION); - c->state = S_CLOSE_WRITING; - /* fallthrough */ - case S_CLOSE_WRITING: - rc = do_write(c, "Close message"); - if (rc == 1) { - fprintf(stderr, "Close message fully sent, waiting for reply\n"); - c->state = S_RECV_CLOSE_REPLY; - } else if (rc == -1) { - return; - } - break; - case S_RECV_CLOSE_REPLY: - rc = do_read(c, "close reply", S_CLEANUP); - if (rc == -1) - return; - break; - case S_CLEANUP: - c->state = S_DONE; - fprintf(stderr, "Disconnected and cleaned up\n"); - uv_poll_stop(c->poll); - uv_stop(c->loop); - return; - default: - return; - } -} - -/* Helper: initialize session and uv poll */ -static int client_init(client_t *c) { - c->session = ssh_new(); - if (!c->session) { - fprintf(stderr, "ERROR: ssh_new error"); - return -1; - } - ssh_options_set(c->session, SSH_OPTIONS_HOST, c->host); - ssh_options_set(c->session, SSH_OPTIONS_PORT, &c->port); - ssh_options_set(c->session, SSH_OPTIONS_USER, c->user); - - /* Non-blocking mode */ - ssh_set_blocking(c->session, 0); - - c->channel = NULL; - c->poll = NULL; - c->reply = NULL; - c->reply_len = 0; - c->reply_cap = 0; - c->state = S_CONNECT; - - /* Initialize write buffer to empty */ - c->write_buf.data = NULL; - c->write_buf.len = 0; - c->write_buf.sent = 0; - - return 0; -} - -// Walk callback to close remaining handles -static void close_walk_cb(uv_handle_t* handle, void* arg) { - if (!uv_is_closing(handle)) { - uv_close(handle, NULL); - } -} - -int main(int argc, char **argv) { - if (argc < 6) { - fprintf(stderr, "Usage: %s \n", argv[0]); - return 1; - } - client_t *client = calloc(1, sizeof(client_t)); - if (!client) { - fprintf(stderr, "Failed to allocate client\n"); - return 1; - } - - client->host = argv[1]; - client->port = atoi(argv[2]); - client->user = argv[3]; - client->password = argv[4]; - client->subsystem = argv[5]; - - if (client_init(client) != 0) { - fprintf(stderr, "Failed to init client\n"); - return 2; - } - - /* create the uv loop and poll handle*/ - client->loop = uv_default_loop(); - - /* We need to ensure we have a valid fd to initialize uv_poll */ - int err = ssh_connect(client->session); - if (err == SSH_ERROR) { - fprintf(stderr, "Initial ssh_connect failed: %s\n", ssh_get_error(client->session)); - ssh_free(client->session); - free(client); - return 3; - } - /* At this point ssh_get_fd should return a valid fd for uv_poll */ - client->fd = ssh_get_fd(client->session); - if (client->fd < 0) { - fprintf(stderr, "Could not get SSH session fd\n"); - ssh_disconnect(client->session); - ssh_free(client->session); - free(client); - return 4; - } - - client->poll = calloc(1, sizeof(uv_poll_t)); - if (!client->poll) { - fprintf(stderr, "Failed to allocate poll handle\n"); - ssh_disconnect(client->session); - ssh_free(client->session); - free(client); - return 5; - } - - err = uv_poll_init(client->loop, client->poll, client->fd); - if (err < 0) { - fprintf(stderr, "uv_poll_init failed: %s\n", uv_strerror(err)); - free(client->poll); - client->poll = NULL; - ssh_disconnect(client->session); - ssh_free(client->session); - free(client); - return 6; - } - client->poll->data = client; - - /* Watch for read/write events; libssh manages what it needs depending on state */ - err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); - if (err < 0) { - fprintf(stderr, "uv_poll_start failed: %s\n", uv_strerror(err)); - uv_close((uv_handle_t*)client->poll, NULL); - free(client->poll); - client->poll = NULL; - ssh_disconnect(client->session); - ssh_free(client->session); - free(client); - return 7; - } - - /* If the preliminary ssh_connect returned SSH_AGAIN, the state should still be CONNECT. - Otherwise, poll_cb will drive the next steps. */ - fprintf(stderr, "Starting libuv loop\n"); - uv_run(client->loop, UV_RUN_DEFAULT); - fprintf(stderr, "Stopped libuv loop\n"); - - /* cleanup */ - /* graceful close of ssh channel & session */ - if (client->channel) { - ssh_channel_send_eof(client->channel); - ssh_channel_close(client->channel); - ssh_channel_free(client->channel); - client->channel = NULL; - } - if (client->session) { - ssh_disconnect(client->session); - ssh_free(client->session); - } - - // see MAKE_VALGRIND_HAPPY in libuv/test/task.h - // walk the loop to close any remaining handles - uv_walk(client->loop, close_walk_cb, NULL); - // run the loop one more time to let close callbacks execute - uv_run(client->loop, UV_RUN_DEFAULT); - - // now it's safe to close the loop - err = uv_loop_close(client->loop); - if (err != 0) { - fprintf(stderr, "WARNING: Loop close failed: %s\n", uv_strerror(err)); - - // If we still have handles, print debug info - if (err == UV_EBUSY) { - fprintf(stderr, "There are still active handles in the loop. This is a leak.\n"); - } - } - uv_library_shutdown(); - - if (client->reply) - free(client->reply); - - if (client->poll) - free(client->poll); - - free(client); - - fprintf(stderr, "Exited\n"); - return 0; -} diff --git a/libuv_libssh/make.sh b/libuv_libssh/make.sh deleted file mode 100755 index 0dd4b58..0000000 --- a/libuv_libssh/make.sh +++ /dev/null @@ -1 +0,0 @@ -gcc -Wall -g -o ancc async_netconf_client_2.c -lssh -luv diff --git a/src/ssh.act b/src/ssh.act index 2fd4a6d..1e3bfe1 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -58,7 +58,7 @@ actor Client(cap: net.TCPConnectCap, """Disconnect the SSH client""" NotImplemented - def send_nc_payload() -> str: + def send_payload() -> str: """Send payload""" NotImplemented @@ -86,9 +86,27 @@ actor main(env): # get netconf server's capabilities c.set_payload(']]>]]>') # c.set_payload(']]>]]>') - print("\npayload 1:\n", c.get_payload(), "\npayload 1 end") - print("\nNC response 1:\n", c.send_nc_payload(), "\nNC response 1 end\n") + print("\npayload 1:\n" + c.get_payload() + "\npayload 1 end") + print("\nNC response 1:\n" + c.send_payload() + "\nNC response 1 end\n") + # c.send_payload() c.disconnect() + c2 = Client( + net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), + "localhost", + "foo", + on_connect, + on_close, + password="bar", + port=22, + subsystem="ssh", + ) + + c2.set_payload('ls -ahl\n') + print("\npayload SSH:\n" + c2.get_payload() + "\npayload SSH end") + print("\nSSH response:\n" + c2.send_payload() + "\nSSH response end\n") + + c2.disconnect() + env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 6a44ed4..b200765 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -19,6 +19,9 @@ enum client_state { S_AUTH, S_CHANNEL_OPEN, S_SUBSYSTEM, + S_SHELL_REQUEST, + S_SEND_COMMAND, + S_RECV_COMMAND_OUTPUT, S_SEND_HELLO, S_RECV_HELLO, S_SEND_GET, @@ -33,6 +36,7 @@ enum client_state { }; enum client_operation { + OP_SSH, OP_HELLO, OP_HELLO_REPLY, OP_GET, @@ -76,6 +80,8 @@ typedef struct { static char *client_operation_to_str(enum client_operation op) { switch (op) { + case OP_SSH: + return "OP_SSH"; case OP_HELLO: return "OP_HELLO"; case OP_HELLO_REPLY: @@ -96,11 +102,13 @@ static char *client_operation_to_str(enum client_operation op) { /* Simple helper to append to reply buffer */ static int append_reply(client_t *c, const char *buf, size_t n) { - if (n == 0) return 0; + if (n == 0) + return 0; if (c->reply_len + n + 1 > c->reply_cap) { size_t newcap = (c->reply_cap == 0) ? BUF_SIZE : c->reply_cap * 2; - while (newcap < c->reply_len + n + 1) + while (newcap < c->reply_len + n + 1) { newcap *= 2; + } char *p = realloc(c->reply, newcap); if (!p) { printf("ERROR: realloc failed\n"); @@ -152,7 +160,7 @@ static int do_write(client_t *c, enum client_operation operation) { } else if (wrote == SSH_AGAIN || wrote == 0) { return 0; /* not writable now */ } else { - char error_msg[256]; + char error_msg[256] = { 0 }; snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", client_operation_to_str(operation)); set_error(c, error_msg); return -1; /* error */ @@ -161,19 +169,21 @@ static int do_write(client_t *c, enum client_operation operation) { /* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ static int do_read(client_t *c, enum client_operation operation, enum client_state next_state) { - char buf[BUF_SIZE]; + char buf[BUF_SIZE] = { 0 }; int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - if (n > 0) { if (append_reply(c, buf, (size_t)n) != 0) { set_error(c, "realloc failed"); return -1; } +#ifdef DEBUG_MODE printf("Read %s %d bytes (total %zu)\n", client_operation_to_str(operation), n, c->reply_len); - +#endif /* Check for RFC6242 end-of-message token */ - if (strstr(c->reply, "]]>]]>") != NULL) { + if (!strcmp(c->subsystem, "netconf") && strstr(c->reply, "]]>]]>") != NULL) { +#ifdef DEBUG_MODE printf("=== NETCONF %s ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); +#endif // save response so it can be returned if (operation == OP_GET_REPLY) { c->response = strdup(c->reply); @@ -186,19 +196,20 @@ static int do_read(client_t *c, enum client_operation operation, enum client_sta } return 0; /* more data expected */ } else if (n == 0) { - if (ssh_channel_is_eof(c->channel)) { - printf("Channel EOF during %s\n", client_operation_to_str(operation)); - if (c->reply_len > 0) { - printf("=== NETCONF %s (partial) ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); + if (ssh_channel_is_eof(c->channel) || (c->reply && (c->reply_len > 0))) { + if (c->reply && c->reply_len > 0) { + c->response = strdup(c->reply); +#ifdef DEBUG_MODE + printf("=== Reply %s (partial) ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); +#endif } c->state = S_CLEANUP; return 1; /* complete via EOF */ } return 0; /* no data now, wait */ } else { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_read_nonblocking failed during %s", client_operation_to_str(operation)); - set_error(c, error_msg); + snprintf(buf, sizeof(buf), "ssh_channel_read_nonblocking failed during %s", client_operation_to_str(operation)); + set_error(c, buf); return -1; /* error */ } } @@ -207,7 +218,14 @@ static int do_read(client_t *c, enum client_operation operation, enum client_sta static void poll_cb(uv_poll_t *handle, int status, int events) { (void)status; int rc = 0; - client_t *c = (client_t*)handle->data; + client_t *c = NULL; + + if (handle == NULL || handle->data == NULL) { + printf("ERROR: handle == NULL || handle->data == NULL\n"); + return; + } + + c = (client_t*)handle->data; if (c->state == S_DONE || c->state == S_ERROR) { if (c->state == S_DONE) @@ -220,18 +238,6 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { /* Drive a simple state machine */ switch (c->state) { - case S_CONNECT: - rc = ssh_connect(c->session); - if (rc == SSH_OK) { - printf("Connected (SSH_OK)\n"); - c->state = S_AUTH; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_connect failed"); - return; - } - /* fallthrough */ case S_AUTH: rc = ssh_userauth_password(c->session, NULL, c->password); if (rc == SSH_AUTH_SUCCESS) { @@ -264,26 +270,68 @@ static void poll_cb(uv_poll_t *handle, int status, int events) { } /* fallthrough */ case S_SUBSYSTEM: - rc = ssh_channel_request_subsystem(c->channel, c->subsystem); - if (rc == SSH_OK) { - printf("Requested subsystem: '%s'\n", c->subsystem); - init_write_buffer(c, NETCONF_HELLO_MSG); - c->state = S_SEND_HELLO; - } else if (rc == SSH_AGAIN) { - return; + if (!strcmp(c->subsystem, "ssh")) { + /* For SSH subsystem, request a shell instead */ + c->state = S_SHELL_REQUEST; } else { - set_error(c, "ssh_channel_request_subsystem failed"); - return; + /* For other subsystems (like netconf), request the subsystem */ + rc = ssh_channel_request_subsystem(c->channel, c->subsystem); + if (rc == SSH_OK) { + printf("Requested subsystem: %s\n", c->subsystem); + if (!strcmp(c->subsystem, "netconf")) + init_write_buffer(c, NETCONF_HELLO_MSG); + c->state = S_SEND_HELLO; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_request_subsystem failed"); + return; + } } - /* fallthrough */ - case S_SEND_HELLO: - rc = do_write(c, OP_HELLO); + /* fallthrough for ssh subsystem */ + case S_SHELL_REQUEST: + if (!strcmp(c->subsystem, "ssh")) { + rc = ssh_channel_request_shell(c->channel); + if (rc == SSH_OK) { + printf("SSH shell established successfully\n"); + init_write_buffer(c, c->payload); + c->state = S_SEND_COMMAND; + } else if (rc == SSH_AGAIN) { + return; + } else { + set_error(c, "ssh_channel_request_shell failed"); + return; + } + } + break; + case S_SEND_COMMAND: + rc = do_write(c, OP_SSH); if (rc == 1) { - c->state = S_RECV_HELLO; + // printf("Command '%s' sent, waiting for output...\n", c->payload); + c->state = S_RECV_COMMAND_OUTPUT; } else if (rc == -1) { return; } break; + case S_RECV_COMMAND_OUTPUT: + rc = do_read(c, OP_SSH, S_CLEANUP); + if (rc == -1) { + return; + } + break; + case S_SEND_HELLO: + if (!strcmp(c->subsystem, "netconf")) { + rc = do_write(c, OP_HELLO); + if (rc == 1) { + c->state = S_RECV_HELLO; + } else if (rc == -1) { + return; + } + } else { + /* Skip NETCONF hello for non-NETCONF subsystems */ + c->state = S_DONE; + } + break; case S_RECV_HELLO: rc = do_read(c, OP_HELLO_REPLY, S_SEND_GET); if (rc == -1) @@ -378,78 +426,103 @@ B_str sshQ_version() { return $R_CONT(c$cont, B_None); } -$R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { - pin_actor_affinity(); - +/* Helper: initialize client */ +static int client_init(client_t *c, sshQ_Client self) { int err = 0; - client_t *client = calloc(1, sizeof(client_t)); - if (client == NULL) { - printf("error allocating client_t\n"); - return $R_CONT(c$cont, B_None); + c->session = ssh_new(); + if (!c->session) { + printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); + goto err; } - client->loop = uv_default_loop(); + c->poll = calloc(1, sizeof(uv_poll_t)); + if (!c->poll) { + printf("Failed to allocate poll handle\n"); + goto err; + } - client->session = ssh_new(); - if (client->session == NULL) { - printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); - return $R_CONT(c$cont, B_None); + c->loop = uv_default_loop(); + if (c->loop == NULL) { + printf("uv_default_loop failed\n"); + goto err; } - client->poll = NULL; - client->channel = NULL; - client->reply = NULL; - client->reply_len = 0; - client->reply_cap = 0; - client->state = S_CONNECT; + c->channel = NULL; + c->reply = NULL; + c->reply_len = 0; + c->reply_cap = 0; + c->state = S_CONNECT; /* Initialize write buffer to empty */ - client->write_buf.data = NULL; - client->write_buf.len = 0; - client->write_buf.sent = 0; - - client->host = (const char *)fromB_str(self->host); - client->port = self->port->val; - client->user = (const char *)fromB_str(self->username); - client->password = (const char *)fromB_str(self->password); - client->subsystem = (const char *)fromB_str(self->subsystem); + c->write_buf.data = NULL; + c->write_buf.len = 0; + c->write_buf.sent = 0; -#ifdef DEBUG_MODE - // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS - err = ssh_set_log_level(SSH_LOG_FUNCTIONS); - if (err != SSH_OK) - { - printf("%s: ssh_set_log_level() Error setting log level: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } -#endif + c->host = (const char *)fromB_str(self->host); + c->port = self->port->val; + c->user = (const char *)fromB_str(self->username); + c->password = (const char *)fromB_str(self->password); + c->subsystem = (const char *)fromB_str(self->subsystem); - err = ssh_session_set_disconnect_message(client->session, "Disconnecting SSH, powered by Acton"); + err = ssh_session_set_disconnect_message(c->session, "Disconnecting SSH, powered by Acton"); if (err != SSH_OK) { printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + goto err; } - err = ssh_options_set(client->session, SSH_OPTIONS_HOST, client->host); + err = ssh_options_set(c->session, SSH_OPTIONS_HOST, c->host); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + goto err; } - err = ssh_options_set(client->session, SSH_OPTIONS_PORT, &client->port); + err = ssh_options_set(c->session, SSH_OPTIONS_PORT, &c->port); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + goto err; } - err = ssh_options_set(client->session, SSH_OPTIONS_USER, client->user); + err = ssh_options_set(c->session, SSH_OPTIONS_USER, c->user); if (err != SSH_OK) { printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); + goto err; } - ssh_set_blocking(client->session, 0); + return 0; +err: + if (c->session) { + ssh_disconnect(c->session); + ssh_free(c->session); + } + return -1; +} + +$R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { + pin_actor_affinity(); + + int err = 0; + + client_t *client = calloc(1, sizeof(client_t)); + if (client == NULL) { + printf("error allocating client_t\n"); + goto err; + } + + if (client_init(client, self) != 0) { + fprintf(stderr, "Failed to init client\n"); + goto err; + } + +#ifdef DEBUG_MODE + // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS + err = ssh_set_log_level(SSH_LOG_FUNCTIONS); + if (err != SSH_OK) + { + printf("%s: ssh_set_log_level() Error setting log level: %d\n", __FUNCTION__, err); + return $R_CONT(c$cont, B_None); + } +#endif // should it auto-parse user config? for example from /home/user/.ssh/ // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); @@ -461,34 +534,24 @@ B_str sshQ_version() { err = ssh_connect(client->session); if (err == SSH_ERROR) { printf("%s: ssh_connect() Error connecting to SSH server: '%s' (%d)\n", __FUNCTION__, ssh_get_error(client->session), err); - return $R_CONT(c$cont, B_None); + goto err; } + // connected, change state + client->state = S_AUTH; + + ssh_set_blocking(client->session, 0); // At this point ssh_get_fd should return a valid fd for uv_poll client->fd = ssh_get_fd(client->session); if (client->fd < 0) { printf("Could not get SSH session fd\n"); - ssh_disconnect(client->session); - ssh_free(client->session); - return $R_CONT(c$cont, B_None); - } - - client->poll = calloc(1, sizeof(uv_poll_t)); - if (!client->poll) { - printf("Failed to allocate poll handle\n"); - ssh_disconnect(client->session); - ssh_free(client->session); - return $R_CONT(c$cont, B_None); + goto err; } err = uv_poll_init(client->loop, client->poll, client->fd); if (err < 0) { printf("uv_poll_init failed: %s\n", uv_strerror(err)); - free(client->poll); - client->poll = NULL; - ssh_disconnect(client->session); - ssh_free(client->session); - return $R_CONT(c$cont, B_None); + goto err; } client->poll->data = client; @@ -496,12 +559,7 @@ B_str sshQ_version() { err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); if (err < 0) { printf("uv_poll_start failed: %s\n", uv_strerror(err)); - uv_close((uv_handle_t*)client->poll, NULL); - free(client->poll); - client->poll = NULL; - ssh_disconnect(client->session); - ssh_free(client->session); - return $R_CONT(c$cont, B_None); + goto err; } self->_client = toB_u64((unsigned long)client); @@ -509,6 +567,25 @@ B_str sshQ_version() { $action f = ($action) self->on_connect; f->$class->__asyn__(f, self); + return $R_CONT(c$cont, B_None); +err: + if (client) { + if (client->poll) { + uv_close((uv_handle_t*)client->poll, NULL); + free(client->poll); + client->poll = NULL; + } + if (client->loop) { + uv_loop_close(client->loop); + client->loop = NULL; + } + if (client->session) { + ssh_disconnect(client->session); + ssh_free(client->session); + } + free(client); + client = NULL; + } return $R_CONT(c$cont, B_None); } @@ -517,7 +594,7 @@ B_str sshQ_version() { return $R_CONT(c$cont, B_None); } -$R sshQ_ClientD_send_nc_payloadG_local (sshQ_Client self, $Cont c$cont) { +$R sshQ_ClientD_send_payloadG_local (sshQ_Client self, $Cont c$cont) { int err = 0; client_t *client = (client_t*)fromB_u64(self->_client); @@ -548,6 +625,7 @@ B_str sshQ_version() { printf("self->_client == NULL\n"); goto out; } + client_t *client = (client_t*)fromB_u64(self->_client); if (client == NULL) { printf("client == NULL, nothing to cleanup\n"); @@ -569,16 +647,19 @@ B_str sshQ_version() { if (client->loop) { // see MAKE_VALGRIND_HAPPY in libuv/test/task.h + // walk the loop to close any remaining handles uv_walk(client->loop, close_walk_cb, NULL); + // run the loop one more time to let close callbacks execute - uv_run(client->loop, UV_RUN_DEFAULT); + // TODO: if this is executed after a previous c.disconnect() call in ssh.act + // then it will crash here. no matter if it's the same client object or a new one + // uv_run(client->loop, UV_RUN_DEFAULT); // now it's safe to close the loop err = uv_loop_close(client->loop); if (err != 0) { printf("WARNING: Loop close failed: %s\n", uv_strerror(err)); - // If we still have handles, print debug info if (err == UV_EBUSY) { printf("There are still active handles in the loop. This is a leak.\n"); @@ -607,6 +688,7 @@ B_str sshQ_version() { if (ssh_finalize()) { printf("%s: ssh_finalize error", __FUNCTION__); } + out: return $R_CONT(c$cont, B_None); } From ae3c04ce586958bd8be7636465410d852e9d4b11 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Mon, 25 Aug 2025 14:16:47 +0200 Subject: [PATCH 34/37] fix 2nd Client failing because same default loop used --- src/ssh.act | 20 ++++++++++---------- src/ssh.ext.c | 24 ++++++++++++++++++++---- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/ssh.act b/src/ssh.act index 1e3bfe1..175dfaa 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -72,7 +72,8 @@ actor main(env): # print("Connection closed", error) return - c = Client( + print("NETCONF start...\n") + c1 = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", "foo", @@ -84,14 +85,14 @@ actor main(env): ) # get netconf server's capabilities - c.set_payload(']]>]]>') - # c.set_payload(']]>]]>') - print("\npayload 1:\n" + c.get_payload() + "\npayload 1 end") - print("\nNC response 1:\n" + c.send_payload() + "\nNC response 1 end\n") - # c.send_payload() + c1.set_payload(']]>]]>') + # c1.set_payload(']]>]]>') + print("\n=========== NC payload ===========\n" + c1.get_payload() + "\n=========== NC payload END ===========") + print("\n=========== NC response ===========\n" + c1.send_payload() + "\n=========== NC response END ===========\n") - c.disconnect() + c1.disconnect() + print("SSH start...\n") c2 = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", @@ -102,10 +103,9 @@ actor main(env): port=22, subsystem="ssh", ) - c2.set_payload('ls -ahl\n') - print("\npayload SSH:\n" + c2.get_payload() + "\npayload SSH end") - print("\nSSH response:\n" + c2.send_payload() + "\nSSH response end\n") + print("\n=========== SSH payload ===========\n" + c2.get_payload() + "\n=========== SSH payload END ===========") + print("\n=========== SSH response ===========\n" + c2.send_payload() + "\n=========== SSH response END ===========\n") c2.disconnect() diff --git a/src/ssh.ext.c b/src/ssh.ext.c index b200765..a202a91 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -442,11 +442,20 @@ static int client_init(client_t *c, sshQ_Client self) { goto err; } - c->loop = uv_default_loop(); + c->loop = malloc(sizeof(uv_loop_t)); if (c->loop == NULL) { - printf("uv_default_loop failed\n"); + printf("Failed to allocate uv loop\n"); goto err; } + uv_loop_init(c->loop); + + // NOTE: uv_default_loop() can not be used since it error when having + // two clients at once + // c->loop = uv_default_loop(); // can not be used + // if (c->loop == NULL) { + // printf("uv_default_loop failed\n"); + // goto err; + // } c->channel = NULL; c->reply = NULL; @@ -495,6 +504,10 @@ static int client_init(client_t *c, sshQ_Client self) { ssh_disconnect(c->session); ssh_free(c->session); } + if (c->loop) { + uv_loop_close(c->loop); + free(c->loop); + } return -1; } @@ -502,7 +515,6 @@ static int client_init(client_t *c, sshQ_Client self) { pin_actor_affinity(); int err = 0; - client_t *client = calloc(1, sizeof(client_t)); if (client == NULL) { printf("error allocating client_t\n"); @@ -553,6 +565,7 @@ static int client_init(client_t *c, sshQ_Client self) { printf("uv_poll_init failed: %s\n", uv_strerror(err)); goto err; } + client->poll->data = client; /* Watch for read/write events; libssh manages what it needs depending on state */ @@ -577,11 +590,13 @@ static int client_init(client_t *c, sshQ_Client self) { } if (client->loop) { uv_loop_close(client->loop); + free(client->loop); client->loop = NULL; } if (client->session) { ssh_disconnect(client->session); ssh_free(client->session); + client->session = NULL; } free(client); client = NULL; @@ -654,7 +669,7 @@ static int client_init(client_t *c, sshQ_Client self) { // run the loop one more time to let close callbacks execute // TODO: if this is executed after a previous c.disconnect() call in ssh.act // then it will crash here. no matter if it's the same client object or a new one - // uv_run(client->loop, UV_RUN_DEFAULT); + uv_run(client->loop, UV_RUN_DEFAULT); // now it's safe to close the loop err = uv_loop_close(client->loop); @@ -665,6 +680,7 @@ static int client_init(client_t *c, sshQ_Client self) { printf("There are still active handles in the loop. This is a leak.\n"); } } + free(client->loop); uv_library_shutdown(); } From 401b5db1e0069983deca8e1e006a79f76a8946b5 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Wed, 1 Apr 2026 08:45:41 +0200 Subject: [PATCH 35/37] Redesign SSH Client to async event-driven API using RTS event loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the synchronous blocking model (uv_run per operation) with an async callback-driven architecture that integrates with the Acton RTS event loop. The Client actor now exposes write()/close() methods and delivers data via on_receive, on_error, and on_remote_close callbacks instead of the previous get/set_payload + send_payload pattern. Key changes: - Use get_uv_loop() (RTS loop) instead of private per-client uv loops - Simplify C state machine: AUTH → AUTH_KBDINT → CHANNEL_OPEN → REQUEST_TYPE → READY - Add keyboard-interactive auth fallback when password auth is denied - Add buffered async writes with partial-write handling - Make password and subsystem optional; support shell or subsystem channels - Remove fallthrough between states so each transition gets its own poll cycle - Rewrite main actor with a NETCONF state machine example - Add Build.act with libssh zig dependency Signed-off-by: Antonio Prcela --- Build.act | 9 + src/ssh.act | 155 ++++----- src/ssh.ext.c | 849 +++++++++++++++++--------------------------------- 3 files changed, 371 insertions(+), 642 deletions(-) create mode 100644 Build.act diff --git a/Build.act b/Build.act new file mode 100644 index 0000000..21ee62f --- /dev/null +++ b/Build.act @@ -0,0 +1,9 @@ +name = "acton_ssh" +fingerprint = 0xb650ac2857701cb1 +zig_dependencies = { + "libssh": ( + url="https://github.com/precla/libssh/archive/refs/heads/zig-build.tar.gz", + hash="libssh-0.11.0-kUQJanViKgBVUmjYqAyLKjnqqHdDvSkdnU1qaMwFPzV-", + artifacts=[ "ssh" ] + ) +} diff --git a/src/ssh.act b/src/ssh.act index 175dfaa..c9daed4 100755 --- a/src/ssh.act +++ b/src/ssh.act @@ -10,103 +10,116 @@ actor Client(cap: net.TCPConnectCap, host: str, username: str, on_connect: action(Client) -> None, - on_close: action(Client, str) -> None, + on_receive: action(Client, bytes) -> None, + on_error: action(Client, str) -> None, + on_remote_close: ?action(Client) -> None, + password: ?str=None, key: ?str=None, - password: str, - port: u16=22, - subsystem: str - ): - """SSH Client""" + port: int=22, + subsystem: ?str=None): + """SSH Client - # haha, this is really a pointer :P - var _client: u64 = 0 + Opens an SSH connection. If subsystem is None, opens a shell channel. + If subsystem is set (e.g. "netconf"), opens a subsystem channel instead. + """ - var _payload: str = "" + var _client: u64 = 0 proc def _pin_affinity() -> None: NotImplemented _pin_affinity() - def get_client(): - return _client + action def write(data: bytes) -> None: + """Write data to the SSH channel""" + NotImplemented - action def get_affinity() -> u64: + action def close(on_close: action(Client) -> None) -> None: + """Close the connection""" NotImplemented - def set_payload(p: str): - _payload = p + def reconnect(): + close(_connect) - def get_payload(): - return _payload + def _connect(c): + NotImplemented proc def _init() -> None: """Initialize the SSH client""" NotImplemented _init() -# action def close(on_close: action(TLSConnection) -> None) -> None: -# """Close the connection""" -# NotImplemented -# -# def reconnect(): -# close(_connect) -# -# def _connect(c): -# NotImplemented - - proc def disconnect() -> None: - """Disconnect the SSH client""" - NotImplemented - - def send_payload() -> str: - """Send payload""" - NotImplemented - actor main(env): - def on_connect(client: Client): - # print("Client connected") - return + var _buf: bytes = b"" + + NETCONF_HELLO = b""" + + + urn:ietf:params:netconf:base:1.0 + +]]>]]>""" + + NETCONF_GET_CONFIG = b""" + + + + + + +]]>]]>""" + + NETCONF_CLOSE = b""" + + +]]>]]>""" + + var _state: int = 0 # 0=wait hello, 1=wait get-config reply, 2=wait close reply - def on_close(client: Client, error: str): - # print("Connection closed", error) - return - - print("NETCONF start...\n") - c1 = Client( + def on_connect(client: Client): + print("Connected! Sending NETCONF hello...") + client.write(NETCONF_HELLO) + + def on_receive(client: Client, data: bytes): + _buf += data + s = _buf.decode() + if "]]>]]>" in s: + print("=== NETCONF message ===") + print(s) + print("=======================") + _buf = b"" + + st = _state + if st == 0: + print("Got server hello, sending get-config...") + _state = 1 + client.write(NETCONF_GET_CONFIG) + elif st == 1: + print("Got config, sending close-session...") + _state = 2 + client.write(NETCONF_CLOSE) + elif st == 2: + print("Got close reply, done!") + def _on_close(c: Client): + env.exit(0) + client.close(_on_close) + + def on_error(client: Client, error: str): + print("Error:", error) + env.exit(1) + + def on_remote_close(client: Client): + print("Remote closed connection") + env.exit(0) + + c = Client( net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), "localhost", "foo", on_connect, - on_close, + on_receive, + on_error, + on_remote_close, password="bar", port=830, subsystem="netconf", ) - - # get netconf server's capabilities - c1.set_payload(']]>]]>') - # c1.set_payload(']]>]]>') - print("\n=========== NC payload ===========\n" + c1.get_payload() + "\n=========== NC payload END ===========") - print("\n=========== NC response ===========\n" + c1.send_payload() + "\n=========== NC response END ===========\n") - - c1.disconnect() - - print("SSH start...\n") - c2 = Client( - net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))), - "localhost", - "foo", - on_connect, - on_close, - password="bar", - port=22, - subsystem="ssh", - ) - c2.set_payload('ls -ahl\n') - print("\n=========== SSH payload ===========\n" + c2.get_payload() + "\n=========== SSH payload END ===========") - print("\n=========== SSH response ===========\n" + c2.send_payload() + "\n=========== SSH response END ===========\n") - - c2.disconnect() - - env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index a202a91..54394d9 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -6,705 +6,412 @@ // acton includes #include -#include "netconf.h" - -#ifndef DEBUG_MODE -// #define DEBUG_MODE /* uncomment for pretty prints */ -#endif - -#define BUF_SIZE 512 +/* Declared in rts/io.h but not always in the include path */ +uv_loop_t *get_uv_loop(); enum client_state { - S_CONNECT, S_AUTH, + S_AUTH_KBDINT, S_CHANNEL_OPEN, - S_SUBSYSTEM, - S_SHELL_REQUEST, - S_SEND_COMMAND, - S_RECV_COMMAND_OUTPUT, - S_SEND_HELLO, - S_RECV_HELLO, - S_SEND_GET, - S_SEND_GET_WRITING, - S_RECV_GET, - S_CLOSE, - S_CLOSE_WRITING, - S_RECV_CLOSE_REPLY, - S_CLEANUP, - S_DONE, + S_REQUEST_TYPE, + S_READY, S_ERROR }; -enum client_operation { - OP_SSH, - OP_HELLO, - OP_HELLO_REPLY, - OP_GET, - OP_GET_REPLY, - OP_CLOSE, - OP_CLOSE_REPLY -}; - -typedef struct { - const char *data; - size_t len; - size_t sent; -} write_buffer_t; - typedef struct { - uv_loop_t *loop; - ssh_session session; ssh_channel channel; uv_poll_t *poll; - const char *subsystem; - int fd; enum client_state state; - const char *host; - int port; - const char *user; - const char *password; - - /* current write buffer */ - write_buffer_t write_buf; - char *payload; - char *response; + /* pending write buffer */ + char *write_buf; + size_t write_len; + size_t write_sent; - /* read buffer */ - char *reply; - size_t reply_len; - size_t reply_cap; + /* actor reference for callbacks */ + sshQ_Client actor; } client_t; -static char *client_operation_to_str(enum client_operation op) { - switch (op) { - case OP_SSH: - return "OP_SSH"; - case OP_HELLO: - return "OP_HELLO"; - case OP_HELLO_REPLY: - return "OP_HELLO_REPLY"; - case OP_GET: - return "OP_GET"; - case OP_GET_REPLY: - return "OP_GET_REPLY"; - case OP_CLOSE: - return "OP_CLOSE"; - case OP_CLOSE_REPLY: - return "OP_CLOSE_REPLY"; - default: - return ""; - } - return ""; -} +/* Forward declarations */ +static void poll_cb(uv_poll_t *handle, int status, int events); -/* Simple helper to append to reply buffer */ -static int append_reply(client_t *c, const char *buf, size_t n) { - if (n == 0) - return 0; - if (c->reply_len + n + 1 > c->reply_cap) { - size_t newcap = (c->reply_cap == 0) ? BUF_SIZE : c->reply_cap * 2; - while (newcap < c->reply_len + n + 1) { - newcap *= 2; - } - char *p = realloc(c->reply, newcap); - if (!p) { - printf("ERROR: realloc failed\n"); - return -1; - } - c->reply = p; - c->reply_cap = newcap; - } - memcpy(c->reply + c->reply_len, buf, n); - c->reply_len += n; - c->reply[c->reply_len] = '\0'; - return 0; -} +/* Helper: set up SSH session, connect, and start polling on the RTS loop */ +static int client_setup(client_t *c, sshQ_Client self) { + int err = 0; -/* Print libssh error and transition to error */ -static void set_error(client_t *c, const char *msg) { - printf("ERROR: %s: %s\n", msg, ssh_get_error(c->session)); - c->state = S_ERROR; -} + c->actor = self; + c->channel = NULL; + c->write_buf = NULL; + c->write_len = 0; + c->write_sent = 0; -/* Initialize write buffer for sending data */ -static void init_write_buffer(client_t *c, const char *data) { - c->write_buf.data = data; - c->write_buf.len = strlen(data); - c->write_buf.sent = 0; -} + c->session = ssh_new(); + if (!c->session) + return -1; -/* Generic write operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_write(client_t *c, enum client_operation operation) { - if (c->write_buf.sent >= c->write_buf.len) { - return 1; /* complete */ + const char *host = (const char *)fromB_str(self->host); + int port = (int)fromB_int(self->port); + const char *user = (const char *)fromB_str(self->username); + + ssh_options_set(c->session, SSH_OPTIONS_HOST, host); + ssh_options_set(c->session, SSH_OPTIONS_PORT, &port); + ssh_options_set(c->session, SSH_OPTIONS_USER, user); + ssh_session_set_disconnect_message(c->session, "Disconnecting SSH, powered by Acton"); + + /* Blocking TCP connect (fast) */ + err = ssh_connect(c->session); + if (err != SSH_OK) { + ssh_free(c->session); + c->session = NULL; + return -1; } - int wrote = ssh_channel_write(c->channel, c->write_buf.data + c->write_buf.sent, - (uint32_t)(c->write_buf.len - c->write_buf.sent)); + c->state = S_AUTH; + ssh_set_blocking(c->session, 0); - if (wrote > 0) { - c->write_buf.sent += (size_t)wrote; - if (c->write_buf.sent >= c->write_buf.len) { - printf("%s fully sent\n", client_operation_to_str(operation)); - return 1; /* complete */ - } - return 0; /* more data to send */ - } else if (wrote == SSH_ERROR) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write failed for %s", client_operation_to_str(operation)); - set_error(c, error_msg); - return -1; /* error */ - } else if (wrote == SSH_AGAIN || wrote == 0) { - return 0; /* not writable now */ - } else { - char error_msg[256] = { 0 }; - snprintf(error_msg, sizeof(error_msg), "ssh_channel_write returned unexpected value for %s", client_operation_to_str(operation)); - set_error(c, error_msg); - return -1; /* error */ + c->fd = ssh_get_fd(c->session); + if (c->fd < 0) { + ssh_disconnect(c->session); + ssh_free(c->session); + c->session = NULL; + return -1; } -} -/* Generic read operation - returns 1 if complete, 0 if more needed, -1 if error */ -static int do_read(client_t *c, enum client_operation operation, enum client_state next_state) { - char buf[BUF_SIZE] = { 0 }; - int n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf)-1, 0); - if (n > 0) { - if (append_reply(c, buf, (size_t)n) != 0) { - set_error(c, "realloc failed"); - return -1; - } -#ifdef DEBUG_MODE - printf("Read %s %d bytes (total %zu)\n", client_operation_to_str(operation), n, c->reply_len); -#endif - /* Check for RFC6242 end-of-message token */ - if (!strcmp(c->subsystem, "netconf") && strstr(c->reply, "]]>]]>") != NULL) { -#ifdef DEBUG_MODE - printf("=== NETCONF %s ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); -#endif - // save response so it can be returned - if (operation == OP_GET_REPLY) { - c->response = strdup(c->reply); - } - /* Reset reply buffer for next message */ - c->reply_len = 0; - c->reply[0] = '\0'; - c->state = next_state; - return 1; /* complete */ - } - return 0; /* more data expected */ - } else if (n == 0) { - if (ssh_channel_is_eof(c->channel) || (c->reply && (c->reply_len > 0))) { - if (c->reply && c->reply_len > 0) { - c->response = strdup(c->reply); -#ifdef DEBUG_MODE - printf("=== Reply %s (partial) ===\n%s\n=== end ===\n", client_operation_to_str(operation), c->reply); -#endif - } - c->state = S_CLEANUP; - return 1; /* complete via EOF */ - } - return 0; /* no data now, wait */ - } else { - snprintf(buf, sizeof(buf), "ssh_channel_read_nonblocking failed during %s", client_operation_to_str(operation)); - set_error(c, buf); - return -1; /* error */ + /* Use the RTS event loop instead of a private loop */ + c->poll = (uv_poll_t *)acton_malloc(sizeof(uv_poll_t)); + err = uv_poll_init(get_uv_loop(), c->poll, c->fd); + if (err < 0) { + ssh_disconnect(c->session); + ssh_free(c->session); + c->session = NULL; + return -1; + } + + c->poll->data = c; + err = uv_poll_start(c->poll, UV_READABLE | UV_WRITABLE, poll_cb); + if (err < 0) { + uv_close((uv_handle_t *)c->poll, NULL); + ssh_disconnect(c->session); + ssh_free(c->session); + c->session = NULL; + return -1; } + + return 0; } -/* Called whenever the polled fd has activity (readable/writable) */ +/* Called whenever the polled fd has activity — drives connection state machine + * and delivers data to Acton via callbacks */ static void poll_cb(uv_poll_t *handle, int status, int events) { - (void)status; - int rc = 0; - client_t *c = NULL; - - if (handle == NULL || handle->data == NULL) { - printf("ERROR: handle == NULL || handle->data == NULL\n"); + if (!handle || !handle->data) return; - } - c = (client_t*)handle->data; + client_t *c = (client_t *)handle->data; + sshQ_Client self = c->actor; + int rc; - if (c->state == S_DONE || c->state == S_ERROR) { - if (c->state == S_DONE) - printf("uv loop is done. stopping it\n"); - else if (c->state == S_ERROR) - printf("uv loop encountered and error. stopping it\n"); + if (status < 0) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str((char *)uv_strerror(status))); uv_poll_stop(c->poll); return; } - /* Drive a simple state machine */ switch (c->state) { - case S_AUTH: - rc = ssh_userauth_password(c->session, NULL, c->password); + case S_AUTH: { + if ((void *)self->password == (void *)B_None || self->password == NULL) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("No authentication method available")); + uv_poll_stop(c->poll); + return; + } + rc = ssh_userauth_password(c->session, NULL, (const char *)fromB_str(self->password)); if (rc == SSH_AUTH_SUCCESS) { - printf("Authenticated (password)\n"); c->state = S_CHANNEL_OPEN; } else if (rc == SSH_AUTH_AGAIN) { return; } else { - set_error(c, "ssh_userauth_password failed"); + /* Password auth failed/denied — try keyboard-interactive */ + c->state = S_AUTH_KBDINT; + } + break; + } + case S_AUTH_KBDINT: { + rc = ssh_userauth_kbdint(c->session, NULL, NULL); + if (rc == SSH_AUTH_INFO) { + int nprompts = ssh_userauth_kbdint_getnprompts(c->session); + for (int i = 0; i < nprompts; i++) { + ssh_userauth_kbdint_setanswer(c->session, i, + (const char *)fromB_str(self->password)); + } + /* Need another round — stay in S_AUTH_KBDINT */ + return; + } else if (rc == SSH_AUTH_SUCCESS) { + c->state = S_CHANNEL_OPEN; + } else if (rc == SSH_AUTH_AGAIN) { + return; + } else { + char errmsg[512]; + snprintf(errmsg, sizeof(errmsg), "Authentication failed: %s", ssh_get_error(c->session)); + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str(errmsg)); + uv_poll_stop(c->poll); return; } - /* fallthrough */ + break; + } case S_CHANNEL_OPEN: if (!c->channel) { c->channel = ssh_channel_new(c->session); if (!c->channel) { - set_error(c, "ssh_channel_new failed"); + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("Failed to create SSH channel")); + uv_poll_stop(c->poll); return; } } rc = ssh_channel_open_session(c->channel); if (rc == SSH_OK) { - printf("Channel opened\n"); - c->state = S_SUBSYSTEM; + c->state = S_REQUEST_TYPE; } else if (rc == SSH_AGAIN) { return; } else { - set_error(c, "ssh_channel_open_session failed"); + char errmsg[512]; + snprintf(errmsg, sizeof(errmsg), "Failed to open channel: %s", ssh_get_error(c->session)); + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str(errmsg)); + uv_poll_stop(c->poll); return; } - /* fallthrough */ - case S_SUBSYSTEM: - if (!strcmp(c->subsystem, "ssh")) { - /* For SSH subsystem, request a shell instead */ - c->state = S_SHELL_REQUEST; - } else { - /* For other subsystems (like netconf), request the subsystem */ - rc = ssh_channel_request_subsystem(c->channel, c->subsystem); - if (rc == SSH_OK) { - printf("Requested subsystem: %s\n", c->subsystem); - if (!strcmp(c->subsystem, "netconf")) - init_write_buffer(c, NETCONF_HELLO_MSG); - c->state = S_SEND_HELLO; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_channel_request_subsystem failed"); - return; - } - } - /* fallthrough for ssh subsystem */ - case S_SHELL_REQUEST: - if (!strcmp(c->subsystem, "ssh")) { + break; + case S_REQUEST_TYPE: + if ((void *)self->subsystem == (void *)B_None || self->subsystem == NULL) { + /* No subsystem — request a shell */ rc = ssh_channel_request_shell(c->channel); - if (rc == SSH_OK) { - printf("SSH shell established successfully\n"); - init_write_buffer(c, c->payload); - c->state = S_SEND_COMMAND; - } else if (rc == SSH_AGAIN) { - return; - } else { - set_error(c, "ssh_channel_request_shell failed"); - return; - } + } else { + rc = ssh_channel_request_subsystem(c->channel, (const char *)fromB_str(self->subsystem)); } - break; - case S_SEND_COMMAND: - rc = do_write(c, OP_SSH); - if (rc == 1) { - // printf("Command '%s' sent, waiting for output...\n", c->payload); - c->state = S_RECV_COMMAND_OUTPUT; - } else if (rc == -1) { + if (rc == SSH_OK) { + c->state = S_READY; + /* Connection fully established — notify Acton */ + $action f = ($action)self->on_connect; + f->$class->__asyn__(f, self); + } else if (rc == SSH_AGAIN) { return; - } - break; - case S_RECV_COMMAND_OUTPUT: - rc = do_read(c, OP_SSH, S_CLEANUP); - if (rc == -1) { + } else { + char errmsg[512]; + snprintf(errmsg, sizeof(errmsg), "Failed to request shell/subsystem: %s", ssh_get_error(c->session)); + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str(errmsg)); + uv_poll_stop(c->poll); return; } break; - case S_SEND_HELLO: - if (!strcmp(c->subsystem, "netconf")) { - rc = do_write(c, OP_HELLO); - if (rc == 1) { - c->state = S_RECV_HELLO; - } else if (rc == -1) { + + case S_READY: + /* Flush any pending write data */ + if (c->write_buf && c->write_sent < c->write_len && (events & UV_WRITABLE)) { + int wrote = ssh_channel_write(c->channel, + c->write_buf + c->write_sent, + (uint32_t)(c->write_len - c->write_sent)); + if (wrote > 0) { + c->write_sent += (size_t)wrote; + if (c->write_sent >= c->write_len) { + free(c->write_buf); + c->write_buf = NULL; + c->write_len = 0; + c->write_sent = 0; + } + } else if (wrote == SSH_ERROR) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("SSH channel write failed")); + uv_poll_stop(c->poll); return; } - } else { - /* Skip NETCONF hello for non-NETCONF subsystems */ - c->state = S_DONE; - } - break; - case S_RECV_HELLO: - rc = do_read(c, OP_HELLO_REPLY, S_SEND_GET); - if (rc == -1) - return; - break; - case S_SEND_GET: - init_write_buffer(c, c->payload); - c->state = S_SEND_GET_WRITING; - /* fallthrough */ - case S_SEND_GET_WRITING: - rc = do_write(c, OP_GET); - if (rc == 1) { - c->state = S_RECV_GET; - } else if (rc == -1) { - return; + /* SSH_AGAIN / 0: try again on next poll */ } - break; - case S_RECV_GET: - rc = do_read(c, OP_GET_REPLY, S_CLOSE); - if (rc == -1) - return; - break; - case S_CLOSE: - init_write_buffer(c, NETCONF_CLOSE_SESSION_MSG); - c->state = S_CLOSE_WRITING; - /* fallthrough */ - case S_CLOSE_WRITING: - rc = do_write(c, OP_CLOSE); - if (rc == 1) { - printf("Close message fully sent, waiting for reply\n"); - c->state = S_RECV_CLOSE_REPLY; - } else if (rc == -1) { - return; + + /* Read all available data from the channel */ + { + char buf[8192]; + int n; + while ((n = ssh_channel_read_nonblocking(c->channel, buf, sizeof(buf), 0)) > 0) { + $action2 f = ($action2)self->on_receive; + f->$class->__asyn__(f, self, to$bytesD_len(buf, n)); + } + if (ssh_channel_is_eof(c->channel)) { + uv_poll_stop(c->poll); + if (self->on_remote_close) { + $action f = ($action)self->on_remote_close; + f->$class->__asyn__(f, self); + } + } else if (n < 0) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str((char *)ssh_get_error(c->session))); + uv_poll_stop(c->poll); + } } break; - case S_RECV_CLOSE_REPLY: - rc = do_read(c, OP_CLOSE_REPLY, S_CLEANUP); - if (rc == -1) - return; - break; - case S_CLEANUP: - c->state = S_DONE; - printf("Disconnected and cleaned up\n"); - uv_poll_stop(c->poll); - uv_stop(c->loop); - return; - default: - return; - } -} -// Walk callback to close remaining handles -static void close_walk_cb(uv_handle_t* handle, void* arg) { - if (!uv_is_closing(handle)) { - uv_close(handle, NULL); + case S_ERROR: + uv_poll_stop(c->poll); + break; } } -void noop_free(void *ptr) { -} +/* ---- Module-level functions ---- */ void sshQ___ext_init__() { - // TODO: can we avoid custom malloc in libssh? like let libssh use stock - // malloc and instead we would explicitly call free() from a finalizer() - // All things related to buffers for receiving data and similarly would have - // to be allocated on the GC-heap though since that data is passed outside - // of the SSH actor - // libssh_replace_allocator( - // acton_gc_malloc, - // acton_gc_realloc, - // acton_gc_calloc, - // noop_free, - // acton_gc_strdup, - // acton_gc_strndup); int r = ssh_init(); if (r != SSH_OK) printf("SSH init failed (%d)\n", r); -#ifdef DEBUG_MODE - else - printf("SSH extension successfully initialized\n"); -#endif } B_str sshQ_version() { - return to$str("0.1.0"); + return to$str("0.2.0"); } -// Client +/* ---- Client actor methods ---- */ -$R sshQ_ClientD__pin_affinityG_local (sshQ_Client self, $Cont c$cont) { +$R sshQ_ClientD__pin_affinityG_local(sshQ_Client self, $Cont c$cont) { pin_actor_affinity(); return $R_CONT(c$cont, B_None); } -/* Helper: initialize client */ -static int client_init(client_t *c, sshQ_Client self) { - int err = 0; - - c->session = ssh_new(); - if (!c->session) { - printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); - goto err; - } - - c->poll = calloc(1, sizeof(uv_poll_t)); - if (!c->poll) { - printf("Failed to allocate poll handle\n"); - goto err; - } - - c->loop = malloc(sizeof(uv_loop_t)); - if (c->loop == NULL) { - printf("Failed to allocate uv loop\n"); - goto err; - } - uv_loop_init(c->loop); - - // NOTE: uv_default_loop() can not be used since it error when having - // two clients at once - // c->loop = uv_default_loop(); // can not be used - // if (c->loop == NULL) { - // printf("uv_default_loop failed\n"); - // goto err; - // } - - c->channel = NULL; - c->reply = NULL; - c->reply_len = 0; - c->reply_cap = 0; - c->state = S_CONNECT; - - /* Initialize write buffer to empty */ - c->write_buf.data = NULL; - c->write_buf.len = 0; - c->write_buf.sent = 0; - - c->host = (const char *)fromB_str(self->host); - c->port = self->port->val; - c->user = (const char *)fromB_str(self->username); - c->password = (const char *)fromB_str(self->password); - c->subsystem = (const char *)fromB_str(self->subsystem); - - err = ssh_session_set_disconnect_message(c->session, "Disconnecting SSH, powered by Acton"); - if (err != SSH_OK) { - printf("%s: ssh_session_set_disconnect_message() Error setting disconnect message: %d\n", __FUNCTION__, err); - goto err; - } - - err = ssh_options_set(c->session, SSH_OPTIONS_HOST, c->host); - if (err != SSH_OK) { - printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_HOST': %d\n", __FUNCTION__, err); - goto err; - } - - err = ssh_options_set(c->session, SSH_OPTIONS_PORT, &c->port); - if (err != SSH_OK) { - printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PORT': %d\n", __FUNCTION__, err); - goto err; - } - - err = ssh_options_set(c->session, SSH_OPTIONS_USER, c->user); - if (err != SSH_OK) { - printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_USER': %d\n", __FUNCTION__, err); - goto err; - } - - return 0; -err: - if (c->session) { - ssh_disconnect(c->session); - ssh_free(c->session); - } - if (c->loop) { - uv_loop_close(c->loop); - free(c->loop); - } - return -1; -} - -$R sshQ_ClientD__initG_local (sshQ_Client self, $Cont c$cont) { +$R sshQ_ClientD__initG_local(sshQ_Client self, $Cont c$cont) { pin_actor_affinity(); - int err = 0; - client_t *client = calloc(1, sizeof(client_t)); - if (client == NULL) { - printf("error allocating client_t\n"); - goto err; - } - - if (client_init(client, self) != 0) { - fprintf(stderr, "Failed to init client\n"); - goto err; - } - -#ifdef DEBUG_MODE - // available: SSH_LOG_NOLOG, SSH_LOG_WARNING, SSH_LOG_PROTOCOL, SSH_LOG_PACKET, SSH_LOG_FUNCTIONS - err = ssh_set_log_level(SSH_LOG_FUNCTIONS); - if (err != SSH_OK) - { - printf("%s: ssh_set_log_level() Error setting log level: %d\n", __FUNCTION__, err); - return $R_CONT(c$cont, B_None); - } -#endif - - // should it auto-parse user config? for example from /home/user/.ssh/ - // err = ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, "0"); - // if (err != SSH_OK) { - // printf("%s: ssh_options_set() Error setting SSH option 'SSH_OPTIONS_PROCESS_CONFIG': %d\n", __FUNCTION__, err); - // return $R_CONT(c$cont, B_None); - // } - - err = ssh_connect(client->session); - if (err == SSH_ERROR) { - printf("%s: ssh_connect() Error connecting to SSH server: '%s' (%d)\n", __FUNCTION__, ssh_get_error(client->session), err); - goto err; - } - // connected, change state - client->state = S_AUTH; - - ssh_set_blocking(client->session, 0); - - // At this point ssh_get_fd should return a valid fd for uv_poll - client->fd = ssh_get_fd(client->session); - if (client->fd < 0) { - printf("Could not get SSH session fd\n"); - goto err; - } - - err = uv_poll_init(client->loop, client->poll, client->fd); - if (err < 0) { - printf("uv_poll_init failed: %s\n", uv_strerror(err)); - goto err; - } - - client->poll->data = client; + client_t *client = (client_t *)acton_malloc(sizeof(client_t)); + memset(client, 0, sizeof(client_t)); - /* Watch for read/write events; libssh manages what it needs depending on state */ - err = uv_poll_start(client->poll, UV_READABLE | UV_WRITABLE, poll_cb); - if (err < 0) { - printf("uv_poll_start failed: %s\n", uv_strerror(err)); - goto err; - } - - self->_client = toB_u64((unsigned long)client); - - $action f = ($action) self->on_connect; - f->$class->__asyn__(f, self); - - return $R_CONT(c$cont, B_None); -err: - if (client) { - if (client->poll) { - uv_close((uv_handle_t*)client->poll, NULL); - free(client->poll); - client->poll = NULL; - } - if (client->loop) { - uv_loop_close(client->loop); - free(client->loop); - client->loop = NULL; - } + if (client_setup(client, self) != 0) { + char errmsg[512]; if (client->session) { - ssh_disconnect(client->session); - ssh_free(client->session); - client->session = NULL; + snprintf(errmsg, sizeof(errmsg), "SSH connect failed: %s", ssh_get_error(client->session)); + } else { + snprintf(errmsg, sizeof(errmsg), "SSH initialization failed"); } - free(client); - client = NULL; + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str(errmsg)); + return $R_CONT(c$cont, B_None); } - return $R_CONT(c$cont, B_None); -} -$R sshQ_ClientD_get_affinityG_local (sshQ_Client self, $Cont c$cont) { - printf("sshQ_ClientD_get_affinityG_local, affinity=%ld\n", self->$affinity); + self->_client = toB_u64((unsigned long)client); return $R_CONT(c$cont, B_None); } -$R sshQ_ClientD_send_payloadG_local (sshQ_Client self, $Cont c$cont) { - int err = 0; +$R sshQ_ClientD_writeG_local(sshQ_Client self, $Cont c$cont, B_bytes data) { + client_t *client = (client_t *)fromB_u64(self->_client); + if (!client || !client->channel || client->state != S_READY) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("Cannot write: not connected")); + return $R_CONT(c$cont, B_None); + } - client_t *client = (client_t*)fromB_u64(self->_client); - if (client == NULL) { - printf("ERROR: client == NULL\n"); - goto err_out; + /* If there is already pending data, append to the write buffer */ + if (client->write_buf) { + size_t remaining = client->write_len - client->write_sent; + size_t new_len = remaining + data->nbytes; + char *new_buf = malloc(new_len); + if (!new_buf) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("Write buffer allocation failed")); + return $R_CONT(c$cont, B_None); + } + memcpy(new_buf, client->write_buf + client->write_sent, remaining); + memcpy(new_buf + remaining, data->str, data->nbytes); + free(client->write_buf); + client->write_buf = new_buf; + client->write_len = new_len; + client->write_sent = 0; + return $R_CONT(c$cont, B_None); } - client->payload = (const char *)fromB_str(self->_payload); + /* Try to write directly */ + int wrote = ssh_channel_write(client->channel, (const char *)data->str, (uint32_t)data->nbytes); + if (wrote == (int)data->nbytes) { + return $R_CONT(c$cont, B_None); + } -#ifdef DEBUG_MODE - printf("Starting libuv loop client\n"); -#endif - uv_run(client->loop, UV_RUN_DEFAULT); -#ifdef DEBUG_MODE - printf("Stopped libuv loop client\n"); -#endif + /* Partial or failed write — buffer the remainder for poll_cb to flush */ + size_t sent = (wrote > 0) ? (size_t)wrote : 0; + size_t remaining = data->nbytes - sent; + client->write_buf = malloc(remaining); + if (!client->write_buf) { + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str("Write buffer allocation failed")); + return $R_CONT(c$cont, B_None); + } + memcpy(client->write_buf, (const char *)data->str + sent, remaining); + client->write_len = remaining; + client->write_sent = 0; - return $R_CONT(c$cont, client->response ? to$str(client->response) : B_None); -err_out: return $R_CONT(c$cont, B_None); } -$R sshQ_ClientD_disconnectG_local (sshQ_Client self, $Cont c$cont) { - int err = 0; - - if (self == NULL || self->_client == NULL) { - printf("self->_client == NULL\n"); - goto out; +$R sshQ_ClientD_closeG_local(sshQ_Client self, $Cont c$cont, $action on_close) { + client_t *client = (client_t *)fromB_u64(self->_client); + if (!client) { + on_close->$class->__asyn__(on_close, self); + return $R_CONT(c$cont, B_None); } - client_t *client = (client_t*)fromB_u64(self->_client); - if (client == NULL) { - printf("client == NULL, nothing to cleanup\n"); - goto out; + /* Stop polling */ + if (client->poll) { + uv_poll_stop(client->poll); + uv_close((uv_handle_t *)client->poll, NULL); + client->poll = NULL; } - /* cleanup */ - /* graceful close of ssh channel & session */ + /* Close SSH channel */ if (client->channel) { ssh_channel_send_eof(client->channel); ssh_channel_close(client->channel); ssh_channel_free(client->channel); client->channel = NULL; } + + /* Disconnect SSH session */ if (client->session) { ssh_disconnect(client->session); ssh_free(client->session); + client->session = NULL; } - if (client->loop) { - // see MAKE_VALGRIND_HAPPY in libuv/test/task.h + /* Free write buffer */ + if (client->write_buf) { + free(client->write_buf); + client->write_buf = NULL; + } - // walk the loop to close any remaining handles - uv_walk(client->loop, close_walk_cb, NULL); + self->_client = toB_u64(0); - // run the loop one more time to let close callbacks execute - // TODO: if this is executed after a previous c.disconnect() call in ssh.act - // then it will crash here. no matter if it's the same client object or a new one - uv_run(client->loop, UV_RUN_DEFAULT); + /* Invoke on_close callback */ + on_close->$class->__asyn__(on_close, self); - // now it's safe to close the loop - err = uv_loop_close(client->loop); - if (err != 0) { - printf("WARNING: Loop close failed: %s\n", uv_strerror(err)); - // If we still have handles, print debug info - if (err == UV_EBUSY) { - printf("There are still active handles in the loop. This is a leak.\n"); - } - } - free(client->loop); - uv_library_shutdown(); - } + return $R_CONT(c$cont, B_None); +} - if (client->reply) { - free(client->reply); - client->reply = NULL; - } - if (client->response) { - free(client->response); - client->response = NULL; - } - if (client->poll) { - free(client->poll); - client->poll = NULL; - } - if (client) { - free(client); - client = NULL; - } +$R sshQ_ClientD__connectG_local(sshQ_Client self, $Cont c$cont, sshQ_Client c) { + pin_actor_affinity(); + + client_t *client = (client_t *)acton_malloc(sizeof(client_t)); + memset(client, 0, sizeof(client_t)); - if (ssh_finalize()) { - printf("%s: ssh_finalize error", __FUNCTION__); + if (client_setup(client, self) != 0) { + char errmsg[512]; + if (client->session) { + snprintf(errmsg, sizeof(errmsg), "SSH reconnect failed: %s", ssh_get_error(client->session)); + } else { + snprintf(errmsg, sizeof(errmsg), "SSH reconnect initialization failed"); + } + $action2 f = ($action2)self->on_error; + f->$class->__asyn__(f, self, to$str(errmsg)); + return $R_CONT(c$cont, B_None); } -out: + self->_client = toB_u64((unsigned long)client); return $R_CONT(c$cont, B_None); } From e7dc70439ad91c8bf4c4c39af746b54f5318bf6a Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Wed, 1 Apr 2026 09:20:05 +0200 Subject: [PATCH 36/37] removed unused build.act.json Signed-off-by: Antonio Prcela --- build.act.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 build.act.json diff --git a/build.act.json b/build.act.json deleted file mode 100644 index ec3c9b7..0000000 --- a/build.act.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "dependencies": {}, - "zig_dependencies": { - "libssh": { - "url": "https://github.com/actonlang/libssh/archive/refs/heads/zig-build.tar.gz", - "hash": "122076b4676ec7d777e5efc24ae4b7f1ab4fa3df407ab9649fdd55f9fc594ca053ec", - "artifacts": [ - "ssh" - ] - } - } -} From 52c8da0e8c967c6e571033ad64da069eeaa86981 Mon Sep 17 00:00:00 2001 From: Antonio Prcela Date: Wed, 1 Apr 2026 09:20:21 +0200 Subject: [PATCH 37/37] updated hash to latest libssh Signed-off-by: Antonio Prcela --- Build.act | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.act b/Build.act index 21ee62f..c202459 100644 --- a/Build.act +++ b/Build.act @@ -3,7 +3,7 @@ fingerprint = 0xb650ac2857701cb1 zig_dependencies = { "libssh": ( url="https://github.com/precla/libssh/archive/refs/heads/zig-build.tar.gz", - hash="libssh-0.11.0-kUQJanViKgBVUmjYqAyLKjnqqHdDvSkdnU1qaMwFPzV-", + hash="libssh-0.11.0-kUQJaoJiKgDjZpTAyxxo6GamiMzj9LPxlEr2FacwOxIR", artifacts=[ "ssh" ] ) }