about summary refs log tree commit diff
diff options
context:
space:
mode:
authorC. McEnroe <june@causal.agency>2020-12-04 00:25:51 -0500
committerC. McEnroe <june@causal.agency>2020-12-04 17:53:44 -0500
commit1a57869a9ebc1880e51f829b3b1b63720c52a6aa (patch)
treea7d4e11846e5818fbe1945b93c67bbae544279eb
parentClean up variable expansions in shell script (diff)
downloadimbox-1a57869a9ebc1880e51f829b3b1b63720c52a6aa.tar.gz
imbox-1a57869a9ebc1880e51f829b3b1b63720c52a6aa.zip
Implement getservinfo for SRV lookup
-rw-r--r--Makefile1
-rw-r--r--getservinfo.c150
-rw-r--r--imap.c39
-rw-r--r--imap.h1
4 files changed, 189 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index 925b713..2c78527 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@ MANS = ${BINS:=.1}
 
 -include config.mk
 
+OBJS += getservinfo.o
 OBJS += imap.o
 OBJS += imbox.o
 
diff --git a/getservinfo.c b/getservinfo.c
new file mode 100644
index 0000000..3fc87b8
--- /dev/null
+++ b/getservinfo.c
@@ -0,0 +1,150 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <netdb.h>
+#include <netinet/in.h>
+#include <resolv.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef EAI_BADHINTS
+#define EAI_BADHINTS EAI_BADFLAGS
+#endif
+
+#ifndef EAI_PROTOCOL
+#define EAI_PROTOCOL EAI_BADFLAGS
+#endif
+
+static uint16_t u16(uint8_t **ptr) {
+	uint16_t x = *(*ptr)++;
+	x = x << 8 | *(*ptr)++;
+	return x;
+}
+
+static void nameSkip(uint8_t **ptr) {
+	for (uint8_t len; (len = *(*ptr)++); *ptr += len) {
+		if (len & 0xC0) {
+			(*ptr)++;
+			break;
+		}
+	}
+}
+
+static char *nameString(uint8_t **ptr) {
+	char *name = (char *)(*ptr + 1);
+	for (uint8_t len; (len = **ptr); *ptr += len) {
+		*(*ptr)++ = '.';
+	}
+	return name;
+}
+
+/* A wrapper around getaddrinfo(3) which first performs SRV record (RFC 2782)
+ * lookup. hints must be provided and hints->ai_protocol must be set. SRV
+ * lookup is skipped if servname is numerical. If SRV lookup is successful, the
+ * ai_canonname field of the first addrinfo structure returned is set to the
+ * target name.
+ */
+int getservinfo(
+	const char *hostname, const char *servname,
+	const struct addrinfo *hints, struct addrinfo **res
+) {
+	if (!hints) return EAI_BADHINTS;
+	if (!hints->ai_protocol) return EAI_PROTOCOL;
+	if (hints->ai_flags & AI_NUMERICHOST) return EAI_BADFLAGS;
+	if (hints->ai_flags & AI_NUMERICSERV) return EAI_BADFLAGS;
+
+	char *rest;
+	strtoul(servname, &rest, 10);
+	if (!*rest) return getaddrinfo(hostname, servname, hints, res);
+
+	struct protoent *proto = getprotobynumber(hints->ai_protocol);
+	if (!proto) return EAI_PROTOCOL;
+
+	char dname[256];
+	int len = snprintf(
+		dname, sizeof(dname), "_%s._%s.%s",
+		servname, proto->p_name, hostname
+	);
+	if ((size_t)len >= sizeof(dname)) return EAI_OVERFLOW;
+
+	uint8_t msg[512];
+	len = res_query(dname, 1 /* IN */, 33 /* SRV */, msg, sizeof(msg));
+	if (len < 0) return getaddrinfo(hostname, servname, hints, res);
+
+	uint8_t *ptr = msg;
+	u16(&ptr); // ID
+	uint16_t rcode = u16(&ptr) & 0x000F;
+	uint16_t qdcount = u16(&ptr);
+	uint16_t ancount = u16(&ptr);
+	u16(&ptr); // NSCOUNT
+	u16(&ptr); // ARCOUNT
+
+	if (rcode || !ancount) return getaddrinfo(hostname, servname, hints, res);
+
+	for (uint16_t q = 0; q < qdcount; ++q) {
+		nameSkip(&ptr); // QNAME
+		u16(&ptr); // QTYPE
+		u16(&ptr); // QCLASS
+	}
+
+	// Read only one answer, ignoring Priority and Weight. Does anyone actually
+	// use them?
+	nameSkip(&ptr); // NAME
+	u16(&ptr); // TYPE
+	u16(&ptr); // CLASS
+	u16(&ptr); // TTL
+	u16(&ptr); // TTL
+	u16(&ptr); // RDLENGTH
+	u16(&ptr); // Priority
+	u16(&ptr); // Weight
+
+	uint16_t port = u16(&ptr);
+	hostname = nameString(&ptr);
+	if (!hostname[0]) return EAI_NONAME;
+
+	char myServ[sizeof("65535")];
+	snprintf(myServ, sizeof(myServ), "%hu", port);
+
+	struct addrinfo myHints = *hints;
+#ifdef AI_DEFAULT
+	if (!myHints.ai_flags) myHints.ai_flags = AI_DEFAULT;
+#endif
+	myHints.ai_flags |= AI_NUMERICSERV;
+	myHints.ai_flags &= ~AI_CANONNAME;
+
+	int error = getaddrinfo(hostname, myServ, &myHints, res);
+	if (error) return error;
+
+	(*res)->ai_canonname = strdup(hostname);
+	if (!(*res)->ai_canonname) {
+		freeaddrinfo(*res);
+		return EAI_MEMORY;
+	}
+	return 0;
+}
diff --git a/imap.c b/imap.c
index 10864aa..e21141d 100644
--- a/imap.c
+++ b/imap.c
@@ -26,12 +26,16 @@
  */
 
 #include <err.h>
+#include <netdb.h>
+#include <netinet/in.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/socket.h>
 #include <sysexits.h>
 #include <tls.h>
+#include <unistd.h>
 
 FILE *funopen(
 	const void *cookie,
@@ -43,6 +47,11 @@ FILE *funopen(
 
 #include "imap.h"
 
+int getservinfo(
+	const char *hostname, const char *servname,
+	const struct addrinfo *hints, struct addrinfo **res
+);
+
 const char *Atoms[AtomCap] = {
 #define X(id, str) [id] = str,
 	ENUM_ATOM
@@ -92,10 +101,36 @@ struct IMAP imapOpen(const char *host, const char *port) {
 	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
 	tls_config_free(config);
 
-	error = tls_connect(client, host, port);
-	if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client));
+	struct addrinfo *head;
+	struct addrinfo hints = {
+		.ai_family = PF_UNSPEC,
+		.ai_socktype = SOCK_STREAM,
+		.ai_protocol = IPPROTO_TCP,
+	};
+	error = getservinfo(host, port, &hints, &head);
+	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error));
+
+	int sock = -1;
+	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
+		sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (sock < 0) err(EX_OSERR, "socket");
+
+		error = connect(sock, ai->ai_addr, ai->ai_addrlen);
+		if (!error) break;
+
+		close(sock);
+		sock = -1;
+	}
+	if (sock < 0) err(EX_UNAVAILABLE, "%s:%s", host, port);
+
+	error = tls_connect_socket(
+		client, sock, (head->ai_canonname ? head->ai_canonname : host)
+	);
+	if (error) errx(EX_SOFTWARE, "tls_connect_socket: %s", tls_error(client));
+	freeaddrinfo(head);
 
 	struct IMAP imap = {
+		.sock = sock,
 		.r = funopen(client, imapRead, NULL, NULL, NULL),
 		.w = funopen(client, NULL, imapWrite, NULL, imapClose),
 	};
diff --git a/imap.h b/imap.h
index 3f8050b..c6d96f5 100644
--- a/imap.h
+++ b/imap.h
@@ -165,6 +165,7 @@ static inline void respFree(struct Resp resp) {
 extern bool imapVerbose;
 
 struct IMAP {
+	int sock;
 	FILE *r;
 	FILE *w;
 	size_t cap;