From 52c466d8f38d9eb6a24c8e0a208d605addef70df Mon Sep 17 00:00:00 2001 From: Jianhui Zhao Date: Mon, 20 Nov 2023 22:50:00 +0800 Subject: [PATCH] feat: ssh: Add new module `ssh` via bind `libssh2` Signed-off-by: Jianhui Zhao --- CMakeLists.txt | 22 ++ README.md | 1 + README_ZH.md | 1 + examples/ssh/exec.lua | 29 ++ examples/ssh/scp_recv.lua | 34 +++ examples/ssh/scp_send.lua | 30 ++ ssh.c | 618 ++++++++++++++++++++++++++++++++++++++ ssh.lua | 477 +++++++++++++++++++++++++++++ 8 files changed, 1212 insertions(+) create mode 100755 examples/ssh/exec.lua create mode 100755 examples/ssh/scp_recv.lua create mode 100755 examples/ssh/scp_send.lua create mode 100644 ssh.c create mode 100644 ssh.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 044188b..1e03b8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ set(LUA_INSTALL_PREFIX lib/lua/5.3) option(ECO_SSL_SUPPORT "ssl" ON) option(ECO_UBUS_SUPPORT "ubus" ON) option(ECO_MQTT_SUPPORT "mqtt" ON) +option(ECO_SSH_SUPPORT "ssh" ON) add_executable(eco eco.c) target_link_libraries(eco PRIVATE ${LIBEV_LIBRARY} ${LUA53_LIBRARIES}) @@ -151,6 +152,27 @@ if (ECO_MQTT_SUPPORT) endif() endif() +if (ECO_SSH_SUPPORT) + pkg_search_module(LIBSSH2 libssh2) + if (LIBSSH2_FOUND) + add_library(ssh MODULE ssh.c) + target_link_libraries(ssh PRIVATE ${LIBSSH2_LIBRARIES}) + set_target_properties(ssh PROPERTIES OUTPUT_NAME ssh PREFIX "") + + install( + TARGETS ssh + DESTINATION ${LUA_INSTALL_PREFIX}/eco/core + ) + + install( + FILES ssh.lua + DESTINATION ${LUA_INSTALL_PREFIX}/eco + ) + else() + message(WARNING "Not found libssh2. Skip build eco.ssh") + endif() +endif() + install( TARGETS eco DESTINATION bin diff --git a/README.md b/README.md index 9b1f8ac..fca91f0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Lua-eco also provides some modules for you to build applications quickly: * `netlink`: Provides operations for inter-process communication (IPC) between both the kernel and userspace processes. * `nl80211`: Show/manipulate wireless devices and their configuration. * `termios`: Bind unix API for terminal/serial I/O. +* `ssh`: Bind libssh2. Would you like to try it? Kinda interesting. diff --git a/README_ZH.md b/README_ZH.md index 5c9a2e8..d794705 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -44,6 +44,7 @@ Lua-eco 还提供了一些有用的模块,方便您快速构建应用程序: * `netlink`: 为内核和用户空间进程之间的进程间通信(IPC)提供操作。 * `nl80211`: 显示/操作无线设备及其配置。 * `termios`: 绑定 unix 接口用于操作终端和串口。 +* `ssh`: 绑定 libssh2. 想试试吗?很有趣的! diff --git a/examples/ssh/exec.lua b/examples/ssh/exec.lua new file mode 100755 index 0000000..ada6928 --- /dev/null +++ b/examples/ssh/exec.lua @@ -0,0 +1,29 @@ +#!/usr/bin/env eco + +local ssh = require 'eco.ssh' + +local ipaddr = '127.0.0.1' +local port = 22 +local username = 'root' +local password = '1' + +local session, err = ssh.new(ipaddr, port, username, password) +if not session then + print('new session fail:', err) + return +end + +local data, exit_code, exit_signal = session:exec('uptime') +if not data then + print('exec fail:', exit_code) + return +end + +if exit_signal then + print('Got signal:', exit_signal) +else + print('Exit:', exit_code) + print('Output:', data) +end + +session:free() diff --git a/examples/ssh/scp_recv.lua b/examples/ssh/scp_recv.lua new file mode 100755 index 0000000..f38d9af --- /dev/null +++ b/examples/ssh/scp_recv.lua @@ -0,0 +1,34 @@ +#!/usr/bin/env eco + +local ssh = require 'eco.ssh' + +local ipaddr = '127.0.0.1' +local port = 22 +local username = 'root' +local password = '1' + +local session, err = ssh.new(ipaddr, port, username, password) +if not session then + print('new session fail:', err) + return +end + +-- receive as a string returned +local data, err = session:scp_recv('/etc/os-release') +if not data then + print('recv fail:', err) + return +end + +print(data) + +-- receive to a local file +local n, err = session:scp_recv('/etc/os-release', '/tmp/os-release') +if not n then + print('recv fail:', err) + return +end + +print('Got', n, 'bytes, stored into "/tmp/os-release"') + +session:free() diff --git a/examples/ssh/scp_send.lua b/examples/ssh/scp_send.lua new file mode 100755 index 0000000..efb7cb5 --- /dev/null +++ b/examples/ssh/scp_send.lua @@ -0,0 +1,30 @@ +#!/usr/bin/env eco + +local ssh = require 'eco.ssh' + +local ipaddr = '127.0.0.1' +local port = 22 +local username = 'root' +local password = '1' + +local session, err = ssh.new(ipaddr, port, username, password) +if not session then + print('new session fail:', err) + return +end + +-- send a string +local ok, err = session:scp_send('12345\n', '/tmp/test1') +if not ok then + print('send fail:', err) + return +end + +-- send a file +local ok, err = session:scp_sendfile('test', '/tmp/test2') +if not ok then + print('send fail:', err) + return +end + +session:free() diff --git a/ssh.c b/ssh.c new file mode 100644 index 0000000..4281dea --- /dev/null +++ b/ssh.c @@ -0,0 +1,618 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Author: Jianhui Zhao + */ + +#include + +#include "eco.h" + +#define ECO_SSH_SESSION_MT "eco{ssh.session}" +#define ECO_SSH_CHANNEL_MT "eco{ssh.channel}" + +struct eco_ssh_session { + LIBSSH2_SESSION *session; +}; + +struct eco_ssh_channel { + LIBSSH2_SESSION *session; + LIBSSH2_CHANNEL *channel; +}; + +static int lua_ssh_session_new(lua_State *L) +{ + struct eco_ssh_session *session = lua_newuserdata(L, sizeof(struct eco_ssh_session)); + + lua_pushvalue(L, lua_upvalueindex(1)); + lua_setmetatable(L, -2); + + libssh2_init(0); + + session->session = libssh2_session_init(); + + libssh2_session_set_blocking(session->session, 0); + + return 1; +} + +static int lua_ssh_session_block_directions(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + int dir; + + if (!session->session) + return luaL_error(L, "session freed"); + + dir = libssh2_session_block_directions(session->session); + + lua_pushinteger(L, dir); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_session_handshake(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + int sock = luaL_checkinteger(L, 2); + int rc; + + if (!session->session) + return luaL_error(L, "session freed"); + + rc = libssh2_session_handshake(session->session, sock); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_session_userauth_password(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + const char *username = luaL_checkstring(L, 2); + const char *password = luaL_checkstring(L, 3); + int rc; + + if (!session->session) + return luaL_error(L, "session freed"); + + rc = libssh2_userauth_password(session->session, username, password); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_exec(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + const char *cmd = luaL_checkstring(L, 2); + int rc; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + rc = libssh2_channel_exec(channel->channel, cmd); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns a string, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_read(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int stream_id = luaL_checkinteger(L, 2); + char buffer[4096]; + ssize_t nread; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + nread = libssh2_channel_read_ex(channel->channel, stream_id, buffer, sizeof(buffer)); + if (nread < 0) { + lua_pushnil(L); + lua_pushinteger(L, nread); + return 2; + } + + lua_pushlstring(L, buffer, nread); + + return 1; +} + +/* In case of success, it returns a number indicates writen, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_write(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + size_t len; + const char *data = luaL_checklstring(L, 2, &len); + ssize_t nwritten; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + nwritten = libssh2_channel_write(channel->channel, data, len); + if (nwritten < 0) { + lua_pushnil(L); + lua_pushinteger(L, nwritten); + return 2; + } + + lua_pushinteger(L, nwritten); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_send_eof(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + rc = libssh2_channel_send_eof(channel->channel); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_wait_eof(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + rc = libssh2_channel_wait_eof(channel->channel); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_wait_closed(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + rc = libssh2_channel_wait_closed(channel->channel); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_close(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + rc = libssh2_channel_close(channel->channel); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +static int lua_ssh_channel_get_exit_status(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int exitcode; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + exitcode = libssh2_channel_get_exit_status(channel->channel); + + lua_pushinteger(L, exitcode); + + return 1; +} + +static int lua_ssh_channel_get_exit_signal(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + char *exitsignal; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + + libssh2_channel_get_exit_signal(channel->channel, &exitsignal, NULL, NULL, NULL, NULL, NULL); + + if (exitsignal) + lua_pushstring(L, exitsignal); + else + lua_pushnil(L); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_signal(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc = 0; + + if (!channel->channel) + return luaL_error(L, "channel freed"); + +#ifdef libssh2_channel_signal + rc = libssh2_channel_signal(channel->channel, luaL_checkstring(L, 2)); +#endif + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + lua_pushboolean(L, true); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_channel_free(lua_State *L) +{ + struct eco_ssh_channel *channel = luaL_checkudata(L, 1, ECO_SSH_CHANNEL_MT); + int rc; + + if (!channel->channel) + return 0; + + rc = libssh2_channel_free(channel->channel); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + channel->channel = NULL; + + lua_pushboolean(L, true); + + return 1; +} + +static const luaL_Reg channel_methods[] = { + {"exec", lua_ssh_channel_exec}, + {"read", lua_ssh_channel_read}, + {"write", lua_ssh_channel_write}, + {"send_eof", lua_ssh_channel_send_eof}, + {"wait_eof", lua_ssh_channel_wait_eof}, + {"wait_closed", lua_ssh_channel_wait_closed}, + {"close", lua_ssh_channel_close}, + {"get_exit_status", lua_ssh_channel_get_exit_status}, + {"get_exit_signal", lua_ssh_channel_get_exit_signal}, + {"signal", lua_ssh_channel_signal}, + {"free", lua_ssh_channel_free}, + {NULL, NULL} +}; + +/* In case of success, it returns a userdata, in case of error,it returns nil with an error string */ +static int lua_ssh_session_open_channel(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + struct eco_ssh_channel *lchannel; + LIBSSH2_CHANNEL *channel; + + if (!session->session) + return luaL_error(L, "session freed"); + + channel = libssh2_channel_open_session(session->session); + if (!channel) { + char *err_msg; + + libssh2_session_last_error(session->session, &err_msg, NULL, 0); + lua_pushnil(L); + lua_pushstring(L, err_msg); + + return 2; + } + + lchannel = lua_newuserdata(L, sizeof(struct eco_ssh_channel)); + eco_new_metatable(L, ECO_SSH_CHANNEL_MT, channel_methods); + lua_setmetatable(L, -2); + + lchannel->session = session->session; + lchannel->channel = channel; + + return 1; +} + +/* In case of success, it returns a userdata, in case of error,it returns nil with an error string */ +static int lua_ssh_session_scp_recv(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + const char *path = luaL_checkstring(L, 2); + libssh2_struct_stat fileinfo; + struct eco_ssh_channel *lchannel; + LIBSSH2_CHANNEL *channel; + + if (!session->session) + return luaL_error(L, "session freed"); + + channel = libssh2_scp_recv2(session->session, path, &fileinfo); + if (!channel) { + char *err_msg; + + libssh2_session_last_error(session->session, &err_msg, NULL, 0); + lua_pushnil(L); + lua_pushstring(L, err_msg); + + return 2; + } + + lchannel = lua_newuserdata(L, sizeof(struct eco_ssh_channel)); + eco_new_metatable(L, ECO_SSH_CHANNEL_MT, channel_methods); + lua_setmetatable(L, -2); + + lchannel->session = session->session; + lchannel->channel = channel; + + lua_pushinteger(L, fileinfo.st_size); + + return 2; +} + +/* In case of success, it returns a userdata, in case of error,it returns nil with an error string */ +static int lua_ssh_session_scp_send(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + const char *path = luaL_checkstring(L, 2); + int mode = luaL_checkinteger(L, 3); + size_t size = luaL_checkinteger(L, 4); + struct eco_ssh_channel *lchannel; + LIBSSH2_CHANNEL *channel; + + if (!session->session) + return luaL_error(L, "session freed"); + + channel = libssh2_scp_send(session->session, path, mode & 0777, size); + if (!channel) { + char *err_msg; + + libssh2_session_last_error(session->session, &err_msg, NULL, 0); + lua_pushnil(L); + lua_pushstring(L, err_msg); + + return 2; + } + + lchannel = lua_newuserdata(L, sizeof(struct eco_ssh_channel)); + eco_new_metatable(L, ECO_SSH_CHANNEL_MT, channel_methods); + lua_setmetatable(L, -2); + + lchannel->session = session->session; + lchannel->channel = channel; + + return 1; +} + +static int lua_ssh_session_last_error(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + char *err_msg = ""; + + if (session->session) + libssh2_session_last_error(session->session, &err_msg, NULL, 0); + + lua_pushstring(L, err_msg); + + return 1; +} + +static int lua_ssh_session_last_errno(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + int err_code = 0; + + if (session->session) + err_code = libssh2_session_last_errno(session->session); + + lua_pushinteger(L, err_code); + + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_session_disconnect(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + int reason = luaL_checkinteger(L, 2); + const char *description = luaL_checkstring(L, 3); + int rc; + + if (!session->session) + goto done; + + rc = libssh2_session_disconnect_ex(session->session, reason, description, ""); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + +done: + lua_pushboolean(L, true); + return 1; +} + +/* In case of success, it returns true, in case of error,it returns nil with an error code */ +static int lua_ssh_session_free(lua_State *L) +{ + struct eco_ssh_session *session = luaL_checkudata(L, 1, ECO_SSH_SESSION_MT); + int rc; + + if (!session->session) + goto done; + + rc = libssh2_session_free(session->session); + if (rc) { + lua_pushnil(L); + lua_pushinteger(L, rc); + return 2; + } + + session->session = NULL; + +done: + lua_pushboolean(L, true); + return 1; +} + +static const luaL_Reg session_methods[] = { + {"block_directions", lua_ssh_session_block_directions}, + {"handshake", lua_ssh_session_handshake}, + {"userauth_password", lua_ssh_session_userauth_password}, + {"open_channel", lua_ssh_session_open_channel}, + {"scp_recv", lua_ssh_session_scp_recv}, + {"scp_send", lua_ssh_session_scp_send}, + {"last_error", lua_ssh_session_last_error}, + {"last_errno", lua_ssh_session_last_errno}, + {"disconnect", lua_ssh_session_disconnect}, + {"free", lua_ssh_session_free}, + {NULL, NULL} +}; + +int luaopen_eco_core_ssh(lua_State *L) +{ + lua_newtable(L); + + lua_add_constant(L, "ERROR_NONE", LIBSSH2_ERROR_NONE); + lua_add_constant(L, "ERROR_SOCKET_NONE", LIBSSH2_ERROR_SOCKET_NONE); + lua_add_constant(L, "ERROR_BANNER_RECV", LIBSSH2_ERROR_BANNER_RECV); + lua_add_constant(L, "ERROR_BANNER_SEND", LIBSSH2_ERROR_BANNER_SEND); + lua_add_constant(L, "ERROR_INVALID_MAC", LIBSSH2_ERROR_INVALID_MAC); + lua_add_constant(L, "ERROR_KEX_FAILURE", LIBSSH2_ERROR_KEX_FAILURE); + lua_add_constant(L, "ERROR_ALLOC", LIBSSH2_ERROR_ALLOC); + lua_add_constant(L, "ERROR_SOCKET_SEND", LIBSSH2_ERROR_SOCKET_SEND); + lua_add_constant(L, "ERROR_KEY_EXCHANGE_FAILURE", LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE); + lua_add_constant(L, "ERROR_TIMEOUT", LIBSSH2_ERROR_TIMEOUT); + lua_add_constant(L, "ERROR_HOSTKEY_INIT", LIBSSH2_ERROR_HOSTKEY_INIT); + lua_add_constant(L, "ERROR_HOSTKEY_SIGN", LIBSSH2_ERROR_HOSTKEY_SIGN); + lua_add_constant(L, "ERROR_DECRYPT", LIBSSH2_ERROR_DECRYPT); + lua_add_constant(L, "ERROR_SOCKET_DISCONNECT", LIBSSH2_ERROR_SOCKET_DISCONNECT); + lua_add_constant(L, "ERROR_PROTO", LIBSSH2_ERROR_PROTO); + lua_add_constant(L, "ERROR_PASSWORD_EXPIRED", LIBSSH2_ERROR_PASSWORD_EXPIRED); + lua_add_constant(L, "ERROR_FILE", LIBSSH2_ERROR_FILE); + lua_add_constant(L, "ERROR_METHOD_NONE", LIBSSH2_ERROR_METHOD_NONE); + lua_add_constant(L, "ERROR_AUTHENTICATION_FAILED", LIBSSH2_ERROR_AUTHENTICATION_FAILED); + lua_add_constant(L, "ERROR_PUBLICKEY_UNRECOGNIZED", LIBSSH2_ERROR_PUBLICKEY_UNRECOGNIZED); + lua_add_constant(L, "ERROR_PUBLICKEY_UNVERIFIED", LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED); + lua_add_constant(L, "ERROR_CHANNEL_OUTOFORDER", LIBSSH2_ERROR_CHANNEL_OUTOFORDER); + lua_add_constant(L, "ERROR_CHANNEL_FAILURE", LIBSSH2_ERROR_CHANNEL_FAILURE); + lua_add_constant(L, "ERROR_CHANNEL_REQUEST_DENIED", LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED); + lua_add_constant(L, "ERROR_CHANNEL_UNKNOWN", LIBSSH2_ERROR_CHANNEL_UNKNOWN); + lua_add_constant(L, "ERROR_CHANNEL_WINDOW_EXCEEDED", LIBSSH2_ERROR_CHANNEL_WINDOW_EXCEEDED); + lua_add_constant(L, "ERROR_CHANNEL_PACKET_EXCEEDED", LIBSSH2_ERROR_CHANNEL_PACKET_EXCEEDED); + lua_add_constant(L, "ERROR_CHANNEL_CLOSED", LIBSSH2_ERROR_CHANNEL_CLOSED); + lua_add_constant(L, "ERROR_CHANNEL_EOF_SENT", LIBSSH2_ERROR_CHANNEL_EOF_SENT); + lua_add_constant(L, "ERROR_SCP_PROTOCOL", LIBSSH2_ERROR_SCP_PROTOCOL); + lua_add_constant(L, "ERROR_ZLIB", LIBSSH2_ERROR_ZLIB); + lua_add_constant(L, "ERROR_SOCKET_TIMEOUT", LIBSSH2_ERROR_SOCKET_TIMEOUT); + lua_add_constant(L, "ERROR_SFTP_PROTOCOL", LIBSSH2_ERROR_SFTP_PROTOCOL); + lua_add_constant(L, "ERROR_REQUEST_DENIED", LIBSSH2_ERROR_REQUEST_DENIED); + lua_add_constant(L, "ERROR_METHOD_NOT_SUPPORTED", LIBSSH2_ERROR_METHOD_NOT_SUPPORTED); + lua_add_constant(L, "ERROR_INVAL", LIBSSH2_ERROR_INVAL); + lua_add_constant(L, "ERROR_INVALID_POLL_TYPE", LIBSSH2_ERROR_INVALID_POLL_TYPE); + lua_add_constant(L, "ERROR_PUBLICKEY_PROTOCOL", LIBSSH2_ERROR_PUBLICKEY_PROTOCOL); + lua_add_constant(L, "ERROR_EAGAIN", LIBSSH2_ERROR_EAGAIN); + lua_add_constant(L, "ERROR_BUFFER_TOO_SMALL", LIBSSH2_ERROR_BUFFER_TOO_SMALL); + lua_add_constant(L, "ERROR_BAD_USE", LIBSSH2_ERROR_BAD_USE); + lua_add_constant(L, "ERROR_COMPRESS", LIBSSH2_ERROR_COMPRESS); + lua_add_constant(L, "ERROR_OUT_OF_BOUNDARY", LIBSSH2_ERROR_OUT_OF_BOUNDARY); + lua_add_constant(L, "ERROR_AGENT_PROTOCOL", LIBSSH2_ERROR_AGENT_PROTOCOL); + lua_add_constant(L, "ERROR_SOCKET_RECV", LIBSSH2_ERROR_SOCKET_RECV); + lua_add_constant(L, "ERROR_ENCRYPT", LIBSSH2_ERROR_ENCRYPT); + lua_add_constant(L, "ERROR_BAD_SOCKET", LIBSSH2_ERROR_BAD_SOCKET); + lua_add_constant(L, "ERROR_KNOWN_HOSTS", LIBSSH2_ERROR_KNOWN_HOSTS); + lua_add_constant(L, "ERROR_CHANNEL_WINDOW_FULL", LIBSSH2_ERROR_CHANNEL_WINDOW_FULL); + lua_add_constant(L, "ERROR_KEYFILE_AUTH_FAILED", LIBSSH2_ERROR_KEYFILE_AUTH_FAILED); + lua_add_constant(L, "ERROR_RANDGEN", LIBSSH2_ERROR_RANDGEN); +#ifdef LIBSSH2_ERROR_MISSING_USERAUTH_BANNER + lua_add_constant(L, "ERROR_MISSING_USERAUTH_BANNER", LIBSSH2_ERROR_MISSING_USERAUTH_BANNER); +#endif +#ifdef LIBSSH2_ERROR_ALGO_UNSUPPORTED + lua_add_constant(L, "ERROR_ALGO_UNSUPPORTED", LIBSSH2_ERROR_ALGO_UNSUPPORTED); +#endif + + lua_add_constant(L, "EXTENDED_DATA_STDERR", SSH_EXTENDED_DATA_STDERR); + + lua_add_constant(L, "SESSION_BLOCK_INBOUND", LIBSSH2_SESSION_BLOCK_INBOUND); + lua_add_constant(L, "SESSION_BLOCK_OUTBOUND", LIBSSH2_SESSION_BLOCK_OUTBOUND); + + lua_add_constant(L, "DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT", SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT); + lua_add_constant(L, "DISCONNECT_PROTOCOL_ERROR", SSH_DISCONNECT_PROTOCOL_ERROR); + lua_add_constant(L, "DISCONNECT_KEY_EXCHANGE_FAILED", SSH_DISCONNECT_KEY_EXCHANGE_FAILED); + lua_add_constant(L, "DISCONNECT_RESERVED", SSH_DISCONNECT_RESERVED); + lua_add_constant(L, "DISCONNECT_MAC_ERROR", SSH_DISCONNECT_MAC_ERROR); + lua_add_constant(L, "DISCONNECT_COMPRESSION_ERROR", SSH_DISCONNECT_COMPRESSION_ERROR); + lua_add_constant(L, "DISCONNECT_SERVICE_NOT_AVAILABLE", SSH_DISCONNECT_SERVICE_NOT_AVAILABLE); + lua_add_constant(L, "DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED", SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED); + lua_add_constant(L, "DISCONNECT_HOST_KEY_NOT_VERIFIABLE", SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); + lua_add_constant(L, "DISCONNECT_CONNECTION_LOST", SSH_DISCONNECT_CONNECTION_LOST); + lua_add_constant(L, "DISCONNECT_BY_APPLICATION", SSH_DISCONNECT_BY_APPLICATION); + lua_add_constant(L, "DISCONNECT_TOO_MANY_CONNECTIONS", SSH_DISCONNECT_TOO_MANY_CONNECTIONS); + lua_add_constant(L, "DISCONNECT_AUTH_CANCELLED_BY_USER", SSH_DISCONNECT_AUTH_CANCELLED_BY_USER); + lua_add_constant(L, "DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE", SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE); + lua_add_constant(L, "DISCONNECT_ILLEGAL_USER_NAME", SSH_DISCONNECT_ILLEGAL_USER_NAME); + + eco_new_metatable(L, ECO_SSH_SESSION_MT, session_methods); + lua_pushcclosure(L, lua_ssh_session_new, 1); + lua_setfield(L, -2, "new"); + + return 1; +} diff --git a/ssh.lua b/ssh.lua new file mode 100644 index 0000000..2533f44 --- /dev/null +++ b/ssh.lua @@ -0,0 +1,477 @@ +-- SPDX-License-Identifier: MIT +-- Author: Jianhui Zhao + +local socket = require 'eco.socket' +local ssh = require 'eco.core.ssh' +local file = require 'eco.file' +local time = require 'eco.time' +local sys = require 'eco.sys' + +local M = {} + +local function waitsocket(session, timeout) + local dir = session.session:block_directions() + local ev = 0 + + if dir & ssh.SESSION_BLOCK_INBOUND then + ev = ev | eco.READ + end + + if dir & ssh.SESSION_BLOCK_OUTBOUND then + ev = ev | eco.WRITE + end + + session.iow:modify(ev) + + return session.iow:wait(timeout) +end + +local function open_channel(session) + local channel, err + + while true do + channel, err = session.session:open_channel() + if channel then break end + + if session.session:last_errno() ~= ssh.ERROR_EAGAIN then + return nil, err + end + + if not waitsocket(session, 5.0) then + return nil, 'timeout' + end + end + + return channel +end + +local function channel_scp_recv(session, source) + local channel, size + + while true do + channel, size = session.session:scp_recv(source) + if channel then break end + + if session.session:last_errno() ~= ssh.ERROR_EAGAIN then + return nil, size + end + + if not waitsocket(session, 5.0) then + return nil, 'timeout' + end + end + + return channel, size +end + +local function channel_scp_send(session, dest, mode, size) + local channel, err + + while true do + channel, err = session.session:scp_send(dest, mode, size) + if channel then break end + + if session.session:last_errno() ~= ssh.ERROR_EAGAIN then + return nil, err + end + + if not waitsocket(session, 5.0) then + return nil, 'timeout' + end + end + + return channel +end + +local function channel_call_c_wrap(session, channel, timeout, func, ...) + timeout = timeout or 5.0 + + local deadtime = sys.uptime() + timeout + + while sys.uptime() < deadtime do + local ok, err = channel[func](channel, ...) + if ok then return true end + + if err ~= ssh.ERROR_EAGAIN then + return nil, session.session:last_error() + end + + if not waitsocket(session, deadtime - sys.uptime()) then + return nil, 'timeout' + end + end + + return nil, 'timeout' +end + +local function channel_exec(session, channel, cmd) + return channel_call_c_wrap(session, channel, nil, 'exec', cmd) +end + +local function channel_close(session, channel) + return channel_call_c_wrap(session, channel, nil, 'close') +end + +local function channel_free(session, channel) + return channel_call_c_wrap(session, channel, nil, 'free') +end + +local function channel_send_eof(session, channel) + return channel_call_c_wrap(session, channel, nil, 'send_eof') +end + +local function channel_wait_eof(session, channel) + return channel_call_c_wrap(session, channel, nil, 'wait_eof') +end + +local function channel_wait_closed(session, channel) + return channel_call_c_wrap(session, channel, nil, 'wait_closed') +end + +local function channel_signal(session, channel, signame) + return channel_call_c_wrap(session, channel, nil, 'signal', signame) +end + +local function channel_exec_read_data(session, channel, stream_id, data, timeout) + timeout = timeout or 30.0 + + local deadtime = sys.uptime() + timeout + + while sys.uptime() < deadtime do + local chunk, err = channel:read(stream_id) + if not chunk then + if err ~= ssh.ERROR_EAGAIN then + return nil, session.session:last_error() + end + + if not waitsocket(session, deadtime - sys.uptime()) then + return nil, 'timeout' + end + else + if #chunk == 0 then return true end + + data[#data + 1] = chunk + + -- Avoid blocking for too long while receive too much + time.sleep(0.0001) + end + end + + return nil, 'timeout' +end + +local session_methods = {} + +function session_methods:exec(cmd, timeout) + local channel, err = open_channel(self) + if not channel then + return nil, err + end + + local ok, err = channel_exec(self, channel, cmd) + if not ok then + channel_free(self, channel) + return nil, err + end + + local data = {} + + ok, err = channel_exec_read_data(self, channel, 0, data, timeout) + if not ok then + channel_signal(self, channel, 'KILL') + channel_free(self, channel) + return nil, err + end + + channel_exec_read_data(self, channel, ssh.EXTENDED_DATA_STDERR, data, timeout) + + local ok, err = channel_close(self, channel) + if not ok then + channel_free(self, channel) + return nil, err + end + + local exitcode = channel:get_exit_status() + local exitsignal = channel:get_exit_signal() + + channel_free(self, channel) + + local signals = { HUP = 1, INT = 2, QUIT = 3, ILL = 4, TRAP = 5, ABRT = 6, + BUS = 7, FPE = 8, KILL = 9, USR1 = 10, SEGV = 11, USR2 = 12, PIPE = 13, + ALRM = 14, TERM = 15, STKFLT = 16, CHLD = 17,CONT = 18, STOP = 19, + TSTP = 20, TTIN = 21, TTOU = 22, URG = 23, XCPU = 24, XFSZ = 25, + VTALRM = 26, PROF = 27, WINCH = 28, POLL = 29, PWR = 30, SYS = 31 + } + + if exitsignal then + exitcode = 128 + signals[exitsignal] + end + + return table.concat(data), exitcode, exitsignal +end + +local function scp_recv(session, channel, size, f, data) + local got = 0 + + while got < size do + local chunk, err = channel:read(0) + if not chunk then + if err ~= ssh.ERROR_EAGAIN then + return nil, session.session:last_error() + end + + if not waitsocket(session, 3.0) then + return nil, 'timeout' + end + else + if f then + local ok, err = f:write(chunk) + if not ok then + return nil, err + end + else + data[#data + 1] = chunk + end + + got = got + #chunk + + -- Avoid blocking for too long while receive large file + time.sleep(0.0001) + end + end + + return got +end + +function session_methods:scp_recv(source, dest) + local channel, size = channel_scp_recv(self, source) + if not channel then + return nil, size + end + + local data = {} + local f, err + + if dest then + f, err = io.open(dest, 'w') + if not f then + channel_free(self, channel) + return nil, err + end + end + + local got, err = scp_recv(self, channel, size, f, data) + + if f then f:close() end + + channel_free(self, channel) + + if not got then + return nil, err + end + + if f then + return got + else + return table.concat(data) + end +end + +local function scp_send_data(session, channel, data) + local total = #data + local written = 0 + + while written < total do + local n, err = channel:write(data:sub(written + 1)) + if not n then + if err ~= ssh.ERROR_EAGAIN then + return nil, session:last_error() + end + + if not waitsocket(session, 3.0) then + return nil, 'timeout' + end + else + written = written + n + -- Avoid blocking for too long while receive large file + time.sleep(0.0001) + end + end + + return true +end + +function session_methods:scp_send(data, dest) + local channel, size = channel_scp_send(self, dest, file.S_IRUSR | file.S_IWUSR | file.S_IRGRP | file.S_IROTH, #data) + if not channel then + return nil, size + end + + local ok, err = scp_send_data(self, channel, data) + + channel_send_eof(self, channel) + channel_wait_eof(self, channel) + channel_wait_closed(self, channel) + channel_free(self, channel) + + if not ok then + return nil, err + end + + return true +end + +function session_methods:scp_sendfile(source, dest) + local st, err = file.stat(source) + if not st then + return nil, err + end + + if st['type'] ~= 'REG' then + return nil, source .. ': not a regular file' + end + + local channel, size = channel_scp_send(self, dest, st.mode, st.size) + if not channel then + return nil, size + end + + local fd, err = file.open(source) + if not fd then + channel_free(self, channel) + return nil, err + end + + local ok = true + local data + + while true do + data, err = file.read(fd, 1024) + if not data then break end + + if #data == 0 then break end + + ok, err = scp_send_data(self, channel, data) + if not ok then break end + end + + file.close(fd) + + channel_send_eof(self, channel) + channel_wait_eof(self, channel) + channel_wait_closed(self, channel) + channel_free(self, channel) + + if not ok then + return nil, err + end + + return true +end + +function session_methods:disconnect(reaason, description) + self.disconnected = true + + local deadtime = sys.uptime() + 5.0 + + reaason = reaason or ssh.DISCONNECT_BY_APPLICATION + description = description or 'Normal Shutdown' + + while sys.uptime() < deadtime do + local ok, err = self.session:disconnect(reaason, description) + if ok then return true end + + if err ~= ssh.ERROR_EAGAIN then + return nil, self.session:last_error() + end + + if not waitsocket(self, deadtime - sys.uptime()) then + return nil, 'timeout' + end + end + + return nil, 'timeout' +end + +function session_methods:free() + if not self.disconnected then + self:disconnect() + end + + local deadtime = sys.uptime() + 5.0 + + while sys.uptime() < deadtime do + local ok, err = self.session:free() + if ok then break end + + if err ~= ssh.ERROR_EAGAIN then + break + end + + if not waitsocket(self, deadtime - sys.uptime()) then + break + end + end + + self.sock:close() +end + +local session_metatable = { + __index = session_methods, + __gc = session_methods.free +} + +function M.new(ipaddr, port, username, password) + local sock, err + + if socket.is_ipv4_address(ipaddr) then + sock, err = socket.connect_tcp(ipaddr, port) + elseif socket.is_ipv6_address(ipaddr) then + sock, err = socket.connect_tcp6(ipaddr, port) + else + return nil, 'invalid ipaddr: ' .. ipaddr + end + + if not sock then + return nil, err + end + + local session = ssh.new() + local fd = sock:getfd() + + local obj = { session = session, sock = sock, iow = eco.watcher(eco.IO, fd) } + + local ok + + while true do + ok, err = session:handshake(fd) + if ok then break end + + if err ~= ssh.ERROR_EAGAIN then + err = session:last_error() + return nil, err + end + + if not waitsocket(obj, 5.0) then + return nil, 'handshake timeout' + end + end + + while true do + ok, err = session:userauth_password(username, password) + if ok then break end + + if err ~= ssh.ERROR_EAGAIN then + err = session:last_error() + return nil, err + end + + if not waitsocket(obj, 5.0) then + return nil, 'authentication timeout' + end + end + + return setmetatable(obj, session_metatable) +end + +return M