diff options
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 |