diff --git a/README.md b/README.md
index 3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043..6dcd2f7b6f5e9d408b03fa2aae5c386fd3121aa1 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,6 @@
-- OpenSSL
+- BearSSL
diff --git a/config.sh b/config.sh
index 424cbda346869c7476859d55a600c414c86bbb46..87888929b98a46887dbcb25378385183e4ef2566 100644
--- a/config.sh
+++ b/config.sh
@@ -117,8 +117,8 @@ echo no
fi
done
printf "Checking for scdoc... "
if scdoc -v >/dev/null 2>&1
diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h
index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..16bef51024275bbb6ade9c90a11ae68876df387a 100644
--- a/include/gmni/gmni.h
+++ b/include/gmni/gmni.h
@@ -1,7 +1,7 @@
#ifndef GEMINI_CLIENT_H
#define GEMINI_CLIENT_H
+#include <bearssl_ssl.h>
#include <netdb.h>
-#include <openssl/ssl.h>
#include <stdbool.h>
#include <sys/socket.h>
@@ -52,20 +52,16 @@ struct gemini_response {
enum gemini_status status;
char *meta;
// Response body may be read from here if appropriate:
// Connection state
int fd;
};
struct gemini_options {
// If ai_family != AF_UNSPEC (the default value on most systems), the
// client will connect to this address and skip name resolution.
struct addrinfo *addr;
@@ -75,6 +71,8 @@ // example, to force IPv4/IPv6.
struct addrinfo *hints;
};
+struct gemini_tofu;
// Requests the specified URL via the gemini protocol. If options is non-NULL,
// it may specify some additional configuration to adjust client behavior.
//
@@ -84,6 +82,7 @@ // Caller must call gemini_response_finish afterwards to clean up resources
// before exiting or re-using it for another request.
enum gemini_result gemini_request(const char *url,
struct gemini_options *options,
struct gemini_tofu *tofu,
struct gemini_response *resp);
// Must be called after gemini_request in order to free up the resources
@@ -137,15 +136,20 @@ };
};
struct gemini_parser {
char *buf;
size_t bufsz;
size_t bufln;
bool preformatted;
};
-// Initializes a text/gemini parser which reads from the specified BIO.
-void gemini_parser_init(struct gemini_parser *p, BIO *f);
+// Initializes a text/gemini parser. The provided "read" function will be called
+// with the provided "state" value in order to obtain more gemtext data. The
+// read function should behave like read(3).
+void gemini_parser_init(struct gemini_parser *p,
int (*read)(void *state, void *buf, size_t nbyte),
void *state);
// Finishes this text/gemini parser and frees up its resources.
void gemini_parser_finish(struct gemini_parser *p);
diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h
index a88167ba0fb6606b2b170e5005c55131f1861972..a0981a5296421541a766846bc36c733dab1dfd1a 100644
--- a/include/gmni/tofu.h
+++ b/include/gmni/tofu.h
@@ -1,9 +1,7 @@
#ifndef GEMINI_TOFU_H
#define GEMINI_TOFU_H
+#include <bearssl_x509.h>
#include <limits.h>
-#include <openssl/ssl.h>
-#include <openssl/x509.h>
-#include <time.h>
enum tofu_error {
TOFU_VALID,
@@ -24,7 +22,6 @@ };
struct known_host {
char *host, *fingerprint;
int lineno;
struct known_host *next;
};
@@ -34,7 +31,23 @@ // certificate. Return true to trust this certificate.
typedef enum tofu_action (tofu_callback_t)(enum tofu_error error,
const char *fingerprint, struct known_host *host, void *data);
+struct gemini_tofu;
+struct x509_tofu_context {
+};
struct gemini_tofu {
char known_hosts_path[PATH_MAX+1];
struct known_host *known_hosts;
int lineno;
@@ -42,8 +55,7 @@ tofu_callback_t *callback;
void *cb_data;
};
-void gemini_tofu_init(struct gemini_tofu *tofu,
SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data);
+void gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *data);
void gemini_tofu_finish(struct gemini_tofu *tofu);
#endif
diff --git a/include/util.h b/include/util.h
index 6193fdf48c0ac6d313de7f35a1d09ecba62e3283..8ec9ac5d94092a75813a3e083140fbb88079c29d 100644
--- a/include/util.h
+++ b/include/util.h
@@ -1,5 +1,7 @@
#ifndef GEMINI_UTIL_H
#define GEMINI_UTIL_H
+#include <stdio.h>
+#include <sys/types.h>
struct pathspec {
const char *var;
diff --git a/src/client.c b/src/client.c
index a8a12f3cfbe1e98fd2045eec257cfaf95c319910..e402cc97d96a904a2a8e9e2db40345b222cb85ae 100644
--- a/src/client.c
+++ b/src/client.c
@@ -1,15 +1,15 @@
#include <assert.h>
#include <errno.h>
#include <netdb.h>
-#include <openssl/bio.h>
-#include <openssl/err.h>
-#include <openssl/ssl.h>
+#include <bearssl_ssl.h>
#include <stdlib.h>
+#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <gmni/gmni.h>
+#include <gmni/tofu.h>
#include <gmni/url.h>
static enum gemini_result
@@ -88,9 +88,41 @@
#define GEMINI_META_MAXLEN 1024
#define GEMINI_STATUS_MAXLEN 2
+static int
+sock_read(void *ctx, unsigned char *buf, size_t len)
+{
ssize_t rlen;
rlen = read(*(int *)ctx, buf, len);
if (rlen <= 0) {
if (rlen < 0 && errno == EINTR) {
continue;
}
return -1;
}
return (int)rlen;
+}
+static int
+sock_write(void *ctx, const unsigned char *buf, size_t len)
+{
ssize_t wlen;
wlen = write(*(int *)ctx, buf, len);
if (wlen <= 0) {
if (wlen < 0 && errno == EINTR) {
continue;
}
return -1;
}
return (int)wlen;
+}
enum gemini_result
gemini_request(const char *url, struct gemini_options *options,
struct gemini_response *resp)
struct gemini_tofu *tofu, struct gemini_response *resp)
{
assert(url);
assert(resp);
@@ -128,84 +160,50 @@ free(host);
goto cleanup;
}
resp->ssl_ctx = options->ssl_ctx;
SSL_CTX_up_ref(options->ssl_ctx);
resp->ssl_ctx = SSL_CTX_new(TLS_method());
assert(resp->ssl_ctx);
SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL);
int r;
res = gemini_connect(uri, options, resp, &resp->fd);
if (res != GEMINI_OK) {
free(host);
goto cleanup;
}
free(host);
goto ssl_error;
free(host);
goto ssl_error;
goto ssl_error;
goto ssl_error;
resp->status = X509_V_ERR_UNSPECIFIED;
res = GEMINI_ERR_SSL_VERIFY;
goto cleanup;
resp->status = vr;
res = GEMINI_ERR_SSL_VERIFY;
goto cleanup;
sock_read, &resp->fd, sock_write, &resp->fd);
char req[1024 + 3];
r = snprintf(req, sizeof(req), "%s\r\n", url);
assert(r > 0);
res = GEMINI_ERR_IO;
goto cleanup;
char buf[GEMINI_META_MAXLEN
+ GEMINI_STATUS_MAXLEN
+ 2 /* CRLF */ + 1 /* NUL */];
res = GEMINI_ERR_IO;
goto cleanup;
r = br_sslio_read(&resp->body, &buf[l], 1);
if (r < 0) {
break;
}
}
fprintf(stderr, "invalid line %d '%s'\n", r, buf);
// TODO: Bubble this up properly
fprintf(stderr, "SSL error %d\n", err);
goto ssl_error;
fprintf(stderr, "invalid line '%s'\n", buf);
res = GEMINI_ERR_PROTOCOL;
goto cleanup;
}
@@ -217,9 +215,9 @@ fprintf(stderr, "invalid status\n");
res = GEMINI_ERR_PROTOCOL;
goto cleanup;
}
cleanup:
curl_url_cleanup(uri);
@@ -237,26 +235,18 @@ if (!resp) {
return;
}
BIO_free_all(resp->bio);
resp->bio = NULL;
close(resp->fd);
resp->fd = -1;
}
SSL_free(resp->ssl);
SSL_CTX_free(resp->ssl_ctx);
free(resp->meta);
close(resp->fd);
resp->fd = -1;
br_sslio_close(&resp->body);
}
resp->meta = NULL;
}
@@ -277,11 +267,11 @@ return gai_strerror(resp->status);
case GEMINI_ERR_CONNECT:
return strerror(errno);
case GEMINI_ERR_SSL:
return ERR_error_string(
SSL_get_error(resp->ssl, resp->status),
NULL);
// TODO: more specific
return "SSL error";
case GEMINI_ERR_SSL_VERIFY:
return X509_verify_cert_error_string(resp->status);
// TODO: more specific
return "X.509 certificate not trusted";
case GEMINI_ERR_IO:
return "I/O error";
case GEMINI_ERR_PROTOCOL:
diff --git a/src/gmni.c b/src/gmni.c
index 49abb8524a012c27249006dc45f4688a846e63c3..a8321d06c367128706d19ac283a53e55d95d0a92 100644
--- a/src/gmni.c
+++ b/src/gmni.c
@@ -1,9 +1,8 @@
#include <assert.h>
+#include <bearssl_ssl.h>
#include <errno.h>
#include <getopt.h>
#include <netdb.h>
-#include <openssl/bio.h>
-#include <openssl/err.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
@@ -222,10 +221,7 @@ usage(argv[0]);
return 1;
}
bool exit = false;
struct Curl_URL *url = curl_url();
@@ -242,7 +238,8 @@ char *buf;
curl_url_get(url, CURLUPART_URL, &buf, 0);
struct gemini_response resp;
enum gemini_result r = gemini_request(buf, &opts, &resp);
enum gemini_result r = gemini_request(
buf, &opts, &cfg.tofu, &resp);
free(buf);
@@ -340,11 +337,8 @@
char last = 0;
char buf[BUFSIZ];
for (int n = 1; n > 0;) {
n = BIO_read(resp.bio, buf, BUFSIZ);
if (n == -1) {
fprintf(stderr, "Error: read\n");
return 1;
} else if (n != 0) {
n = br_sslio_read(&resp.body, buf, BUFSIZ);
if (n > 0) {
last = buf[n - 1];
}
ssize_t w = 0;
@@ -370,7 +364,6 @@ next:
gemini_response_finish(&resp);
}
curl_url_cleanup(url);
gemini_tofu_finish(&cfg.tofu);
return ret;
diff --git a/src/gmnlm.c b/src/gmnlm.c
index f31222b3ee2c495ed42c614bbd3c5100b52ae767..0ea492bb85fe024bd250c304808dcb8f0a915f90 100644
--- a/src/gmnlm.c
+++ b/src/gmnlm.c
@@ -1,22 +1,25 @@
#include <assert.h>
+#include <bearssl_ssl.h>
#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
#include <getopt.h>
+#include <gmni/gmni.h>
+#include <gmni/tofu.h>
+#include <gmni/url.h>
#include <libgen.h>
#include <limits.h>
-#include <openssl/bio.h>
-#include <openssl/err.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
+#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
-#include <gmni/gmni.h>
-#include <gmni/tofu.h>
-#include <gmni/url.h>
#include "util.h"
#define ANSI_COLOR_RED "\x1b[91m"
@@ -398,10 +401,13 @@ close(pfd[0]);
FILE *f = fdopen(pfd[1], "w");
// XXX: may affect history, do we care?
for (int n = 1; n > 0;) {
n = BIO_read(resp.bio, buf, BUFSIZ);
if (n == -1) {
fprintf(stderr, "Error: read\n");
return;
if (resp.sc) {
n = br_sslio_read(&resp.body, buf, BUFSIZ);
} else {
n = read(resp.fd, buf, BUFSIZ);
}
if (n < 0) {
n = 0;
}
ssize_t w = 0;
while (w < (ssize_t)n) {
@@ -445,23 +451,19 @@ resp->status = GEMINI_STATUS_BAD_REQUEST;
break;
}
FILE *fp = fopen(path, "r");
if (!fp) {
int fd = open(path, O_RDONLY);
if (fd < 0) {
resp->status = GEMINI_STATUS_NOT_FOUND;
/* Make sure members of resp evaluate to false, so that
gemini_response_finish does not try to free them. */
resp->bio = NULL;
resp->ssl = NULL;
resp->ssl_ctx = NULL;
// Make sure members of resp evaluate to false,
// so that gemini_response_finish does not try
// to free them.
resp->sc = NULL;
resp->meta = NULL;
resp->fd = -1;
free(path);
break;
}
BIO *file = BIO_new_fp(fp, BIO_CLOSE);
resp->bio = BIO_new(BIO_f_buffer());
BIO_push(resp->bio, file);
if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) {
resp->meta = strdup("text/gemini");
} else if (has_suffix(path, ".txt")) {
@@ -471,14 +473,14 @@ resp->meta = strdup("application/x-octet-stream");
}
free(path);
resp->status = GEMINI_STATUS_SUCCESS;
resp->fd = -1;
resp->ssl = NULL;
resp->ssl_ctx = NULL;
resp->fd = fd;
resp->sc = NULL;
return GEMINI_OK;
}
free(scheme);
res = gemini_request(browser->plain_url, &browser->opts, resp);
res = gemini_request(browser->plain_url, &browser->opts,
&browser->tofu, resp);
if (res != GEMINI_OK) {
fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp));
requesting = false;
@@ -835,12 +837,23 @@ }
return fprintf(f, "%s\n", s) - 1;
}
+static int
+resp_read(void *state, void *buf, size_t nbyte)
+{
return br_sslio_read(&resp->body, buf, nbyte);
return read(resp->fd, buf, nbyte);
+}
static bool
display_gemini(struct browser *browser, struct gemini_response *resp)
{
int nlinks = 0;
struct gemini_parser p;
free(browser->page_title);
browser->page_title = NULL;
@@ -1029,10 +1042,13 @@ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
char buf[BUFSIZ];
for (int n = 1; n > 0;) {
n = BIO_read(resp->bio, buf, BUFSIZ);
if (n == -1) {
fprintf(stderr, "Error: read\n");
return 1;
if (resp->sc) {
n = br_sslio_read(&resp->body, buf, BUFSIZ);
} else {
n = read(resp->fd, buf, BUFSIZ);
}
if (n < 0) {
n = 0;
}
ssize_t w = 0;
while (w < (ssize_t)n) {
@@ -1207,11 +1223,7 @@ } else {
open_bookmarks(&browser);
}
&tofu_callback, &browser);
struct gemini_response resp;
browser.running = true;
@@ -1273,7 +1285,6 @@ while (hist && hist->prev) {
hist = hist->prev;
}
history_free(hist);
curl_url_cleanup(browser.url);
free(browser.page_title);
free(browser.plain_url);
diff --git a/src/parser.c b/src/parser.c
index ad2c0e6306872f8dde450063ba8815e2ee7e314a..6f12456ddbe5248b59ea146464a1d76365505c56 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,21 +1,23 @@
#include <assert.h>
#include <ctype.h>
-#include <openssl/bio.h>
#include <stdbool.h>
#include <stddef.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gmni/gmni.h>
void
-gemini_parser_init(struct gemini_parser *p, BIO *f)
+gemini_parser_init(struct gemini_parser *p,
{
p->bufln = 0;
p->bufsz = BUFSIZ;
p->buf = malloc(p->bufsz + 1);
p->buf[0] = 0;
p->preformatted = false;
}
@@ -25,7 +27,6 @@ {
if (!p) {
return;
}
free(p->buf);
}
@@ -42,7 +43,7 @@ p->buf = realloc(p->buf, p->bufsz);
assert(p->buf);
}
ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1);
int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1);
if (n == -1) {
return -1;
} else if (n == 0) {
diff --git a/src/tofu.c b/src/tofu.c
index 48395c08cdbf68b31c5defd15b360f1eb2897a3f..de2bc14ce6a7c269cd430c2beebd64d6b493913b 100644
--- a/src/tofu.c
+++ b/src/tofu.c
@@ -1,95 +1,97 @@
#include <assert.h>
+#include <bearssl_hash.h>
+#include <bearssl_x509.h>
#include <errno.h>
+#include <gmni/gmni.h>
+#include <gmni/tofu.h>
#include <libgen.h>
#include <limits.h>
-#include <openssl/asn1.h>
-#include <openssl/evp.h>
-#include <openssl/ssl.h>
-#include <openssl/x509.h>
-#include <openssl/x509v3.h>
+#include <stdint.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
-#include <time.h>
-#include <gmni/gmni.h>
-#include <gmni/tofu.h>
#include "util.h"
-static int
-verify_callback(X509_STORE_CTX *ctx, void *data)
+static void
+xt_start_chain(const br_x509_class **ctx, const char *server_name)
{
+}
rc = X509_V_ERR_UNSPECIFIED;
goto invalid_cert;
+static void
+xt_start_cert(const br_x509_class **ctx, uint32_t length)
+{
return;
}
rc = X509_V_ERR_CERT_NOT_YET_VALID;
goto invalid_cert;
cc->err = BR_ERR_X509_TRUNCATED;
return;
}
rc = X509_V_ERR_UNSPECIFIED;
goto invalid_cert;
+}
+static void
+xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len)
+{
return;
}
rc = X509_V_ERR_CERT_HAS_EXPIRED;
goto invalid_cert;
cc->err = err;
}
+}
snprintf(&fingerprint[i * 3], 4, "%02X%s",
md[i], i + 1 == sizeof(md) ? "" : ":");
+static void
+xt_end_cert(const br_x509_class **ctx)
+{
return;
cc->err = err;
return;
}
SSL_get_ex_data_X509_STORE_CTX_idx());
rc = X509_V_ERR_HOSTNAME_MISMATCH;
goto invalid_cert;
return;
}
+}
rc = X509_V_ERR_HOSTNAME_MISMATCH;
goto invalid_cert;
+static unsigned
+xt_end_chain(const br_x509_class **ctx)
+{
return (unsigned)cc->err;
return BR_ERR_X509_EMPTY_CHAIN;
}
snprintf(&fingerprint[i * 3], 4, "%02X%s",
cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":");
enum tofu_error error = TOFU_UNTRUSTED_CERT;
while (host) {
if (host->expires < now) {
goto next;
}
if (strcmp(host->host, servername) != 0) {
if (strcmp(host->host, cc->server_name) != 0) {
goto next;
}
if (strcmp(host->fingerprint, fingerprint) == 0) {
@@ -102,66 +104,84 @@ next:
host = host->next;
}
-callback:
host, cc->store->cb_data)) {
case TOFU_ASK:
assert(0); // Invariant
case TOFU_FAIL:
X509_STORE_CTX_set_error(ctx, rc);
break;
return BR_ERR_X509_NOT_TRUSTED;
case TOFU_TRUST_ONCE:
// No further action necessary
return 0;
case TOFU_TRUST_ALWAYS:;
FILE *f = fopen(tofu->known_hosts_path, "a");
FILE *f = fopen(cc->store->known_hosts_path, "a");
if (!f) {
fprintf(stderr, "Error opening %s for writing: %s\n",
tofu->known_hosts_path, strerror(errno));
cc->store->known_hosts_path, strerror(errno));
break;
};
struct tm expires_tm;
ASN1_TIME_to_tm(notAfter, &expires_tm);
time_t expires = mktime(&expires_tm);
fprintf(f, "%s %s %s %jd\n", servername,
"SHA-512", fingerprint, (intmax_t)expires);
fprintf(f, "%s %s %s\n", cc->server_name,
"SHA-512", fingerprint);
fclose(f);
host = calloc(1, sizeof(struct known_host));
host->host = strdup(servername);
host->host = strdup(cc->server_name);
host->fingerprint = strdup(fingerprint);
host->expires = expires;
host->lineno = ++tofu->lineno;
host->next = tofu->known_hosts;
tofu->known_hosts = host;
host->lineno = ++cc->store->lineno;
host->next = cc->store->known_hosts;
cc->store->known_hosts = host;
return 0;
}
+}
-invalid_cert:
+static const br_x509_pkey *
+xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages)
+{
return NULL;
// XXX: BearSSL doesn't pull the usages out of the X.509 for us
*usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN;
}
+const br_x509_class xt_vtable = {
+};
+static void
+x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store)
+{
+}
void
-gemini_tofu_init(struct gemini_tofu *tofu,
+gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data)
{
const struct pathspec paths[] = {
{.var = "GMNIDATA", .path = "/%s"},
{.var = "XDG_DATA_HOME", .path = "/gemini/%s"},
{.var = "HOME", .path = "/.local/share/gemini/%s"}
{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
{.var = "HOME", .path = "/.local/share/gmni/%s"}
};
char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));
char dname[PATH_MAX+1];
size_t n = 0;
path_fmt, "known_hosts");
sizeof(tofu->known_hosts_path),
path_fmt, "known_hosts");
assert(n < sizeof(tofu->known_hosts_path));
strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1);
@@ -179,10 +199,17 @@ free(path_fmt);
tofu->callback = cb;
tofu->cb_data = cb_data;
tofu->known_hosts = NULL;
&tofu->iobuf, sizeof(tofu->iobuf), 1);
FILE *f = fopen(tofu->known_hosts_path, "r");
if (!f) {
return;
@@ -191,6 +218,11 @@ n = 0;
int lineno = 1;
char *line = NULL;
while (getline(&line, &n, f) != -1) {
int ln = strlen(line);
if (line[ln-1] == '\n') {
line[ln-1] = 0;
}
struct known_host *host = calloc(1, sizeof(struct known_host));
char *tok = strtok(line, " ");
assert(tok);
@@ -207,10 +239,6 @@
tok = strtok(NULL, " ");
assert(tok);
host->fingerprint = strdup(tok);
tok = strtok(NULL, " ");
assert(tok);
host->expires = strtoul(tok, NULL, 10);
host->lineno = lineno++;
diff --git a/src/util.c b/src/util.c
index 2f62c29ce40ac012993e1d208d65491b56d204aa..780d0e8803ab9179683bd9a92cbead05624f2681 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1,5 +1,7 @@
#include <assert.h>
+#include <bearssl_ssl.h>
#include <errno.h>
+#include <gmni/gmni.h>
#include <libgen.h>
#include <limits.h>
#include <stdint.h>
@@ -7,7 +9,6 @@ #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
-#include <gmni/gmni.h>
#include "util.h"
static void
@@ -82,7 +83,7 @@ }
fprintf(out, "Downloading %s to %s\n", url, path);
char buf[BUFSIZ];
for (int n = 1; n > 0;) {
n = BIO_read(resp.bio, buf, sizeof(buf));
n = br_sslio_read(&resp.body, buf, sizeof(buf));
if (n == -1) {
fprintf(stderr, "Error: read\n");
return 1;
text/gemini
This content has been proxied by September (3851b).