/* SPDX-License-Identifier: GPL-2.0 */ #include #include #include #include #include #include #include #include #include #include #include "shared/utils.h" #include "minecctl.h" #include "server.h" #include "misc-commands.h" #include "rcon-commands.h" #include "mc-commands.h" #include "shared/systemd.h" #include "misc.h" #include "examples/eula.txt.h" #include "examples/minecctl.conf.h" #include "examples/minecproxy.conf.h" #include "examples/minecproxy.service.h" #include "examples/minecserver@.service.h" #include "examples/README.TXT.h" static bool create_link(int dfd, const char *source, const char *target) { if (symlinkat(target, dfd, source) < 0) { error("Unable to create link %s -> %s: %m", source, target); return false; } return true; } static bool write_cfg_file(int dfd, const char *name, const unsigned char *content, size_t len) { _cleanup_close_ int fd; ssize_t done = 0, r; bool rv = true; fd = openat(dfd, name, O_WRONLY | O_CLOEXEC | O_CREAT | O_EXCL, 0644); if (fd < 0) { error("Unable to create file %s: %m", name); return false; } while (done < len) { r = write(fd, content + done, len - done); if (r < 0) { if (errno == EAGAIN || errno == EINTR) continue; error("Unable to write file %s: %m", name); rv = false; break; } done += r; } return rv; } static bool find_user_service(int xfd, const char *service) { _cleanup_close_ int dfd = -1; _cleanup_free_ char *dpath = NULL; /* FIXME: Make this a macro, make paths #defines */ char sub_path[STRLEN("systemd/user/") + strlen(service) + 1]; char etc_path[STRLEN("/etc/systemd/user/") + strlen(service) + 1]; char usr_path[STRLEN("/usr/lib/systemd/user/") + strlen(service) + 1]; sprintf(sub_path, "systemd/user/%s", service); sprintf(etc_path, "/etc/systemd/user/%s", service); sprintf(usr_path, "/usr/lib/systemd/user/%s", service); /* * We need to check (in order of precedence): * ~/.config/systemd/user/ - user-created * /etc/systemd/user/ - admin-created * ~/.local/share/systemd/user/ - user-installed packages * /usr/lib/systemd/user/ - system-installed packages */ if (faccessat(xfd, sub_path, R_OK, 0) == 0) { info("User service %s already installed in " "$XDG_CONFIG_HOME/systemd/user/", service); return true; } if (access(etc_path, R_OK) == 0) { info("User service %s already installed in " "/etc/systemd/user/", service); return true; } dfd = open_xdg_data_dir(false, &dpath); if (dfd >= 0) { if (faccessat(dfd, sub_path, R_OK, 0) == 0) { info("User service %s already installed in " "%s/systemd/user/", service, dpath); return true; } } if (access(usr_path, R_OK) == 0) { info("User service %s already installed in " "/usr/lib/systemd/user/", service); return true; } return false; } static bool create_user_service(int xfd, const char *service, unsigned char *content, size_t len) { _cleanup_close_ int sfd = -1; _cleanup_close_ int ufd = -1; if (find_user_service(xfd, service)) return true; sfd = open_subdir(xfd, "systemd", true); if (sfd < 0) return false; ufd = open_subdir(sfd, "user", true); if (ufd < 0) return false; if (!write_cfg_file(ufd, service, content, len)) return false; info("Created user service file $XDG_CONFIG_HOME/systemd/user/%s", service); return true; } static bool write_server_cfg(struct cfg *cfg, const char *name, const char *filename, const unsigned char *properties, size_t properties_len, const unsigned char *mcserver, size_t mcserver_len) { _cleanup_close_ int sfd = -1; int cfd, dfd; if (!cfg->cfg_dir || !cfg->data_dir) return false; cfd = dirfd(cfg->cfg_dir); dfd = dirfd(cfg->data_dir); sfd = open_subdir(dfd, name, true); if (sfd < 0) { error("Failed to create server directory \"%s\"", name); return false; } if (!write_cfg_file(cfd, filename, mcserver, mcserver_len)) return false; if (!write_cfg_file(sfd, "eula.txt", ___examples_eula_txt, ___examples_eula_txt_len)) return false; if (!write_cfg_file(sfd, MC_SERVER_PROPERTIES, properties, properties_len)) return false; if (!create_link(sfd, "server.jar", "../server.jar")) return false; return true; } bool do_init(_unused_ struct cfg *cfg) { _cleanup_close_ int xcfd = -1; _cleanup_close_ int mcfd = -1; _cleanup_close_ int xdfd = -1; _cleanup_close_ int mdfd = -1; xcfd = open_xdg_cfg_dir(true, NULL); if (xcfd < 0) return false; mcfd = open_subdir(xcfd, "minecproxy", true); if (mcfd < 0) return false; if (!write_cfg_file(mcfd, MINECPROXY_CFG_FILE, ___examples_minecproxy_conf, ___examples_minecproxy_conf_len)) return false; if (!write_cfg_file(mcfd, MINECCTL_CFG_FILE, ___examples_minecctl_conf, ___examples_minecctl_conf_len)) return false; if (!create_user_service(xcfd, SERVER_SYSTEMD_SERVICE, ___examples_minecserver__service, ___examples_minecserver__service_len)) return false; if (!create_user_service(xcfd, MINECPROXY_SYSTEMD_SERVICE, ___examples_minecproxy_service, ___examples_minecproxy_service_len)) return false; xdfd = open_xdg_data_dir(true, NULL); if (xdfd < 0) return false; mdfd = open_subdir(xdfd, "minecproxy", true); if (mdfd < 0) return false; if (!write_cfg_file(mdfd, "README.TXT", ___examples_README_TXT, ___examples_README_TXT_len)) return false; return true; } static bool valid_name(const char *name) { const char *f = name; if (empty_str(name)) { error("Empty server name given"); return false; } if (*f == '.') return false; while (*f != '\0') { if ((*f >= 'a' && *f <= 'z') || (*f >= 'A' && *f <= 'Z') || (*f >= '0' && *f <= '9') || (*f == ':') || (*f == '-') || (*f == '_') || (*f == '.') || (*f == '\\')) { f++; continue; } error("Server name \"%s\" contains invalid characters", name); return false; } if ((f - name) > (256 - STRLEN("minecserver@"))) { error("Server name \"%s\" is too long", name); return false; } return true; } /* FIXME: Share this */ static inline char tohex(uint8_t val) { static const char hex[] = "0123456789abcdef"; return hex[val & 0x0f]; } static bool generate_random_password(char *buf, size_t len) { size_t written; ssize_t r; const char *s; char *d; if (len < 3) return false; char rnd[(len - 1) / 2]; for (written = 0; written < sizeof(rnd); written += r) { r = getrandom(rnd + written, sizeof(rnd) - written, 0); if (r < 0) { if (errno == EAGAIN || errno == EINTR) continue; error("getrandom failure: %m"); return false; } } for (s = rnd, d = buf; s < (rnd + sizeof(rnd)); s++) { *(d++) = tohex(*s >> 4); *(d++) = tohex(*s); } *d = '\0'; return true; } static unsigned char *create_mc_server(const char *name, uint16_t local_port, size_t *len) { char *mcserver; /* FIXME: Add comments, commented out options */ mcserver = xsprintf(len, "[%s]\n" "type = proxy\n" "name = %s\n" "local = %" PRIu16 "\n" "idle_timeout = 600\n" "start_method = systemd\n" "stop_method = systemd\n", SERVER_CONFIG_FILE_HEADER, name, local_port); if (!mcserver) error("xsprintf: %m"); return (unsigned char *)mcserver; } static unsigned char *create_mc_properties(const char *name, uint16_t mc_port, uint16_t rcon_port, const char *rcon_password, size_t *len) { char hexrnd[16 + 1]; char *prop; if (!rcon_password) { if (!generate_random_password(hexrnd, sizeof(hexrnd))) { error("Failed to generate random password"); return NULL; } } prop = xsprintf(len, "#Minecraft server properties\n" "# This is a partial file, it will be replaced with a\n" "# fleshed out version the first time the Minecraft\n" "# server is executed.\n" "motd=%s\n" "#maybe uncomment the next line if you use minecproxy\n" "#server-ip=127.0.0.1\n" "server-port=%" PRIu16 "\n" "enable-rcon=true\n" "rcon.port=%" PRIu16 "\n" "rcon.password=%s\n", name, mc_port, rcon_port, rcon_password ? rcon_password : hexrnd); if (!prop) error("xsprintf: %m"); return (unsigned char *)prop; } static uint16_t get_port(struct list_head *list) { struct saddr *saddr; if (!list || list_empty(list)) return 0; list_for_each_entry(saddr, list, list) { switch (saddr->st.ss_family) { case AF_INET: if (saddr->in4.sin_addr.s_addr == htonl(INADDR_LOOPBACK) || saddr->in4.sin_addr.s_addr == htonl(INADDR_ANY) || saddr->in4.sin_addr.s_addr == htonl(INADDR_BROADCAST)) return htons(saddr->in4.sin_port); break; case AF_INET6: if (!memcmp(&saddr->in6.sin6_addr, &in6addr_any, sizeof(saddr->in6.sin6_addr)) || !memcmp(&saddr->in6.sin6_addr, &in6addr_loopback, sizeof(saddr->in6.sin6_addr))) return htons(saddr->in6.sin6_port); break; } } return 0; } static bool saddr_port_match(struct list_head *list, uint16_t port) { struct saddr *a; list_for_each_entry(a, list, list) { switch (a->st.ss_family) { case AF_INET: if (htons(a->in4.sin_port) == port) return true; break; case AF_INET6: if (htons(a->in6.sin6_port) == port) return true; break; default: break; } } return false; } static bool select_free_ports(struct cfg *cfg, uint16_t *listen_port, uint16_t *mc_port, uint16_t *rcon_port) { uint16_t lport = cfg->listen_port_min; uint16_t mport = cfg->mc_port_min; uint16_t rport = cfg->rcon_port_min; struct server *server; bool used; for (lport = cfg->listen_port_min, mport = cfg->mc_port_min, rport = cfg->rcon_port_min; lport <= cfg->listen_port_max && mport <= cfg->mc_port_max && rport <= cfg->rcon_port_max; lport++, mport++, rport++) { used = false; list_for_each_entry(server, &cfg->servers, list) { if (!server->scfg.filename) continue; if (saddr_port_match(&server->scfg.locals, lport)) { used = true; break; } if (saddr_port_match(&server->scfg.remotes, mport)) { used = true; break; } if (saddr_port_match(&server->scfg.rcons, rport)) { used = true; break; } } if (!used) { debug(DBG_CFG, "found unused port, " "listen: %" PRIu16 ", " "mc: %" PRIu16 ", " "rcon: %" PRIu16, lport, mport, rport); if (listen_port && *listen_port == 0) *listen_port = lport; if (mc_port && *mc_port == 0) *mc_port = mport; if (rcon_port && *rcon_port == 0) *rcon_port = rport; return true; } } return false; } bool do_new(struct cfg *cfg) { const char *name = cfg->commands[0]; struct server *server; struct server *defserver = NULL; uint16_t local_port = 0, mc_port = 0, rcon_port = 0; const char *rcon_password = NULL; _cleanup_free_ unsigned char *properties = NULL; size_t properties_len; _cleanup_free_ unsigned char *mcserver = NULL; size_t mcserver_len; if (!valid_name(name)) return false; char filename[strlen(name) + STRLEN(".") + STRLEN(SERVER_CONFIG_FILE_SUFFIX) + 1]; sprintf(filename, "%s.%s", name, SERVER_CONFIG_FILE_SUFFIX); if (!server_read_all_configs(cfg, false)) { error("Failed to read all existing server configurations, " "try running the \"lint\" command."); return false; } if (!cfg->cfg_dir || !cfg->data_dir) return false; list_for_each_entry(server, &cfg->servers, list) { if (!server->scfg.filename) { defserver = server; continue; } if (streq(server->scfg.filename, filename)) { error("Server \"%s\" already exists", name); return false; } } if (defserver) { rcon_port = get_port(&defserver->scfg.rcons); mc_port = get_port(&defserver->scfg.remotes); rcon_password = defserver->scfg.rcon_password; } else if (cfg->rcon_password) rcon_password = cfg->rcon_password; dump_config(cfg); if (!select_free_ports(cfg, &local_port, &mc_port, &rcon_port)) { error("Failed to find free port(s)"); return false; } properties = create_mc_properties(name, mc_port, rcon_port, rcon_password, &properties_len); if (!properties) return false; mcserver = create_mc_server(name, local_port, &mcserver_len); if (!mcserver) return false; if (!write_server_cfg(cfg, name, filename, properties, properties_len, mcserver, mcserver_len)) return false; info("Created server configuration %s", name); return true; } static bool recursive_unlink(int dfd) { _cleanup_closedir_ DIR *dir = NULL; struct dirent *dent; int sdfd; dir = fdopendir(dfd); if (!dir) { error("fdopendir: %m"); close(dfd); return false; } errno = 0; while ((dent = readdir(dir))) { if (streq(dent->d_name, ".") || streq(dent->d_name, "..")) continue; switch (dent->d_type) { case DT_DIR: sdfd = openat(dfd, dent->d_name, O_RDONLY | O_CLOEXEC | O_DIRECTORY | O_NOCTTY | O_NOFOLLOW | O_NOATIME); if (sdfd < 0) { error("Unable to open subdir %s: %m", dent->d_name); return false; } verbose("Checking subdir %s", dent->d_name); if (!recursive_unlink(sdfd)) return false; verbose("Deleting subdir %s", dent->d_name); if (unlinkat(dfd, dent->d_name, AT_REMOVEDIR) != 0) { error("Unable to unlink server subdir %s: %m", dent->d_name); return false; } break; case DT_LNK: _fallthrough_; case DT_REG: _fallthrough_; case DT_UNKNOWN: verbose("Unlinking file %s", dent->d_name); if (unlinkat(dfd, dent->d_name, 0) != 0) { error("Failed to unlink file: %s", dent->d_name); return false; } break; default: error("Unexpected file type in server directory: %s", dent->d_name); return false; } errno = 0; } if (errno != 0) { error("readdir: %m"); return false; } return true; } bool do_delete(struct cfg *cfg) { const char *name = cfg->commands[0]; char filename[strlen(name) + STRLEN(".") + STRLEN(SERVER_CONFIG_FILE_SUFFIX) + 1]; int dfd; sprintf(filename, "%s.%s", name, SERVER_CONFIG_FILE_SUFFIX); if (!valid_name(name)) return false; server_load_all_known(cfg); /* FIXME: Stop server first */ if (unlinkat(dirfd(cfg->cfg_dir), filename, 0) != 0) { error("Unable to remove \"%s\": %m", filename); if (!cfg->force) return false; } else { verbose("Removed \"%s\"", filename); if (!cfg->force) return true; } dfd = openat(dirfd(cfg->data_dir), name, O_RDONLY | O_CLOEXEC | O_DIRECTORY | O_NOCTTY | O_NOFOLLOW | O_NOATIME); if (dfd < 0) { error("Unable to open server directory: %m"); return false; } if (!recursive_unlink(dfd)) { error("Failed to remove some file(s) in server directory"); return false; } verbose("Deleting server directory %s", name); if (unlinkat(dirfd(cfg->data_dir), name, AT_REMOVEDIR)) { error("Failed to remove server directory: %m"); return false; } return true; } bool do_list(struct cfg *cfg) { struct server *server; server_load_all_known(cfg); /* server->scfg.filename check excludes servers created from cmdline */ list_for_each_entry(server, &cfg->servers, list) if (server->scfg.filename) info("• %s", server->scfg.name); return true; } static bool saddr_match(struct list_head *la, struct list_head *lb) { struct saddr *a, *b; list_for_each_entry(a, la, list) { list_for_each_entry(b, lb, list) { if (a->st.ss_family != b->st.ss_family) continue; switch (a->st.ss_family) { case AF_INET: if (memcmp(&a->in4.sin_addr, &b->in4.sin_addr, sizeof(a->in4.sin_addr))) continue; if (a->in4.sin_port != b->in4.sin_port) continue; return true; case AF_INET6: if (memcmp(&a->in6.sin6_addr, &b->in6.sin6_addr, sizeof(a->in6.sin6_addr))) continue; if (a->in6.sin6_port != b->in6.sin6_port) continue; return true; default: continue; } } } return false; } bool do_lint(struct cfg *cfg) { struct server *a, *b; unsigned ia, ib; bool rv = true; rv = server_read_all_configs(cfg, true); dump_config(cfg); ia = 0; list_for_each_entry(a, &cfg->servers, list) { ib = 0; list_for_each_entry(b, &cfg->servers, list) { if (ib <= ia) { ib++; continue; } if (a->scfg.announce_port != 0 && b->scfg.announce_port != 0 && a->scfg.announce_port == b->scfg.announce_port) info("%sNote:%s %s and %s appear to have the " "same announce port", ansi_red, ansi_normal, a->scfg.name, b->scfg.name); if (saddr_match(&a->scfg.locals, &b->scfg.locals)) info("%sNote:%s %s and %s appear to share at " "least one local address/port pair", ansi_red, ansi_normal, a->scfg.name, b->scfg.name); if (saddr_match(&a->scfg.remotes, &b->scfg.remotes)) info("%sNote:%s %s and %s appear to share at " "least one remote address/port pair", ansi_red, ansi_normal, a->scfg.name, b->scfg.name); if (saddr_match(&a->scfg.rcons, &b->scfg.rcons)) info("%sNote:%s %s and %s appear to share at " "least one rcon address/port pair", ansi_red, ansi_normal, a->scfg.name, b->scfg.name); ib++; } ia++; } return rv; } bool do_pcount(struct cfg *cfg) { unsigned online, max; struct server *server; const char *error; server = server_get_default(cfg); if (!server) return false; if (do_rcon_pcount(cfg, server, &online, &max, &error)) info("Rcon says %u/%u", online, max); if (do_mc_pcount(cfg, server, &online, &max, &error)) info("MC says %u/%u", online, max); return true; } static bool do_one_status(struct cfg *cfg, struct server *server) { unsigned online, max; const char *error; bool rv = true; info("• %s", server->scfg.name); if (list_empty(&server->scfg.rcons)) info(" rcon : not configured"); else if (do_rcon_pcount(cfg, server, &online, &max, &error)) info(" rcon : %sok%s", ansi_green, ansi_normal); else { info(" rcon : %sfail%s (%s)", ansi_red, ansi_normal, error); rv = false; } if (list_empty(&server->scfg.remotes)) info(" mc : not configured"); else if (do_mc_pcount(cfg, server, &online, &max, &error)) info(" mc : %sok%s", ansi_green, ansi_normal); else { info(" mc : %sfail%s (%s)", ansi_red, ansi_normal, error); rv = false; } if (!server->scfg.systemd_service || !server->scfg.systemd_obj) info(" systemd service : not configured"); else if (systemd_service_running(&server->scfg, &error)) info(" systemd service : %sactive%s", ansi_green, ansi_normal); else { info(" systemd service : %sfail%s (%s)", ansi_red, ansi_normal, error); rv = false; } info(""); return rv; } bool do_status(struct cfg *cfg) { struct server *server; if (cfg->default_set) { server = server_get_default(cfg); if (!server) { error("failed to get default server"); return false; } do_one_status(cfg, server); } else { server_read_all_configs(cfg, false); list_for_each_entry(server, &cfg->servers, list) do_one_status(cfg, server); } systemd_delete(); return true; }