/* Copyright (C) 2022 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Additional permission under GNU GPL version 3 section 7: * * If you modify this Program, or any covered work, by linking or * combining it with OpenSSL (or a modified version of that library), * containing parts covered by the terms of the OpenSSL License and the * original SSLeay license, the licensors of this Program grant you * additional permission to convey the resulting work. Corresponding * Source for a non-source form of such a combination shall include the * source code for the parts of OpenSSL used as well as that of the * covered work. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include char *readpassphrase(const char *prompt, char *buf, size_t bufsiz, int flags); static bool verbose; static void log(const char *format, ...) __attribute__((format(printf, 1, 2))); static void log(const char *format, ...) { va_list ap; if (!verbose) return; va_start(ap, format); fprintf(stderr, "# "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); va_end(ap); } static int prompt1(char **resp, const char *def, const char *desc) { static char *buf; static size_t cap; size_t head = strcspn(desc, "\n"); for (*resp = NULL; !*resp;) { printf("%.*s [%s] ", (int)head, desc, (def ?: "")); ssize_t len = getline(&buf, &cap, stdin); if (len < 0 && feof(stdin)) { printf("\n"); clearerr(stdin); return -1; } else if (len < 0) { err(EX_IOERR, "getline"); } if (len && buf[len-1] == '\n') buf[len-1] = '\0'; if (buf[0] == '?') { printf("\n%s\n", &desc[head+1]); } else if (buf[0]) { *resp = strdup(buf); if (!*resp) err(EX_OSERR, "strdup"); } else if (def) { *resp = strdup(def); if (!*resp) err(EX_OSERR, "strdup"); } } return 0; } static int prompt(char **resp, const char *def, const char *format, ...) __attribute__((format(printf, 3, 4))); static int prompt(char **resp, const char *def, const char *format, ...) { va_list ap; char desc[1024]; va_start(ap, format); vsnprintf(desc, sizeof(desc), format, ap); va_end(ap); return prompt1(resp, def, desc); } static int yesno(bool *resp, bool def, const char *format, ...) __attribute__((format(printf, 3, 4))); static int yesno(bool *resp, bool def, const char *format, ...) { va_list ap; char desc[1024]; va_start(ap, format); vsnprintf(desc, sizeof(desc), format, ap); va_end(ap); char *str; for (;;) { int pop = prompt1(&str, (def ? "yes" : "no"), desc); if (pop) return pop; char yn = str[0]; free(str); if (yn == 'Y' || yn == 'y') { *resp = true; return 0; } else if (yn == 'N' || yn == 'n') { *resp = false; return 0; } } } static struct { char *network; char *config; char *host; char *port; struct addrinfo *addr; } info; static int getNetwork(const char *def) { return prompt( &info.network, def, "Which network would you like to connect to?\n" "Enter the domain name of the IRC network you wish to connect to.\n" "Examples: tilde.chat, libera.chat\n" ); } static int getConfig(const char *configHome) { char *name = strdup(info.network); if (!name) err(EX_OSERR, "strdup"); char *prev = NULL, *next = NULL; while (name) { prev = next; next = strsep(&name, "."); } const char *home = getenv("HOME"); const char *suffix = configHome; size_t homeLen = strlen(home); if (!strncmp(configHome, home, homeLen)) { suffix += homeLen; } int pop = prompt( &info.config, prev, "What would you like to name this configuration?\n" "This is the name you will pass to catgirl to connect to %s.\n" "Example: $ catgirl %s\n" "The configuration will be written to: %s%s/catgirl\n", info.network, prev, (suffix != configHome ? "~" : ""), suffix ); free(name); if (pop) return pop; char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/catgirl/%s", configHome, info.config); if (!access(path, F_OK)) { printf( "There is already a configuration named %s.\n" "Please remove or rename the file: %s%s/catgirl/%s\n" "Otherwise, enter a different name.\n", info.config, (suffix != configHome ? "~" : ""), suffix, info.config ); free(info.config); info.config = NULL; } return 0; } static int srvLookup(char **host, char **port, const char *name) { char dname[256]; snprintf(dname, sizeof(dname), "_ircs._tcp.%s", name); log("Looking up SRV record for %s", dname); uint8_t msg[512]; int len = res_query(dname, 1 /* IN */, 33 /* SRV */, msg, sizeof(msg)); if (len < 12) return -1; uint8_t *ptr = &msg[12]; #define NAME_SKIP \ for (uint8_t n; ptr < &msg[len] && (n = *ptr++); ptr += n) { \ if (n & 0xC0) { ptr++; break; } \ } uint16_t qdcount = msg[4] << 8 | msg[5]; for (uint16_t q = 0; ptr < &msg[len] && q < qdcount; ++q) { NAME_SKIP; // QNAME ptr += 4; // QTYPE, QCLASS } NAME_SKIP; // NAME ptr += 14; // TYPE, CLASS, TTL, RDLENGTH, Priority, Weight if (&msg[len] < ptr + 3) return -1; #undef NAME_SKIP int n = asprintf(port, "%d", ptr[0] << 8 | ptr[1]); if (n < 0) err(EX_OSERR, "asprintf"); ptr += 2; // Name compression is not used for Target. if (!ptr[0]) return -1; char *host0 = (char *)&ptr[1]; for (uint8_t n; ptr < &msg[len] && (n = *ptr); ptr += n) { *ptr++ = '.'; } *host = strdup(host0); if (!*host) err(EX_OSERR, "strdup"); return 0; } static int getHostPort(void) { int error = srvLookup(&info.host, &info.port, info.network); if (!error) { log("Found SRV record %s:%s", info.host, info.port); return 0; } char *ircdot = info.network; if (strncmp(ircdot, "irc.", 4)) { int n = asprintf(&ircdot, "irc.%s", ircdot); if (n < 0) err(EX_OSERR, "asprintf"); } int pop = prompt( &info.host, ircdot, "Which host should catgirl connect to?\n" "Enter the hostname of the IRC server to connect to. Usually this\n" "starts with \"irc.\".\n" ); free(ircdot); if (pop) return pop; return prompt( &info.port, "6697", "Which port should catgirl connect to?\n" "Enter the port number of the IRC server. This must be the port for\n" "TLS, also called SSL.\n" ); } static int getAddr(void) { log("Looking up %s:%s", info.host, info.port); struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, }; int error = getaddrinfo(info.host, info.port, &hints, &info.addr); if (error) { log("%s", gai_strerror(error)); printf("Could not find %s.\n", info.host); return -1; } return 0; } static void unset(char **resp) { free(*resp); *resp = NULL; } int main(int argc, char *argv[]) { for (int opt; 0 < (opt = getopt(argc, argv, "v"));) { switch (opt) { break; case 'v': verbose = true; break; default: return EX_USAGE; } } char configHome[PATH_MAX]; if (getenv("XDG_CONFIG_HOME")) { snprintf( configHome, sizeof(configHome), "%s", getenv("XDG_CONFIG_HOME") ); } else if (getenv("HOME")) { snprintf(configHome, sizeof(configHome), "%s/.config", getenv("HOME")); } else { errx(EX_USAGE, "HOME unset"); } char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/catgirl", configHome); int error = mkdir(path, S_IRWXU); if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path); umask(S_IRWXG | S_IRWXO); printf( "Welcome to catgirl enrollment! This tool should get you connected to\n" "IRC using catgirl :3\n" "\n" "Type ? at a prompt for a more detailed explanation.\n" "The default response is shown in [brackets].\n" "Press enter at a prompt to select the default response.\n" "Press control-D at a prompt to return to the previous one.\n" "\n" ); int pop; for (;;) { if (!info.network) { // TODO: Parse ircs: URL in args. pop = getNetwork((optind < argc ? argv[optind] : NULL)); if (pop) return EX_USAGE; } else if (!info.config) { pop = getConfig(configHome); if (pop) unset(&info.network); } else if (!info.host || !info.port) { pop = getHostPort(); if (pop) unset(&info.config); } else if (!info.addr) { pop = getAddr(); if (pop) { unset(&info.host); unset(&info.port); } } else { break; } } snprintf(path, sizeof(path), "%s/catgirl/%s", configHome, info.config); log("Writing configuration to %s", path); FILE *file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); fprintf(file, "host = %s\n", info.host); if (strcmp(info.port, "6697")) { fprintf(file, "port = %s\n", info.port); } error = fclose(file); if (error) err(EX_IOERR, "%s", path); printf( "\n" "You're all set! You can connect to %s using:\n" "$ catgirl %s\n" "\n", info.network, info.config ); bool run; pop = yesno( &run, true, "Connect now?\n" "There's nothing left to do. This will start catgirl.\n" ); if (pop || !run) return EX_OK; execlp("catgirl", "catgirl", info.config, NULL); err(EX_UNAVAILABLE, "catgirl"); }