summaryrefslogtreecommitdiff
path: root/minecctl
diff options
context:
space:
mode:
authorDavid Härdeman <david@hardeman.nu>2020-06-26 03:23:41 +0200
committerDavid Härdeman <david@hardeman.nu>2020-06-26 03:23:41 +0200
commit06a2edfc72a894054ed710d338504242f15d6071 (patch)
tree00ab94400cab0a5d46dbd68648539003732eb19e /minecctl
parentc9575bb333c036e5470d0ff872169cdb4626e023 (diff)
Make the server handling in minecctl saner
Diffstat (limited to 'minecctl')
-rw-r--r--minecctl/meson.build1
-rw-r--r--minecctl/minecctl-rcon.c80
-rw-r--r--minecctl/minecctl.c468
-rw-r--r--minecctl/minecctl.h22
-rw-r--r--minecctl/misc.c215
-rw-r--r--minecctl/misc.h14
6 files changed, 457 insertions, 343 deletions
diff --git a/minecctl/meson.build b/minecctl/meson.build
index 9f320a6..6db6489 100644
--- a/minecctl/meson.build
+++ b/minecctl/meson.build
@@ -1,6 +1,7 @@
minecctl_sources = [
'minecctl.c',
'minecctl-rcon.c',
+ 'misc.c',
]
dep_readline = dependency('readline')
diff --git a/minecctl/minecctl-rcon.c b/minecctl/minecctl-rcon.c
index fd880f5..b97e03e 100644
--- a/minecctl/minecctl-rcon.c
+++ b/minecctl/minecctl-rcon.c
@@ -13,6 +13,7 @@
#include "minecctl.h"
#include "minecctl-rcon.h"
#include "rcon-protocol.h"
+#include "misc.h"
static void
send_packet(int sfd, const char *buf, size_t len)
@@ -101,31 +102,34 @@ send_msg(int sfd, char *buf, size_t len, enum rcon_packet_type type,
}
static int
-rcon_login(struct cfg *cfg)
+rcon_login(struct cfg *cfg, struct server *server)
{
char buf[4096];
int32_t rtype;
const char *reply;
int fd;
- assert_die(cfg, "invalid arguments");
+ assert_die(cfg && server, "invalid arguments");
- if (!cfg->password)
- cfg->password = ask_password();
+ if (list_empty(&server->rcon_addrs))
+ info("server rcon addrs empty");
- if (!cfg->password)
- die("Can't login - no password");
+ fd = connect_any(&server->rcon_addrs, false);
+
+ if (!server->rcon_password)
+ server->rcon_password = ask_password();
- fd = connect_any(&cfg->addrs, false);
+ if (!server->rcon_password)
+ die("Can't login - no password");
- send_msg(fd, buf, sizeof(buf), RCON_PACKET_LOGIN, cfg->password,
+ send_msg(fd, buf, sizeof(buf), RCON_PACKET_LOGIN, server->rcon_password,
&rtype, &reply);
/* An rcon password isn't exactly super-secret, but can't hurt */
explicit_bzero(buf, sizeof(buf));
- explicit_bzero(cfg->password, strlen(cfg->password));
- xfree(cfg->password);
- cfg->password = NULL;
+ explicit_bzero(server->rcon_password, strlen(server->rcon_password));
+ xfree(server->rcon_password);
+ server->rcon_password = NULL;
if (rtype == RCON_PACKET_LOGIN_OK)
info("Login ok");
@@ -233,10 +237,14 @@ do_status(struct cfg *cfg) {
unsigned epacks, apacks;
unsigned bannedplayers, bannedips;
int fd;
+ struct server *server;
- assert_die(cfg, "invalid arguments");
+ if (list_empty(&cfg->servers))
+ die("No servers defined");
- fd = rcon_login(cfg);
+ server = list_first_entry(&cfg->servers, struct server, list);
+ read_server_config(server);
+ fd = rcon_login(cfg, server);
if (get_one_status(fd, buf, sizeof(buf), "seed", 1,
"Seed : [ %[^]]]", &reply, tbuf))
@@ -311,10 +319,15 @@ get_player_count(int fd, unsigned *current, unsigned *max)
void
do_stop(struct cfg *cfg) {
int fd;
+ struct server *server;
+
+ if (list_empty(&cfg->servers))
+ die("No servers defined");
- assert_die(cfg, "invalid arguments");
+ server = list_first_entry(&cfg->servers, struct server, list);
+ read_server_config(server);
+ fd = rcon_login(cfg, server);
- fd = rcon_login(cfg);
if (cfg->force_stop) {
unsigned current, _unused_ max;
@@ -349,10 +362,15 @@ void
do_pcount(struct cfg *cfg) {
int fd;
unsigned current, max;
+ struct server *server;
+
+ if (list_empty(&cfg->servers))
+ die("No servers defined");
- assert_die(cfg, "invalid arguments");
+ server = list_first_entry(&cfg->servers, struct server, list);
+ read_server_config(server);
+ fd = rcon_login(cfg, server);
- fd = rcon_login(cfg);
if (get_player_count(fd, &current, &max))
info("Players: %u/%u", current, max);
else
@@ -364,23 +382,19 @@ do_console(struct cfg *cfg)
{
char *prompt;
char *cmd;
- const char *sname;
int fd;
+ struct server *server;
- assert_die(cfg, "invalid arguments");
-
- fd = rcon_login(cfg);
+ if (list_empty(&cfg->servers))
+ die("No servers defined");
- if (cfg->server)
- sname = cfg->server->shortname;
- else if (cfg->addrstr)
- sname = cfg->addrstr;
- else
- die("can't find server name");
+ server = list_first_entry(&cfg->servers, struct server, list);
+ read_server_config(server);
+ fd = rcon_login(cfg, server);
prompt = alloca(strlen(program_invocation_short_name) +
- STRLEN(" (") + strlen(sname) + STRLEN("): ") + 1);
- sprintf(prompt, "%s (%s): ", program_invocation_short_name, sname);
+ STRLEN(" (") + strlen(server->shortname) + STRLEN("): ") + 1);
+ sprintf(prompt, "%s (%s): ", program_invocation_short_name, server->shortname);
while (true) {
char *tmp;
@@ -415,10 +429,14 @@ do_console(struct cfg *cfg)
void
do_command(struct cfg *cfg) {
int fd;
+ struct server *server;
- assert_die(cfg, "invalid arguments");
+ if (list_empty(&cfg->servers))
+ die("No servers defined");
- fd = rcon_login(cfg);
+ server = list_first_entry(&cfg->servers, struct server, list);
+ read_server_config(server);
+ fd = rcon_login(cfg, server);
send_cmd(fd, cfg->cmdstr);
}
diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c
index 7c01736..c8cc34d 100644
--- a/minecctl/minecctl.c
+++ b/minecctl/minecctl.c
@@ -2,7 +2,6 @@
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
-#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
@@ -19,143 +18,68 @@
#include "minecctl-rcon.h"
#include "config-parser.h"
#include "server-config-options.h"
+#include "misc.h"
#include "config.h"
static struct cfg *cfg = NULL;
-bool use_colors = false;
-
-/* FIXME: Can be shared */
static void
-set_use_colors()
-{
- int fd;
- const char *e;
-
- if (getenv("NO_COLOR"))
- return;
-
- fd = fileno(stderr);
- if (fd < 0)
- return;
-
- if (!isatty(fd))
- return;
-
- e = getenv("TERM");
- if (!e)
- return;
-
- if (streq(e, "dumb"))
- return;
-
- use_colors = true;
-}
-
-void
-__debug(_unused_ enum debug_lvl lvl, const char *fmt, ...)
-{
- va_list ap;
-
- if (use_colors) {
- if (lvl & DBG_ERROR)
- fprintf(stderr, ANSI_RED);
- else if (!(lvl & (DBG_INFO | DBG_VERBOSE)))
- fprintf(stderr, ANSI_GREY);
- }
-
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
-
- if (use_colors && !(lvl & (DBG_INFO | DBG_VERBOSE)))
- fprintf(stderr, ANSI_NORMAL);
-}
-
-_noreturn_ void
-__die(const char *fmt, ...)
-{
- va_list ap;
-
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
-
- exit(EXIT_FAILURE);
-}
-
-void *
-__zmalloc(const char *fn, int line, size_t size)
+dump_config()
{
- void *ptr;
-
- assert_die(!empty_str(fn) && line > 0 && size > 0, "invalid arguments");
-
- ptr = calloc(1, size);
- if (!ptr)
- die("malloc: %m");
- return ptr;
+ info("Configuration:");
+ info("password : %s", cfg->password);
+ info("cfgdir : %s", cfg->cfgdir);
+ info("addrstr : %s", cfg->addrstr);
+ info("cmdstr : %s", cfg->cmdstr);
+ info("cmd : %p", cfg->cmd);
+ info("force stop : %s", cfg->force_stop ? "yes" : "no");
+ info("servers : %sempty", list_empty(&cfg->servers) ? "" : "not ");
+ /* FIXME: dump servers */
}
-char *
-__xstrdup(const char *fn, int line, const char *s)
+void
+read_server_config(struct server *server)
{
- char *ptr;
-
- assert_die(!empty_str(fn) && line > 0 && !empty_str(s), "invalid arguments");
-
- ptr = strdup(s);
- if (!ptr)
- die("strdup: %m");
- return ptr;
-}
+ char buf[4096];
+ size_t off = 0;
+ ssize_t r;
+ int dfd;
+ int fd;
+ char *pos = buf;
-char *
-__xstrndup(const char *fn, int line, const char *s, size_t n)
-{
- char *ptr;
+ if (!server || !server->filename || server->file_read)
+ return;
- assert_die(!empty_str(fn) && line > 0 && !empty_str(s) && n > 0, "invalid arguments");
+ server->file_read = true;
- ptr = strndup(s, n);
- if (ptr)
- die("strdup: %m");
- return ptr;
-}
+ dfd = open(cfg->cfgdir, O_DIRECTORY | O_PATH | O_CLOEXEC);
+ if (dfd < 0)
+ die("Failed to open %s: %m", cfg->cfgdir);
-void
-__xfree(const char *fn, int line, void *ptr)
-{
- assert_die(!empty_str(fn) && line > 0, "invalid arguments");
+ fd = openat(dfd, server->filename, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ die("Failed to open %s: %m", server->filename);
- free(ptr);
-}
+ close(dfd);
-static void
-dump_config()
-{
- assert_die(cfg, "cfg not set");
+ while (true) {
+ r = read(fd, buf + off, sizeof(buf) - off - 1);
+ if (r < 0)
+ die("Failed to read %s: %m", server->filename);
+ else if (r == 0)
+ break;
- info("Configuration:");
- info("password : %s", cfg->password);
- info("cfgdir : %s", cfg->cfgdir);
- info("addrstr : %s", cfg->addrstr);
- info("cmdstr : %s", cfg->cmdstr);
- info("server : %p", cfg->server);
- info("cmd : %p", cfg->cmd);
- info("force stop : %s", cfg->force_stop ? "yes" : "no");
- info("addrs : %sempty", list_empty(&cfg->addrs) ? "" : "not ");
- info("mcaddrs : %sempty", list_empty(&cfg->mcaddrs) ? "" : "not ");
- info("known_servers : %sempty", list_empty(&cfg->addrs) ? "" : "not ");
-}
+ off += r;
+ if (off == sizeof(buf) - 1)
+ die("Failed to read %s: file too large", server->filename);
+ }
-static void
-parse_server_config(char *buf, const char *filename)
-{
- assert_die(buf && filename && cfg, "invalid arguments");
+ buf[off] = '\0';
+ close(fd);
- if (!config_parse_header(SERVER_CFG_HEADER, &buf))
- die("Unable to parse %s: invalid/missing header", filename);
+ if (!config_parse_header(SERVER_CFG_HEADER, &pos))
+ die("Unable to parse %s: invalid/missing header",
+ server->filename);
/* FIXME: this will cause superflous DNS lookups of other cfg entries */
while (true) {
@@ -163,82 +87,41 @@ parse_server_config(char *buf, const char *filename)
const char *keyname;
struct cfg_value value;
- if (!config_parse_line(filename, &buf, scfg_key_map,
+ if (!config_parse_line(server->filename, &pos, scfg_key_map,
&key, &keyname, &value, false))
break;
switch (key) {
case SCFG_KEY_RCON:
- if (!list_empty(&cfg->addrs))
- die("rcon address defined twice in %s", filename);
- list_replace(&value.saddrs, &cfg->addrs);
+ if (!list_empty(&server->rcon_addrs))
+ die("rcon address defined twice in %s",
+ server->filename);
+ list_replace(&value.saddrs, &server->rcon_addrs);
break;
case SCFG_KEY_RCON_PASSWORD:
- if (!cfg->password)
- cfg->password = xstrdup(value.str);
+ if (server->rcon_password)
+ die("rcon password defined twice in %s",
+ server->filename);
+ server->rcon_password = xstrdup(value.str);
break;
case SCFG_KEY_REMOTE:
- if (!list_empty(&cfg->mcaddrs))
- die("rcon address defined twice in %s", filename);
- list_replace(&value.saddrs, &cfg->mcaddrs);
+ if (!list_empty(&server->mc_addrs))
+ die("rcon address defined twice in %s",
+ server->filename);
+ list_replace(&value.saddrs, &server->mc_addrs);
default:
continue;
}
-
- if (cfg->password &&
- !list_empty(&cfg->addrs) &&
- !list_empty(&cfg->mcaddrs))
- break;
}
- if (!cfg->password)
- verbose("rcon password not found in %s", filename);
+ if (!server->rcon_password)
+ verbose("rcon password not found in %s", server->filename);
- if (list_empty(&cfg->addrs))
- verbose("rcon address not found in %s", filename);
+ if (list_empty(&server->rcon_addrs))
+ verbose("rcon address not found in %s", server->filename);
- if (list_empty(&cfg->mcaddrs))
- verbose("mc server address not found in %s", filename);
-}
-
-static void
-read_server_config()
-{
- char buf[4096];
- size_t off = 0;
- ssize_t r;
- int dfd;
- int fd;
-
- assert_die(cfg && cfg->server && cfg->cfgdir, "invalid arguments");
-
- dfd = open(cfg->cfgdir, O_DIRECTORY | O_PATH | O_CLOEXEC);
- if (dfd < 0)
- die("Failed to open %s: %m", cfg->cfgdir);
-
- fd = openat(dfd, cfg->server->filename, O_RDONLY | O_CLOEXEC);
- if (fd < 0)
- die("Failed to open %s: %m", cfg->server->filename);
-
- close(dfd);
-
- while (true) {
- r = read(fd, buf + off, sizeof(buf) - off - 1);
- if (r < 0)
- die("Failed to read %s: %m", cfg->server->filename);
- else if (r == 0)
- break;
-
- off += r;
- if (off == sizeof(buf) - 1)
- die("Failed to read %s: file too large", cfg->server->filename);
- }
-
- buf[off] = '\0';
- close(fd);
-
- parse_server_config(buf, cfg->server->filename);
- dump_config();
+ if (list_empty(&server->mc_addrs))
+ verbose("mc server address not found in %s", server->filename);
}
_noreturn_ static void
@@ -279,32 +162,21 @@ usage(bool no_error)
exit(no_error ? EXIT_FAILURE : EXIT_SUCCESS);
}
-static char *
-strv_join(char * const *strv)
+static struct server *
+allocate_server()
{
- size_t len = 0;
- char *r, *to;
-
- for (unsigned i = 0; strv[i]; i++)
- len += strlen(strv[i]) + 1;
-
- if (len == 0)
- return NULL;
-
- r = zmalloc(len);
- to = r;
+ struct server *server;
- for (unsigned i = 0; strv[i]; i++) {
- if (i > 0)
- *(to++) = ' ';
- to = stpcpy(to, strv[i]);
- }
+ server = zmalloc(sizeof(*server));
+ list_init(&server->rcon_addrs);
+ list_init(&server->mc_addrs);
+ list_init(&server->list);
- return r;
+ return server;
}
static void
-get_known_servers()
+get_servers()
{
struct dirent *dent;
DIR *dir;
@@ -322,7 +194,7 @@ get_known_servers()
if (!is_valid_server_config_filename(dent, NULL))
continue;
- server = zmalloc(sizeof(*server));
+ server = allocate_server();
server->filename = xstrdup(dent->d_name);
suffix = strrchr(dent->d_name, '.');
@@ -330,112 +202,116 @@ get_known_servers()
*suffix = '\0';
server->shortname = xstrdup(dent->d_name);
- list_add(&server->list, &cfg->known_servers);
+ list_add(&server->list, &cfg->servers);
}
closedir(dir);
}
-static void
-do_list(struct cfg *cfg)
-{
- struct server *server;
-
- list_for_each_entry(server, &cfg->known_servers, list)
- info("%s", server->shortname);
-}
-
-static void
-set_server(const char *name)
+static bool
+str_to_addrs(const char *str, struct list_head *list)
{
- struct server *server;
+ struct cfg_value value;
+ char *tmp = NULL;
+ bool rv = false;
+
+ /* strtosockaddrs mangles the input string */
+ tmp = xstrdup(str);
+ if (!strtosockaddrs(tmp, &value, false)) {
+ error("Unable to parse address: %s", str);
+ goto out;
+ }
- assert_die(cfg, "invalid arguments");
- assert_die(!cfg->server, "can't set server twice");
+ if (value.type != CFG_VAL_TYPE_ADDRS) {
+ error("Unexpected return value from strtosockaddrs");
+ goto out;
+ }
- list_for_each_entry(server, &cfg->known_servers, list) {
- if (streq(name, server->shortname)) {
- cfg->server = server;
- return;
- }
+ if (list_empty(&value.saddrs)) {
+ error("Found no valid addresses for %s", str);
+ goto out;
}
- error("\"%s\" is not a known server or command", name);
- usage(false);
+ list_replace(&value.saddrs, list);
+ rv = true;
+
+out:
+ xfree(tmp);
+ return rv;
}
-char *
-ask_password()
+static bool
+create_server_from_cmdline_args()
{
- struct termios old, new;
- char *password = NULL;
- size_t len = 0;
- ssize_t r;
+ struct server *server;
- assert_die(cfg, "invalid arguments");
+ if (!cfg->addrstr && !cfg->mcaddrstr)
+ return false;
- if (!isatty(STDIN_FILENO))
- return NULL;
+ server = allocate_server();
- if (tcgetattr(STDIN_FILENO, &old) < 0)
- return NULL;
+ if (cfg->addrstr) {
+ if (!str_to_addrs(cfg->addrstr, &server->rcon_addrs))
+ goto error;
- new = old;
- new.c_lflag &= ~ECHO;
- new.c_lflag |= ICANON;
- if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new) < 0)
- return NULL;
+ server->shortname = cfg->addrstr;
+ cfg->addrstr = NULL;
+ }
- fprintf(stderr, "Password: ");
- r = getline(&password, &len, stdin);
+ if (cfg->mcaddrstr) {
+ if (!str_to_addrs(cfg->mcaddrstr, &server->mc_addrs))
+ goto error;
- tcsetattr(STDIN_FILENO, TCSAFLUSH, &old);
+ if (!server->shortname)
+ server->shortname = cfg->mcaddrstr;
+ else
+ xfree(cfg->mcaddrstr);
- if (r < 0) {
- info("Error in getline: %m");
- clearerr(stdin);
- free(password);
- return NULL;
+ cfg->mcaddrstr = NULL;
}
- while (r > 0 && password[r - 1] == '\n')
- password[--r] = '\0';
+ if (cfg->password) {
+ server->rcon_password = cfg->password;
+ cfg->password = NULL;
+ }
- return password;
+ list_add(&server->list, &cfg->servers);
+ return true;
+
+error:
+ /* FIXME: free addrs */
+ xfree(server->shortname);
+ xfree(server);
+ return false;
}
-int
-connect_any(struct list_head *addrs, bool may_fail)
+static void
+do_list(struct cfg *cfg)
{
- struct saddr *saddr;
- bool connected = false;
- int sfd;
+ struct server *server;
- if (list_empty(addrs))
- die("No address to connect to");
+ list_for_each_entry(server, &cfg->servers, list)
+ info("%s", server->shortname);
+}
- list_for_each_entry(saddr, addrs, list) {
- sfd = socket(saddr->storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
- if (sfd < 0)
- die("socket: %m");
+static void
+set_server(const char *name)
+{
+ struct server *server;
- socket_set_low_latency(sfd, true, true, true);
+ assert_die(cfg, "invalid arguments");
- if (connect(sfd, (struct sockaddr *)&saddr->storage, saddr->addrlen) < 0) {
- close(sfd);
- continue;
+ list_for_each_entry(server, &cfg->servers, list) {
+ if (streq(name, server->shortname)) {
+ /* This puts the chosen server first in the list */
+ list_del(&server->list);
+ list_add(&server->list, &cfg->servers);
+ return;
}
-
- connected = true;
- break;
}
- if (!connected && may_fail)
- return -1;
- else if (!connected)
- die("Failed to connect to remote host");
- else
- return sfd;
+ error("\"%s\" is not a known server or command", name);
+ usage(false);
}
static inline void
@@ -561,7 +437,6 @@ parse_cmdline(int argc, char * const *argv)
usage(false);
}
- list_init(&cfg->addrs);
cfg->cfgdir = DEFAULT_CFG_DIR;
while (true) {
@@ -632,52 +507,43 @@ parse_cmdline(int argc, char * const *argv)
int
main(int argc, char **argv)
{
+ int rv = EXIT_FAILURE;
+
debug_mask = DBG_ERROR | DBG_INFO;
set_use_colors();
cfg = zmalloc(sizeof(*cfg));
- list_init(&cfg->addrs);
- list_init(&cfg->mcaddrs);
- list_init(&cfg->known_servers);
+ list_init(&cfg->servers);
parse_cmdline(argc, argv);
- get_known_servers();
+ get_servers();
parse_command(&argv[optind]);
- if (!cfg->cmd)
- die("Failed to parse command");
-
- if (cfg->server) {
- read_server_config();
-
- } else if (cfg->addrstr) {
- struct cfg_value value;
-
- /* FIXME: create a struct server here, fill in details like name */
- if (!strtosockaddrs(cfg->addrstr, &value, false))
- die("Unable to parse address: %s", cfg->addrstr);
-
- if (value.type != CFG_VAL_TYPE_ADDRS)
- die("Unexpected return value from strtosockaddrs");
-
- if (list_empty(&value.saddrs))
- die("Found no valid addresses for %s", cfg->addrstr);
-
- list_replace(&value.saddrs, &cfg->addrs);
+ if (!cfg->cmd) {
+ error("Failed to parse command");
+ goto out;
}
+ if (cfg->addrstr || cfg->mcaddrstr)
+ if (!create_server_from_cmdline_args())
+ goto out;
+
+ /* FIXME: Should return bool */
cfg->cmd(cfg);
+ rv = EXIT_SUCCESS;
+
+out:
+ /* FIXME: Not enough cleanup */
if (cfg->password)
explicit_bzero(cfg->password, strlen(cfg->password));
-
xfree(cfg->password);
xfree(cfg->addrstr);
xfree(cfg->mcaddrstr);
xfree(cfg);
- exit(EXIT_SUCCESS);
+ exit(rv);
}
diff --git a/minecctl/minecctl.h b/minecctl/minecctl.h
index 405a217..7146a08 100644
--- a/minecctl/minecctl.h
+++ b/minecctl/minecctl.h
@@ -2,30 +2,30 @@
#define foominecctlhfoo
struct server {
+ bool file_read;
char *filename;
char *shortname;
+ char *rcon_password;
+ struct list_head rcon_addrs;
+ struct list_head mc_addrs;
struct list_head list;
};
struct cfg {
- char *password;
+ /* command line arguments */
char *cfgdir;
+ char *password;
char *addrstr;
char *mcaddrstr;
char *cmdstr;
- struct server *server;
- void (*cmd)(struct cfg *cfg);
bool force_stop;
- struct list_head addrs;
- struct list_head mcaddrs;
- struct list_head known_servers;
-};
-extern bool use_colors;
-
-char *ask_password();
+ /* bookkeeping */
+ void (*cmd)(struct cfg *cfg);
+ struct list_head servers;
+};
-int connect_any(struct list_head *addrs, bool may_fail);
+void read_server_config(struct server *server);
#endif
diff --git a/minecctl/misc.c b/minecctl/misc.c
new file mode 100644
index 0000000..13b4377
--- /dev/null
+++ b/minecctl/misc.c
@@ -0,0 +1,215 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <string.h>
+#include <termios.h>
+
+#include "utils.h"
+#include "minecctl.h"
+
+bool use_colors = false;
+
+/* FIXME: Can be shared */
+void
+set_use_colors()
+{
+ int fd;
+ const char *e;
+
+ if (getenv("NO_COLOR"))
+ return;
+
+ fd = fileno(stderr);
+ if (fd < 0)
+ return;
+
+ if (!isatty(fd))
+ return;
+
+ e = getenv("TERM");
+ if (!e)
+ return;
+
+ if (streq(e, "dumb"))
+ return;
+
+ use_colors = true;
+}
+
+char *
+strv_join(char * const *strv)
+{
+ size_t len = 0;
+ char *r, *to;
+
+ for (unsigned i = 0; strv[i]; i++)
+ len += strlen(strv[i]) + 1;
+
+ if (len == 0)
+ return NULL;
+
+ r = zmalloc(len);
+ to = r;
+
+ for (unsigned i = 0; strv[i]; i++) {
+ if (i > 0)
+ *(to++) = ' ';
+ to = stpcpy(to, strv[i]);
+ }
+
+ return r;
+}
+
+char *
+ask_password()
+{
+ struct termios old, new;
+ char *password = NULL;
+ size_t len = 0;
+ ssize_t r;
+
+ if (!isatty(STDIN_FILENO))
+ return NULL;
+
+ if (tcgetattr(STDIN_FILENO, &old) < 0)
+ return NULL;
+
+ new = old;
+ new.c_lflag &= ~ECHO;
+ new.c_lflag |= ICANON;
+ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new) < 0)
+ return NULL;
+
+ fprintf(stderr, "Password: ");
+ r = getline(&password, &len, stdin);
+
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &old);
+
+ if (r < 0) {
+ info("Error in getline: %m");
+ clearerr(stdin);
+ free(password);
+ return NULL;
+ }
+
+ while (r > 0 && password[r - 1] == '\n')
+ password[--r] = '\0';
+
+ return password;
+}
+
+int
+connect_any(struct list_head *addrs, bool may_fail)
+{
+ struct saddr *saddr;
+ bool connected = false;
+ int sfd;
+
+ if (list_empty(addrs))
+ die("No address to connect to");
+
+ list_for_each_entry(saddr, addrs, list) {
+ verbose("Attempting connection to %s", saddr->addrstr);
+ sfd = socket(saddr->storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sfd < 0)
+ die("socket: %m");
+
+ socket_set_low_latency(sfd, true, true, true);
+
+ if (connect(sfd, (struct sockaddr *)&saddr->storage, saddr->addrlen) < 0) {
+ close(sfd);
+ continue;
+ }
+
+ connected = true;
+ break;
+ }
+
+ if (!connected && may_fail)
+ return -1;
+ else if (!connected)
+ die("Failed to connect to remote host");
+
+ return sfd;
+}
+
+void
+__debug(_unused_ enum debug_lvl lvl, const char *fmt, ...)
+{
+ va_list ap;
+
+ if (use_colors) {
+ if (lvl & DBG_ERROR)
+ fprintf(stderr, ANSI_RED);
+ else if (!(lvl & (DBG_INFO | DBG_VERBOSE)))
+ fprintf(stderr, ANSI_GREY);
+ }
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ if (use_colors && !(lvl & (DBG_INFO | DBG_VERBOSE)))
+ fprintf(stderr, ANSI_NORMAL);
+}
+
+_noreturn_ void
+__die(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ exit(EXIT_FAILURE);
+}
+
+void *
+__zmalloc(const char *fn, int line, size_t size)
+{
+ void *ptr;
+
+ assert_die(!empty_str(fn) && line > 0 && size > 0, "invalid arguments");
+
+ ptr = calloc(1, size);
+ if (!ptr)
+ die("malloc: %m");
+ return ptr;
+}
+
+char *
+__xstrdup(const char *fn, int line, const char *s)
+{
+ char *ptr;
+
+ assert_die(!empty_str(fn) && line > 0 && !empty_str(s), "invalid arguments");
+
+ ptr = strdup(s);
+ if (!ptr)
+ die("strdup: %m");
+ return ptr;
+}
+
+char *
+__xstrndup(const char *fn, int line, const char *s, size_t n)
+{
+ char *ptr;
+
+ assert_die(!empty_str(fn) && line > 0 && !empty_str(s) && n > 0, "invalid arguments");
+
+ ptr = strndup(s, n);
+ if (ptr)
+ die("strdup: %m");
+ return ptr;
+}
+
+void
+__xfree(const char *fn, int line, void *ptr)
+{
+ assert_die(!empty_str(fn) && line > 0, "invalid arguments");
+
+ free(ptr);
+}
+
diff --git a/minecctl/misc.h b/minecctl/misc.h
new file mode 100644
index 0000000..ca36dab
--- /dev/null
+++ b/minecctl/misc.h
@@ -0,0 +1,14 @@
+#ifndef foomischfoo
+#define foomischfoo
+
+extern bool use_colors;
+
+void set_use_colors();
+
+char *strv_join(char * const *strv);
+
+int connect_any(struct list_head *addrs, bool may_fail);
+
+char *ask_password();
+
+#endif