Skip to content

CFE-2986: Added getdir command to cf-net, copies directory and files#6049

Open
SimonThalvorsen wants to merge 1 commit intocfengine:masterfrom
SimonThalvorsen:CFE-2986
Open

CFE-2986: Added getdir command to cf-net, copies directory and files#6049
SimonThalvorsen wants to merge 1 commit intocfengine:masterfrom
SimonThalvorsen:CFE-2986

Conversation

@SimonThalvorsen
Copy link
Contributor

Ticket: CFE-2986
Changelog: Title

@SimonThalvorsen SimonThalvorsen requested a review from larsewi March 2, 2026 10:02
@larsewi larsewi changed the title Added getdir command to cf-net, copies directory and files CFE-2986: Added getdir command to cf-net, copies directory and files Mar 2, 2026
Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nicer if we extended cf-net get to get both files and directories, instead of having two separate sub-commands. Please investigate how rsync does this and try to do it in a similar way here.

@olehermanse
Copy link
Member

olehermanse commented Mar 4, 2026

It would be nicer if we extended cf-net get to get both files and directories, instead of having two separate sub-commands. Please investigate how rsync does this and try to do it in a similar way here.

@larsewi on the other hand, cf-net was written mainly as a protocol testing tool, and the commands mirror their protocol command names. If the GET protocol command in CFEngine network protocol can only GET a file, it might be a bit confusing that cf-net get is something very different.

There is also some advantage to not being too dynamic with this stuff. If you make a CLI that forces the user to specify if they are copying a file or a folder, you can prevent some annoying / confusing situations.

We could for example make a new subcommand, copy;

$ cf-net copy --file promises.cf
Copying file 1.2.3.4:/var/cfengine/masterfiles/promises.cf -> ./promises.cf
$ cf-net copy --file services/
Error: 'services/' does not look like a file, did you mean --directory?
$ cf-net copy --file services/.
Error: 'services/.' does not look like a file, did you mean --directory?
$ cf-net copy --directory services/
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/
$ cf-net copy services/
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/
$ cf-net copy services
Warning: Ambiguous if 'services' is a file or a directory - consider adding --file or --directory.
Copying directory 1.2.3.4:/var/cfengine/masterfiles/services/ -> ./services/

Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this up a little. Maybe a recursive solution would be more elegant given that it is a recursive problem.

cf-net/cf-net.c Outdated
filedata->print_stats = opts->print_stats;

filedata->ret = ProtocolStatGet(conn, filedata->remote_file,
filedata->local_file, 0644, filedata->print_stats);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should use the remote file permissions (available from the ProtocolStat) instead?

cf-net/cf-net.c Outdated
char curr_dir[PATH_MAX];
snprintf(curr_dir, sizeof(curr_dir), "%s/%s", local_dir, curr);

GetFileData *filedata = calloc(1, sizeof(GetFileData));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not need to be heap allocated

cf-net/cf-net.c Outdated
continue;
}

snprintf(filedata->local_file, PATH_MAX, "%s", curr_dir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check for truncation on all the calls to snprintf

cf-net/cf-net.c Outdated
Comment on lines +1034 to +1035
return invalid_command("getdir");
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable break after return

cf-net/cf-net.c Outdated
if (local_dir != NULL)
{
Log(LOG_LEVEL_INFO,
"Warning: multiple occurences of -o in command, "\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Warning: multiple occurences of -o in command, "\
"Warning: multiple occurrences of -o in command, "\

Log(LOG_LEVEL_ERR, "Failed to get remote file: %s:%s",
conn->this_server, remote_path);
return false;
}

Check notice

Code scanning / CodeQL

Pointer argument is dereferenced without checking for NULL Note

Parameter conn in CFNetGetWithPerms() is dereferenced without an explicit null-check
}

// Helper: Create local directory path
static bool create_local_dir(const char *local_base, const char *subdir,

Check notice

Code scanning / CodeQL

Pointer argument is dereferenced without checking for NULL Note

Parameter conn in CFNetGetWithPerms() is dereferenced without an explicit null-check
char *local_dir = NULL;

int argc = 0;
while (args[argc] != NULL)

Check warning

Code scanning / CodeQL

Poorly documented large function Warning

Poorly documented function: fewer than 2% comments for a function of 115 lines.
@SimonThalvorsen SimonThalvorsen requested a review from larsewi March 9, 2026 14:42
Copy link
Contributor

@larsewi larsewi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a recursion depth limit to prevent a potential malicious or misconfigured server with symlink loops (or deeply nested dirs) to cause stack overflow.

We should probably stick to a maximum path size for the same reasons above.

cf-net/cf-net.c Outdated
char local_full[PATH_MAX];
written = snprintf(local_full, sizeof(local_full), "%s/%s", local_path, item);
if (written < 0 || written >= sizeof(local_full)) {
Log(LOG_LEVEL_ERR, "Path too long for full local path: %s",local_full);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you print the truncated path and not the intended full path. This could be a bit misleading.

if (written < 0 || written >= len) {
Log(LOG_LEVEL_ERR, "Path too long for local path: %s", temp);
free(local_dir);
return -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temp is leaked here

cf-net/cf-net.c Outdated
free(local_dir);
return -1;
}
int written = snprintf(temp, len, "%s/%s", local_dir, basename(remote_dir));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using StringFormat()

cf-net/cf-net.c Outdated
char *remote_dir = args[0];
if (specified_path)
{
size_t len = strlen(local_dir) + strlen(basename(remote_dir)) + 2; // / and '\0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basename() may modify it's argument

}

// Helper: Recursively process directory entries
static void process_dir_recursive(AgentConnection *conn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns void. If all files fail to download, CFNetGetDir still returns 0 (success).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see why this is an issue, failing to stat/get/mkdir will all log their respective errors. So you would rather it return -1 on any one error instead of continue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you were to use it in a script, then it would be easier to check the exit value rather then parsing log messages. Does the other commands exit with 0 on failure?

}

// Helper: Get a single file with permissions
static bool CFNetGetWithPerms(AgentConnection *conn, const char *remote_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File permissions are preserved via ProtocolGet, but created directories don't get the remote directory's permissions applied.

cf-net/cf-net.c Outdated
SeqDestroy(items);
}

static int CFNetGetDir( CFNetOptions *opts, const char *hostname, char **args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static int CFNetGetDir( CFNetOptions *opts, const char *hostname, char **args)
static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args)

cf-net/cf-net.c Outdated
return -1;
}
int written = snprintf(temp, len, "%s/%s", local_dir, basename(remote_dir));
if (written < 0 || written >= len) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (written < 0 || written >= len) {
if (written < 0 || written >= len)
{

cf-net/cf-net.c Outdated
assert(conn != NULL && remote_path != NULL && local_path != NULL);

struct stat perms;
if (!ProtocolStat(conn, remote_path, &perms)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to be consistent with the braces. CONTRIBUTING.md says to put them on a new line. Look through the rest of the code, there are more cases like this.

Suggested change
if (!ProtocolStat(conn, remote_path, &perms)) {
if (!ProtocolStat(conn, remote_path, &perms))
{

cf-net/cf-net.c Outdated

if (has_output_path) {
written = snprintf(path, sizeof(path), "%s/%s/", local_base, subdir);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Allman style, not K&R style (} else {)

Ticket: CFE-2986
Changelog: Title

Signed-off-by: Simon Halvorsen <simon.halvorsen@northern.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants