diff options
author | Ryo Nakamura <upa@haeena.net> | 2022-10-15 21:59:25 +0900 |
---|---|---|
committer | Ryo Nakamura <upa@haeena.net> | 2022-10-15 21:59:25 +0900 |
commit | 303a9eb974f884b5f9f7e14fdd83a821f21e32e6 (patch) | |
tree | 91c535639e39e7f204db7a194854f4865dfff184 |
initial commit
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | CMakeLists.txt | 10 | ||||
-rw-r--r-- | src/file.h | 28 | ||||
-rw-r--r-- | src/main.c | 135 | ||||
-rw-r--r-- | src/platform.c | 27 | ||||
-rw-r--r-- | src/platform.h | 6 | ||||
-rw-r--r-- | src/ssh.c | 237 | ||||
-rw-r--r-- | src/ssh.h | 24 | ||||
-rw-r--r-- | src/util.h | 47 |
9 files changed, 515 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..385a397 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.13) + +project(sscp C) + +set(CMAKE_C_FLAGS "-Wall -g") + +add_executable(sscp src/main.c src/platform.c src/ssh.c) +target_include_directories(sscp PUBLIC ./src /usr/local/include) +target_link_directories(sscp PUBLIC /usr/local/lib) +target_link_libraries(sscp ssh) diff --git a/src/file.h b/src/file.h new file mode 100644 index 0000000..7c5168d --- /dev/null +++ b/src/file.h @@ -0,0 +1,28 @@ +#ifndef _FILE_H_ +#define _FILE_H_ + +struct path { + char *path; + bool remote; +}; + +struct file { + struct path src; /* copy source */ + struct path dst; /* copy desitnation */ + size_t size; /* size of this file */ +}; + +struct chunk { + struct file *f; + size_t off; /* offset of this chunk on the file f */ + size_t len; /* length of this chunk */ +}; + +struct file *file_expand(char **src_array, char *dst) +{ + /* return array of files expanded from sources and dst */ + return NULL; +} + + +#endif /* _FILE_H_ */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..bec9a00 --- /dev/null +++ b/src/main.c @@ -0,0 +1,135 @@ +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <unistd.h> + +#include <util.h> +#include <ssh.h> +#include <file.h> +#include <platform.h> + + +#define DEFAULT_MIN_CHUNK_SZ (2 << 20) /* 2MB */ + +void usage(bool print_help) { + printf("sscp: super scp, copy files over multiple ssh connections\n" + "\n" + "Usage: sscp [rvC] [-n max_conns] [-s min_chunk_sz] [-S max_chunk_sz]\n" + " [-l login_name] [-p port] [-i identity_file]\n" + " [-c cipher_spec] source ... target\n" + "\n"); + + if (!print_help) + return; + + printf(" -r copy directory recusrively\n" + " -n NR_CONNECTIONS max number of connections (default: # of cpu cores)\n" + " -s CHUNKSIZE min chunk size (default: 2MB)\n" + " -S CHUNKSIZE max chunk size (default: filesize / nr_conn)\n" + "\n" + " -l LOGIN_NAME login name\n" + " -p PORT port number\n" + " -i IDENTITY identity to be used for ssh\n" + " -c CIPHER cipher spec, see `ssh -Q cipher`\n" + " -C enable compression on libssh\n" + " -v increment output level\n" + " -h print this help\n" + "\n"); +} + +int main(int argc, char **argv) +{ + struct ssh_opts opts; + int nr_conn = nr_cpus(); + bool recursive = false; + int min_chunk_sz = DEFAULT_MIN_CHUNK_SZ; + int max_chunk_sz = 0; + char ch; + + memset(&opts, 0, sizeof(opts)); + + while ((ch = getopt(argc, argv, "r:n:s:S:l:p:i:c:Cvh")) != -1) { + switch (ch) { + case 'r': + recursive = true; + break; + case 'n': + nr_conn = atoi(optarg); + if (nr_conn < 1) { + pr_err("invalid number of connections: %s\n", optarg); + return 1; + } + break; + case 's': + min_chunk_sz = atoi(optarg); + if (min_chunk_sz < getpagesize()) { + pr_err("min chunk size must be " + "larger than or equal to %d: %s\n", + getpagesize(), optarg); + return 1; + } + if (min_chunk_sz % getpagesize() != 0) { + pr_err("min chunk size must be " + "multiple of page size %d: %s\n", + getpagesize(), optarg); + return -1; + } + break; + case 'S': + max_chunk_sz = atoi(optarg); + if (max_chunk_sz < getpagesize()) { + pr_err("max chunk size must be " + "larger than or equal to %d: %s\n", + getpagesize(), optarg); + return 1; + } + if (max_chunk_sz % getpagesize() != 0) { + pr_err("max chunk size must be " + "multiple of page size %d: %s\n", + getpagesize(), optarg); + return -1; + } + break; + case 'l': + opts.login_name = optarg; + break; + case 'p': + opts.port = optarg; + break; + case 'i': + opts.identity = optarg; + break; + case 'c': + opts.cipher = optarg; + break; + case 'C': + opts.compress++; + break; + case 'v': + opts.debuglevel++; + break; + case 'h': + usage(true); + return 1; + default: + usage(false); + return 1; + } + } + + if (max_chunk_sz > 0 && min_chunk_sz > max_chunk_sz) { + pr_err("smaller max chunk size than min chunk size: %d < %d\n", + max_chunk_sz, min_chunk_sz); + return 1; + } + + printf("opts.port %s\n", opts.port); + + int n; + for (n = 0; n < argc; n++) { + printf("%d %s\n", n, argv[n]); + } + printf("optind %d", optind); + + return 0; +} diff --git a/src/platform.c b/src/platform.c new file mode 100644 index 0000000..5e0573e --- /dev/null +++ b/src/platform.c @@ -0,0 +1,27 @@ +#include <util.h> +#include <platform.h> + +#ifdef __APPLE__ +#include <sys/types.h> +#include <sys/sysctl.h> +#elif linux +#else +#error unsupported platform +#endif + + +#ifdef __APPLE__ +int nr_cpus() +{ + int n; + size_t size = sizeof(n); + + if (sysctlbyname("machdep.cpu.core_count", &n, &size, NULL, 0) != 0) { + pr_err("failed to get number of cpu cores: %s\n", strerrno()); + return -1; + } + + return n; +} +#endif + diff --git a/src/platform.h b/src/platform.h new file mode 100644 index 0000000..b93142b --- /dev/null +++ b/src/platform.h @@ -0,0 +1,6 @@ +#ifndef _PLATFORM_H_ +#define _PLATFORM_H_ + +int nr_cpus(); + +#endif /* _PLATFORM_H_ */ diff --git a/src/ssh.c b/src/ssh.c new file mode 100644 index 0000000..6643b8e --- /dev/null +++ b/src/ssh.c @@ -0,0 +1,237 @@ +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <ssh.h> +#include <util.h> + +static int ssh_verify_known_hosts(ssh_session session); + + +static int ssh_set_opts(ssh_session ssh, struct ssh_opts *opts) +{ + ssh_set_log_level(opts->debuglevel); + + if (opts->login_name && + ssh_options_set(ssh, SSH_OPTIONS_USER, opts->login_name) < 0) { + pr_err("failed to set login name\n"); + return -1; + } + + if (opts->port && + ssh_options_set(ssh, SSH_OPTIONS_PORT_STR, opts->port) < 0) { + pr_err("failed to set port number\n"); + return -1; + } + + if (opts->identity && + ssh_options_set(ssh, SSH_OPTIONS_IDENTITY, opts->identity) < 0) { + pr_err("failed to set identity\n"); + return -1; + } + + if (opts->cipher) { + if (ssh_options_set(ssh, SSH_OPTIONS_CIPHERS_C_S, opts->cipher) < 0) { + pr_err("failed to set cipher client to server\n"); + return -1; + } + if (ssh_options_set(ssh, SSH_OPTIONS_CIPHERS_S_C, opts->cipher) < 0) { + pr_err("failed to set cipher client to server\n"); + return -1; + } + } + + if (opts->compress && + ssh_options_set(ssh, SSH_OPTIONS_COMPRESSION, "yes") < 0) { + pr_err("failed to enable ssh compression\n"); + return -1; + } + + return 0; +} + +static int ssh_authenticate(ssh_session ssh, struct ssh_opts *opts) +{ + int auth_bit_mask; + int ret; + + /* none method */ + ret = ssh_userauth_none(ssh, NULL); + if (ret == SSH_AUTH_SUCCESS) + return 0; + + auth_bit_mask = ssh_userauth_list(ssh, NULL); + + if (auth_bit_mask & SSH_AUTH_METHOD_NONE && + ssh_userauth_none(ssh, NULL) == SSH_AUTH_SUCCESS) { + return 0; + } + + if (auth_bit_mask & SSH_AUTH_METHOD_PUBLICKEY && + ssh_userauth_publickey_auto(ssh, NULL, NULL) == SSH_AUTH_SUCCESS) { + return 0; + } + + if (auth_bit_mask & SSH_AUTH_METHOD_PASSWORD) { + if (!opts->password) { + opts->password = getpass("Password: "); + } + if (ssh_userauth_password(ssh, NULL, opts->password) == SSH_AUTH_SUCCESS) + return 0; + } + + pr_err("authentication failure: %s\n", ssh_get_error(ssh)); + return -1; +} + +static ssh_session ssh_make_ssh_session(char *sshdst, struct ssh_opts *opts) +{ + ssh_session ssh = ssh_new(); + + if (ssh_set_opts(ssh, opts) != 0) + goto free_out; + + if (ssh_options_set(ssh, SSH_OPTIONS_HOST, sshdst) != SSH_OK) { + pr_err("failed to set destination host\n"); + goto free_out; + } + + if (ssh_connect(ssh) != SSH_OK) { + pr_err("failed to connect ssh server: %s\n", ssh_get_error(ssh)); + goto free_out; + } + + if (ssh_authenticate(ssh, opts) != 0) { + pr_err("authentication failed: %s\n", ssh_get_error(ssh)); + goto disconnect_out; + } + + if (ssh_verify_known_hosts(ssh) != 0) { + goto disconnect_out; + } + + return ssh; + +disconnect_out: + ssh_disconnect(ssh); +free_out: + ssh_free(ssh); + return NULL; +} + +sftp_session ssh_make_sftp_session(char *sshdst, struct ssh_opts *opts) +{ + sftp_session sftp; + ssh_session ssh = ssh_make_ssh_session(sshdst, opts); + + if (!ssh) { + return NULL; + } + + sftp = sftp_new(ssh); + if (!sftp) { + pr_err("failed to allocate sftp session: %s\n", ssh_get_error(ssh)); + goto err_out; + } + + if (sftp_init(sftp) != SSH_OK) { + pr_err("failed to initialize sftp session: err code %d\n", + sftp_get_error(sftp)); + goto err_out; + } + + return sftp; +err_out: + ssh_disconnect(ssh); + ssh_free(ssh); + return NULL; +} + + +/* copied from https://api.libssh.org/stable/libssh_tutor_guided_tour.html*/ +static int ssh_verify_known_hosts(ssh_session session) +{ + enum ssh_known_hosts_e state; + unsigned char *hash = NULL; + ssh_key srv_pubkey = NULL; + size_t hlen; + char buf[10]; + char *hexa; + char *p; + int cmp; + int rc; + + rc = ssh_get_server_publickey(session, &srv_pubkey); + if (rc < 0) { + return -1; + } + + rc = ssh_get_publickey_hash(srv_pubkey, + SSH_PUBLICKEY_HASH_SHA1, + &hash, + &hlen); + ssh_key_free(srv_pubkey); + if (rc < 0) { + return -1; + } + + state = ssh_session_is_known_server(session); + switch (state) { + case SSH_KNOWN_HOSTS_OK: + /* OK */ + + break; + case SSH_KNOWN_HOSTS_CHANGED: + fprintf(stderr, "Host key for server changed: it is now:\n"); + //ssh_print_hexa("Public key hash", hash, hlen); + fprintf(stderr, "For security reasons, connection will be stopped\n"); + ssh_clean_pubkey_hash(&hash); + + return -1; + case SSH_KNOWN_HOSTS_OTHER: + fprintf(stderr, "The host key for this server was not found but an other" + "type of key exists.\n"); + fprintf(stderr, "An attacker might change the default server key to" + "confuse your client into thinking the key does not exist\n"); + ssh_clean_pubkey_hash(&hash); + + return -1; + case SSH_KNOWN_HOSTS_NOT_FOUND: + fprintf(stderr, "Could not find known host file.\n"); + fprintf(stderr, "If you accept the host key here, the file will be" + "automatically created.\n"); + + /* FALL THROUGH to SSH_SERVER_NOT_KNOWN behavior */ + + case SSH_KNOWN_HOSTS_UNKNOWN: + hexa = ssh_get_hexa(hash, hlen); + fprintf(stderr,"The server is unknown. Do you trust the host key?\n"); + fprintf(stderr, "Public key hash: %s\n", hexa); + ssh_string_free_char(hexa); + ssh_clean_pubkey_hash(&hash); + p = fgets(buf, sizeof(buf), stdin); + if (p == NULL) { + return -1; + } + + cmp = strncasecmp(buf, "yes", 3); + if (cmp != 0) { + return -1; + } + + rc = ssh_session_update_known_hosts(session); + if (rc < 0) { + fprintf(stderr, "Error %s\n", strerror(errno)); + return -1; + } + + break; + case SSH_KNOWN_HOSTS_ERROR: + fprintf(stderr, "Error %s", ssh_get_error(session)); + ssh_clean_pubkey_hash(&hash); + return -1; + } + + ssh_clean_pubkey_hash(&hash); + return 0; +} diff --git a/src/ssh.h b/src/ssh.h new file mode 100644 index 0000000..3397fd7 --- /dev/null +++ b/src/ssh.h @@ -0,0 +1,24 @@ +#ifndef _SSH_H_ +#define _SSH_H_ + +#include <libssh/libssh.h> +#include <libssh/sftp.h> + + +struct ssh_opts { + char *login_name; /* -l */ + char *port; /* -p */ + char *identity; /* -i */ + char *cipher; /* -c */ + int compress; /* -C */ + int debuglevel; /* -v */ + + char *password; /* filled at the first connecting phase */ +}; + +/* ssh_make_sftp_session() creates sftp_session. sshdst accpets + * user@hostname and hostname notations (by libssh). + */ +sftp_session ssh_make_sftp_session(char *sshdst, struct ssh_opts *opts); + +#endif /* _SSH_H_ */ diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..848827d --- /dev/null +++ b/src/util.h @@ -0,0 +1,47 @@ +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include <stdio.h> +#include <string.h> +#include <errno.h> + +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) + +#define pr_v(level, fmt, ...) do { \ + if (verbose >= level) { \ + fprintf(stdout, "\x1b[1m\x1b[34m" \ + "%s(): \x1b[0m" fmt, \ + __func__, ##__VA_ARGS__); \ + } \ + } while (0) + +#define pr_v1(fmt, ...) pr_v(1, fmt, ##__VA_ARGS__) +#define pr_v2(fmt, ...) pr_v(2, fmt, ##__VA_ARGS__) +#define pr_v3(fmt, ...) pr_v(3, fmt, ##__VA_ARGS__) + + +#define pr_info(fmt, ...) fprintf(stdout, "%s(): " fmt, \ + __func__, ##__VA_ARGS__) + +#define pr_warn(fmt, ...) fprintf(stderr, "\x1b[1m\x1b[33m" \ + "WARN:%s(): " fmt "\x1b[0m", \ + __func__, ##__VA_ARGS__) + +#define pr_err(fmt, ...) fprintf(stderr, "\x1b[1m\x1b[31m" \ + "ERR:%s(): " fmt "\x1b[0m", \ + __func__, ##__VA_ARGS__) + +#define pr_debug(fmt, ...) \ + do { \ + if (unlikely(debug)) { \ + fprintf(stderr, "\x1b[1m\x1b[33m" \ + "DEBUG:%s(): " fmt "\x1b[0m", \ + __func__, ##__VA_ARGS__); \ + } \ + } while (0) + + +#define strerrno() strerror(errno) + +#endif /* _UTIL_H_ */ |