627 lines
17 KiB
C
627 lines
17 KiB
C
#include "include/conf.h"
|
|
|
|
#include <ctype.h>
|
|
#include <strings.h>
|
|
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
|
|
#include "include/def.h"
|
|
#include "include/metadata.h"
|
|
|
|
static bool sh_meta_parse_name(char name[SH_META_NAME_MAX + 1],
|
|
const char *val,
|
|
size_t val_len);
|
|
static bool sh_meta_parse_extensions(
|
|
char exts[SH_META_EXTENSION_MAX + 1][SH_META_EXTENSIONS_MAX],
|
|
size_t *countp,
|
|
const char *val,
|
|
size_t val_len);
|
|
static bool sh_meta_parse_user(uid_t *userp, const char *val, size_t val_len);
|
|
static bool sh_meta_parse_groups(gid_t grps[SH_META_GROUPS_MAX],
|
|
size_t *countp,
|
|
const char *val,
|
|
size_t val_len);
|
|
static bool sh_meta_parse_target(char target[SH_META_PATH_MAX + 1],
|
|
const char *val,
|
|
size_t val_len);
|
|
static bool
|
|
sh_meta_parse_copy(bool *copy, bool copy_set, const char *val, size_t val_len);
|
|
static bool sh_meta_parse_key(char key[SH_META_KEY_MAX + 1],
|
|
const char *val,
|
|
size_t val_len);
|
|
|
|
static uid_t sh_get_uid_by_name(const char *value, size_t length);
|
|
static gid_t sh_get_gid_by_name(const char *value, size_t length);
|
|
static bool sh_meta_is_delim(const char **bufp);
|
|
|
|
bool sh_Metadata_read(sh_Metadata *metadata, const char *buf) {
|
|
if (!metadata || !buf) {
|
|
return false;
|
|
}
|
|
|
|
bool in_meta = false;
|
|
while (*buf) {
|
|
/* Find seperator */
|
|
in_meta = sh_meta_is_delim(&buf);
|
|
if (in_meta) {
|
|
break;
|
|
}
|
|
|
|
/* Skip to next line */
|
|
while (*buf && *buf != '\n' && *buf != '\r') {
|
|
++buf;
|
|
}
|
|
if (*buf == '\r') {
|
|
++buf;
|
|
}
|
|
if (*buf == '\n') {
|
|
++buf;
|
|
}
|
|
}
|
|
|
|
if (!in_meta) {
|
|
sh_print_error("No metadata block found (did you forget # -----...?)");
|
|
return NULL;
|
|
}
|
|
|
|
*metadata->name = '\0';
|
|
metadata->user = (uid_t)-1;
|
|
metadata->extensions_count = 0;
|
|
metadata->groups_count = 0;
|
|
*metadata->target = '\0';
|
|
|
|
bool copy_set = false;
|
|
|
|
while (*buf) {
|
|
if (*buf != '#') {
|
|
sh_print_error("Metadata lines must start with '#'");
|
|
return false;
|
|
}
|
|
|
|
if (sh_meta_is_delim(&buf)) {
|
|
in_meta = false;
|
|
break;
|
|
}
|
|
|
|
/* Skip '#' if not delimiter */
|
|
++buf;
|
|
|
|
/* Skip any whitespace after '#' */
|
|
if (!sh_str_lstrip(&buf)) {
|
|
return false;
|
|
}
|
|
const char *key = buf;
|
|
|
|
/* Locate ':' to separate key and value */
|
|
const char *colon = strchr(buf, ':');
|
|
if (!colon) {
|
|
sh_print_error(
|
|
"Metadata lines must have a ':' key-value separator");
|
|
return false;
|
|
}
|
|
|
|
/* Trim trailing whitespace before ':' */
|
|
const char *key_end = colon - 1;
|
|
while (key_end > key && (*key_end == ' ' || *key_end == '\t')) {
|
|
--key_end;
|
|
}
|
|
|
|
const size_t key_len =
|
|
(size_t)(key_end >= key ? (key_end - key + 1) : 0);
|
|
if (key_len == 0) {
|
|
sh_print_errorf("Metadata key '%.*s' must not be empty",
|
|
(int)key_len, key);
|
|
return false;
|
|
}
|
|
|
|
/* Start of value */
|
|
const char *val = colon + 1;
|
|
while (*val == ' ' || *val == '\t') {
|
|
++val;
|
|
}
|
|
|
|
/* Find end of value (until \n, \r or \0) */
|
|
const char *val_end = val;
|
|
while (*val_end && *val_end != '\n' && *val_end != '\r') {
|
|
++val_end;
|
|
}
|
|
|
|
/* Trim trailing whitespace from value */
|
|
const char *value_end = val_end - 1;
|
|
while (value_end >= val && (*value_end == ' ' || *value_end == '\t')) {
|
|
--value_end;
|
|
}
|
|
|
|
size_t val_len = (size_t)(value_end >= val ? (value_end - val + 1) : 0);
|
|
if (val_len == 0) {
|
|
sh_print_errorf("Metadata key '%.*s' value must not be empty",
|
|
(int)key_len, key);
|
|
return false;
|
|
}
|
|
|
|
/* Safety check for newlines in key/value */
|
|
if (sh_strnchr(key, '\n', key_len) || sh_strnchr(key, '\r', key_len) ||
|
|
sh_strnchr(val, '\n', val_len) || sh_strnchr(val, '\r', val_len)) {
|
|
sh_print_errorf(
|
|
"Metadata key '%.*s' or value contains an unexpected newline",
|
|
(int)key_len, key);
|
|
return false;
|
|
}
|
|
|
|
bool ret = false;
|
|
|
|
if (key_len == 4 && strncasecmp(key, "Name", 4) == 0) {
|
|
ret = sh_meta_parse_name(metadata->name, val, val_len);
|
|
} else if (key_len == 10 && strncasecmp(key, "Extensions", 10) == 0) {
|
|
ret = sh_meta_parse_extensions(metadata->extensions,
|
|
&metadata->extensions_count, val,
|
|
val_len);
|
|
} else if (key_len == 4 && strncasecmp(key, "User", 4) == 0) {
|
|
ret = sh_meta_parse_user(&metadata->user, val, val_len);
|
|
} else if (key_len == 6 && strncasecmp(key, "Groups", 6) == 0) {
|
|
ret = sh_meta_parse_groups(metadata->groups,
|
|
&metadata->groups_count, val, val_len);
|
|
} else if (key_len == 6 && strncasecmp(key, "Target", 6) == 0) {
|
|
ret = sh_meta_parse_target(metadata->target, val, val_len);
|
|
} else if (key_len == 4 && strncasecmp(key, "Copy", 4) == 0) {
|
|
ret = sh_meta_parse_copy(&metadata->copy, copy_set, val, val_len);
|
|
copy_set = ret;
|
|
} else if (key_len == 3 && strncasecmp(key, "Key", 3) == 0) {
|
|
ret = sh_meta_parse_key(metadata->key, val, val_len);
|
|
} else {
|
|
sh_print_errorf("Unknown metadata key '%.*s'", (int)key_len, key);
|
|
return false;
|
|
}
|
|
|
|
if (!ret) {
|
|
return false;
|
|
}
|
|
|
|
/* Advance buf to the next line */
|
|
buf = val_end;
|
|
if (*buf == '\r' && *(buf + 1) == '\n') {
|
|
buf += 2;
|
|
} else if (*buf == '\r' || *buf == '\n') {
|
|
++buf;
|
|
}
|
|
}
|
|
|
|
if (in_meta) {
|
|
sh_print_error(
|
|
"No closing metadata delimiter found (did you forget #-----...?)");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void sh_Metadata_print(const sh_Metadata *metadata) {
|
|
if (!metadata) {
|
|
return;
|
|
}
|
|
|
|
printf("Name: %s\n", metadata->name);
|
|
|
|
if (metadata->extensions_count > 0) {
|
|
printf("Extensions (%zu):", metadata->extensions_count);
|
|
|
|
for (size_t idx = 0; idx < metadata->extensions_count; ++idx) {
|
|
printf(" %s%s", metadata->extensions[idx],
|
|
idx + 1 == metadata->extensions_count ? "" : ",");
|
|
}
|
|
|
|
printf("\n");
|
|
} else {
|
|
printf("Extensions: (none)\n");
|
|
}
|
|
|
|
const struct passwd *pwd = getpwuid(metadata->user);
|
|
printf("User: %s (%d)\n", pwd ? pwd->pw_name : "unknown", metadata->user);
|
|
|
|
if (metadata->groups_count > 0) {
|
|
printf("groups (%zu):", metadata->groups_count);
|
|
|
|
for (size_t idx = 0; idx < metadata->groups_count; ++idx) {
|
|
const struct group *grp = getgrgid(metadata->groups[idx]);
|
|
|
|
printf(" %s (%u)%s", grp->gr_name, grp->gr_gid,
|
|
idx + 1 == metadata->groups_count ? "" : ",");
|
|
}
|
|
|
|
printf("\n");
|
|
} else {
|
|
printf("Groups: (none)\n");
|
|
}
|
|
|
|
printf("Target: %s\n", metadata->target);
|
|
printf("Copy: %s\n", metadata->copy ? "yes" : "no");
|
|
|
|
if (*metadata->key) {
|
|
printf("Key: %s\n", metadata->key);
|
|
}
|
|
}
|
|
|
|
bool sh_Metadata_is_full_script(const sh_Metadata *metadata) {
|
|
if (!metadata) {
|
|
return false;
|
|
}
|
|
|
|
if (*metadata->target == '\0') {
|
|
return false;
|
|
}
|
|
|
|
if (metadata->user == (uid_t)-1 || metadata->user == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (metadata->extensions_count > 0) {
|
|
for (size_t idx = 0; idx < metadata->extensions_count; ++idx) {
|
|
if (!*metadata->extensions[idx]) {
|
|
return false;
|
|
}
|
|
|
|
if (!sh_is_lowstr(metadata->extensions[idx]) ||
|
|
!sh_is_valid_identifier(metadata->extensions[idx],
|
|
strlen(metadata->extensions[idx]))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (metadata->groups_count > 0) {
|
|
for (size_t idx = 0; idx < metadata->groups_count; ++idx) {
|
|
if (metadata->groups[idx] == (gid_t)-1 ||
|
|
metadata->groups[idx] == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_name(char name[SH_META_NAME_MAX + 1],
|
|
const char *val,
|
|
size_t val_len) {
|
|
if (val_len > SH_META_NAME_MAX) {
|
|
sh_print_errorf("Deployer name '%.*s' too long", (int)val_len, val);
|
|
return false;
|
|
}
|
|
if (*name != '\0') {
|
|
sh_print_errorf("Deployer name '%s' already set", name);
|
|
return false;
|
|
}
|
|
|
|
strncpy(name, val, val_len);
|
|
name[SH_META_NAME_MAX] = '\0';
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_extensions(
|
|
char exts[SH_META_EXTENSION_MAX + 1][SH_META_EXTENSIONS_MAX],
|
|
size_t *countp,
|
|
const char *val,
|
|
size_t val_len) {
|
|
if (*countp != 0) {
|
|
sh_print_error("Deployer extensions already set");
|
|
return false;
|
|
}
|
|
|
|
size_t count = 0;
|
|
const char *ptr = val;
|
|
const char *end = val + val_len;
|
|
|
|
while (ptr < end) {
|
|
/* Skip leading whitespace */
|
|
while (ptr < end && (*ptr == ' ' || *ptr == '\t')) {
|
|
++ptr;
|
|
}
|
|
if (ptr >= end) {
|
|
break;
|
|
}
|
|
|
|
const char *tok_start = ptr;
|
|
|
|
/* Find end of token */
|
|
while (ptr < end && *ptr != ' ' && *ptr != '\t') {
|
|
++ptr;
|
|
}
|
|
const char *tok_end = ptr;
|
|
|
|
const size_t len = (size_t)(tok_end - tok_start);
|
|
if (len == 0) {
|
|
sh_print_error("Length 0 extension is invalid");
|
|
return false;
|
|
}
|
|
|
|
if (count >= SH_META_EXTENSIONS_MAX) {
|
|
sh_print_error("Too many deployer extensions");
|
|
return false;
|
|
}
|
|
|
|
if (len > SH_META_EXTENSION_MAX) {
|
|
sh_print_errorf("Extension identifier '%.*s' too long", (int)len,
|
|
tok_start);
|
|
return false;
|
|
}
|
|
|
|
if (!sh_is_valid_identifier(tok_start, len)) {
|
|
sh_print_errorf("Invalid extension identifier '%.*s'", (int)len,
|
|
tok_start);
|
|
return false;
|
|
}
|
|
|
|
/* Lowercase into struct storage */
|
|
for (size_t idx = 0; idx < len; ++idx) {
|
|
exts[count][idx] = (char)tolower((unsigned char)tok_start[idx]);
|
|
}
|
|
exts[count][len] = '\0';
|
|
|
|
/* Check uniqueness */
|
|
for (size_t idx = 0; idx < count; ++idx) {
|
|
if (strcmp(exts[idx], exts[count]) == 0) {
|
|
sh_print_errorf("Duplicate extension identifier '%s'",
|
|
exts[idx]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
++count;
|
|
}
|
|
|
|
if (count == 0) {
|
|
sh_print_error(
|
|
"Deployer 'Extensions' must have at least one identifier");
|
|
return false;
|
|
}
|
|
|
|
*countp = count;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_user(uid_t *userp, const char *val, size_t val_len) {
|
|
uid_t user = *userp;
|
|
|
|
if (user != (uid_t)-1 && user != 0) {
|
|
sh_print_errorf("Deployer user '%u' already set", *userp);
|
|
return false;
|
|
}
|
|
|
|
user = sh_get_uid_by_name(val, val_len);
|
|
if (user == (uid_t)-1 || user == 0) {
|
|
return false;
|
|
}
|
|
|
|
*userp = user;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_groups(gid_t grps[SH_META_GROUPS_MAX],
|
|
size_t *countp,
|
|
const char *val,
|
|
size_t val_len) {
|
|
if (*countp != 0) {
|
|
sh_print_errorf("Deployer groups (%zu) already set", *countp);
|
|
return false;
|
|
}
|
|
|
|
size_t count = 0;
|
|
const char *ptr = val;
|
|
const char *end = val + val_len;
|
|
|
|
while (ptr < end) {
|
|
/* Skip leading whitespace */
|
|
while (ptr < end && (*ptr == ' ' || *ptr == '\t')) {
|
|
++ptr;
|
|
}
|
|
if (ptr >= end) {
|
|
break;
|
|
}
|
|
|
|
const char *tok_start = ptr;
|
|
|
|
/* Find end of token */
|
|
while (ptr < end && *ptr != ' ' && *ptr != '\t') {
|
|
++ptr;
|
|
}
|
|
const char *tok_end = ptr;
|
|
|
|
size_t len = (size_t)(tok_end - tok_start);
|
|
if (len == 0) {
|
|
sh_print_error("Length 0 group name is invalid");
|
|
return false;
|
|
}
|
|
|
|
if (count >= SH_META_GROUPS_MAX) {
|
|
sh_print_error("Too many deployer groups");
|
|
return false;
|
|
}
|
|
|
|
const gid_t gid = sh_get_gid_by_name(tok_start, len);
|
|
if (gid == (gid_t)-1 || gid == 0) {
|
|
return false;
|
|
}
|
|
|
|
/* Ensure uniqueness */
|
|
for (size_t idx = 0; idx < count; ++idx) {
|
|
if (grps[idx] == gid) {
|
|
sh_print_errorf("Duplicate group entry '%.*s'", (int)len,
|
|
tok_start);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
grps[count++] = gid;
|
|
}
|
|
|
|
if (count == 0) {
|
|
sh_print_error("Deployer 'Groups' must have at least one group");
|
|
return false;
|
|
}
|
|
|
|
*countp = count;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_target(char target[SH_META_PATH_MAX + 1],
|
|
const char *val,
|
|
size_t val_len) {
|
|
|
|
if (val_len > SH_META_PATH_MAX) {
|
|
sh_print_errorf("Deployer target '%.*s' too long", (int)val_len, val);
|
|
return false;
|
|
}
|
|
if (*target != '\0') {
|
|
sh_print_errorf("Deployer target '%s' already set", target);
|
|
return false;
|
|
}
|
|
|
|
strncpy(target, val, val_len);
|
|
target[SH_META_PATH_MAX] = '\0';
|
|
|
|
return target;
|
|
}
|
|
|
|
static bool
|
|
sh_meta_parse_copy(bool *copy, bool copy_set, const char *val, size_t val_len) {
|
|
if (copy_set) {
|
|
sh_print_error("Deployer copy flag already set");
|
|
return false;
|
|
}
|
|
|
|
if ((val_len == 4 && strncmp(val, "true", 4) == 0) ||
|
|
(val_len == 3 && strncmp(val, "yes", 3) == 0) ||
|
|
(val_len == 1 && *val == '1')) {
|
|
*copy = true;
|
|
} else if ((val_len == 5 && strncmp(val, "false", 5) == 0) ||
|
|
(val_len == 2 && strncmp(val, "no", 2) == 0) ||
|
|
(val_len == 1 && *val == '0')) {
|
|
*copy = false;
|
|
} else {
|
|
sh_print_error("Invalid value for deployer flag copy (allowed: "
|
|
"true|yes|1 or false|no|0)");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sh_meta_parse_key(char key[SH_META_KEY_MAX + 1],
|
|
const char *val,
|
|
size_t val_len) {
|
|
if (val_len > SH_META_KEY_MAX) {
|
|
sh_print_errorf("Deployer key '%.*s' too long", (int)val_len, val);
|
|
return false;
|
|
}
|
|
if (*key != '\0') {
|
|
sh_print_errorf("Deployer key '%s' already set", key);
|
|
return false;
|
|
}
|
|
|
|
strncpy(key, val, val_len);
|
|
key[SH_META_KEY_MAX] = '\0';
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Helpers */
|
|
|
|
static uid_t sh_get_uid_by_name(const char *value, size_t length) {
|
|
if (length > SH_META_USERNAME_MAX) {
|
|
sh_print_errorf("Username '%.*s' too long", (int)length, value);
|
|
return (uid_t)-1;
|
|
}
|
|
|
|
char name[SH_META_USERNAME_MAX + 1] = {0};
|
|
strncpy(name, value, length);
|
|
|
|
const struct passwd *pwd = getpwnam(name);
|
|
|
|
if (pwd) {
|
|
if (pwd->pw_uid == 0) {
|
|
sh_print_error(
|
|
"Deployers as the root user are strictly prohibited");
|
|
return (uid_t)-1;
|
|
}
|
|
|
|
return pwd->pw_uid;
|
|
}
|
|
|
|
sh_print_errorf("No user '%s' found", name);
|
|
return (uid_t)-1;
|
|
}
|
|
|
|
static gid_t sh_get_gid_by_name(const char *value, size_t length) {
|
|
if (length > SH_META_GROUP_MAX) {
|
|
sh_print_errorf("Group '%.*s' too long", (int)length, value);
|
|
return (gid_t)-1;
|
|
}
|
|
|
|
char name[SH_META_GROUP_MAX + 1] = {0};
|
|
strncpy(name, value, length);
|
|
|
|
const struct group *grp = getgrnam(name);
|
|
|
|
if (grp) {
|
|
if (grp->gr_gid == 0) {
|
|
sh_print_error(
|
|
"Deployers as the root group are strictly prohibited");
|
|
return (gid_t)-1;
|
|
}
|
|
|
|
return grp->gr_gid;
|
|
}
|
|
|
|
sh_print_errorf("No group '%s' found", name);
|
|
return (gid_t)-1;
|
|
}
|
|
|
|
static bool sh_meta_is_delim(const char **bufp) {
|
|
const char *buf = *bufp;
|
|
|
|
if (*buf != '#') {
|
|
return false;
|
|
}
|
|
|
|
/* Skip '#' */
|
|
++buf;
|
|
|
|
/* Skip whispace after '#' */
|
|
if (!sh_str_lstrip(&buf)) {
|
|
return false;
|
|
}
|
|
|
|
/* Check for delimiter line: at least 5 dashes */
|
|
size_t dash_count = 0;
|
|
while (*buf == '-') {
|
|
++dash_count;
|
|
++buf;
|
|
}
|
|
|
|
if (dash_count >= 5) {
|
|
/* Found delimiter line, now skip rest of line */
|
|
while (*buf && *buf != '\n' && *buf != '\r') {
|
|
++buf;
|
|
}
|
|
|
|
/* Skip newline(s) (handle CR, LF, CRLF) */
|
|
if (*buf == '\r') {
|
|
++buf;
|
|
}
|
|
if (*buf == '\n') {
|
|
++buf;
|
|
}
|
|
|
|
*bufp = buf;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|