372 lines
12 KiB
C
372 lines
12 KiB
C
// testSsh.c -- exercises the SSH/SFTP library against a throwaway, unprivileged sshd on
|
|
// loopback. The test generates a host key and a client key, starts sshd on an ephemeral port
|
|
// authenticating the CURRENT user by that key, then drives sshConnect / sshAuthKey / sshExec
|
|
// and the sftp* file operations from a Lua context. Not part of `make test` (it needs a system
|
|
// sshd + ssh-keygen); run it with `make ssh-test`.
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "calog.h"
|
|
#include "calogSsh.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <netinet/in.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define PUMP_LIMIT 6000
|
|
#define RESULT_SLOTS 12
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic int64_t results[RESULT_SLOTS];
|
|
static _Atomic int32_t doneCount = 0;
|
|
static _Atomic int32_t errorCount = 0;
|
|
static int32_t testsRun = 0;
|
|
static int32_t testsFailed = 0;
|
|
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
|
static int freePort(void);
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void onError(uint64_t contextId, const char *message, void *userData);
|
|
static void pumpUntilDone(int32_t target);
|
|
static bool runCommand(const char *path, char *const argv[]);
|
|
static bool waitForListen(int port);
|
|
|
|
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
|
testsRun++;
|
|
if (!condition) {
|
|
testsFailed++;
|
|
printf("FAIL %s:%d %s\n", file, line, message);
|
|
}
|
|
}
|
|
|
|
|
|
static int freePort(void) {
|
|
struct sockaddr_in addr;
|
|
socklen_t len;
|
|
int fd;
|
|
int port;
|
|
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = 0;
|
|
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
len = sizeof(addr);
|
|
if (getsockname(fd, (struct sockaddr *)&addr, &len) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
port = (int)ntohs(addr.sin_port);
|
|
close(fd);
|
|
return port;
|
|
}
|
|
|
|
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_fetch_add(&doneCount, 1);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 2 || args[0].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "report expects (tag, value)");
|
|
}
|
|
if (args[0].as.i < 0 || args[0].as.i >= RESULT_SLOTS) {
|
|
return calogFail(result, calogErrArgE, "report: tag out of range");
|
|
}
|
|
atomic_store(&results[args[0].as.i], (args[1].type == calogIntE) ? args[1].as.i : (int64_t)args[1].as.r);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
|
(void)contextId;
|
|
(void)userData;
|
|
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
|
|
atomic_fetch_add(&errorCount, 1);
|
|
}
|
|
|
|
|
|
static void pumpUntilDone(int32_t target) {
|
|
struct timespec ts = { 0, 1000000 };
|
|
int i;
|
|
|
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
|
calogPump(calog);
|
|
if (atomic_load(&doneCount) >= target) {
|
|
calogPump(calog);
|
|
return;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
|
|
|
|
static bool runCommand(const char *path, char *const argv[]) {
|
|
pid_t pid;
|
|
int status;
|
|
|
|
pid = fork();
|
|
if (pid < 0) {
|
|
return false;
|
|
}
|
|
if (pid == 0) {
|
|
int devnull;
|
|
devnull = open("/dev/null", 1);
|
|
if (devnull >= 0) {
|
|
dup2(devnull, 1);
|
|
dup2(devnull, 2);
|
|
}
|
|
execv(path, argv);
|
|
_exit(127);
|
|
}
|
|
if (waitpid(pid, &status, 0) != pid) {
|
|
return false;
|
|
}
|
|
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
}
|
|
|
|
|
|
static bool waitForListen(int port) {
|
|
int i;
|
|
|
|
for (i = 0; i < 200; i++) {
|
|
struct sockaddr_in addr;
|
|
struct timespec ts = { 0, 20000000 };
|
|
int fd;
|
|
int rc;
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
return false;
|
|
}
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = htons((uint16_t)port);
|
|
rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
|
close(fd);
|
|
if (rc == 0) {
|
|
return true;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
CalogContextT *ctx;
|
|
struct passwd *pw;
|
|
const char *tmp;
|
|
const char *user;
|
|
char dir[256];
|
|
char hostKey[320];
|
|
char clientKey[320];
|
|
char clientPub[328];
|
|
char authKeys[320];
|
|
char remoteFile[384];
|
|
char sshdArg[400];
|
|
char akoArg[400];
|
|
char script[8192];
|
|
char *cpCmd[5];
|
|
char *sshdCmd[16];
|
|
pid_t sshd;
|
|
int port;
|
|
int32_t i;
|
|
|
|
tmp = getenv("TMPDIR");
|
|
if (tmp == NULL) {
|
|
tmp = "/tmp";
|
|
}
|
|
pw = getpwuid(getuid());
|
|
if (pw == NULL) {
|
|
printf("cannot determine current user\n");
|
|
return 1;
|
|
}
|
|
user = pw->pw_name;
|
|
snprintf(dir, sizeof(dir), "%s/calogSshTest_%ld", tmp, (long)getpid());
|
|
{
|
|
char *mkdirCmd[4];
|
|
mkdirCmd[0] = (char *)"/bin/mkdir";
|
|
mkdirCmd[1] = (char *)"-p";
|
|
mkdirCmd[2] = dir;
|
|
mkdirCmd[3] = NULL;
|
|
if (!runCommand("/bin/mkdir", mkdirCmd)) {
|
|
printf("mkdir failed\n");
|
|
return 1;
|
|
}
|
|
}
|
|
snprintf(hostKey, sizeof(hostKey), "%s/host_ed25519", dir);
|
|
snprintf(clientKey, sizeof(clientKey), "%s/client_ed25519", dir);
|
|
snprintf(clientPub, sizeof(clientPub), "%s.pub", clientKey);
|
|
snprintf(authKeys, sizeof(authKeys), "%s/authorized_keys", dir);
|
|
snprintf(remoteFile, sizeof(remoteFile), "%s/remote.txt", dir);
|
|
|
|
{
|
|
char *kh[9];
|
|
char *kc[9];
|
|
kh[0] = (char *)"/usr/bin/ssh-keygen"; kh[1] = (char *)"-q"; kh[2] = (char *)"-t"; kh[3] = (char *)"ed25519";
|
|
kh[4] = (char *)"-f"; kh[5] = hostKey; kh[6] = (char *)"-N"; kh[7] = (char *)""; kh[8] = NULL;
|
|
kc[0] = (char *)"/usr/bin/ssh-keygen"; kc[1] = (char *)"-q"; kc[2] = (char *)"-t"; kc[3] = (char *)"ed25519";
|
|
kc[4] = (char *)"-f"; kc[5] = clientKey; kc[6] = (char *)"-N"; kc[7] = (char *)""; kc[8] = NULL;
|
|
if (!runCommand("/usr/bin/ssh-keygen", kh) || !runCommand("/usr/bin/ssh-keygen", kc)) {
|
|
printf("ssh-keygen failed\n");
|
|
return 1;
|
|
}
|
|
}
|
|
// authorized_keys = the client public key.
|
|
cpCmd[0] = (char *)"/bin/cp";
|
|
cpCmd[1] = clientPub;
|
|
cpCmd[2] = authKeys;
|
|
cpCmd[3] = NULL;
|
|
if (!runCommand("/bin/cp", cpCmd)) {
|
|
printf("cp authorized_keys failed\n");
|
|
return 1;
|
|
}
|
|
|
|
port = freePort();
|
|
if (port < 0) {
|
|
printf("no free port\n");
|
|
return 1;
|
|
}
|
|
snprintf(sshdArg, sizeof(sshdArg), "%d", port);
|
|
snprintf(akoArg, sizeof(akoArg), "AuthorizedKeysFile=%s", authKeys);
|
|
sshdCmd[0] = (char *)"/usr/sbin/sshd";
|
|
sshdCmd[1] = (char *)"-D";
|
|
sshdCmd[2] = (char *)"-p";
|
|
sshdCmd[3] = sshdArg;
|
|
sshdCmd[4] = (char *)"-h";
|
|
sshdCmd[5] = hostKey;
|
|
sshdCmd[6] = (char *)"-o";
|
|
sshdCmd[7] = (char *)"PidFile=none";
|
|
sshdCmd[8] = (char *)"-o";
|
|
sshdCmd[9] = (char *)"UsePAM=no";
|
|
sshdCmd[10] = (char *)"-o";
|
|
sshdCmd[11] = (char *)"StrictModes=no";
|
|
sshdCmd[12] = (char *)"-o";
|
|
sshdCmd[13] = akoArg;
|
|
sshdCmd[14] = NULL;
|
|
sshd = fork();
|
|
if (sshd < 0) {
|
|
printf("fork sshd failed\n");
|
|
return 1;
|
|
}
|
|
if (sshd == 0) {
|
|
int devnull;
|
|
devnull = open("/dev/null", 1);
|
|
if (devnull >= 0) {
|
|
dup2(devnull, 1);
|
|
dup2(devnull, 2);
|
|
}
|
|
execv("/usr/sbin/sshd", sshdCmd);
|
|
_exit(127);
|
|
}
|
|
if (!waitForListen(port)) {
|
|
printf("sshd did not start listening\n");
|
|
kill(sshd, SIGTERM);
|
|
waitpid(sshd, NULL, 0);
|
|
return 1;
|
|
}
|
|
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
kill(sshd, SIGTERM);
|
|
waitpid(sshd, NULL, 0);
|
|
return 1;
|
|
}
|
|
calogSetErrorHandler(calog, onError, NULL);
|
|
calogRegister(calog, "report", nativeReport, NULL);
|
|
calogRegister(calog, "done", nativeDone, NULL);
|
|
calogSshRegister(calog);
|
|
for (i = 0; i < RESULT_SLOTS; i++) {
|
|
atomic_store(&results[i], -1);
|
|
}
|
|
|
|
snprintf(script, sizeof(script),
|
|
"local h = sshConnect('127.0.0.1', %d)\n"
|
|
"report(1, h and 1 or 0)\n"
|
|
"local ok = sshAuthKey(h, '%s', '%s', '%s', '')\n"
|
|
"report(2, ok and 1 or 0)\n"
|
|
"local r = sshExec(h, 'echo hello')\n"
|
|
"report(3, (r.stdout == 'hello\\n' and r.exitCode == 0) and 1 or 0)\n"
|
|
"sftpPut(h, '%s', 'sftp payload')\n"
|
|
"report(4, sftpGet(h, '%s') == 'sftp payload' and 1 or 0)\n"
|
|
"local st = sftpStat(h, '%s')\n"
|
|
"report(5, (st ~= nil and st.size == 12 and st.isDir == false) and 1 or 0)\n"
|
|
"local found = 0\n"
|
|
"for _, e in ipairs(sftpList(h, '%s')) do if e.name == 'remote.txt' then found = 1 end end\n"
|
|
"report(6, found)\n"
|
|
"sftpRemove(h, '%s')\n"
|
|
"report(7, sftpStat(h, '%s') == nil and 1 or 0)\n"
|
|
"local big = sshExec(h, 'seq 1 20000')\n"
|
|
"report(8, (big.exitCode == 0 and #big.stdout > 100000 and big.stdout:sub(-6) == '20000\\n') and 1 or 0)\n"
|
|
"sshClose(h)\n"
|
|
"done()",
|
|
port, user, clientKey, clientPub, remoteFile, remoteFile, remoteFile, dir, remoteFile, remoteFile);
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
calogContextEval(ctx, script);
|
|
pumpUntilDone(1);
|
|
|
|
CHECK(atomic_load(&results[1]) == 1, "sshConnect returns a handle");
|
|
CHECK(atomic_load(&results[2]) == 1, "sshAuthKey authenticates with the client key");
|
|
CHECK(atomic_load(&results[3]) == 1, "sshExec runs a command and returns stdout + exitCode");
|
|
CHECK(atomic_load(&results[4]) == 1, "sftpPut then sftpGet round-trips a file");
|
|
CHECK(atomic_load(&results[5]) == 1, "sftpStat reports the file size and that it is not a directory");
|
|
CHECK(atomic_load(&results[6]) == 1, "sftpList includes the written file");
|
|
CHECK(atomic_load(&results[7]) == 1, "sftpRemove deletes the file (stat then returns nil)");
|
|
CHECK(atomic_load(&results[8]) == 1, "sshExec drains a large multi-realloc output completely");
|
|
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
|
|
|
|
calogContextClose(ctx);
|
|
calogDestroy(calog);
|
|
calogSshShutdown();
|
|
|
|
kill(sshd, SIGTERM);
|
|
waitpid(sshd, NULL, 0);
|
|
{
|
|
char *rmCmd[4];
|
|
rmCmd[0] = (char *)"/bin/rm";
|
|
rmCmd[1] = (char *)"-rf";
|
|
rmCmd[2] = dir;
|
|
rmCmd[3] = NULL;
|
|
(void)runCommand("/bin/rm", rmCmd);
|
|
}
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|