2 Commits

Author SHA1 Message Date
Ryo Nakamura
16d3b5b772 fix github actions to obtain libssh version dynamically 2025-08-11 15:44:23 +09:00
Ryo Nakamura
27646fc71d bump libssh version to 0.11.2 2025-08-11 15:10:33 +09:00
16 changed files with 97 additions and 221 deletions

View File

@@ -1 +0,0 @@
build

View File

@@ -10,6 +10,34 @@ env:
BUILD_TYPE: Release
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: apply the patch to libssh
run: |
git -C libssh fetch --all --tags --prune
patch -d libssh -p1 < patch/$(git -C libssh describe).patch
# TODO: just building docker does not require packages. fix CMakeLists
- name: install build dependency
run: sudo ./scripts/install-build-deps.sh
- name: Configure Cmake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
- name: Build single binary mscp
run: make -C ${{github.workspace}}/build build-single-binary
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
${{github.workspace}}/build/mscp.linux.x86_64.static
source-release:
runs-on: ubuntu-latest
steps:

View File

@@ -22,7 +22,7 @@ jobs:
- rocky-8.9
- rocky-9.3
- almalinux-9.3
- alpine-3.22
- alpine-3.19
- arch-base
steps:
- uses: actions/checkout@v4

View File

@@ -168,7 +168,7 @@ enable_testing()
#
# When edit DIST_IDS and DIST_VERS, also edit .github/workflows/test.yaml
list(APPEND DIST_IDS ubuntu ubuntu ubuntu rocky rocky almalinux alpine arch)
list(APPEND DIST_VERS 20.04 22.04 24.04 8.9 9.3 9.3 3.22 base)
list(APPEND DIST_VERS 20.04 22.04 24.04 8.9 9.3 9.3 3.19 base)
list(LENGTH DIST_IDS _DIST_LISTLEN)
math(EXPR DIST_LISTLEN "${_DIST_LISTLEN} - 1")
@@ -208,16 +208,6 @@ foreach(x RANGE ${DIST_LISTLEN})
--add-host=ip6-localhost:::1
${DOCKER_IMAGE} /mscp/scripts/test-in-container.sh)
add_custom_target(docker-run-${DOCKER_INDEX}
COMMENT "Start ${DOCKER_IMAGE} container"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMAND
${CE} run --init --rm --privileged
--sysctl net.ipv6.conf.all.disable_ipv6=0
--add-host=ip6-localhost:::1
-it
${DOCKER_IMAGE} /mscp/scripts/test-in-container.sh bash)
list(APPEND DOCKER_BUILDS docker-build-${DOCKER_INDEX})
list(APPEND DOCKER_BUILDS_NO_CACHE docker-build-${DOCKER_INDEX}-no-cache)
list(APPEND DOCKER_TESTS docker-test-${DOCKER_INDEX})
@@ -288,9 +278,9 @@ add_custom_target(build-single-binary
COMMENT "Build mscp as a single binary in alpine conatiner"
WORKING_DIRECTORY ${mscp_SOURCE_DIR}
BYPRODUCTS ${CMAKE_BINARY_DIR}/${SINGLEBINARYFILE}
DEPENDS docker-build-alpine-3.22
DEPENDS docker-build-alpine-3.19
COMMAND
${CE} run --rm -v ${CMAKE_BINARY_DIR}:/out mscp-alpine:3.22
${CE} run --rm -v ${CMAKE_BINARY_DIR}:/out mscp-alpine:3.19
cp /mscp/build/mscp /out/${SINGLEBINARYFILE})

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22
FROM alpine:3.19
# do not use REQUIREDPKGS build argument because
# this Dockerfile compiles mscp with conan,so we do not need
@@ -19,10 +19,6 @@ RUN ssh-keygen -A \
&& ssh-keygen -f /root/.ssh/id_rsa -N "" \
&& cat /root/.ssh/id_rsa.pub > /root/.ssh/authorized_keys
# disable PerSourcePenaltie, which would distrub test:
# https://undeadly.org/cgi?action=article;sid=20240607042157
RUN echo "PerSourcePenalties=no" > /etc/ssh/sshd_config.d/90-mscp-test.conf
# create test user
RUN addgroup -S test \
&& adduser -S test -G test \

View File

@@ -61,6 +61,12 @@ sudo dnf copr enable upaaa/mscp
sudo dnf install mscp
```
- Single binary `mscp` for x86_64 (not optimal performance)
```console
wget https://github.com/upa/mscp/releases/latest/download/mscp.linux.x86_64.static -O /usr/local/bin/mscp
chmod 755 /usr/local/bin/mscp
```
## Build
@@ -80,7 +86,7 @@ cd mscp
# prepare patched libssh
git submodule update --init
patch -d libssh -p1 < patch/$(git -C libssh describe).patch
patch -d libssh -p1 < patch/$(git --git-dir=./libssh/.git describe).patch
# install build dependency
bash ./scripts/install-build-deps.sh

View File

@@ -1 +1 @@
0.2.3
0.2.1

22
debian/changelog vendored
View File

@@ -1,24 +1,4 @@
mscp (0.2.3) UNRELEASED; urgency=medium
* bump libssh version to 0.11.2 (#25)
* adopt new AIO read API of libssh
* fix path handling when remote dst path has suffix '/' (#24)
* fix remote path handling including '~' (partially)
* try pubkey auth first to avoid PerSourcePenalties
* remove the single-binary version of mscp from release
-- Ryo Nakamura <upa@haeena.net> Tue, 12 Aug 2025 18:11:47 +0900
mscp (0.2.2) unstable; urgency=medium
* bump cmake_minimum_version on libssh (#32)
* fix quiet mode (#30)
* use openssl@3 on macOS (#29)
* add archlinux support (#28)
-- Ryo Nakamura <upa@haeena.net> Wed, 16 Apr 2025 17:01:17 +0900
mscp (0.2.1) unstable; urgency=medium
mscp (0.2.1) UNRELEASED; urgency=medium
* fix broken checkpoint files when copying multiple files (#16)
* fix broken password inputs for resume transfers (#17 and #18)

View File

@@ -37,7 +37,7 @@ index d22178e7..2d6aa0a7 100644
int ssh_buffer_validate_length(struct ssh_buffer_struct *buffer, size_t len);
diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h
index 3bddb019..1d5d7761 100644
index 3bddb019..c6b01c1c 100644
--- a/include/libssh/libssh.h
+++ b/include/libssh/libssh.h
@@ -373,6 +373,7 @@ enum ssh_options_e {
@@ -64,7 +64,7 @@ index 3bddb019..1d5d7761 100644
LIBSSH_API void ssh_buffer_free(ssh_buffer buffer);
#define SSH_BUFFER_FREE(x) \
do { if ((x) != NULL) { ssh_buffer_free(x); x = NULL; } } while(0)
@@ -886,6 +889,12 @@ LIBSSH_API void *ssh_buffer_get(ssh_buffer buffer);
@@ -886,6 +889,11 @@ LIBSSH_API void *ssh_buffer_get(ssh_buffer buffer);
LIBSSH_API uint32_t ssh_buffer_get_len(ssh_buffer buffer);
LIBSSH_API int ssh_session_set_disconnect_message(ssh_session session, const char *message);
@@ -72,7 +72,6 @@ index 3bddb019..1d5d7761 100644
+
+LIBSSH_API const char **ssh_ciphers(void);
+LIBSSH_API const char **ssh_hmacs(void);
+LIBSSH_API void ssh_use_openssh_proxy_jumps(int);
+
#ifndef LIBSSH_LEGACY_0_4
#include "libssh/legacy.h"
@@ -288,7 +287,7 @@ index 2cb64037..51f4c87e 100644
rc = connect(s, itr->ai_addr, itr->ai_addrlen);
if (rc == -1) {
diff --git a/src/misc.c b/src/misc.c
index 774211fb..74e57959 100644
index 774211fb..ae62ddfe 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -71,6 +71,8 @@
@@ -343,37 +342,6 @@ index 774211fb..74e57959 100644
/**
* @brief Write the requested number of bytes to a local file.
*
@@ -2227,6 +2265,17 @@ ssh_proxyjumps_free(struct ssh_list *proxy_jump_list)
}
}
+static bool force_openssh_proxy_jumps;
+
+/**
+ * @breif set use openssh proxy jumps without the OPENSSH_PROXYJUMP env var
+ */
+void
+ssh_use_openssh_proxy_jumps(int v)
+{
+ force_openssh_proxy_jumps = (v > 0);
+}
+
/**
* @brief Check if libssh proxy jumps is enabled
*
@@ -2241,7 +2290,12 @@ ssh_libssh_proxy_jumps(void)
{
const char *t = getenv("OPENSSH_PROXYJUMP");
+ if (force_openssh_proxy_jumps)
+ return false;
+
return !(t != NULL && t[0] == '1');
}
+
+
/** @} */
diff --git a/src/options.c b/src/options.c
index 785296dd..a82d4d81 100644
--- a/src/options.c

View File

@@ -38,12 +38,6 @@ make -C build install DESTDIR=%{buildroot}
%changelog
* Tue Aug 12 2025 Ryo Nakamura <upa@haeena.net> - 0.2.3-1
- RPM release for v0.2.3
* Wed Apr 16 2025 Ryo Nakamura <upa@haeena.net> - 0.2.2-1
- RPM release for v0.2.2
* Sat May 11 2024 Ryo Nakamura <upa@haeena.net> - 0.2.1-1
- RPM release for v0.2.1

View File

@@ -28,11 +28,5 @@ for port in 22 8022; do
ssh-keyscan -p $port ::1 >> ${HOME}/.ssh/known_hosts
done
if [ $# -gt 0 ]; then
# command arguments are passed, exec them
"$@"
else
# no arguments passed, run the test
python3 -m pytest -v ../test
fi
# Run test
python3 -m pytest -v ../test

View File

@@ -205,7 +205,7 @@ struct target *validate_targets(char **arg, int len)
*/
struct target *t, *t0;
int n, nslash;
int n;
if ((t = calloc(len, sizeof(struct target))) == NULL) {
pr_err("calloc: %s", strerrno());
@@ -223,33 +223,9 @@ struct target *validate_targets(char **arg, int len)
}
}
/* expand remote path, e.g., empty dst path and '~' */
for (n = 0; n < len; n++) {
if (!t[n].host)
continue;
/* this target is a remote path. check the path and
* expand it. this part is derived from
* openssh-portal prepare_remote_path() function.
*/
char *path = t[n].path;
if (*path == '\0' || strcmp(path, "~") == 0)
t[n].path = strdup(".");
else if (strncmp(path, "~/", 2) == 0) {
if ((nslash = strspn(path + 2, "/")) == strlen(path + 2))
t[n].path = strdup(".");
else
t[n].path = strdup(path + 2 + nslash);
}
if (!t[n].path) {
pr_err("strdup failed: %s", strerrno());
goto free_target_out;
}
}
/* check all user@host are identical. t[len - 1] is the
* destination, so we need to check t[0] to t[len - 2] having
* the identical remote notation */
/* check all user@host are identical. t[len - 1] is destination,
* so we need to check t[0] to t[len - 2] having the identical
* remote notation */
t0 = &t[0];
for (n = 1; n < len - 1; n++) {
if (compare_remote(t0, &t[n]) != 0)

View File

@@ -275,13 +275,6 @@ struct mscp *mscp_init(struct mscp_opts *o, struct mscp_ssh_opts *s)
}
pr_notice("bitrate limit: %lu bps", o->bitrate);
/* workaround: set libssh using openssh proxyjump
* https://gitlab.com/libssh/libssh-mirror/-/issues/319 */
ssh_use_openssh_proxy_jumps(1);
/* call ssh_init() because libssh is statically linked */
ssh_init();
return m;
free_out:
@@ -329,12 +322,10 @@ int mscp_set_dst_path(struct mscp *m, const char *dst_path)
return -1;
}
if (!non_null_string(dst_path)) {
priv_set_errv("empty dst path");
return -1;
}
strncpy(m->dst_path, dst_path, PATH_MAX);
if (!non_null_string(dst_path))
strncpy(m->dst_path, ".", 1);
else
strncpy(m->dst_path, dst_path, PATH_MAX);
return 0;
}
@@ -402,9 +393,6 @@ void *mscp_scan_thread(void *arg)
if (pool_size(m->src_pool) > 1)
a.dst_path_should_dir = true;
if (m->dst_path[strlen(m->dst_path) - 1] == '/')
a.dst_path_should_dir = true;
if (mscp_stat(m->dst_path, &ds, dst_sftp) == 0) {
if (S_ISDIR(ds.st_mode))
a.dst_path_is_dir = true;

View File

@@ -410,45 +410,46 @@ static int copy_chunk_l2r(struct chunk *c, int fd, sftp_file sf, int nr_ahead, i
return 0;
}
static int copy_chunk_r2l(struct chunk *c, sftp_file sf, int fd,
int nr_ahead, int buf_sz,
struct bwlimit *bw, size_t *counter)
static int copy_chunk_r2l(struct chunk *c, sftp_file sf, int fd, int nr_ahead, int buf_sz,
struct bwlimit *bw, size_t *counter)
{
ssize_t read_bytes, write_bytes, remain, thrown, len, requested;
sftp_aio reqs[nr_ahead];
ssize_t read_bytes, write_bytes, remaind, thrown;
char buf[buf_sz];
int i;
int idx;
struct {
int id;
ssize_t len;
} reqs[nr_ahead];
if (c->len == 0)
return 0;
remain = thrown = c->len;
remaind = thrown = c->len;
for (i = 0; i < nr_ahead && thrown > 0; i++) {
len = min(thrown, sizeof(buf));
requested = sftp_aio_begin_read(sf, len, &reqs[i]);
if (requested == SSH_ERROR) {
priv_set_errv("sftp_aio_begin_read: %d",
for (idx = 0; idx < nr_ahead && thrown > 0; idx++) {
reqs[idx].len = min(thrown, sizeof(buf));
reqs[idx].id = sftp_async_read_begin(sf, reqs[idx].len);
if (reqs[idx].id < 0) {
priv_set_errv("sftp_async_read_begin: %d",
sftp_get_error(sf->sftp));
return -1;
}
thrown -= requested;
bwlimit_wait(bw, requested);
thrown -= reqs[idx].len;
bwlimit_wait(bw, reqs[idx].len);
}
for (i = 0; remain > 0; i = (i + 1) % nr_ahead) {
read_bytes = sftp_aio_wait_read(&reqs[i], buf, sizeof(buf));
for (idx = 0; remaind > 0; idx = (idx + 1) % nr_ahead) {
read_bytes = sftp_async_read(sf, buf, reqs[idx].len, reqs[idx].id);
if (read_bytes == SSH_ERROR) {
priv_set_errv("sftp_aio_wait_read: %d",
sftp_get_error(sf->sftp));
priv_set_errv("sftp_async_read: %d", sftp_get_error(sf->sftp));
return -1;
}
if (thrown > 0) {
len = min(thrown, sizeof(buf));
requested = sftp_aio_begin_read(sf, len, &reqs[i]);
thrown -= requested;
bwlimit_wait(bw, requested);
reqs[idx].len = min(thrown, sizeof(buf));
reqs[idx].id = sftp_async_read_begin(sf, reqs[idx].len);
thrown -= reqs[idx].len;
bwlimit_wait(bw, reqs[idx].len);
}
write_bytes = write(fd, buf, read_bytes);
@@ -463,13 +464,13 @@ static int copy_chunk_r2l(struct chunk *c, sftp_file sf, int fd,
}
*counter += write_bytes;
remain -= write_bytes;
remaind -= read_bytes;
}
if (remain < 0) {
priv_set_errv("invalid remain bytes %ld. last async_read bytes %ld. "
if (remaind < 0) {
priv_set_errv("invalid remaind bytes %ld. last async_read bytes %ld. "
"last write bytes %ld",
remain, read_bytes, write_bytes);
remaind, read_bytes, write_bytes);
return -1;
}

View File

@@ -117,11 +117,23 @@ static int ssh_authenticate(ssh_session ssh, struct mscp_ssh_opts *opts)
int auth_bit_mask;
int ret;
/* try publickey auth first */
char *p = opts->passphrase ? opts->passphrase : NULL;
if (ssh_userauth_publickey_auto(ssh, NULL, p) == SSH_AUTH_SUCCESS)
/* 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;
auth_bit_mask = ssh_userauth_list(ssh, NULL);
if (auth_bit_mask & SSH_AUTH_METHOD_PUBLICKEY) {
char *p = opts->passphrase ? opts->passphrase : NULL;
if (ssh_userauth_publickey_auto(ssh, NULL, p) == SSH_AUTH_SUCCESS)
return 0;
}
auth_bit_mask = ssh_userauth_list(ssh, NULL);
if (auth_bit_mask & SSH_AUTH_METHOD_PASSWORD) {
if (!opts->password) {

View File

@@ -226,62 +226,6 @@ def test_copy_file_under_root_to_dir(mscp, src_prefix, dst_prefix):
src.cleanup()
dst.cleanup(preserve_dir = True)
@pytest.mark.parametrize("src_prefix, dst_prefix", param_remote_prefix)
def test_dst_has_suffix_slash(mscp, src_prefix, dst_prefix):
"""
if dst path has suffix '/' like "dir/" and does not exist,
mscp should create dir/ and put dir/src-file-name.
"""
dstdir = "non_existent_dstdir/"
shutil.rmtree(dstdir, ignore_errors=True)
src = File("src", size = 1024 * 1024).make()
dst = File(f"{dstdir}/src")
run2ok([mscp, "-vvv", src_prefix + src.path,
dst_prefix + dstdir])
assert check_same_md5sum(src, dst)
src.cleanup()
dst.cleanup()
param_tilde_paths = [
("src", "localhost:~/dst"),
("localhost:~/src", "dst"),
]
@pytest.mark.parametrize("src_path, dst_path", param_tilde_paths)
def test_remote_path_contains_tilde(mscp, src_path, dst_path):
"""
if remote path contains '~' as prefix, it should be expanded as '.'.
Note that `~user` notation is not supported yet.
"""
def extract_and_expand(path):
path = path if not ':' in path else path[path.index(':')+1:]
return path.replace('~', os.environ["HOME"])
src_f_path = extract_and_expand(src_path)
dst_f_path = extract_and_expand(dst_path)
src = File(src_f_path, size = 1024 * 1024).make()
dst = File(dst_f_path)
run2ok([mscp, "-vvv", src_path, dst_path])
assert check_same_md5sum(src, dst)
src.cleanup(preserve_dir=True)
dst.cleanup(preserve_dir=True)
def test_remote_path_contains_tilde2(mscp):
src = File("src", size = 1024 * 1024).make()
dst = File(f"{os.environ['HOME']}/src")
run2ok([mscp, "-vvv", src.path, f"localhost:~"])
assert check_same_md5sum(src, dst)
src.cleanup(preserve_dir=True)
dst.cleanup(preserve_dir=True)
@pytest.mark.parametrize("src_prefix, dst_prefix", param_remote_prefix)
def test_min_chunk(mscp, src_prefix, dst_prefix):