Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Author: Andreas Fredriksson <deplinenoise (at) gmail (dot) com>
Uploader: deplinenoise@gmail.com
Type: dev/cross
Architecture: m68k-amigaos
Version: 1.0
Version: 1.1

ABOUT
===============================================================================
Expand Down Expand Up @@ -131,7 +131,10 @@ Arguments:

Options:
-fsroot Specifies the file serving directory. The executable
must live in this directory as well. (default: cwd)
must live in this directory as well. (default: the
directory exe_path names, or cwd when it names none.
An Amiga path like c:info names no host directory; a
Windows drive path like c:\dir\prog does)

-port The TCP port to connect to (default: 7001)

Expand Down
53 changes: 50 additions & 3 deletions src/controller.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ static const char * const usage_string =
"\n"
"Options:\n"
" -fsroot Specifies the file serving directory. The executable\n"
" must live in this directory as well. (default: cwd)\n"
" must live in this directory as well. (default: the\n"
" directory exe_path names, or cwd when it names none.\n"
" An Amiga path like c:info names no host directory; a\n"
" Windows drive path like c:\\dir\\prog does)\n"
"\n"
" -port The TCP port to connect to (default: 7001)\n"
"\n"
Expand Down Expand Up @@ -410,6 +413,38 @@ int main(int argc, char** argv)
goto cleanup;
}

/* Without an explicit -fsroot, derive it from exe_path's directory so
* `rl-controller host some/dir/Exe` works without a separate -fsroot. An
* absolute Amiga path (e.g. c:list) is run directly on the target rather
* than served from the host, and has no directory to derive. */
char derived_fsroot[sizeof(ctrl.root_handle.native_path)];

if (!fsroot[0])
{
const char *exe_name;

switch (rl_split_exe_path(sizeof(derived_fsroot), derived_fsroot, &exe_name, ctrl.executable))
{
case RL_EXE_PATH_SPLIT:
fsroot = derived_fsroot;
ctrl.executable = exe_name;
break;

case RL_EXE_PATH_DIR_TOO_LONG:
RL_LOG_CONSOLE(("the directory part of '%s' is longer than %d characters; pass -fsroot <path>\n",
ctrl.executable, (int) sizeof(derived_fsroot) - 1));
goto cleanup;

case RL_EXE_PATH_NO_NAME:
RL_LOG_CONSOLE(("'%s' ends with a path separator and names no executable\n",
ctrl.executable));
goto cleanup;

default:
break;
}
}

#ifdef RL_WIN32
if (0 != rl_init_socket())
goto cleanup;
Expand Down Expand Up @@ -439,13 +474,25 @@ int main(int argc, char** argv)
}
}

/* make sure root path stored doesn't contain a path separator */
/* Make sure the root path stored doesn't end with a path separator, since
* the file server joins it with one when it resolves a name against the
* root. A root that is nothing but a separator needs a spelling that
* survives that join: on Win32 the empty string, which resolves against the
* current drive, and elsewhere the separator itself, since realpath("")
* fails and would deny every request. */
{
size_t dirlen = strlen(ctrl.root_handle.native_path);

if (dirlen > 0 && ctrl.root_handle.native_path[dirlen-1] == NATIVE_PATH_TERMINATOR)
{
#ifdef RL_WIN32
ctrl.root_handle.native_path[dirlen-1] = '\0';
#else
if (dirlen > 1)
{
ctrl.root_handle.native_path[dirlen-1] = '\0';
}
#endif
}
}

Expand Down
69 changes: 69 additions & 0 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,75 @@ int rl_string_copy(size_t dest_size, char *dest, const char *source)
return *source ? -1 : 0;
}

static INLINE int is_path_separator(char ch)
{
if ('/' == ch)
return 1;

#ifdef RL_WIN32
if ('\\' == ch)
return 1;
#endif

return 0;
}

/*
* Splits the directory part off an executable path so that it can serve as the
* file serving root, leaving *name_out pointing at the name the target is asked
* to launch. Separators are '/', and '\' on Win32 as well.
*/
int rl_split_exe_path(size_t dir_size, char *dir_out, const char **name_out, const char *exe_path)
{
const char *colon = rl_strchr(exe_path, ':');
const char *last_separator = NULL;
const char *cursor;
size_t dirlen;

if (NULL != colon)
{
#ifdef RL_WIN32
/* A drive prefix is a colon in second position followed by a separator,
* and names a directory on this machine. A colon anywhere else makes
* this one of the target's own paths, e.g. "c:list", which it resolves
* without any help from the file server. */
if (exe_path + 1 != colon || !is_path_separator(colon[1]))
return RL_EXE_PATH_NO_DIRECTORY;
#else
return RL_EXE_PATH_NO_DIRECTORY;
#endif
}

for (cursor = exe_path; '\0' != *cursor; ++cursor)
{
if (is_path_separator(*cursor))
last_separator = cursor;
}

if (NULL == last_separator)
return RL_EXE_PATH_NO_DIRECTORY;

if ('\0' == last_separator[1])
return RL_EXE_PATH_NO_NAME;

dirlen = (size_t) (last_separator - exe_path);

/* The filesystem root, and the root of a drive, are nothing but their
* separator, so it stays part of the directory: "/prog" would otherwise
* leave no directory at all, and "c:\prog" the drive-relative "c:". */
if (0 == dirlen || (NULL != colon && colon + 1 == last_separator))
++dirlen;

if (dirlen + 1 > dir_size)
return RL_EXE_PATH_DIR_TOO_LONG;

rl_memcpy(dir_out, exe_path, dirlen);
dir_out[dirlen] = '\0';
*name_out = last_separator + 1;

return RL_EXE_PATH_SPLIT;
}

static INLINE void strbuf_append_ch(rl_strbuf_t *buf, char ch)
{
if (buf->buffer != buf->max)
Expand Down
13 changes: 13 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,19 @@ rl_dict_destroy(rl_dict_t *dict);

/* String utilities */

/*
* Result of rl_split_exe_path().
*/
enum
{
RL_EXE_PATH_NO_DIRECTORY, /* a bare name, or a path the target resolves itself */
RL_EXE_PATH_SPLIT, /* dir_out holds the directory, *name_out the name */
RL_EXE_PATH_DIR_TOO_LONG, /* the directory part does not fit in dir_out */
RL_EXE_PATH_NO_NAME /* the path ends with a separator and names nothing */
};

int rl_split_exe_path(size_t dir_size, char *dir_out, const char **name_out, const char *exe_path);

typedef struct rl_strbuf_tag
{
char *buffer;
Expand Down
150 changes: 150 additions & 0 deletions test/rlaunch_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,141 @@ UTEST(strbuf, append_str_len_stops_at_the_requested_length)
ASSERT_STREQ("abc", buffer);
}

/* -------------------------------------------------------------------------
* util.c -- executable path splitting
*
* Without -fsroot the controller serves the directory this splits off, and
* asks the target to launch what is left, so a wrong answer here means the
* wrong directory is exported to the Amiga.
* ------------------------------------------------------------------------- */

UTEST(split_exe_path, a_bare_name_has_no_directory)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY,
rl_split_exe_path(sizeof(dir), dir, &name, "Prog"));
ASSERT_TRUE(NULL == name);
}

UTEST(split_exe_path, splits_the_last_component_off)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "some/dir/Prog"));
ASSERT_STREQ("some/dir", dir);
ASSERT_STREQ("Prog", name);

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "./Prog"));
ASSERT_STREQ(".", dir);
ASSERT_STREQ("Prog", name);
}

/* "/Prog" has to serve the root itself, not the empty string, which reads as
* "no -fsroot given" and would silently serve the current directory instead. */
UTEST(split_exe_path, an_executable_in_the_root_keeps_the_separator)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "/Prog"));
ASSERT_STREQ("/", dir);
ASSERT_STREQ("Prog", name);

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "/opt/Prog"));
ASSERT_STREQ("/opt", dir);
ASSERT_STREQ("Prog", name);
}

/* Paths with a device prefix are resolved by the target itself; there is no
* host-side directory in them to serve. */
UTEST(split_exe_path, a_target_path_has_no_directory)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY,
rl_split_exe_path(sizeof(dir), dir, &name, "c:list"));
ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY,
rl_split_exe_path(sizeof(dir), dir, &name, "DH0:tools/Prog"));
}

UTEST(split_exe_path, a_trailing_separator_names_no_executable)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_NO_NAME,
rl_split_exe_path(sizeof(dir), dir, &name, "some/dir/"));
ASSERT_EQ(RL_EXE_PATH_NO_NAME,
rl_split_exe_path(sizeof(dir), dir, &name, "/"));
}

UTEST(split_exe_path, a_directory_that_does_not_fit_is_reported)
{
char dir[4];
const char *name = NULL;

/* Three characters and the terminator is an exact fit. */
ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "abc/Prog"));
ASSERT_STREQ("abc", dir);

ASSERT_EQ(RL_EXE_PATH_DIR_TOO_LONG,
rl_split_exe_path(sizeof(dir), dir, &name, "abcd/Prog"));
}

#if defined(RL_WIN32)
UTEST(split_exe_path, a_drive_letter_path_is_a_host_path)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "C:\\Users\\user\\some.exe"));
ASSERT_STREQ("C:\\Users\\user", dir);
ASSERT_STREQ("some.exe", name);

/* The drive's own root keeps its separator: "C:" alone would resolve
* against whatever the current directory on that drive happens to be. */
ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "C:\\some.exe"));
ASSERT_STREQ("C:\\", dir);
ASSERT_STREQ("some.exe", name);

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "dir\\Prog"));
ASSERT_STREQ("dir", dir);
ASSERT_STREQ("Prog", name);

/* A colon that is not a drive prefix still belongs to the target. */
ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY,
rl_split_exe_path(sizeof(dir), dir, &name, "C:dir"));
}
#else
/* A backslash is an ordinary filename character here, and a drive letter is
* not a thing, so "C:\..." is just another path only the target can resolve. */
UTEST(split_exe_path, a_backslash_is_not_a_separator)
{
char dir[260];
const char *name = NULL;

ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY,
rl_split_exe_path(sizeof(dir), dir, &name, "C:\\Users\\user\\some.exe"));

ASSERT_EQ(RL_EXE_PATH_SPLIT,
rl_split_exe_path(sizeof(dir), dir, &name, "dir/odd\\name"));
ASSERT_STREQ("dir", dir);
ASSERT_STREQ("odd\\name", name);
}
#endif

/* -------------------------------------------------------------------------
* file_server.c -- path handling
* ------------------------------------------------------------------------- */
Expand All @@ -416,6 +551,21 @@ UTEST(fix_path, joins_against_the_served_root)
ASSERT_STREQ(expected, dest);
}

#if defined(RL_POSIX)
/*
* An executable in the filesystem root leaves "/" as the served root. The join
* doubles the separator there, which POSIX resolves to the same file, and the
* root check has to accept the result rather than deny every request.
*/
UTEST(fix_path, joins_against_the_filesystem_root)
{
char dest[260];

ASSERT_EQ(0, fix_path(dest, sizeof(dest), "rlaunch-no-such-entry", "/"));
ASSERT_STREQ("//rlaunch-no-such-entry", dest);
}
#endif

UTEST(fix_path, rejects_traversal_outside_the_served_root)
{
char dest[260];
Expand Down
Loading