/* UnrealIRCd module manager. * (C) Copyright 2019 Bram Matthys ("Syzop") and the UnrealIRCd Team. * License: GPLv2 or later * See https://www.unrealircd.org/docs/Module_manager for user documentation. */ #include "unrealircd.h" #ifndef _WIN32 #define MODULEMANAGER_CONNECT_TIMEOUT 7 #define MODULEMANAGER_READ_TIMEOUT 20 typedef struct ManagedModule ManagedModule; struct ManagedModule { ManagedModule *prev, *next; char *repo_url; char *name; char *author; char *troubleshooting; char *documentation; char *version; char *source; char *sha256sum; char *min_unrealircd_version; char *max_unrealircd_version; char *description; MultiLine *post_install_text; }; static ManagedModule *managed_modules = NULL; /* We normally do a 'make install' after upgrading a module. * However we will skip it if --no-install is done. * This is only done during 'make', as it is unexpected to * already install modules at the final location before * 'make install' was issued. */ static int no_make_install = 0; /* Forward declarations */ int mm_valid_module_name(char *name); void free_managed_module(ManagedModule *m); SSL_CTX *mm_init_tls(void) { SSL_CTX *ctx_client; char buf1[512], buf2[512]; char *curl_ca_bundle = buf1; SSL_load_error_strings(); SSLeay_add_ssl_algorithms(); ctx_client = SSL_CTX_new(SSLv23_client_method()); if (!ctx_client) return NULL; #ifdef HAS_SSL_CTX_SET_MIN_PROTO_VERSION SSL_CTX_set_min_proto_version(ctx_client, TLS1_2_VERSION); #endif SSL_CTX_set_options(ctx_client, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1); /* Verify peer certificate */ snprintf(buf1, sizeof(buf1), "%s/tls/curl-ca-bundle.crt", CONFDIR); if (!file_exists(buf1)) { snprintf(buf2, sizeof(buf2), "%s/doc/conf/tls/curl-ca-bundle.crt", BUILDDIR); if (!file_exists(buf2)) { fprintf(stderr, "ERROR: Neither %s nor %s exist.\n" "Cannot use module manager without curl-ca-bundle.crt\n", buf1, buf2); exit(-1); } curl_ca_bundle = buf2; } SSL_CTX_load_verify_locations(ctx_client, curl_ca_bundle, NULL); SSL_CTX_set_verify(ctx_client, SSL_VERIFY_PEER, NULL); /* Limit ciphers as well */ SSL_CTX_set_cipher_list(ctx_client, UNREALIRCD_DEFAULT_CIPHERS); return ctx_client; } int parse_url(const char *url, char **host, int *port, char **document) { char *p; static char hostbuf[256]; static char documentbuf[512]; if (strncmp(url, "https://", 8)) { fprintf(stderr, "ERROR: URL Must start with https! URL: %s\n", url); return 0; } url += 8; /* skip over https:// part */ p = strchr(url, '/'); if (!p) return 0; strlncpy(hostbuf, url, sizeof(hostbuf), p - url); strlcpy(documentbuf, p, sizeof(documentbuf)); *host = hostbuf; *document = documentbuf; // TODO: parse port, rather than hardcode: *port = 443; return 1; } int mm_http_request(char *url, char *fname, int follow_redirects) { char *host = NULL; int port = 0; char *document = NULL; char hostandport[256]; char buf[1024]; int n; FILE *fd; SSL_CTX *ctx_client; SSL *ssl = NULL; BIO *socket = NULL; char *errstr = NULL; int got_data = 0, first_line = 1; int http_redirect = 0; if (!parse_url(url, &host, &port, &document)) return 0; snprintf(hostandport, sizeof(hostandport), "%s:%d", host, port); ctx_client = mm_init_tls(); if (!ctx_client) { fprintf(stderr, "ERROR: TLS initalization failure (I)\n"); return 0; } alarm(MODULEMANAGER_CONNECT_TIMEOUT); socket = BIO_new_ssl_connect(ctx_client); if (!socket) { fprintf(stderr, "ERROR: TLS initalization failure (II)\n"); goto out1; } BIO_get_ssl(socket, &ssl); if (!ssl) { fprintf(stderr, "ERROR: Could not get TLS connection from BIO -- strange\n"); goto out2; } BIO_set_conn_hostname(socket, hostandport); SSL_set_tlsext_host_name(ssl, host); if (BIO_do_connect(socket) != 1) { fprintf(stderr, "ERROR: Could not connect to %s\n", hostandport); //config_report_ssl_error(); FIXME? goto out2; } if (BIO_do_handshake(socket) != 1) { fprintf(stderr, "ERROR: Could not connect to %s (TLS handshake failed)\n", hostandport); //config_report_ssl_error(); FIXME? goto out2; } if (!verify_certificate(ssl, host, &errstr)) { fprintf(stderr, "Certificate problem for %s: %s\n", host, errstr); goto out2; } snprintf(buf, sizeof(buf), "GET %s HTTP/1.1\r\n" "User-Agent: UnrealIRCd %s\r\n" "Host: %s\r\n" "Connection: close\r\n" "\r\n", document, VERSIONONLY, hostandport); BIO_puts(socket, buf); fd = fopen(fname, "wb"); if (!fd) { fprintf(stderr, "Could not write to temporary file '%s': %s\n", fname, strerror(errno)); goto out2; } alarm(MODULEMANAGER_READ_TIMEOUT); while ((n = BIO_read(socket, buf, sizeof(buf)-1)) > 0) { buf[n] = '\0'; if (got_data) { fwrite(buf, n, 1, fd); // TODO: check for write errors } else { /* Still need to parse header */ char *line, *p = NULL; for (line = strtoken(&p, buf, "\n"); line; line = strtoken(&p, NULL, "\n")) { if (first_line) { if (http_redirect) { if (!strncmp(line, "Location: ", 10)) { line += 10; stripcrlf(line); fclose(fd); BIO_free_all(socket); SSL_CTX_free(ctx_client); if (strncmp(line, "https://", 8)) { fprintf(stderr, "Invalid HTTP Redirect to '%s' -- must start with https://\n", line); return 0; } printf("Following redirect to %s\n", line); return mm_http_request(line, fname, 0); } continue; } if (!strncmp(line, "HTTP/1.1 301", 12) || !strncmp(line, "HTTP/1.1 302", 12) || !strncmp(line, "HTTP/1.1 303", 12) || !strncmp(line, "HTTP/1.1 307", 12) || !strncmp(line, "HTTP/1.1 308", 12)) { if (!follow_redirects) { fprintf(stderr, "ERROR: received HTTP(S) redirect while already following another HTTP(S) redirect.\n"); goto out3; } http_redirect = 1; continue; } if (strncmp(line, "HTTP/1.1 200", 12)) { stripcrlf(line); if (strlen(line) > 128) line[128] = '\0'; fprintf(stderr, "Error while fetching %s: %s\n", url, line); goto out3; } first_line = 0; } if (!*line || !strcmp(line, "\r")) { int remaining_bytes; got_data = 1; /* Bit of a hack here to write part of the data * that is not part of the header but the data response.. * We need to jump over the NUL byte and then check * to see if we can access it.. since it could be either * a NUL byte due to the strtoken() or a NUL byte simply * because that was all the data from BIO_read() at this point. */ if (*line == '\0') line += 1; /* jump over \0 */ else line += 2; /* jump over \r\0 */ remaining_bytes = n - (line - buf); if (remaining_bytes > 0) fwrite(line, remaining_bytes, 1, fd); break; /* must break the for loop here */ } } } } if (!got_data) { fprintf(stderr, "Error while fetching %s: unable to retrieve data\n", url); goto out3; } fclose(fd); BIO_free_all(socket); SSL_CTX_free(ctx_client); return 1; out3: fclose(fd); out2: BIO_free_all(socket); out1: SSL_CTX_free(ctx_client); alarm(0); return 0; } typedef enum ParseModuleHeaderStage { PMH_STAGE_LOOKING = 0, PMH_STAGE_MODULEHEADER = 1, PMH_STAGE_MOD_HEADER = 2, PMH_STAGE_GOT_NAME = 3, PMH_STAGE_GOT_VERSION = 4, PMH_STAGE_GOT_DESCRIPTION = 5, PMH_STAGE_GOT_AUTHOR = 6, PMT_STAGE_DONE = 7, } ParseModuleHeaderStage; typedef enum ParseModuleConfigStage { PMC_STAGE_LOOKING = 0, PMC_STAGE_STARTED = 1, PMC_STAGE_FINISHED = 2, } ParseModuleConfigStage; int parse_quoted_string(char *buf, char *dest, size_t destlen) { char *p, *p2; size_t max; char *i, *o; p = strchr(buf, '"'); if (!p) return 0; p2 = strrchr(p+1, '"'); if (!p2) return 0; max = p2 - p; if (max > destlen) max = destlen; strlcpy(dest, p+1, max); unreal_del_quotes(dest); return 1; } #undef CheckNull #define CheckNull(x) if ((!(x)->value) || (!(*((x)->value)))) { config_error("%s:%i: missing parameter", m->name, (x)->line_number); return 0; } /** Parse a module { } line from a module (not repo!!) */ int mm_module_file_config(ManagedModule *m, ConfigEntry *ce) { ConfigEntry *cep; if (ce->value) { config_error("%s:%d: module { } block should not have a name.", m->name, ce->line_number); return 0; } for (cep = ce->items; cep; cep = cep->next) { if (!strcmp(cep->name, "source") || !strcmp(cep->name, "version") || !strcmp(cep->name, "author") || !strcmp(cep->name, "sha256sum") || !strcmp(cep->name, "description") ) { config_error("%s:%d: module::%s should not be in here (it only exists in repository entries)", m->name, cep->line_number, cep->name); return 0; } else if (!strcmp(cep->name, "troubleshooting")) { CheckNull(cep); safe_strdup(m->troubleshooting, cep->value); } else if (!strcmp(cep->name, "documentation")) { CheckNull(cep); safe_strdup(m->documentation, cep->value); } else if (!strcmp(cep->name, "min-unrealircd-version")) { CheckNull(cep); safe_strdup(m->min_unrealircd_version, cep->value); } else if (!strcmp(cep->name, "max-unrealircd-version")) { CheckNull(cep); safe_strdup(m->max_unrealircd_version, cep->value); } else if (!strcmp(cep->name, "post-install-text")) { if (cep->items) { ConfigEntry *cepp; for (cepp = cep->items; cepp; cepp = cepp->next) addmultiline(&m->post_install_text, cepp->name); } else { CheckNull(cep); addmultiline(&m->post_install_text, cep->value); } } /* unknown items are silently ignored for future compatibility */ } if (!m->documentation) { config_error("%s:%d: module::documentation missing", m->name, ce->line_number); return 0; } if (!m->troubleshooting) { config_error("%s:%d: module::troubleshooting missing", m->name, ce->line_number); return 0; } if (!m->min_unrealircd_version) { config_error("%s:%d: module::min-unrealircd-version missing", m->name, ce->line_number); return 0; } /* max_unrealircd_version is optional */ /* post_install_text is optional */ return 1; } #undef CheckNull int mm_parse_module_file(ManagedModule *m, char *buf, unsigned int line_offset) { ConfigFile *cf; ConfigEntry *ce; cf = config_parse_with_offset(m->name, buf, line_offset); if (!cf) return 0; /* eg: parse errors */ /* Parse the module { } block (only one!) */ for (ce = cf->items; ce; ce = ce->next) { if (!strcmp(ce->name, "module")) { int n = mm_module_file_config(m, ce); config_free(cf); return n; } } config_free(cf); config_error("No module block found within module source file. Contact author.\n"); return 1; } #define MODULECONFIGBUFFER 16384 ManagedModule *mm_parse_module_c_file(char *modulename, char *fname) { char buf[1024]; FILE *fd; ParseModuleHeaderStage parse_module_header = PMH_STAGE_LOOKING; ParseModuleConfigStage parse_module_config = PMC_STAGE_LOOKING; char *moduleconfig = NULL; int linenr = 0, module_config_start_line = 0; char module_header_name[128]; char module_header_version[64]; char module_header_description[256]; char module_header_author[128]; ManagedModule *m = NULL; *module_header_name = *module_header_version = *module_header_description = *module_header_author = '\0'; if (!mm_valid_module_name(modulename)) { fprintf(stderr, "Module file '%s' contains forbidden characters\n", modulename); return NULL; } fd = fopen(fname, "r"); if (!fd) { fprintf(stderr, "Unable to open module '%s', file '%s': %s\n", modulename, fname, strerror(errno)); return NULL; } moduleconfig = safe_alloc(MODULECONFIGBUFFER); /* should be sufficient */ while ((fgets(buf, sizeof(buf), fd))) { linenr++; stripcrlf(buf); /* parse module header stuff: */ switch (parse_module_header) { case PMH_STAGE_LOOKING: if (strstr(buf, "ModuleHeader")) parse_module_header = PMH_STAGE_MODULEHEADER; else break; /*fallthrough*/ case PMH_STAGE_MODULEHEADER: if (strstr(buf, "MOD_HEADER")) parse_module_header = PMH_STAGE_MOD_HEADER; break; case PMH_STAGE_MOD_HEADER: if (parse_quoted_string(buf, module_header_name, sizeof(module_header_name))) parse_module_header = PMH_STAGE_GOT_NAME; break; case PMH_STAGE_GOT_NAME: if (parse_quoted_string(buf, module_header_version, sizeof(module_header_version))) parse_module_header = PMH_STAGE_GOT_VERSION; break; case PMH_STAGE_GOT_VERSION: if (parse_quoted_string(buf, module_header_description, sizeof(module_header_description))) parse_module_header = PMH_STAGE_GOT_DESCRIPTION; break; case PMH_STAGE_GOT_DESCRIPTION: if (parse_quoted_string(buf, module_header_author, sizeof(module_header_author))) parse_module_header = PMH_STAGE_GOT_AUTHOR; break; default: break; } /* parse module config stuff: */ switch (parse_module_config) { case PMC_STAGE_LOOKING: if (strstr(buf, "<<>>")){ module_config_start_line = linenr; parse_module_config = PMC_STAGE_STARTED; } break; case PMC_STAGE_STARTED: if (!strstr(buf, "<<>>")) { strlcat(moduleconfig, buf, MODULECONFIGBUFFER); strlcat(moduleconfig, "\n", MODULECONFIGBUFFER); } else { parse_module_config = PMC_STAGE_FINISHED; } break; default: /* Nothing to be done anymore */ break; } } fclose(fd); if (!*module_header_name || !*module_header_version || !*module_header_description || !*module_header_author) { fprintf(stderr, "Error parsing module header in %s\n", modulename); safe_free(moduleconfig); return NULL; } if (strcmp(module_header_name, modulename)) { fprintf(stderr, "ERROR: Mismatch in module name in header (%s) and filename (%s)\n", module_header_name, modulename); safe_free(moduleconfig); return NULL; } if (!*moduleconfig) { fprintf(stderr, "ERROR: Module does not contain module config data (<<>>)\n" "This means it is not meant to be managed by the module manager\n"); safe_free(moduleconfig); return NULL; } /* Fill in the fields from MOD_HEADER() */ m = safe_alloc(sizeof(ManagedModule)); safe_strdup(m->name, module_header_name); safe_strdup(m->version, module_header_version); safe_strdup(m->description, module_header_description); safe_strdup(m->author, module_header_author); if (!mm_parse_module_file(m, moduleconfig, module_config_start_line)) { fprintf(stderr, "ERROR: Problem with module manager data block within the %s module C source file.\n" "You are suggested to contact the module author and paste the above to him/her\n", m->name); free_managed_module(m); safe_free(moduleconfig); return NULL; } safe_free(moduleconfig); return m; } void print_documentation(void) { fprintf(stderr, "See https://www.unrealircd.org/docs/Module_manager for more information.\n"); } char *mm_sourceslist_file(void) { static char buf1[512], buf2[512]; snprintf(buf1, sizeof(buf1), "%s/modules.sources.list", CONFDIR); if (!file_exists(buf1)) { /* Possibly UnrealIRCd is not installed yet, so use this one */ snprintf(buf2, sizeof(buf2), "%s/doc/conf/modules.sources.list", BUILDDIR); if (!file_exists(buf2)) { fprintf(stderr, "ERROR: Neither '%s' nor '%s' exist.\n" "No module repositories configured.\n", buf1, buf2); print_documentation(); exit(-1); } return buf2; } return buf1; } /** Free a managed module struct */ void free_managed_module(ManagedModule *m) { safe_free(m->repo_url); safe_free(m->name); safe_free(m->source); safe_free(m->sha256sum); safe_free(m->version); safe_free(m->author); safe_free(m->troubleshooting); safe_free(m->documentation); safe_free(m->min_unrealircd_version); safe_free(m->max_unrealircd_version); safe_free(m->description); freemultiline(m->post_install_text); safe_free(m); } /** Check for valid module name */ int mm_valid_module_name(char *name) { char *p; if (strncmp(name, "third/", 6)) return 0; name += 6; if (strstr(name, "..")) return 0; for (p = name; *p; p++) if (!isalnum(*p) && !strchr("._-", *p)) return 0; return 1; } #undef CheckNull #define CheckNull(x) if ((!(x)->value) || (!(*((x)->value)))) { config_error("%s:%i: missing parameter", repo_url, (x)->line_number); goto fail_mm_repo_module_config; } /** Parse a module { } line from a repository */ ManagedModule *mm_repo_module_config(char *repo_url, ConfigEntry *ce) { ConfigEntry *cep; ManagedModule *m = safe_alloc(sizeof(ManagedModule)); if (!ce->value) { config_error("%s:%d: module { } with no name", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (strncmp(ce->value, "third/", 6)) { config_error("%s:%d: module { } name must start with: third/", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!mm_valid_module_name(ce->value)) { config_error("%s:%d: module { } with illegal name: %s", repo_url, ce->line_number, ce->value); goto fail_mm_repo_module_config; } safe_strdup(m->name, ce->value); safe_strdup(m->repo_url, repo_url); for (cep = ce->items; cep; cep = cep->next) { if (!strcmp(cep->name, "source")) { CheckNull(cep); safe_strdup(m->source, cep->value); } else if (!strcmp(cep->name, "sha256sum")) { CheckNull(cep); safe_strdup(m->sha256sum, cep->value); } else if (!strcmp(cep->name, "version")) { CheckNull(cep); safe_strdup(m->version, cep->value); } else if (!strcmp(cep->name, "author")) { CheckNull(cep); safe_strdup(m->author, cep->value); } else if (!strcmp(cep->name, "troubleshooting")) { CheckNull(cep); safe_strdup(m->troubleshooting, cep->value); } else if (!strcmp(cep->name, "documentation")) { CheckNull(cep); safe_strdup(m->documentation, cep->value); } else if (!strcmp(cep->name, "min-unrealircd-version")) { CheckNull(cep); safe_strdup(m->min_unrealircd_version, cep->value); } else if (!strcmp(cep->name, "max-unrealircd-version")) { CheckNull(cep); safe_strdup(m->max_unrealircd_version, cep->value); } else if (!strcmp(cep->name, "description")) { CheckNull(cep); safe_strdup(m->description, cep->value); } else if (!strcmp(cep->name, "post-install-text")) { if (cep->items) { ConfigEntry *cepp; for (cepp = cep->items; cepp; cepp = cepp->next) addmultiline(&m->post_install_text, cepp->name); } else { CheckNull(cep); addmultiline(&m->post_install_text, cep->value); } } /* unknown items are silently ignored for future compatibility */ } if (!m->source) { config_error("%s:%d: module::source missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->sha256sum) { config_error("%s:%d: module::sha256sum missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->version) { config_error("%s:%d: module::version missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->author) { config_error("%s:%d: module::author missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->documentation) { config_error("%s:%d: module::documentation missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->troubleshooting) { config_error("%s:%d: module::troubleshooting missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } if (!m->min_unrealircd_version) { config_error("%s:%d: module::min-unrealircd-version missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } /* max_unrealircd_version is optional */ if (!m->description) { config_error("%s:%d: module::description missing", repo_url, ce->line_number); goto fail_mm_repo_module_config; } /* post_install_text is optional */ return m; fail_mm_repo_module_config: free_managed_module(m); return NULL; } #undef CheckNull int mm_parse_repo_db(char *url, char *filename) { ConfigFile *cf; ConfigEntry *ce; ManagedModule *m; cf = config_load(filename, url); if (!cf) return 0; /* eg: parse errors */ for (ce = cf->items; ce; ce = ce->next) { if (!strcmp(ce->name, "module")) { m = mm_repo_module_config(url, ce); if (!m) { config_free(cf); return 0; } AddListItem(m, managed_modules); } } config_free(cf); return 1; } int mm_refresh_repository(void) { char *sourceslist = mm_sourceslist_file(); FILE *fd; char buf[512]; char *tmpfile; int linenr = 0; int success = 0; int numrepos = 0; if (!file_exists(TMPDIR)) { (void)mkdir(TMPDIR, S_IRUSR|S_IWUSR|S_IXUSR); /* Create the tmp dir, if it doesn't exist */ if (!file_exists(TMPDIR)) { /* This is possible if the directory structure does not exist, * eg if ~/unrealircd does not exist at all then ~/unrealircd/tmp * cannot be mkdir'ed either. */ fprintf(stderr, "ERROR: %s does not exist (yet?), cannot use module manager\n", TMPDIR); fprintf(stderr, " This can only happen if you did not use ./Config or if you rm -rf'ed after running ./Config.\n"); exit(-1); } } printf("Reading module repository list from '%s'...\n", mm_sourceslist_file()); fd = fopen(sourceslist, "r"); if (!fd) { fprintf(stderr, "ERROR: Could not open '%s': %s\n", sourceslist, strerror(errno)); return 0; } while ((fgets(buf, sizeof(buf), fd))) { char *line = buf; linenr++; stripcrlf(line); /* Skip whitespace */ while (*line == ' ') line++; /* Skip empty lines and ones that start with a hash mark (#) */ if (!*line || (*line == '#')) continue; if (strncmp(line, "https://", 8)) { fprintf(stderr, "ERROR in %s on line %d: URL should start with https://", sourceslist, linenr); fclose(fd); return 0; } printf("Checking module repository %s...\n", line); numrepos++; tmpfile = unreal_mktemp(TMPDIR, "mm"); if (mm_http_request(line, tmpfile, 1)) { if (!mm_parse_repo_db(line, tmpfile)) { fclose(fd); return 0; } success++; } } fclose(fd); if (numrepos == 0) { fprintf(stderr, "ERROR: No repositories listed in module repository list. " "Did you remove the default UnrealIRCd repository?\n" "All commands, except for './unrealircd module uninstall third/name-of-module', are unavailable.\n"); return 0; } return success ? 1 : 0; } #define COLUMN_STATUS 0 #define COLUMN_NAME 1 #define COLUMN_VERSION 2 void mm_list_print(char *status, char *name, char *version, char *description, int largest_column[3]) { int padstatus = MAX(largest_column[COLUMN_STATUS] - strlen(status), 0); int padname = MAX(largest_column[COLUMN_NAME] - strlen(name), 0); int padversion = MAX(largest_column[COLUMN_VERSION] - strlen(version), 0); printf("| %s%*s | %s%*s | %s%*s | %s\n", status, padstatus, "", name, padname, "", version, padversion, "", description); } int mm_check_module_compatibility(ManagedModule *m) { if (strchr(m->min_unrealircd_version, '*')) { /* By wildcard, eg: "5.*" */ if (!match_simple(m->min_unrealircd_version, VERSIONONLY)) return 0; } else { /* By strcmp, eg: "5.0.0" */ if (strnatcasecmp(m->min_unrealircd_version, VERSIONONLY) > 0) return 0; } if (m->max_unrealircd_version) { if (strchr(m->max_unrealircd_version, '*')) { /* By wildcard, eg: "5.*" */ if (!match_simple(m->max_unrealircd_version, VERSIONONLY)) return 0; } else { /* By strcmp, eg: "5.0.5" */ if (strnatcasecmp(m->max_unrealircd_version, VERSIONONLY) <= 0) return 0; } } return 1; } #define MMMS_INSTALLED 0x0001 #define MMMS_UPGRADE_AVAILABLE 0x0002 #define MMMS_UNAVAILABLE 0x0004 int mm_get_module_status(ManagedModule *m) { FILE *fd; char fname[512]; const char *our_sha256sum; snprintf(fname, sizeof(fname), "%s/src/modules/%s.c", BUILDDIR, m->name); if (!file_exists(fname)) { if (!mm_check_module_compatibility(m)) return MMMS_UNAVAILABLE; return 0; } our_sha256sum = sha256sum_file(fname); if (!strcasecmp(our_sha256sum, m->sha256sum)) { return MMMS_INSTALLED; } else { if (!mm_check_module_compatibility(m)) return MMMS_INSTALLED|MMMS_UNAVAILABLE; return MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE; } return 0; } char *mm_get_module_status_string(ManagedModule *m) { int status = mm_get_module_status(m); if (status == 0) return ""; else if (status == MMMS_UNAVAILABLE) return "unav"; else if (status == MMMS_INSTALLED) return "inst"; else if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE)) return "inst/UNAV"; else if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE)) return "inst/UPD"; return "UNKNOWN?"; } char *mm_get_module_status_string_long(ManagedModule *m) { int status = mm_get_module_status(m); if (status == 0) return "Not installed"; else if (status == MMMS_UNAVAILABLE) return "Unavailable for your UnrealIRCd version"; else if (status == MMMS_INSTALLED) return "Installed and up to date"; else if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE)) return "Installed, an upgrade is available but not for your UnrealIRCd version"; else if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE)) return "Installed, upgrade available"; return "UNKNOWN?"; } /** Find a module by name, return NULL if not found. */ ManagedModule *mm_find_module(char *name) { ManagedModule *m; for (m = managed_modules; m; m = m->next) if (!strcasecmp(name, m->name)) return m; return NULL; } /** Count the unknown modules (untracked modules) */ int count_unknown_modules(void) { DIR *fd; struct dirent *dir; int count = 0; char dirname[512]; snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR); fd = opendir(dirname); if (fd) { while ((dir = readdir(fd))) { char *fname = dir->d_name; if (filename_has_suffix(fname, ".c")) { char modname[512], *p; snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c")); if (!mm_find_module(modname)) count++; } } closedir(fd); } return count; } void mm_list(char *searchname) { ManagedModule *m; int largest_column[3]; int padname; int padversion; struct dirent *dir; DIR *fd; char dirname[512]; char *status; int first_unknown = 1; if (searchname) printf("Searching for '%s' in names of all available modules...\n", searchname); memset(&largest_column, 0, sizeof(largest_column)); largest_column[COLUMN_STATUS] = strlen("inst/UNAV"); largest_column[COLUMN_NAME] = strlen("Name:"); largest_column[COLUMN_VERSION] = strlen("Version:"); for (m = managed_modules; m; m = m->next) { if (strlen(m->name) > largest_column[COLUMN_NAME]) largest_column[COLUMN_NAME] = strlen(m->name); if (strlen(m->version) > largest_column[COLUMN_VERSION]) largest_column[COLUMN_VERSION] = strlen(m->version); } /* We try to produce neat output, but not at all costs */ if (largest_column[COLUMN_NAME] > 32) largest_column[COLUMN_NAME] = 32; if (largest_column[COLUMN_VERSION] > 16) largest_column[COLUMN_VERSION] = 16; mm_list_print("Status:", "Name:", "Version:", "Description:", largest_column); printf("|=======================================================================================\n"); for (m = managed_modules; m; m = m->next) { if (searchname && !strstr(m->name, searchname)) continue; status = mm_get_module_status_string(m); mm_list_print(status, m->name, m->version, m->description, largest_column); } snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR); fd = opendir(dirname); if (fd) { while ((dir = readdir(fd))) { char *fname = dir->d_name; if (filename_has_suffix(fname, ".c")) { char modname[512], *p; snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c")); if (searchname && !strstr(searchname, modname)) continue; if (!mm_find_module(modname)) { if (first_unknown) { printf("|---------------------------------------------------------------------------------------\n"); first_unknown = 0; } mm_list_print("UNKNOWN", modname, "", "", largest_column); } } } closedir(fd); } printf("|=======================================================================================\n"); printf("\nStatus column legend:\n" " : not installed\n" "inst : module installed\n" "inst/UPD : module installed, upgrade available (latest version differs from yours)\n" "unav : module not available for your UnrealIRCd version\n" "inst/UNAV : module installed, upgrade exists but is not available for your UnrealIRCd version (too old UnrealIRCd version?)\n" "UNKNOWN : module does not exist in any repository (perhaps you installed it manually?), module will be left untouched\n"); printf("\nFor more information about a particular module, use './unrealircd module info name-of-module'\n\n"); print_documentation(); } int mm_compile(ManagedModule *m, char *tmpfile, int test) { char newpath[512]; char cmd[512]; const char *basename; char *p; FILE *fd; char buf[512]; int n; if (test) printf("Test compiling %s...\n", m->name); else printf("Compiling %s...\n", m->name); basename = unreal_getfilename(test ? tmpfile : m->name); snprintf(newpath, sizeof(newpath), "%s/src/modules/third/%s%s", BUILDDIR, basename, test ? "" : ".c"); if (!test) { /* If the file already exists then we are upgrading. * It's a good idea to backup the file rather than * just delete it. Perhaps the user had local changes * he/she wished to preserve and accidently went * through the upgrade procedure. */ char backupfile[512]; snprintf(backupfile, sizeof(backupfile), "%s.bak", newpath); unlink(backupfile); (void)rename(newpath, backupfile); } if (!unreal_copyfileex(tmpfile, newpath, 0)) return 0; snprintf(cmd, sizeof(cmd), "cd \"%s\"; $MAKE custommodule MODULEFILE=\"%s\"", BUILDDIR, filename_strip_suffix(basename, ".c") ); fd = popen(cmd, "r"); if (!fd) { fprintf(stderr, "ERROR: Could not issue command: %s\n", cmd); unlink(newpath); return 0; } while((fgets(buf, sizeof(buf), fd))) { printf("%s", buf); } n = pclose(fd); if (test) { /* Remove the XXXXXXX.modname.c file */ unlink(newpath); /* Remove the XXXXXXX.modname.so file */ newpath[strlen(newpath)-2] = '\0'; // cut off .c strlcat(newpath, ".so", sizeof(newpath)); unlink(newpath); } if (WIFEXITED(n) && (WEXITSTATUS(n) == 0)) return 1; fprintf(stderr, "ERROR: Compile errors encountered while compiling module '%s'\n" "You are suggested to contact the author (%s) of this module:\n%s\n", m->name, m->author, m->troubleshooting); return 0; } /** Actually download and install the module. * This assumes compatibility checks have already been done. */ void mm_install_module(ManagedModule *m) { const char *basename = unreal_getfilename(m->source); char *tmpfile; const char *sha256; if (!basename) basename = "mod.c"; tmpfile = unreal_mktemp(TMPDIR, basename); printf("Downloading %s from %s...\n", m->name, m->source); if (!mm_http_request(m->source, tmpfile, 1)) { fprintf(stderr, "Repository %s seems to list a module file that cannot be retrieved (%s).\n", m->repo_url, m->source); fprintf(stderr, "Fatal error encountered. Contact %s: %s\n", m->author, m->troubleshooting); exit(-1); } sha256 = sha256sum_file(tmpfile); if (!sha256) { fprintf(stderr, "ERROR: Temporary file '%s' has disappeared -- strange\n", tmpfile); fprintf(stderr, "Fatal error encountered. Check for errors above. Perhaps try running the command again?\n"); exit(-1); } if (strcasecmp(sha256, m->sha256sum)) { fprintf(stderr, "ERROR: SHA256 Checksum mismatch\n" "Expected (value in repository list): %s\n" "Received (value of downloaded file): %s\n", m->sha256sum, sha256); fprintf(stderr, "Fatal error encountered, see above. Try running the command again in 5-10 minutes.\n" "If the issue persists, contact the repository manager of %s\n", m->repo_url); exit(-1); } if (!mm_compile(m, tmpfile, 1)) { fprintf(stderr, "Fatal error encountered, see above.\n"); exit(-1); } if (!mm_compile(m, tmpfile, 0)) { fprintf(stderr, "The test compile went OK earlier, but the final compile did not. BAD!!\n"); exit(-1); } printf("Module %s compiled successfully\n", m->name); } /** Uninstall a module. * This function takes a string rather than a ManagedModule * because it also allows uninstalling of unmanaged (local) modules. */ void mm_uninstall_module(char *modulename) { struct dirent *dir; DIR *fd; char dirname[512], fullname[512]; int found = 0; snprintf(dirname, sizeof(dirname), "%s/src/modules/third", BUILDDIR); fd = opendir(dirname); if (fd) { while ((dir = readdir(fd))) { char *fname = dir->d_name; if (filename_has_suffix(fname, ".c") || filename_has_suffix(fname, ".so") || filename_has_suffix(fname, ".dll")) { char modname[512], *p; snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, NULL)); if (!strcasecmp(modname, modulename)) { found = 1; snprintf(fullname, sizeof(fullname), "%s/%s", dirname, fname); //printf("Deleting '%s'\n", fullname); unlink(fullname); } } } closedir(fd); } if (!found) { fprintf(stderr, "ERROR: Module '%s' is not installed, so can't uninstall.\n", modulename); exit(-1); } printf("Module '%s' uninstalled successfully\n", modulename); } void mm_make_install(void) { char cmd[512]; int n; if (no_make_install) return; printf("Running 'make install'...\n"); snprintf(cmd, sizeof(cmd), "cd \"%s\"; $MAKE install 1>/dev/null 2>&1", BUILDDIR); n = system(cmd); } void mm_install(int argc, char *args[], int upgrade) { ManagedModule *m; MultiLine *l; char *name = args[1]; int status; if (!name) { fprintf(stderr, "ERROR: Use: module install third/name-of-module\n"); exit(-1); } if (strncmp(name, "third/", 6)) { fprintf(stderr, "ERROR: Use: module install third/name-of-module\nYou must prefix the modulename with third/\n"); exit(-1); } m = mm_find_module(name); if (!m) { fprintf(stderr, "ERROR: Module '%s' not found\n", name); exit(-1); } status = mm_get_module_status(m); if (status == MMMS_UNAVAILABLE) { fprintf(stderr, "ERROR: Module '%s' exists, but is not compatible with your UnrealIRCd version:\n" "Your UnrealIRCd version : %s\n" "Minimum version required : %s\n", name, VERSIONONLY, m->min_unrealircd_version); if (m->max_unrealircd_version) fprintf(stderr, "Maximum version : %s\n", m->max_unrealircd_version); exit(-1); } if (upgrade && (status == MMMS_INSTALLED)) { /* If updating, and we are already on latest version, then don't upgrade */ printf("Module %s is the latest version, no upgrade needed\n", m->name); } mm_install_module(m); mm_make_install(); if (m->post_install_text) { printf("Post-installation information for %s from the author:\n", m->name); printf("---\n"); for (l = m->post_install_text; l; l = l->next) printf(" %s\n", l->line); printf("---\n"); } else { printf("Don't forget to add a 'loadmodule' line for the module and rehash\n"); } } void mm_uninstall(int argc, char *args[]) { ManagedModule *m; char *name = args[1]; if (!name) { fprintf(stderr, "ERROR: Use: module uninstall third/name-of-module\n"); exit(-1); } if (strncmp(name, "third/", 6)) { fprintf(stderr, "ERROR: Use: module uninstall third/name-of-module\nYou must prefix the modulename with third/\n"); exit(-1); } mm_uninstall_module(name); mm_make_install(); exit(0); } void mm_upgrade(int argc, char *args[]) { ManagedModule *m; char *name = args[1]; int upgraded = 0; int uptodate_already = 0; int update_unavailable = 0; int i = 1, n; if (args[i] && !strcmp(args[i], "--no-install")) { no_make_install = 1; i++; } name = args[i]; if (name) { // TODO: First check if it needs an upgrade? ;) mm_install(argc, args, 1); exit(0); } /* Without arguments means: check all installed modules */ for (m = managed_modules; m; m = m->next) { int status = mm_get_module_status(m); if (status == (MMMS_INSTALLED|MMMS_UPGRADE_AVAILABLE)) { args[1] = m->name; mm_install(1, args, 1); upgraded++; } else if (status == MMMS_INSTALLED) { uptodate_already++; } else if (status == (MMMS_INSTALLED|MMMS_UNAVAILABLE)) { update_unavailable++; } } printf("All actions were successful. %d module(s) upgraded, %d already up-to-date\n", upgraded, uptodate_already); if (update_unavailable) printf("%d module(s) have updates but not for your UnrealIRCd version\n", update_unavailable); if ((n = count_unknown_modules())) printf("%d module(s) are unknown/untracked\n", n); printf("For more details, you can always run ./unrealircd module list\n"); } void mm_info(int argc, char *args[]) { ManagedModule *m; MultiLine *l; char *name = args[1]; if (!name) { fprintf(stderr, "ERROR: Use: unrealircd module info name-of-module\n"); exit(-1); } m = mm_find_module(name); if (!m) { // TODO: we should probably be a bit more specific if the module exists locally (UNAV) */ fprintf(stderr, "ERROR: Module '%s' not found in any repository\n", name); exit(-1); } printf("Name: %s\n" "Version: %s\n" "Description: %s\n" "Author: %s\n" "Documentation: %s\n" "Troubleshooting: %s\n" "Source: %s\n" "Min. UnrealIRCd version: %s\n", m->name, m->version, m->description, m->author, m->documentation, m->troubleshooting, m->source, m->min_unrealircd_version); if (m->max_unrealircd_version) printf("Min. UnrealIRCd version: %s\n", m->max_unrealircd_version); printf("Status: %s\n", mm_get_module_status_string_long(m)); if (m->post_install_text) { printf("------ Post-installation text ------\n"); for (l = m->post_install_text; l; l = l->next) printf(" %s\n", l->line); printf("------ End of post-install text ------\n"); } } void mm_usage(void) { fprintf(stderr, "Use any of the following actions:\n" "unrealircd module list List all the available and installed modules\n" "unrealircd module info name-of-module Show more information about the module\n" "unrealircd module install name-of-module Install the specified module\n" "unrealircd module uninstall name-of-module Uninstall the specified module\n" "unrealircd module upgrade name-of-module Upgrade the specified module (if needed)\n" "unrealircd module upgrade Upgrade all modules (if needed)\n" "unrealircd module generate-repository Generate a repository index (you are\n" " unlikely to need this, only for repo admins)\n"); print_documentation(); exit(-1); } void print_md_block(FILE *fdo, ManagedModule *m) { fprintf(fdo, "module \"%s\"\n{\n", m->name); fprintf(fdo, "\tdescription \"%s\";\n", unreal_add_quotes(m->description)); fprintf(fdo, "\tversion \"%s\";\n", unreal_add_quotes(m->version)); fprintf(fdo, "\tauthor \"%s\";\n", unreal_add_quotes(m->author)); fprintf(fdo, "\tdocumentation \"%s\";\n", unreal_add_quotes(m->documentation)); fprintf(fdo, "\ttroubleshooting \"%s\";\n", unreal_add_quotes(m->troubleshooting)); fprintf(fdo, "\tsource \"%s\";\n", unreal_add_quotes(m->source)); fprintf(fdo, "\tsha256sum \"%s\";\n", unreal_add_quotes(m->sha256sum)); fprintf(fdo, "\tmin-unrealircd-version \"%s\";\n", unreal_add_quotes(m->min_unrealircd_version)); if (m->max_unrealircd_version) fprintf(fdo, "\tmax-unrealircd-version \"%s\";\n", unreal_add_quotes(m->max_unrealircd_version)); if (m->post_install_text) { MultiLine *l; fprintf(fdo, "\tpost-install-text\n" "\t{\n"); for (l = m->post_install_text; l; l = l->next) fprintf(fdo, "\t\t\"%s\";\n", unreal_add_quotes(l->line)); fprintf(fdo, "\t}\n"); } fprintf(fdo, "}\n\n"); } void mm_generate_repository_usage(void) { fprintf(stderr, "Usage: ./unrealircd module generate-repository [optional-minimum-version-filter]\n"); fprintf(stderr, "For example: ./unrealircd module generate-repository https://www.unrealircd.org/modules/ src/modules/third modules.lst\n"); } void mm_generate_repository(int argc, char *args[]) { DIR *fd; struct dirent *dir; int count = 0; char *urlbasepath; char *dirname; char *outputfile; char *minversion; char modname[128]; char fullname[512]; ManagedModule *m; FILE *fdo; urlbasepath = args[1]; dirname = args[2]; outputfile = args[3]; minversion = args[4]; if (!urlbasepath || !dirname || !outputfile) { mm_generate_repository_usage(); exit(-1); } if ((strlen(urlbasepath) < 2) || (urlbasepath[strlen(urlbasepath)-1] != '/')) { fprintf(stderr, "Error: the URL base path must end with a slash\n"); mm_generate_repository_usage(); exit(-1); } fd = opendir(dirname); if (!fd) { fprintf(stderr, "Cannot open directory '%s': %s\n", dirname, strerror(errno)); exit(-1); } fdo = fopen(outputfile, "w"); if (!fdo) { fprintf(stderr, "Could not open file '%s' for writing: %s\n", outputfile, strerror(errno)); exit(-1); } while ((dir = readdir(fd))) { char *fname = dir->d_name; if (filename_has_suffix(fname, ".c")) { int hide = 0; snprintf(fullname, sizeof(fullname), "%s/%s", dirname, fname); snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(fname, ".c")); printf("Processing: %s\n", modname); m = mm_parse_module_c_file(modname, fullname); if (!m) { fprintf(stderr, "WARNING: Skipping module '%s' due to errors\n", modname); continue; } m->sha256sum = strdup(sha256sum_file(fullname)); m->source = safe_alloc(512); snprintf(m->source, 512, "%s%s.c", urlbasepath, modname + 6); /* filter */ if (minversion && m->min_unrealircd_version && strncmp(minversion, m->min_unrealircd_version, strlen(minversion))) hide = 1; /* /filter */ if (!hide) print_md_block(fdo, m); free_managed_module(m); m = NULL; } } closedir(fd); fclose(fdo); } void mm_parse_c_file(int argc, char *args[]) { char *fullname = args[1]; const char *basename; char modname[256]; ManagedModule *m; if (!fullname) { fprintf(stderr, "Usage: ./unrealircd module parse-c-file path/to/file.c\n"); exit(-1); } if (!file_exists(fullname)) { fprintf(stderr, "ERROR: Unable to open C file '%s'\n", fullname); exit(-1); } basename = unreal_getfilename(fullname); snprintf(modname, sizeof(modname), "third/%s", filename_strip_suffix(basename, ".c")); printf("Processing: %s\n", modname); m = mm_parse_module_c_file(modname, fullname); if (!m) { fprintf(stderr, "Errors encountered. See above\n"); exit(-1); } m->sha256sum = strdup(sha256sum_file(fullname)); m->source = strdup("..."); print_md_block(stdout, m); free_managed_module(m); exit(0); } int mm_detect_make_is_gmake(void) { FILE *fd; char buf[512], *s; fd = popen("$MAKE --version 2>&1", "r"); if (fd) { *buf = '\0'; s = fgets(buf, sizeof(buf), fd); pclose(fd); if (s && strstr(s, "GNU Make")) return 1; /* Good! We are done. */ } return 0; } void mm_detect_make(void) { FILE *fd; char *s; char buf[512]; /* Get or set $MAKE */ s = getenv("MAKE"); if (!s) setenv("MAKE", "make", 1); if (mm_detect_make_is_gmake()) return; /* Try again with MAKE=gmake */ setenv("MAKE", "gmake", 1); if (mm_detect_make_is_gmake()) return; fprintf(stderr, "ERROR: GNU Make is not found as 'make' or 'gmake'\n"); exit(-1); } void mm_self_test(void) { char name[512]; if (!file_exists(BUILDDIR)) { fprintf(stderr, "ERROR: Directory %s does not exist.\n" "The UnrealIRCd source is required for the module manager to work!\n", BUILDDIR); exit(-1); } else { snprintf(name, sizeof(name), "%s/src/modules/third/Makefile", BUILDDIR); if (!file_exists(name)) { fprintf(stderr, "ERROR: Directory %s exists, but your UnrealIRCd is not compiled yet.\n" "You must compile your UnrealIRCd first (run './Config', then 'make install')\n", BUILDDIR); exit(-1); } } mm_detect_make(); } void modulemanager(int argc, char *args[]) { if (!args[0]) mm_usage(); mm_self_test(); /* The following operations do not require reading * of the repository list and are always available: */ if (!strcasecmp(args[0], "uninstall") || !strcasecmp(args[0], "remove")) { mm_uninstall(argc, args); exit(0); } else if (!strcasecmp(args[0], "generate-repository")) { mm_generate_repository(argc, args); exit(0); } else if (!strcasecmp(args[0], "parse-c-file")) { mm_parse_c_file(argc, args); exit(0); } /* Fetch the repository list */ if (!mm_refresh_repository()) { fprintf(stderr, "Fatal error encountered\n"); exit(-1); } if (!strcasecmp(args[0], "list")) mm_list(args[1]); else if (!strcasecmp(args[0], "info")) mm_info(argc, args); else if (!strcasecmp(args[0], "install")) { mm_install(argc, args, 0); fprintf(stderr, "All actions were successful.\n"); } else if (!strcasecmp(args[0], "upgrade")) mm_upgrade(argc, args); else mm_usage(); } #endif