diff options
author | 2025-04-09 13:24:53 -0700 | |
---|---|---|
committer | 2025-04-11 00:15:18 -0700 | |
commit | f277f41d83d450a1f28d350bb7d6da075d1d2741 (patch) | |
tree | e197a7de7e26b47cec57a46acdde4b6c74889ad5 /src | |
download | github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.tar.gz github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.tar.zst github-mirror-f277f41d83d450a1f28d350bb7d6da075d1d2741.zip |
Initial Commit
Add cmake github workflow
Install dependencies in gh action
Fix missing directory
Fix compiler errors
Fix group name to gid conversion
Force cjson to statically link
Add header guards to query headers
Add install and cpack to CMakeLists.txt
Add config search and CLI arg parsing
Improve docs for git.c
Fix program continuing on -h flag
Diffstat (limited to 'src')
-rw-r--r-- | src/alloc.h | 27 | ||||
-rw-r--r-- | src/buffer.c | 51 | ||||
-rw-r--r-- | src/buffer.h | 34 | ||||
-rw-r--r-- | src/config.c | 307 | ||||
-rw-r--r-- | src/config.h | 51 | ||||
-rw-r--r-- | src/git.c | 362 | ||||
-rw-r--r-- | src/git.h | 24 | ||||
-rw-r--r-- | src/github.c | 290 | ||||
-rw-r--r-- | src/github.h | 30 | ||||
-rw-r--r-- | src/github_types.c | 151 | ||||
-rw-r--r-- | src/github_types.h | 28 | ||||
-rw-r--r-- | src/main.c | 132 | ||||
-rw-r--r-- | src/precheck.c | 112 | ||||
-rw-r--r-- | src/precheck.h | 16 |
14 files changed, 1615 insertions, 0 deletions
diff --git a/src/alloc.h b/src/alloc.h new file mode 100644 index 0000000..0a09a30 --- /dev/null +++ b/src/alloc.h @@ -0,0 +1,27 @@ +// +// Created by Anshul Gupta on 4/9/25. +// + +#ifndef ALLOC_H +#define ALLOC_H + +#ifdef TEST_ALLOC +#include <setjmp.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> + +#include <cmocka.h> +#define gmalloc(size) test_malloc(size) +#define gcalloc(num, size) test_calloc(num, size) +#define grealloc(ptr, size) test_realloc(ptr, size) +#define gfree(ptr) test_free(ptr) +#else +#include <stdlib.h> +#define gmalloc(size) malloc(size) +#define gcalloc(num, size) calloc(num, size) +#define grealloc(ptr, size) realloc(ptr, size) +#define gfree(ptr) free(ptr) +#endif + +#endif // ALLOC_H diff --git a/src/buffer.c b/src/buffer.c new file mode 100644 index 0000000..51a3e3c --- /dev/null +++ b/src/buffer.c @@ -0,0 +1,51 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#include "buffer.h" + +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +#include "alloc.h" + +buffer_t buffer_new(size_t cap) +{ + buffer_t buf; + + if (cap == 0) + return (buffer_t) {NULL, 0, 0}; + + cap = (cap + 7) & ~7; // Align to 8 bytes + buf.data = gmalloc(cap); + if (!buf.data) + abort(); + + return (buffer_t) {buf.data, 0, cap}; +} + +void buffer_free(buffer_t buf) { gfree(buf.data); } + +void buffer_reserve(buffer_t *buf, size_t cap) +{ + if (buf->cap >= cap) + return; + + cap = (cap + 7) & ~7; // Align to 8 bytes + uint8_t *new_data = grealloc(buf->data, cap); + if (!new_data) + abort(); + buf->data = new_data; + buf->cap = cap; +} + +void buffer_append(buffer_t *buf, const void *data, size_t len) +{ + if (buf->len + len > buf->cap) + buffer_reserve(buf, buf->len + len); + + memcpy(buf->data + buf->len, data, len); + buf->len += len; + assert(buf->len <= buf->cap); +} diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..ea7c7c3 --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,34 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#ifndef BUFFER_H +#define BUFFER_H + +#include <stddef.h> +#include <stdint.h> + +typedef struct { + /// Pointer to the start of the buffer + uint8_t *data; + /// Size of the buffer + size_t len; + /// Size of the allocated buffer + size_t cap; +} buffer_t; + +/// Create a new buffer with the given initial capacity +buffer_t buffer_new(size_t cap); + +/// Free the buffer +void buffer_free(buffer_t buf); + +/// Resize the buffer to the given capacity +/// Does nothing if the new capacity is less than or equal to the +/// current capacity. +void buffer_reserve(buffer_t *buf, size_t cap); + +/// Append data to the buffer +void buffer_append(buffer_t *buf, const void *data, size_t len); + +#endif // BUFFER_H diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..d20f4f5 --- /dev/null +++ b/src/config.c @@ -0,0 +1,307 @@ +// +// Created by Anshul Gupta on 4/6/25. +// + +#include "config.h" + +#include <ctype.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> + +const char *config_locations[] = { + "/etc/github_mirror/config.ini", + "/usr/local/etc/github_mirror/config.ini", + "/usr/local/github_mirror/config.ini", + "config.ini", + NULL, +}; + +enum config_section { + section_none, + section_github, + section_git, +}; + + +static char *file_read(const char *path, size_t *size_out) +{ + char *contents = NULL; + + // Open the file for reading + const int fd = open(path, O_RDONLY); + if (fd < 0) { + perror("Error reading config file"); + return NULL; + } + + // Stat the file to get its size + struct stat st; + if (fstat(fd, &st) < 0) { + perror("Error getting file size"); + goto end; + } + + // Get the size of the file + const size_t size = st.st_size; + if (size == 0) { + fprintf(stderr, "Error reading config file: file is empty\n"); + goto end; + } + + // Map the file into memory + contents = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + if (contents == MAP_FAILED) { + perror("Error reading config file"); + goto end; + } + + // Output file size + if (size_out) + *size_out = size; + +end: + close(fd); + return contents; +} + +static char *trim(char *start, char *end) +{ + while (start < end && isspace(*start)) + start++; + while (end > start && isspace(*(end - 1))) + end--; + *end = '\0'; + return start; +} + +static int parse_line_inner(struct config *cfg, enum config_section section, + char *key, char *value) +{ + char *endptr; + struct passwd *pw; + struct group *gr; + + switch (section) { + case section_none: + fprintf(stderr, + "Unexpected key-value pair outside of section: %s=%s\n", + key, value); + return -1; + case section_github: + if (!strcmp(key, "endpoint")) + cfg->endpoint = value; + else if (!strcmp(key, "token")) + cfg->token = value; + else if (!strcmp(key, "user_agent")) + cfg->user_agent = value; + else if (!strcmp(key, "owner")) + cfg->owner = value; + else { + fprintf(stderr, + "Error parsing config file: unknown key: %s\n", + key); + return -1; + } + break; + case section_git: + if (!strcmp(key, "base")) + cfg->git_base = value; + else if (!strcmp(key, "owner")) { + // If the value is a number, set the owner to that + // number + cfg->git_owner = strtol(value, &endptr, 10); + if (*endptr == '\0') + return 0; + + // Otherwise, convert the string into a uid + if (!((pw = getpwnam(value)))) { + fprintf(stderr, + "Error parsing config file: unknown " + "user: %s\n", + value); + return -1; + } + cfg->git_owner = pw->pw_uid; + } else if (!strcmp(key, "group")) { + // If the value is a number, set the group to that + // number + cfg->git_group = strtol(value, &endptr, 10); + if (*endptr == '\0') + return 0; + + // Otherwise, convert the string into a gid + if (!((gr = getgrnam(value)))) { + fprintf(stderr, + "Error parsing config file: unknown " + "group: %s\n", + value); + return -1; + } + cfg->git_group = gr->gr_gid; + } else { + fprintf(stderr, + "Error parsing config file: unknown key: %s\n", + key); + return -1; + } + break; + } + return 0; +} + +static int parse_line(struct config *cfg, char *line, + enum config_section *section) +{ + switch (*line) { + case ';': + case '#': + case '\0': + // Ignore comments and empty lines + return 0; + case '[': { + // Handle section headers + char *close = strchr(line, ']'); + if (!close) { + fprintf(stderr, + "Error parsing config file: invalid section " + "header: %s\n", + line); + return -1; + } + *close = '\0'; + char *section_name = trim(line + 1, close); + if (!strcmp(section_name, "github")) + *section = section_github; + else if (!strcmp(section_name, "git")) + *section = section_git; + else { + fprintf(stderr, + "Error parsing config file: unknown section: " + "%s\n", + section_name); + return -1; + } + return 0; + } + default: { + // Handle key-value pairs + char *line_end = line + strlen(line); + char *equals = strchr(line, '='); + if (!equals) { + fprintf(stderr, + "Error parsing config file: invalid line: %s\n", + line); + return -1; + } + *equals = '\0'; + char *key = trim(line, equals); + char *value = trim(equals + 1, line_end); + return parse_line_inner(cfg, *section, key, value); + } + } +} + +static int config_parse(struct config *cfg) +{ + char *ptr = cfg->contents; + char *end = cfg->contents + cfg->contents_len; + enum config_section section = section_none; + + while (ptr < end) { + // Find the end of the line + char *newline = memchr(ptr, '\n', end - ptr); + char *line_end = newline ? newline : end; + + // Handle line endings + char *actual_end = line_end; + if (actual_end > ptr && *(actual_end - 1) == '\r') + actual_end--; + + // Null-terminate the line + if (line_end < end) + *line_end = '\0'; + + // Trim whitespace + char *line = trim(ptr, actual_end); + + // Parse the line + if (parse_line(cfg, line, §ion) < 0) + return -1; + + // Move to the next line + ptr = newline ? newline + 1 : end; + } + + return 0; +} + +static void config_defaults(struct config *cfg) +{ + cfg->endpoint = GH_DEFAULT_ENDPOINT; + cfg->user_agent = GH_DEFAULT_USER_AGENT; + cfg->git_base = "/srv/git"; + cfg->git_owner = getuid(); + cfg->git_group = getgid(); +} + +static int config_validate(const struct config *cfg) +{ + if (!cfg->token) { + fprintf(stderr, + "Error: missing required field: github.token\n"); + return -1; + } + if (!cfg->owner) { + fprintf(stderr, + "Error: missing required field: github.owner\n"); + return -1; + } + return 0; +} + + +struct config *config_read(const char *path) +{ + struct config *cfg = calloc(1, sizeof(*cfg)); + if (!cfg) { + perror("error allocating config"); + return NULL; + } + config_defaults(cfg); + + // Read the config file + cfg->contents = file_read(path, &cfg->contents_len); + if (!cfg->contents) + goto fail; + + // Parse the config file + if (config_parse(cfg) < 0) + goto fail2; + + // Validate the config file + if (config_validate(cfg) < 0) + goto fail2; + + return cfg; + +fail2: + munmap(cfg->contents, cfg->contents_len); +fail: + free(cfg); + return NULL; +} + +void config_free(struct config *config) +{ + if (!config || !config->contents) + return; + munmap(config->contents, config->contents_len); + free(config); +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..0d7d827 --- /dev/null +++ b/src/config.h @@ -0,0 +1,51 @@ +// +// Created by Anshul Gupta on 4/6/25. +// + +#ifndef CONFIG_H +#define CONFIG_H + +#include <stdlib.h> +#include <sys/types.h> + +#define GH_DEFAULT_ENDPOINT "https://api.github.com/graphql" +#define GH_DEFAULT_USER_AGENT "github_mirror/0.1" + +extern const char *config_locations[]; + +struct config { + /// The content of the config file + char *contents; + size_t contents_len; + + const char *endpoint; + const char *token; + const char *user_agent; + + /// The owner of the repositories + const char *owner; + + /// The filepath to the git mirrors + /// Default: /srv/git + const char *git_base; + /// User to give ownership of the git mirrors + uid_t git_owner; + /// Group to give ownership of the git mirrors + gid_t git_group; +}; + +/** + * Read the INI config file at the given path. + * Will print to stderr errors if the file cannot be read. + * @param path Path to the config file + * @return A pointer to the config struct, or NULL on error + */ +struct config *config_read(const char *path); + +/** + * Free the config struct + * @param config The config struct to free + */ +void config_free(struct config *config); + +#endif // CONFIG_H diff --git a/src/git.c b/src/git.c new file mode 100644 index 0000000..6720e54 --- /dev/null +++ b/src/git.c @@ -0,0 +1,362 @@ +// +// Created by Anshul Gupta on 4/6/25. +// + +#include "git.h" + +#include <errno.h> +#include <grp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +extern char **environ; + +/** + * Constructs the full path to the git repository based on the base path, owner, + * and name. If the name is NULL, it constructs the path to the owner's + * directory. + * @param base Base path for the git repository + * @param owner Owner of the repository + * @param name Name of the repository + * @return A string containing the full path to the git repository or owner's + * directory. + */ +static char *get_git_path(const char *base, const char *owner, const char *name) +{ + if (!base || !owner) + return NULL; + + const char *format = name ? "%s/%s/%s.git" : "%s/%s"; + + // Calculate the length of the string + const int len = snprintf(NULL, 0, format, base, owner, name); + if (len < 0) + return NULL; + + // Allocate memory for the string + char *path = malloc(len + 1); + if (!path) + return NULL; + + // Format the string + if (snprintf(path, len + 1, format, base, owner, name) < 0) { + free(path); + return NULL; + } + return path; +} + +/** + * Adds authentication information to the HTTPS URL for git. + * @param url The HTTPS URL to modify + * @param user The username for authentication + * @param token The token for authentication + * @return A new string containing the modified URL with authentication + * information + */ +static char *add_url_auth(const char *url, const char *user, const char *token) +{ + const char *https_prefix = "https://"; + const size_t prefix_len = strlen(https_prefix); + size_t new_len; + char *new_url; + + if (!url || !user || !token) + return NULL; + + // Find the position of "https://" + if (strncmp(url, https_prefix, prefix_len) != 0) { + fprintf(stderr, "Error: URL does not start with https://\n"); + return NULL; + } + + // Calculate the length of the new URL + new_len = strlen(url) + strlen(user) + strlen(token) + + 2; // 2 for "@" and ":" + + // Allocate memory for the new URL + if (!((new_url = malloc(new_len + 1)))) { + perror("malloc"); + return NULL; + } + + // Construct the new URL + snprintf(new_url, new_len + 1, "https://%s:%s@%s", user, token, + url + prefix_len); + + return new_url; +} + +/** + * Drops the permissions of the current process to the specified user and group. + * @param ctx Repository context + * @return 0 on success, -1 on error + */ +static int drop_perms(const struct repo_ctx *ctx) +{ + // Drop supplementary groups + if (setgroups(0, NULL) != 0) { + perror("setgroups"); + return -1; + } + // Set gid + if (setgid(ctx->cfg->git_group) == -1) { + perror("setgid"); + return -1; + } + // Set uid + if (setuid(ctx->cfg->git_owner) == -1) { + perror("setuid"); + return -1; + } + return 0; +} + +/** + * Checks if the git repository at the specified path is a mirror. + * @param path Path to the git repository + * @param ctx Repository context + * @return 1 if the repository is a mirror, 0 if not + */ +static int contains_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return 0; + } + + if (pid == 0) { + // Child process + + // Redirect stdout to /dev/null + const int devnull = open("/dev/null", O_WRONLY); + if (devnull == -1) { + perror("open"); + _exit(127); + } + if (dup2(devnull, STDOUT_FILENO) == -1) { + perror("dup2"); + close(devnull); + _exit(127); + } + close(devnull); + + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + char *args[] = { + "git", "--git-dir", (char *) path, + "config", "--get", "remote.origin.mirror", + NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return 0; + } + if (WIFEXITED(status)) { + // Check if the exit status is 0 (success) + if (WEXITSTATUS(status) == 0) + return 1; // Repo exists + if (WEXITSTATUS(status) == 1) + return 0; // Repo does not exist + } + fprintf(stderr, "Error: git command failed with status %d\n", + WEXITSTATUS(status)); + return 0; // Error occurred +} + +/** + * Creates a mirror of the git repository at the specified path. + * @param path Full path to the git repository + * @param ctx Context containing the repository information + * @return 0 on success, -1 on error + */ +static int create_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + // Child process + // Convert the URL to a format that git can use + char *url = add_url_auth(ctx->url, ctx->username, + ctx->cfg->token); + + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + + char *args[] = { + "git", "clone", "--mirror", + url, (char *) path, NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return -1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + return 0; // Success + fprintf(stderr, "Error: git clone failed with status %d\n", + WEXITSTATUS(status)); + return -1; // Error occurred +} + +/** + * Creates the directory structure for the git repository. + * @param ctx Repository context + * @return 0 on success, -1 on error + */ +static int create_git_path(const struct repo_ctx *ctx) +{ + // Create owner directory if it doesn't exist + char *owner_path = + get_git_path(ctx->cfg->git_base, ctx->cfg->owner, NULL); + if (!owner_path) + return -1; + if (mkdir(owner_path, 0755) == -1 && errno != EEXIST) { + perror("mkdir"); + free(owner_path); + return -1; + } + // Set the permissions of the owner directory to 0775 + if (chmod(owner_path, 0775) == -1) { + perror("chmod"); + free(owner_path); + return -1; + } + free(owner_path); + + // Create repo directory if it doesn't exist + char *repo_path = get_git_path(ctx->cfg->git_base, ctx->cfg->owner, + ctx->name); + if (!repo_path) + return -1; + if (mkdir(repo_path, 0755) == -1 && errno != EEXIST) { + perror("mkdir"); + free(repo_path); + return -1; + } + + // Chown the repo directory to the specified user and group + if (chown(repo_path, ctx->cfg->git_owner, ctx->cfg->git_group) == -1) { + perror("chown"); + free(repo_path); + return -1; + } + + free(repo_path); + return 0; +} + +/** + * Updates the git repository at the specified path from the remote. + * @param path Full path to the git repository + * @param ctx Context containing the repository information + * @return 0 on success, -1 on error + */ +static int update_mirror(const char *path, const struct repo_ctx *ctx) +{ + const pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + // Child process + // Change uid and gid to the user specified in the config + if (drop_perms(ctx)) + _exit(127); + + char *args[] = { + "git", "--git-dir", (char *) path, "remote", + "update", "--prune", NULL, + }; + execvp("git", args); + perror("execvp"); + _exit(127); // execvp only returns on error + } + + int status; + pid_t result; + while ((result = waitpid(pid, &status, 0)) == -1 && errno == EINTR) { + } + if (result == -1) { + perror("waitpid"); + return -1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + return 0; // Success + fprintf(stderr, "Error: git remote update failed with status %d\n", + WEXITSTATUS(status)); + return -1; // Error occurred +} + +int git_mirror_repo(const struct repo_ctx *ctx) +{ + char *path = get_git_path(ctx->cfg->git_base, ctx->cfg->owner, + ctx->name); + if (!path) { + perror("get_git_path"); + return -1; + } + + // Check whether repo exists + if (contains_mirror(path, ctx)) { + // Repo exists, so we can just update it + printf("Repo already exists, updating...\n"); + if (update_mirror(path, ctx) == -1) { + perror("update_mirror"); + free(path); + return -1; + } + free(path); + return 0; + } + + // Repo does not exist, so we need to clone it + printf("Repo does not exist, cloning...\n"); + if (create_git_path(ctx) == -1) { + perror("create_git_path"); + free(path); + return -1; + } + if (create_mirror(path, ctx) == -1) { + perror("create_mirror"); + free(path); + return -1; + } + + free(path); + return 0; +} diff --git a/src/git.h b/src/git.h new file mode 100644 index 0000000..1240b0f --- /dev/null +++ b/src/git.h @@ -0,0 +1,24 @@ +// +// Created by Anshul Gupta on 4/6/25. +// + +#ifndef GIT_H +#define GIT_H + +#include "config.h" + +struct repo_ctx { + const struct config *cfg; + + /// Name of the repo + const char *name; + /// HTTPS URL of the repo + const char *url; + /// GitHub username for authentication + const char *username; +}; + +int git_mirror_repo(const struct repo_ctx *ctx); + + +#endif // GIT_H diff --git a/src/github.c b/src/github.c new file mode 100644 index 0000000..18bd992 --- /dev/null +++ b/src/github.c @@ -0,0 +1,290 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#include "github.h" + +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include <cJSON.h> +#include <curl/curl.h> + +#include "queries/identity.h" +#include "queries/list_repos.h" + +#include "buffer.h" +#include "github_types.h" + +struct gh_impl { + struct github_ctx ctx; + CURL *curl; +}; + +static CURLcode gh_impl_send(const struct gh_impl *client, const char *query, + cJSON *args, buffer_t *buf); + +static int gh_handle_error(const cJSON *root); + +github_client *github_client_new(const struct github_ctx ctx) +{ + struct gh_impl *c = malloc(sizeof(*c)); + if (!c) + return NULL; + + c->ctx.endpoint = strdup(ctx.endpoint); + c->ctx.token = strdup(ctx.token); + c->ctx.user_agent = strdup(ctx.user_agent); + c->curl = curl_easy_init(); + return c; +} + +github_client *github_client_dup(github_client *client) +{ + struct gh_impl *c = client; + if (!c) + return NULL; + + struct gh_impl *dup = malloc(sizeof(*dup)); + if (!dup) + return NULL; + + dup->ctx.endpoint = strdup(c->ctx.endpoint); + dup->ctx.token = strdup(c->ctx.token); + dup->ctx.user_agent = strdup(c->ctx.user_agent); + dup->curl = curl_easy_duphandle(c->curl); + + return dup; +} + +void github_client_free(github_client *client) +{ + struct gh_impl *c = client; + if (!c) + return; + curl_easy_cleanup(c->curl); + free((char *) c->ctx.endpoint); + free((char *) c->ctx.token); + free((char *) c->ctx.user_agent); + free(c); +} + +char *github_client_identity(const github_client *client) +{ + char *login = NULL; + const struct gh_impl *c = client; + buffer_t buf = buffer_new(4096); + + const CURLcode ret = gh_impl_send(c, identity, NULL, &buf); + if (ret != CURLE_OK) { + fprintf(stderr, "Failed to send request: %s\n", + curl_easy_strerror(ret)); + goto fail; + } + + // Parse the response + cJSON *root = cJSON_Parse((const char *) buf.data); + if (!root) { + const char *err = cJSON_GetErrorPtr(); + if (err) + fprintf(stderr, "Error parsing response: %s\n", err); + goto fail; + } + + // Check for errors + if (gh_handle_error(root) < 0) + goto fail1; + + // Get login from json + login = identity_from_json(root); + +fail1: + cJSON_Delete(root); +fail: + buffer_free(buf); + return login; +} + +int github_client_list_user_repos(const github_client *client, + const char *username, const char *after, + struct list_repos_res *res) +{ + int status = 0; + const struct gh_impl *c = client; + buffer_t buf = buffer_new(4096); + + cJSON *args = cJSON_CreateObject(); + if (!args) { + status = -1; + goto end; + } + cJSON_AddItemToObject(args, "username", cJSON_CreateString(username)); + cJSON_AddItemToObject(args, "after", cJSON_CreateString(after)); + + const CURLcode ret = gh_impl_send(c, list_repos, args, &buf); + if (ret != CURLE_OK) { + fprintf(stderr, "Failed to send request: %s\n", + curl_easy_strerror(ret)); + status = -1; + goto end; + } + + // Parse the response + cJSON *root = cJSON_Parse((const char *) buf.data); + if (!root) { + const char *err = cJSON_GetErrorPtr(); + if (err) + fprintf(stderr, "Error parsing response: %s\n", err); + status = -1; + goto end; + } + + // Check for errors + if (gh_handle_error(root) < 0) { + status = -1; + goto end; + } + + // Convert json to struct + if (list_repos_from_json(root, res) < 0) { + fprintf(stderr, "Failed to parse response\n"); + status = -1; + } + +end: + buffer_free(buf); + return status; +} + +/** + * Wraps the query in a JSON object with a "query" key and an optional + * "variables" key + * @param query GraphQL query + * @param args GraphQL arguments + * @return JSON string + */ +static char *wrap_query(const char *query, cJSON *args) +{ + cJSON *root = NULL, *query_str = NULL; + char *str = NULL; + + root = cJSON_CreateObject(); + if (!root) + return NULL; + + query_str = cJSON_CreateString(query); + if (!query_str) + goto end; + + // Transfer ownership of the string to root + cJSON_AddItemToObject(root, "query", query_str); + + // Add the args object if it exists + if (args) + cJSON_AddItemToObject(root, "variables", args); + + str = cJSON_Print(root); +end: + cJSON_Delete(root); + return str; +} + +static size_t write_data(const void *ptr, const size_t size, size_t nmemb, + void *stream) +{ + (void) size; // unused + + buffer_t *buf = stream; + buffer_append(buf, ptr, nmemb); + return nmemb; +} + +/** + * Send a GraphQL query to the GitHub API + * @param client Github Client + * @param query GraphQL query + * @return CURLcode + */ +static CURLcode gh_impl_send(const struct gh_impl *client, const char *query, + cJSON *args, buffer_t *buf) +{ + struct curl_slist *headers = NULL; + char auth[1024]; + + // Set the URL + curl_easy_setopt(client->curl, CURLOPT_URL, client->ctx.endpoint); + + // Set the authorization header + snprintf(auth, sizeof(auth), "Authorization: bearer %s", + client->ctx.token); + headers = curl_slist_append(headers, auth); + // Set the content type to JSON + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers); + + // Set user agent + curl_easy_setopt(client->curl, CURLOPT_USERAGENT, + client->ctx.user_agent); + + // Set the request type to POST + curl_easy_setopt(client->curl, CURLOPT_CUSTOMREQUEST, "POST"); + + // Prepare request body + char *wrapped_query = wrap_query(query, args); + + // Set the request body + curl_easy_setopt(client->curl, CURLOPT_POSTFIELDS, wrapped_query); + curl_easy_setopt(client->curl, CURLOPT_POSTFIELDSIZE, + strlen(wrapped_query)); + + // Set the write function to capture the response + curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, (void *) buf); + + // Perform the request + const CURLcode ret = curl_easy_perform(client->curl); + + // Append null terminator to the buffer + if (ret == CURLE_OK) + buffer_append(buf, "\0", 1); + + // Cleanup + free(wrapped_query); + curl_slist_free_all(headers); + + return ret; +} + +/** + * Handle errors in the response. + * Will check for the "errors" key in the response and print the error messages. + * Returns failure if any errors are found. + * @param root Parsed JSON response + * @return 0 on success (no errors), -1 on failure + */ +static int gh_handle_error(const cJSON *root) +{ + // Check for errors + cJSON *errors = cJSON_GetObjectItemCaseSensitive(root, "errors"); + if (!errors || !cJSON_IsArray(errors)) { + // No errors + return 0; + } + + cJSON *err; + cJSON_ArrayForEach(err, errors) + { + // Get the error message + cJSON *message = cJSON_GetObjectItemCaseSensitive(err, + "message"); + if (message && cJSON_IsString(message)) { + fprintf(stderr, "Github Error: %s\n", + message->valuestring); + } else { + fprintf(stderr, "Github Error: Unknown error\n"); + } + } + + return -1; +} diff --git a/src/github.h b/src/github.h new file mode 100644 index 0000000..4979fde --- /dev/null +++ b/src/github.h @@ -0,0 +1,30 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#ifndef GITHUB_H +#define GITHUB_H + +#include "github_types.h" + +typedef void github_client; + +struct github_ctx { + const char *endpoint; + const char *token; + const char *user_agent; +}; + +github_client *github_client_new(struct github_ctx ctx); + +github_client *github_client_dup(github_client *client); + +void github_client_free(github_client *client); + +char *github_client_identity(const github_client *client); + +int github_client_list_user_repos(const github_client *client, + const char *username, const char *after, + struct list_repos_res *res); + +#endif // GITHUB_H diff --git a/src/github_types.c b/src/github_types.c new file mode 100644 index 0000000..ea1d6c5 --- /dev/null +++ b/src/github_types.c @@ -0,0 +1,151 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <cJSON.h> + +#include "github_types.h" + + +char *identity_from_json(const cJSON *root) +{ + cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data"); + if (!data || !cJSON_IsObject(data)) { + fprintf(stderr, "Error: data object not found\n"); + return NULL; + } + cJSON *viewer = cJSON_GetObjectItemCaseSensitive(data, "viewer"); + if (!viewer || !cJSON_IsObject(viewer)) { + fprintf(stderr, "Error: viewer object not found\n"); + return NULL; + } + cJSON *login = cJSON_GetObjectItemCaseSensitive(viewer, "login"); + if (!login || !cJSON_IsString(login)) { + fprintf(stderr, "Error: login not found\n"); + return NULL; + } + + return strdup(login->valuestring); +} + +int list_repos_from_json(cJSON *root, struct list_repos_res *res) +{ + int status = 0; + cJSON *repo; + + // Initialize the response structure + memset(res, 0, sizeof(*res)); + + // Get the data object + cJSON *data = cJSON_GetObjectItemCaseSensitive(root, "data"); + if (!data || !cJSON_IsObject(data)) { + fprintf(stderr, "Error: data object not found\n"); + status = -1; + goto end; + } + + // Get the repositoryOwner object + cJSON *repositoryOwner = cJSON_GetObjectItemCaseSensitive( + data, "repositoryOwner"); + if (!repositoryOwner || !cJSON_IsObject(repositoryOwner)) { + fprintf(stderr, "Error: repositoryOwner object not found\n"); + status = -1; + goto end; + } + + // Get the repositories object + cJSON *repositories = cJSON_GetObjectItemCaseSensitive(repositoryOwner, + "repositories"); + if (!repositories || !cJSON_IsObject(repositories)) { + fprintf(stderr, "Error: repositories object not found\n"); + status = -1; + goto end; + } + + // Get the pageInfo object + cJSON *page_info = cJSON_GetObjectItemCaseSensitive(repositories, + "pageInfo"); + if (!page_info || !cJSON_IsObject(page_info)) { + fprintf(stderr, "Error: pageInfo object not found\n"); + status = -1; + goto end; + } + + // Get the hasNextPage and endCursor values + cJSON *has_next_page = cJSON_GetObjectItemCaseSensitive(page_info, + "hasNextPage"); + if (!has_next_page || !cJSON_IsBool(has_next_page)) { + fprintf(stderr, "Error: hasNextPage not found\n"); + status = -1; + goto end; + } + res->has_next_page = cJSON_IsTrue(has_next_page); + + cJSON *end_cursor = cJSON_GetObjectItemCaseSensitive(page_info, + "endCursor"); + if (!end_cursor || !cJSON_IsString(end_cursor)) { + fprintf(stderr, "Error: endCursor not found\n"); + status = -1; + goto end; + } + res->end_cursor = strdup(end_cursor->valuestring); + + // Get the nodes array + cJSON *nodes = cJSON_GetObjectItemCaseSensitive(repositories, "nodes"); + if (!nodes || !cJSON_IsArray(nodes)) { + fprintf(stderr, "Error: nodes array not found\n"); + status = -1; + goto end; + } + + // Iterate over the nodes array + size_t len = cJSON_GetArraySize(nodes); + res->repos = malloc(sizeof(*res->repos) * len); + cJSON_ArrayForEach(repo, nodes) + { + cJSON *name = cJSON_GetObjectItemCaseSensitive(repo, "name"); + if (!name || !cJSON_IsString(name)) { + fprintf(stderr, "Error: name not found\n"); + status = -1; + goto end; + } + cJSON *url = cJSON_GetObjectItemCaseSensitive(repo, "url"); + if (!url || !cJSON_IsString(url)) { + fprintf(stderr, "Error: url not found\n"); + status = -1; + goto end; + } + cJSON *is_fork = cJSON_GetObjectItemCaseSensitive(repo, + "isFork"); + if (!is_fork || !cJSON_IsBool(is_fork)) { + fprintf(stderr, "Error: isFork not found\n"); + status = -1; + goto end; + } + + res->repos[res->repos_len].name = strdup(name->valuestring); + res->repos[res->repos_len].url = strdup(url->valuestring); + res->repos[res->repos_len].is_fork = cJSON_IsTrue(is_fork); + res->repos_len++; + } + +end: + cJSON_Delete(root); + if (status != 0) + list_repos_res_free(*res); + return status; +} + +void list_repos_res_free(struct list_repos_res res) +{ + free(res.end_cursor); + for (size_t i = 0; i < res.repos_len; i++) { + free(res.repos[i].name); + free(res.repos[i].url); + } + free(res.repos); +} diff --git a/src/github_types.h b/src/github_types.h new file mode 100644 index 0000000..ab3f884 --- /dev/null +++ b/src/github_types.h @@ -0,0 +1,28 @@ +// +// Created by Anshul Gupta on 4/4/25. +// + +#ifndef GITHUB_TYPES_H +#define GITHUB_TYPES_H + +#include <cJSON.h> + +char *identity_from_json(const cJSON *root); + +struct list_repos_res { + int has_next_page; + char *end_cursor; + + struct { + char *name; + char *url; + int is_fork; + } *repos; + + size_t repos_len; +}; + +int list_repos_from_json(cJSON *root, struct list_repos_res *res); +void list_repos_res_free(struct list_repos_res res); + +#endif // GITHUB_TYPES_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..db11d5a --- /dev/null +++ b/src/main.c @@ -0,0 +1,132 @@ +#include <getopt.h> +#include <string.h> +#include <unistd.h> + +#include <curl/curl.h> + +#include "config.h" +#include "git.h" +#include "github.h" +#include "github_types.h" +#include "precheck.h" + +static int load_config(int argc, char **argv, struct config **cfg_out) +{ + int opt, opt_idx = 0; + size_t i; + char *cfg_path = NULL; + + static struct option long_options[] = { + {"config", required_argument, 0, 'c'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0}}; + + while ((opt = getopt_long(argc, argv, "C:c:h", long_options, + &opt_idx)) != -1) { + switch (opt) { + case 'C': + case 'c': + cfg_path = optarg; + break; + case 'h': + fprintf(stderr, + "Usage: %s [--config <file>] [--help]\n", + argv[0]); + return 0; + default: + fprintf(stderr, "Unknown option: %c\n", opt); + fprintf(stderr, + "Usage: %s [--config <file>] [--help]\n", + argv[0]); + return 1; + } + } + + // Config file given, use it + if (cfg_path) { + *cfg_out = config_read(cfg_path); + return *cfg_out == NULL; + } + + // No config file given, try the default locations + for (i = 0; config_locations[i]; i++) { + fprintf(stderr, "Trying config file: %s\n", + config_locations[i]); + *cfg_out = config_read(config_locations[i]); + if (*cfg_out) { + fprintf(stderr, "Using config file: %s\n", + config_locations[i]); + return 0; + } + } + fprintf(stderr, "Failed to read config file\n"); + return 1; +} + +int main(int argc, char **argv) +{ + curl_global_init(CURL_GLOBAL_DEFAULT); + + struct config *cfg = NULL; + const int ret = load_config(argc, argv, &cfg); + if (ret != 0 || !cfg) + return ret; + + if (precheck_self(cfg)) { + fprintf(stderr, "Precheck failed\n"); + config_free(cfg); + return 1; + } + + const struct github_ctx ctx = { + .endpoint = cfg->endpoint, + .token = cfg->token, + .user_agent = cfg->user_agent, + }; + github_client *client = github_client_new(ctx); + if (!client) { + fprintf(stderr, "Failed to create GitHub client\n"); + return 1; + } + + // Get identity + char *login = github_client_identity(client); + + struct list_repos_res res; + char *end_cursor = NULL; + int status = 0; + do { + if (github_client_list_user_repos(client, cfg->owner, + end_cursor, &res)) + return 1; + + for (size_t i = 0; i < res.repos_len; i++) { + printf("Repo: %s\t%s\n", res.repos[i].name, + res.repos[i].url); + + const struct repo_ctx repo = { + .cfg = cfg, + .name = res.repos[i].name, + .url = res.repos[i].url, + .username = login, + }; + if (git_mirror_repo(&repo) != 0) { + fprintf(stderr, "Failed to mirror repo\n"); + status = 1; + break; + } + } + + free(end_cursor); + end_cursor = strdup(res.end_cursor); + + list_repos_res_free(res); + } while (res.has_next_page); + + free(end_cursor); + github_client_free(client); + free(login); + config_free(cfg); + curl_global_cleanup(); + return status; +} diff --git a/src/precheck.c b/src/precheck.c new file mode 100644 index 0000000..a9fce35 --- /dev/null +++ b/src/precheck.c @@ -0,0 +1,112 @@ +// +// Created by Anshul Gupta on 4/7/25. +// + +#include "precheck.h" + +#include <stdio.h> +#include <sys/errno.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> +#ifdef __linux__ +#include <sys/capability.h> +#endif + +/** + * Check if git is installed and available in the PATH. + * @return 1 if git is available, 0 if not. + */ +static int has_git(void) +{ + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return 0; + } + + if (pid == 0) { + // Child process + char *args[] = {"git", "--version", NULL}; + execvp("git", args); + _exit(127); // execvp only returns on error + } + + // Parent process + int status; + pid_t result; + do { + result = waitpid(pid, &status, 0); + } while (result == -1 && errno == EINTR); + if (result == -1) { + perror("waitpid"); + return 0; + } + + // Check exit status + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + return 1; // git is available + } + + fprintf(stderr, "Error: git is not installed or not found in PATH\n"); + return 0; // git is not available +} + +/** + * Check if we have CAP_CHOWN capability or if we are root. + * @return 1 if we have chown capability, 0 if not. + */ +static int has_chown(void) +{ +#ifdef __linux__ + cap_t caps = cap_get_proc(); + if (caps == NULL) { + perror("cap_get_proc"); + return 0; + } + + cap_flag_value_t cap_value; + if (cap_get_flag(caps, CAP_CHOWN, CAP_EFFECTIVE, &cap_value) == -1) { + perror("cap_get_flag"); + cap_free(caps); + return 0; + } + cap_free(caps); + + if (cap_value == CAP_SET) + return 1; // We have CAP_CHOWN capability + fprintf(stderr, "Error: CAP_CHOWN capability is not set\n"); + return 0; // We don't have CAP_CHOWN capability +#else + if (geteuid() == 0) + return 1; + fprintf(stderr, "Error: not running as root\n"); + return 0; +#endif +} + +/** + * Check if the git base directory exists. + * @param path Path to the directory to check + * @return 1 if the directory exists, 0 if not. + */ +static int git_base_exists(const char *path) +{ + struct stat st; + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + return 1; + } + fprintf(stderr, "Error: git base directory does not exist: %s\n", path); + return 0; +} + +int precheck_self(const struct config *cfg) +{ + if (!has_git()) + return -1; + if (!has_chown()) + return -1; + if (!git_base_exists(cfg->git_base)) + return -1; + return 0; +} diff --git a/src/precheck.h b/src/precheck.h new file mode 100644 index 0000000..b030016 --- /dev/null +++ b/src/precheck.h @@ -0,0 +1,16 @@ +// +// Created by Anshul Gupta on 4/7/25. +// + +#ifndef PRECHECK_H +#define PRECHECK_H + +#include "config.h" + +/** + * Precheck the system for required dependencies and configurations. + * @return 0 if all checks pass, non-zero if any check fails. + */ +int precheck_self(const struct config *cfg); + +#endif // PRECHECK_H |