#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libgeminiclient.h" enum { GEMINI_STATE_INIT = 0, GEMINI_STATE_REQUEST, GEMINI_STATE_RESPONSE, GEMINI_STATE_READ, GEMINI_STATE_DONE, GEMINI_STATE_ERROR }; #ifndef GEMINI_DEFAULT_PORT #define GEMINI_DEFAULT_PORT 1965 #endif #define ISASCIIBLANK(c) ((c) == '\t' || (c) == ' ') #define ISASCIILOWER(c) ((c) >= 'a' && (c) <= 'z') #define ISASCIIUPPER(c) ((c) >= 'A' && (c) <= 'Z') #define ISASCIIDIGIT(c) ((c) >= '0' && (c) <= '9') #define ISASCIIXDIGIT(c) (ISASCIIDIGIT(c) || \ ((c) >= 'A' && (c) <= 'F') || ((c) >= 'a' && (c) <= 'f')) #define ISASCIIALPHA(c) (ISASCIILOWER(c) || ISASCIIUPPER(c)) #define ISASCIIALNUM(c) (ISASCIIALPHA(c) || ISASCIIDIGIT(c)) #define GET_HOSTNAME_PORT 0x01 #define GET_HOSTNAME_VALID 0x02 static int set_tofu(struct gemini_tofu **, const char *, const char *, time_t); static int load_tofu(struct gemini_tofu **, int); static int check_tofu(struct gemini_tofu *, const char *, const char *); static int write_tofu(struct gemini_tofu *, int); static void free_tofu(struct gemini_tofu *); static size_t escapequery(char *, size_t, const char *, size_t); static size_t get_hostname(char *, size_t, const char *, size_t, int); static int setflock(int, int); void gemini_init(struct gemini *g) { g->tls = NULL; g->tls_config = NULL; g->tofu = NULL; g->keymem = NULL; g->certmem = NULL; g->keyfile = NULL; g->certfile = NULL; g->tofufile = NULL; g->proxy = NULL; g->meta = NULL; g->extra = NULL; g->reqlen = 0; g->reslen = 0; g->metalen = 0; g->extralen = 0; g->keylen = 0; g->certlen = 0; g->index = 0; g->socketfd = -1; g->tofufd = -1; g->tofumod = 0; g->flags = 0; g->state = GEMINI_STATE_INIT; g->redirects = 0; g->maxredirects = GEMINI_REDIRECT_MAX; g->status = 0; g->port = 0; } void gemini_envinit(struct gemini *g) { const char *s; char *p; unsigned long ul; gemini_init(g); g->keyfile = getenv("GEMINI_KEYFILE"); g->certfile = getenv("GEMINI_CERTFILE"); g->certfile = getenv("GEMINI_TOFUFILE"); g->proxy = getenv("GEMINI_PROXY"); if ((s = getenv("GEMINI_MAX_REDIRECTS")) != NULL) { errno = 0; ul = strtoul(s, &p, 0); if (errno == 0 && (p == s || *p != '\0')) errno = EINVAL; if (errno == 0 && ul > INT_MAX) errno = ERANGE; if (errno == 0) g->maxredirects = ul; else warn("GEMINI_MAX_REDIRECTS"); } if ((s = getenv("GEMINI_PORT")) != NULL) { errno = 0; ul = strtoul(s, &p, 0); if (errno == 0 && (p == s || *p != '\0')) errno = EINVAL; if (errno == 0 && ul > SHRT_MAX) errno = ERANGE; if (errno == 0) g->port = ul; else warn("GEMINI_PORT"); } } void gemini_reset(struct gemini *g) { if (g->tls_config != NULL) { tls_config_free(g->tls_config); g->tls_config = NULL; } if (g->tls) { tls_free(g->tls); g->tls = NULL; } g->tls = NULL; g->tls_config = NULL; g->meta = NULL; g->extra = NULL; g->reqlen = 0; g->reslen = 0; g->metalen = 0; g->extralen = 0; g->index = 0; g->state = GEMINI_STATE_INIT; g->status = 0; if (g->port == 0) g->port = GEMINI_DEFAULT_PORT; } struct gemini * gemini_create(void) { struct gemini *g; if ((g = malloc(sizeof(*g))) == NULL) return (NULL); gemini_init(g); return (g); } int gemini_connect_query(struct gemini *g, const char *url, const char *q) { char b[GEMINI_URL_MAX + 1]; size_t urllen; size_t querylen; if ((urllen = strlen(url)) > GEMINI_URL_MAX) { errno = EINVAL; return (-1); } (void)memcpy(b, url, urllen); b[urllen] = '\0'; if (q != NULL) { b[urllen] = '?'; querylen = strlen(q); if (urllen == GEMINI_URL_MAX || escapequery(b + urllen + 1, GEMINI_URL_MAX - urllen - 1 + 1, q, querylen) > GEMINI_URL_MAX - urllen - 1) { errno = EINVAL; return (-1); } } return (gemini_connect(g, b)); } int gemini_connect(struct gemini *g, const char *url) { char portstr[16]; const char *hash; char *portptr; size_t urllen; size_t i; size_t j; int e; short port; gemini_reset(g); if (g->port < 0) { errno = EINVAL; return (-1); } if ((urllen = strlen(url)) > GEMINI_URL_MAX) { errno = ENAMETOOLONG; return (-1); } /* The request is simply: $URL\r\n */ g->reqlen = urllen + 2; /* Create the `port' string */ for (port = g->port, i = 14; port > 0; i--, port /= 10) portstr[i] = port % 10 + '0'; portstr[15] = '\0'; portptr = portstr + i + 1; /* Extract the `hostname' */ if ((i = get_hostname(g->request, GEMINI_HOSTNAME_MAX + 1, url, urllen, (g->socketfd < 0 ? GET_HOSTNAME_PORT : 0) | GET_HOSTNAME_VALID)) == 0 || i > GEMINI_URL_MAX) { errno = EINVAL; return (-1); } /* Proxy */ if (g->proxy != NULL && strncpy(g->request, g->proxy, sizeof(g->request)) >= g->request + sizeof(g->request)) { errno = EINVAL; return (-1); } /* Initialize TLS */ if ((g->tls_config = tls_config_new()) == NULL) return (-1); tls_config_insecure_noverifycert(g->tls_config); tls_config_insecure_noverifyname(g->tls_config); tls_config_insecure_noverifytime(g->tls_config); if (g->keyfile != NULL && g->keymem == NULL) g->keymem = tls_load_file(g->keyfile, &g->keylen, NULL); if (g->certfile != NULL && g->certmem == NULL) g->certmem = tls_load_file(g->certfile, &g->certlen, NULL); if ((g->keymem != NULL && tls_config_set_key_mem(g->tls_config, g->keymem, g->keylen) != 0) || (g->certmem != NULL && tls_config_set_cert_mem(g->tls_config, g->certmem, g->certlen) != 0)) { warnx("TLS-Config Error: %s", tls_config_error(g->tls_config)); goto err; } if ((g->tls = tls_client()) == NULL) { tls_config_free(g->tls_config); g->tls_config = NULL; return (-1); } /* Connect */ if (tls_configure(g->tls, g->tls_config) != 0 || (g->socketfd >= 0 && tls_connect_socket(g->tls, g->socketfd, g->request) != 0) || (g->socketfd < 0 && tls_connect(g->tls, g->request, (strchr(g->request, ':') == NULL ? portptr : NULL))) || (e = tls_handshake(g->tls)) == -1) { warnx("TLS Error: %s", tls_error(g->tls)); goto err; } if ((hash = tls_peer_cert_hash(g->tls)) == NULL) goto err; if (g->tofufile != NULL) { if (g->tofufd < 0 && (g->tofufd = open(g->tofufile, (g->flags & GEMINI_TOFU_WRITE) ? (O_RDWR | O_CREAT) : O_RDONLY, 0600)) < 0 && errno != ENOENT) { warn("Could not open: %s", g->tofufile); goto err; } if (g->tofufd >= 0 && (g->flags & GEMINI_TOFU_WRITE) && setflock(g->tofufd, F_WRLCK) == -1) { (void)close(g->tofufd); g->tofufd = -1; warn("Could not lock the TOFU file"); goto err; } if (g->tofu == NULL && g->tofufd >= 0 && load_tofu(&g->tofu, g->tofufd) < 0) { (void)setflock(g->tofufd, F_UNLCK); (void)close(g->tofufd); g->tofufd = -1; warn("Could not load: %s", g->tofufile); goto err; } } /* The certificate against the current list */ if (g->tofu != NULL && !check_tofu(g->tofu, g->request, hash)) goto err; /* * Add the host's information to the list. * Even without a file-backing this is still useful for * long-running clients. * open gemini://example.com * redirect gemini://example.com/ * input gemini://example.com/?inputdata * redirect gemini://example.com/some/path */ switch (set_tofu(&g->tofu, g->request, hash, tls_peer_cert_notafter(g->tls))) { case 0: break; case 1: g->tofumod = 1; break; default: goto err; } /* Construct the request */ (void)memcpy(g->request, url, urllen); g->request[g->reqlen - 2] = '\r'; g->request[g->reqlen - 1] = '\n'; g->state = GEMINI_STATE_REQUEST; return (e); err: gemini_reset(g); return (-1); } ssize_t gemini_read(struct gemini *g, void *b, size_t bs) { char *cb; ssize_t r; size_t i = 0; size_t j; int c; if (g->state == GEMINI_STATE_ERROR) return (-1); if (g->state == GEMINI_STATE_DONE) return (0); if (g->state == GEMINI_STATE_REQUEST) { while (g->index < g->reqlen) { if ((r = tls_write(g->tls, g->request + g->index, g->reqlen - g->index)) <= 0) goto errt; g->index += r; } g->reslen = 0; g->index = 0; g->status = 0; g->state = GEMINI_STATE_RESPONSE; } if (g->state == GEMINI_STATE_RESPONSE) { do { if (g->reslen >= sizeof(g->response)) goto errinval; if ((r = tls_read(g->tls, g->response + g->reslen, sizeof(g->response) - g->reslen)) <= 0) goto errt; for (i = g->reslen, g->reslen += r; i < g->reslen && g->response[i] != '\n'; i++) /* do nothing */; } while (i == g->reslen); g->extra = g->response + i + 1; g->extralen = g->reslen - i - 1; g->reslen = i - (g->response[i - 1] == '\r'); g->response[g->reslen] = '\0'; if (g->reslen < 2) goto errinval; if (!ISASCIIDIGIT(g->response[0]) || !ISASCIIDIGIT(g->response[1])) goto errinval; if (g->reslen > 2 && !ISASCIIBLANK(g->response[2])) goto errinval; g->meta = g->response + (g->reslen > 2 ? 3 : 2); g->metalen = g->reslen > 2 ? g->reslen - 3 : 0; g->status = (g->response[0] - '0') * 10 + (g->response[1] - '0'); if (g->response[0] != '3') g->redirects = 0; if (g->response[0] != '2' && g->extralen > 0) goto errinval; switch (g->response[0]) { case '0': goto errinval; case '1': goto done; case '2': g->index = 0; g->state = GEMINI_STATE_READ; break; case '3': if (g->maxredirects == 0) goto done; if (g->redirects >= g->maxredirects) goto err; g->redirects++; if (gemini_connect_query(g, g->meta, NULL) != 0) goto err; return (gemini_read(g, b, bs)); case '4': goto done; case '5': goto done; case '6': goto done; case '7': goto errinval; case '8': goto errinval; case '9': goto errinval; default: abort(); /* unreachable */ } } if (g->state != GEMINI_STATE_READ) abort(); /* unreachable */ if (g->index < g->extralen) { i = g->extralen - g->index; i = i < bs ? i : bs; (void)memcpy(b, g->extra + g->index, i); g->index += i; return (i); } return (tls_read(g->tls, b, bs)); eof: if (g->state != GEMINI_STATE_READ) goto err; done: g->state = GEMINI_STATE_DONE; return (0); errt: if (r == 0) goto eof; if (r != -1) return (r); warnx("TLS Error: %s", tls_error(g->tls)); goto err; errinval: g->status = -1; g->meta = "Invalid response"; goto err; err: g->state = GEMINI_STATE_ERROR; return (-1); } void gemini_fini(struct gemini *g) { if ((g->flags & GEMINI_TOFU_WRITE) && g->tofufd >= 0 && g->tofumod && (lseek(g->tofufd, 0, SEEK_SET) < 0 || write_tofu(g->tofu, g->tofufd) != 0 || ftruncate(g->tofufd, lseek(g->tofufd, 0, SEEK_CUR)) != 0)) warn("Could not write: %s", g->tofufile); if (g->tofu != NULL) { free_tofu(g->tofu); g->tofu = NULL; } if (g->tofufd >= 0) { (void)setflock(g->tofufd, F_UNLCK); (void)close(g->tofufd); g->tofufd = -1; } g->tofumod = 0; if (g->keymem != NULL) { free(g->keymem); g->keymem = NULL; g->keylen = 0; } if (g->certmem != NULL) { free(g->certmem); g->certmem = NULL; g->certlen = 0; } gemini_reset(g); } void gemini_destroy(struct gemini *g) { gemini_fini(g); free(g); } int set_tofu(struct gemini_tofu **tofu, const char *host, const char *hash, time_t date) { struct gemini_tofu *prev; struct gemini_tofu *new; for (prev = NULL, new = *tofu; new != NULL;) for (prev = NULL, new = *tofu; new != NULL; prev = new, new = new->next) if (strcasecmp(new->host, host) == 0) { if (strcasecmp(new->hash, hash) == 0 && difftime(new->date, date) <= 0) return (1); free(new->host); free(new->hash); if (prev == NULL) *tofu = new->next; else prev->next = new->next; free(new); prev = NULL; new = *tofu; break; } /* Append the new entry */ if ((new = malloc(sizeof(*new))) == NULL) return (-1); new->next = NULL; new->host = strdup(host); new->hash = strdup(hash); new->date = date; if (prev == NULL) *tofu = new; else prev->next = new; return (0); } int load_tofu(struct gemini_tofu **tofup, int fd) { char b[4096]; struct tm tm; struct gemini_tofu *tofu = NULL; const char *host; const char *hash; char *p; ssize_t r; size_t i; size_t e; size_t o; for (o = 0; o < sizeof(b);) { if ((r = read(fd, b + o, sizeof(b) - o)) < 0) goto err; o += r; if (memchr(b, '\n', o) == NULL) { if (r == 0) break; else continue; } /* Get the host entry */ for (i = 0, e = 0; e < o && !isblank(b[e]); e++) /* do nothing */; if (e == o) { /* blank line */ o = 0; continue; } b[e] = '\0'; host = b + i; /* Get the hash entry */ for (i = e + 1; i < o && isblank(b[i]); i++) /* do nothing */; for (e = i; e < o && !isblank(b[e]); e++) /* do nothing */; b[e] = '\0'; hash = b + i; /* Get the date entry */ for (i = e + 1; i < o && isblank(b[i]); i++) /* do nothing */; for (e = i; e < o && b[e] != '\n'; e++) /* do nothing */; b[e] = '\0'; if ((p = strptime(b + i, "%s", &tm)) == NULL || *p != '\0') { warnx("Invalid time: %s", b + i); goto err; } if (set_tofu(&tofu, host, hash, timegm(&tm)) != 0) goto err; if (++e < o) (void)memmove(b, b + e, o - e); o = e >= o ? 0 : o - e; } if (o > 0) goto err; *tofup = tofu; return (0); err: free_tofu(tofu); return (-1); } int check_tofu(struct gemini_tofu *tofu, const char *host, const char *hash) { time_t date; int r = 1; date = time(NULL); for (; tofu != NULL; tofu = tofu->next) if (strcasecmp(tofu->host, host) == 0 && difftime(tofu->date, date) <= 0) r = strcasecmp(tofu->hash, hash) == 0; return (r); } int write_tofu(struct gemini_tofu *tofu, int fd) { char b[4096]; struct tm *tm; time_t t; ssize_t r; size_t i; size_t l; int c = 0; for (; tofu != NULL; tofu = tofu->next) { if ((l = strlen(tofu->host)) >= sizeof(b)) { c = -1; continue; } (void)memcpy(b, tofu->host, l); b[l++] = ' '; i = l; if ((l = strlen(tofu->hash)) >= sizeof(b) - i) { c = -1; continue; } (void)memcpy(b + i, tofu->hash, l); b[i + l++] = ' '; i += l; t = tofu->date; tm = gmtime(&t); if ((l = strftime(b + i, sizeof(b) - i, "%s", tm)) == 0) { c = -1; continue; } b[i + l++] = '\n'; for (l = i + l, i = 0; i < l && (r = write(fd, b + i, l - i)) >= 0; i += r) /* do nothing */; if (r < 0) { c = -1; break; } } return (c); } void free_tofu(struct gemini_tofu *tofu) { struct gemini_tofu *next; while (tofu != NULL) { free(tofu->host); free(tofu->hash); next = tofu->next; free(tofu); tofu = next; } } size_t escapequery(char *dst, size_t dl, const char *src, size_t sl) { size_t i; size_t j; size_t r; int c; /* Count the truncated length */ for (i = 0, j = 0; i < sl && j < dl; i++, j++) if (!ISASCIIALNUM(src[i]) && strchr("!$&'()*+,-.:;=@_~", src[i]) == NULL) { if (dl - j < 3) break; j += 2; } /* Count the total length */ for (r = j; i < sl; i++, r++) if (!ISASCIIALNUM(src[i]) && strchr("!$&'()*+,-.:;=@_~", src[i]) == NULL) r += 2; if (dst == NULL) return (r); /* Write backwards to prevent clobbering (src == dst) */ if (j < dl) (void)memset(dst + j, '\0', dl - j); for (; i > 0; i--, j--) if (!ISASCIIALNUM(src[i - 1]) && strchr("!$&'()*+,-.:;=@_~", src[i - 1]) == NULL) { c = src[i - 1] & 0x0F; c += c < 10 ? '0' : 'A' - 10; dst[j - 1] = c; c = (src[i - 1] >> 4) & 0x0F; c += c < 10 ? '0' : 'A' - 10; dst[j - 2] = c; dst[j - 3] = '%'; j -= 2; } else dst[j - 1] = src[i - 1]; return (r); } size_t pctmatch(const char *s, size_t l, const char *c) { size_t i; if (c == NULL) c = ""; for (i = 0; i < l; i++) { if (l - i >= 3 && s[i] == '%' && ISASCIIXDIGIT(s[i + 1]) && ISASCIIXDIGIT(s[i + 2])) i += 2; else if (!ISASCIIALNUM(s[i]) && strchr(c, s[i]) == NULL) break; } return (i); } size_t get_hostname(char *dst, size_t dl, const char *src, size_t sl, int f) { size_t hs = 0; size_t he; size_t v; if (sl == 0) return (0); /* Optionally skip the `scheme:' */ if (ISASCIIALPHA(src[0])) { for (he = 1; he < sl && (ISASCIIALNUM(src[he]) || src[he] == '+' || src[he] == '-' || src[he] == '.'); he++) /* do nothing */; if (he < sl && src[he] == ':') { if (strncasecmp(src + hs, "gemini", he - hs) != 0) return (0); hs = he + 1; } } /* Optionally skip the `//' */ if (sl - hs >= 2 && src[hs + 0] == '/' && src[hs + 1] == '/') hs += 2; /* Optionally skip the `userinfo@' */ he = pctmatch(src + hs, sl - hs, "~$&'()*+,-.:;=_~"); if (hs + he < sl && src[hs + he] == '@') hs += he + 1; /* * Process any IP-literal. * It is not really worth it to actually parse IPv6 strings * just to extract the hostname for tls_connect(3). * Otherwise process the `reg-name', of which IPv4 addresses * are a subset. */ if (hs < sl && src[hs] == '[') { for (he = hs + 1; he < sl && (ISASCIIALNUM(src[he]) || strchr("~$&'()*+,-.:;=_~", src[he]) != NULL); he++) /* do nothing */; if (he >= sl || src[he] != ']') return (0); } else if ((he = hs + pctmatch(src + hs, sl - hs, "~$&'()*+,-.;=_~")) == hs) return (0); /* Include the `:port', if requested. */ v = he; if (v < sl && src[v] == ':') for (v++; v < sl && ISASCIIDIGIT(src[v]); v++) /* do nothing */; if (f & GET_HOSTNAME_PORT) he = v; if (f & GET_HOSTNAME_VALID) { v = he; if (v < sl && src[v] == '/') v += 1 + pctmatch(src + v + 1, sl - v - 1, "!$&'()*+,-./:;=@_~"); if (v < sl && src[v] == '?') v += 1 + pctmatch(src + v + 1, sl - v - 1, "!$&'()*+,-./:;=?@_~"); if (v < sl && src[v] == '#') v += 1 + pctmatch(src + v + 1, sl - v - 1, "!$&'()*+,-./:;=?@_~"); if (v != sl) return (0); } /* Copy the `hostname' to the destination. */ if (he - hs < dl) { (void)memmove(dst, src + hs, he - hs); if (he - hs + 1 < dl) dst[he - hs] = '\0'; } return (he - hs); } int setflock(int fd, int lock) { struct flock l; (void)memset(&l, 0, sizeof(l)); l.l_type = lock; l.l_whence = SEEK_SET; l.l_start = 0; l.l_len = 0; return (fcntl(fd, F_SETLK, &l)); }