#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_RESPONSE_WHITESPACE, GEMINI_STATE_RESPONSE_META, GEMINI_STATE_READ, GEMINI_STATE_DONE, GEMINI_STATE_ERROR }; #ifndef GEMINI_DEFAULT_PORT #define GEMINI_DEFAULT_PORT 1965 #endif #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 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 gemini_bounce(struct gemini *, const char *); 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); 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->metalen = 0; g->reqlen = 0; g->keylen = 0; g->certlen = 0; g->index = 0; 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_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->metalen = 0; g->reqlen = 0; g->index = 0; g->state = GEMINI_STATE_INIT; g->redirects = 0; g->status = 0; } struct gemini * gemini_create(void) { struct gemini *g; if ((g = malloc(sizeof(*g))) == NULL) return (NULL); gemini_init(g); return (g); } int gemini_open(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; if (g->port == 0) g->port = GEMINI_DEFAULT_PORT; if (g->state != GEMINI_STATE_INIT || 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, GET_HOSTNAME_PORT | GET_HOSTNAME_VALID)) == 0 || i > GEMINI_URL_MAX) { 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)); 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 || tls_connect(g->tls, g->request, (strchr(g->request, ':') == NULL ? portptr : NULL)) != 0 || tls_handshake(g->tls)) { warnx("TLS Error: %s", tls_error(g->tls)); goto err; } if (g->tofufile != NULL) { if ((hash = tls_peer_cert_hash(g->tls)) == NULL) goto err; if (g->tofufd < 0 && (g->tofufd = open(g->tofufile, ((g->flags & GEMINI_TOFU_WRITE) ? (O_RDWR | O_CREAT) : O_RDONLY) | O_EXLOCK, 0600)) < 0 && errno != ENOENT) { warn("Could not open: %s", g->tofufile); goto err; } if (g->tofu == NULL && g->tofufd >= 0 && load_tofu(&g->tofu, g->tofufd) < 0) { (void)close(g->tofufd); g->tofufd = -1; warn("Could not load: %s", g->tofufile); goto err; } if (g->tofu != NULL) { if (!check_tofu(g->tofu, g->request, hash)) goto err; if ((e = set_tofu(&g->tofu, g->request, hash, tls_peer_cert_notafter(g->tls))) < 0) goto err; if (e > 0) g->tofumod = 1; } } /* 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 (0); err: gemini_reset(g); return (-1); } void gemini_close(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)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_destroy(g); free(g); } 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; cb = b; if (g->state == GEMINI_STATE_ERROR) return (-1); if (g->state == GEMINI_STATE_DONE) return (0); if (g->state == GEMINI_STATE_REQUEST) { if (tls_write(g->tls, g->request, g->reqlen) != g->reqlen) goto errt; g->status = 0; g->index = 0; g->state = GEMINI_STATE_RESPONSE; } for (;;) { i = i != r ? i : 0; if (i == 0 && (r = tls_read(g->tls, b, bs)) < 0) goto errt; if (r == 0) goto eof; switch (g->state) { case GEMINI_STATE_RESPONSE: /* This is (probably) over-engineered. */ for (; i < r && g->index < 2; i++, g->index++) { if (!ISASCIIDIGIT(cb[i])) goto errinval; g->status = g->status * 10 + (cb[i] - '0'); } if (g->index < 2) break; g->state = GEMINI_STATE_RESPONSE_WHITESPACE; break; case GEMINI_STATE_RESPONSE_WHITESPACE: /* The unspecified maximum is kind-of absurd. */ for (; i < r && (cb[i] == ' ' || cb[i] == '\t'); i++) /* do nothing */; if (i == r) break; g->metalen = 0; g->state = GEMINI_STATE_RESPONSE_META; break; case GEMINI_STATE_RESPONSE_META: /* * If the white-space had a reasonable maximum * then this would be easier. */ if (i == 0 && g->meta[g->metalen - 1] == '\r' && cb[0] == '\n') { i++; g->meta[--g->metalen] = '\0'; } else if (i == 0 && cb[0] == '\r' && cb[1] == '\n') { i += 2; } else { for (j = i + (i == 0); j < r && (cb[j] != '\n' || cb[j - 1] != '\r'); j++) /* do nothing */; j -= j < r; if (g->metalen + j - i > GEMINI_META_MAX + j == r) goto errinval; (void)memcpy(g->meta + g->metalen, cb + i, j - i); g->meta[g->metalen += j - i] = '\0'; if (j == r) { i = j; break; } i = j + 2; } switch (g->status / 10) { case 0: goto errinval; case 1: r = -1 /* Do not refill the buffer */; if (g->flags & GEMINI_PROMPT) c = RPP_ECHO_ON; else if (g->flags & GEMINI_SECPROMPT) c = RPP_ECHO_OFF; else goto errunsup; /* Remove any control-characters */ for (i = 0, j = 0; i < g->metalen; i++) if (!iscntrl(g->meta[i])) g->meta[j++] = g->meta[i]; for (i = 0; i < g->reqlen && g->request[i] != '?'; i++) /* do nothing */; if (i == g->reqlen) g->request[i -= 2] = '?'; cb = g->request + ++i; j = GEMINI_URL_MAX - i; if (readpassphrase(g->meta, cb, j, c) == NULL) goto err; /* * readpassphrase(3) already truncates * the query, so there is no reason to * prevent the escape function from * doing so as well. */ (void)escapequery(cb, j, cb, strlen(cb)); g->request[GEMINI_URL_MAX] = '\0'; if (gemini_bounce(g, g->request) != 0) goto err; return (gemini_read(g, b, bs)); case 2: g->state = GEMINI_STATE_READ; break; case 3: if (g->redirects >= g->maxredirects) goto errredir; g->redirects++; if (gemini_bounce(g, g->meta) != 0) goto err; return (gemini_read(g, b, bs)); case 4: goto errg; case 5: goto errg; case 6: goto errcert; case 7: goto errinval; case 8: goto errinval; case 9: goto errinval; default: abort(); /* unreachable */ } break; case GEMINI_STATE_READ: if (i > 0) { (void)memmove(cb, cb + i, r - i); r -= i; } return (r); default: abort(); /* unreachable */; } } eof: if (g->state != GEMINI_STATE_READ) { warnx("Unexpected EOF"); goto err; } g->state = GEMINI_STATE_DONE; return (0); errt: warnx("TLS Error: %s", tls_error(g->tls)); goto err; errg: warnx("Gemini Error %02hd: %s", g->status, g->meta); goto err; errr: warnx("Too many redirects"); goto err; errcert: warnx("Certificate Error %02hd: %s", g->status, g->meta); goto err; errinval: warnx("Invalid response"); goto err; errunsup: warnx("Unsupported status: %02hd", g->status); goto err; errredir: warnx("Redirect %02hd: %s", g->status, g->meta); goto err; err: g->state = GEMINI_STATE_ERROR; return (-1); } int gemini_bounce(struct gemini *g, const char *url) { char b[GEMINI_URL_MAX + 1]; size_t l; gemini_reset(g); if ((l = strlen(url)) > GEMINI_URL_MAX) { errno = EINVAL; return (-1); } (void)memcpy(b, url, l); b[l] = '\0'; return (gemini_open(g, b)); } 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. */ if ((f & GET_HOSTNAME_PORT) && he < sl && src[he] == ':') for (he++; he < sl && ISASCIIDIGIT(src[he]); he++) /* do nothing */; 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); }