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 diff --git a/src/ssh.act b/src/ssh.act old mode 100644 new mode 100755 index a8b0268..3b8e6b4 --- a/src/ssh.act +++ b/src/ssh.act @@ -1,12 +1,10 @@ import net -import testing + def version() -> str: - """Get the libssh version""" + """Get the acton-ssh version""" NotImplemented -def _test_version(): - testing.assertEqual("0.11.0", version()) actor Client(cap: net.TCPConnectCap, host: str, @@ -14,19 +12,34 @@ 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, + subsystem: str ): """SSH Client""" + 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 + + 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""" @@ -37,7 +50,11 @@ 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 @@ -45,18 +62,46 @@ 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): + """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""" + NotImplemented + _init() + # print("SSH Channel created") + + def setPayload(p: str): + payload = p + + def getPayload(): + return payload + + def sendNCPayload() -> str: + """Send payload""" + NotImplemented actor main(env): def on_connect(client: Client): - print("Connected") + # 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", @@ -64,6 +109,22 @@ actor main(env): on_connect, on_close, password="bar", - port=2223, + port=830, + 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") + + # 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() + env.exit(0) diff --git a/src/ssh.ext.c b/src/ssh.ext.c index 2112598..feeffcf 100644 --- a/src/ssh.ext.c +++ b/src/ssh.ext.c @@ -1,6 +1,52 @@ +#include #include -#include -// TODO: figure out how to include rts/log so we get access to log_error etc +#include +#include +#include +// acton includes +#include + +#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 + +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); + } + 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) { } @@ -11,110 +57,308 @@ 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(); - printf("SSH extension initialized %d\n", r); + 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 () { - if (LIBSSH_VERSION_MINOR != 11) - return to$str("invalid"); - return to$str("0.11.0"); +B_str sshQ_version() { + return to$str("0.1.0"); } -// TODO: crap function for test, to be replaced with something -int show_remote_processes(ssh_session session) +$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 + * @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) { - ssh_channel channel; - int rc; - char buffer[256]; - int nbytes; + int err = 0; + int nbytes = 0; + int len = 0; + size_t buflen = 0; + char tmp[1024] = {0}; - channel = ssh_channel_new(session); - if (channel == NULL) - return 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__, err); + goto error; + } - rc = ssh_channel_open_session(channel); - if (rc != SSH_OK) - { - ssh_channel_free(channel); - return rc; - } + 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'; + } - rc = ssh_channel_request_exec(channel, "ps aux"); - if (rc != SSH_OK) - { - ssh_channel_close(channel); - ssh_channel_free(channel); - return rc; - } + 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); + } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); - while (nbytes > 0) - { - if (write(1, buffer, nbytes) != (unsigned int) nbytes) + if (nbytes < 0) { - ssh_channel_close(channel); - ssh_channel_free(channel); - return SSH_ERROR; + printf("%s: ssh_channel_read() error (%d)\n", __FUNCTION__, errno); + goto error; } - nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0); - } - if (nbytes < 0) - { - ssh_channel_close(channel); - ssh_channel_free(channel); + 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; - ssh_channel_send_eof(channel); - ssh_channel_close(channel); - ssh_channel_free(channel); + 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; + 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) { - //log_error("Failed to create SSH session"); + if (session == NULL) + { + printf("%s: ssh_new() Failed to create SSH session\n", __FUNCTION__); 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); - - 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"); - int rc = ssh_connect(session); - if (rc != SSH_OK) { - //log_error("Error connecting to SSH server: %s", ssh_get_error(session)); - $action2 f = ($action2) self->on_close; - f->$class->__asyn__(f, self, to$str(ssh_get_error(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_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) + { + 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 != 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 != 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); + + 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); +} + +// Channel + +$R sshQ_ChannelD__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); + } + } + + // 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); } - rc = ssh_userauth_password(session, NULL, fromB_str(self->password)); - if (rc == SSH_OK) { - printf("Connected\n"); - show_remote_processes(session); - } else { - printf("Error: %s\n", ssh_get_error(session)); + 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); } -// self->_connected = true; -// $action f = ($action) self->on_connect; -// f->$class->__asyn__(f, self); + 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 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), 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(response)); +}