#include <stdio.h>
#include <openssl/err.h>
#include <assert.h>
#include <string.h>
#include <ctype.h>
#include "gmni.h"
#include "tofu.h"
#include "url.h"
/* From gmni.c */
struct tofu_config {
struct gemini_tofu tofu;
enum tofu_action action;
};
/* From gmni.c */
static enum tofu_action
tofu_callback(enum tofu_error error, const char *fingerprint,
struct known_host *host, void *data)
{
struct tofu_config *cfg = (struct tofu_config *)data;
enum tofu_action action = cfg->action;
switch (error) {
case TOFU_VALID:
assert(0); // Invariant
case TOFU_INVALID_CERT:
fprintf(stderr,
"The server presented an invalid certificate with fingerprint %s.\n",
fingerprint);
if (action == TOFU_TRUST_ALWAYS) {
action = TOFU_TRUST_ONCE;
}
break;
case TOFU_UNTRUSTED_CERT:
fprintf(stderr,
"The certificate offered by this server is of unknown trust. "
"Its fingerprint is: \n"
"%s\n\n"
"Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint);
break;
case TOFU_FINGERPRINT_MISMATCH:
fprintf(stderr,
"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
"The unknown certificate's fingerprint is:\n"
"%s\n\n"
"The expected fingerprint is:\n"
"%s\n\n"
"If you're certain that this is correct, edit %s:%d\n",
fingerprint, host->fingerprint,
cfg->tofu.known_hosts_path, host->lineno);
return TOFU_FAIL;
}
if (action == TOFU_ASK) {
return TOFU_FAIL;
}
return action;
}
/* From gmnlm.c */
static char *
trim_ws(char *in)
{
while (*in && isspace(*in)) ++in;
return in;
}
/* Determine whether the response can be used. */
const bool
bad_response(enum gemini_result r, struct gemini_response *resp)
{
/* Check response */
if (r != GEMINI_OK) {
fprintf(stderr, "Error: %s\n", gemini_strerr(r, resp));
return true;
}
/* Check request was successful */
if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) {
fprintf(stderr, "Error: Unsuccessful request. Status: %d %s\n",
resp->status, resp->meta);
return true;
}
/* Check response is Gemtext */
if (strncmp(resp->meta, "text/gemini", 11) != 0) {
fprintf(stderr, "Error: Response is not text/gemini. Meta: %s\n",
resp->meta);
return true;
}
return false;
}
/* Determine whether the given token is a link and has a url */
const bool
token_is_good_link(struct gemini_token tok)
{
if (tok.token == GEMINI_LINK) {
return (strlen(tok.link.url) != 0) && (tok.link.url != NULL);
} else {
return false;
}
}
/* Return url up to and including the last '/' */
char *
get_url_without_filename(const char *url)
{
size_t last_slash;
for (size_t i = 0; i < strlen(url); ++i) {
if (url[i] == '/')
last_slash = ++i;
}
return strndup(url, last_slash);
}
/* Add scheme and domain to url if needed */
char *
get_absolute_url(char *url, char *base_url)
{
bool not_absolute = false, colon = false, slash = false;
size_t i = 0;
while (i < strlen(url) && !(not_absolute)) {
switch (url[i]) {
case ':':
if (i == 0 || colon) {
/* If colon is first char then no scheme */
/* 2 colons but no slashes means no scheme */
not_absolute = true;
break;
} else {
/* This is the first colon */
colon = true;
break;
}
case '/':
if (colon && slash) {
/* Last 2 chars were ":/" so url has scheme. Assuming there is
a valid domain after, url is absolute. */
return url;
} else if (colon) {
/* This slash directly follows a colon */
slash = true;
break;
} else {
/* This slash is before any colon, so no scheme */
not_absolute = true;
break;
}
default:
/* If there has been a colon but this character is not a slash,
then the url is not absolute. Break the loop. */
if (colon)
not_absolute = true;
}
++i;
}
/* remove leading slashes */
while (*url && (*url == '/')) ++url;
char *abs_url = malloc(strlen(base_url) + strlen(url) + 1);
strcpy(abs_url, base_url);
return strcat(abs_url, url);
}
int
main(int argc, char *argv[])
{
/* Must have exactly one argument */
if (argc != 2) {
fprintf(stderr, "Usage: gemlogger /path/to/config/file.gmi");
return 1;
}
/* Open config file */
FILE *config_fp = fopen(argv[1], "r");
if (!config_fp) {
fprintf(stderr, "Failed to access config file %s.\n", argv[1]);
return 1;
}
/* Setup for using gmni */
struct addrinfo hints = {0};
struct gemini_options opts = {
.hints = &hints,
};
struct tofu_config cfg;
cfg.action = TOFU_ASK;
SSL_load_error_strings();
ERR_load_crypto_strings();
opts.ssl_ctx = SSL_CTX_new(TLS_method());
gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg);
/* Make a gmni response from the config file */
BIO *file = BIO_new_fp(config_fp, BIO_CLOSE);
struct gemini_response config_resp;
config_resp.bio = BIO_new(BIO_f_buffer());
BIO_push(config_resp.bio, file);
config_resp.meta = strdup("text/gemini");
config_resp.status = GEMINI_STATUS_SUCCESS;
config_resp.fd = -1;
config_resp.ssl = NULL;
config_resp.ssl_ctx = NULL;
/* Parse config file */
FILE *out_fp = stdout;
bool good_output_file = true;
struct gemini_parser config_parser;
gemini_parser_init(&config_parser, config_resp.bio);
struct gemini_token config_tok;
while (gemini_parser_next(&config_parser, &config_tok) == 0) {
if (config_tok.token == GEMINI_HEADING) {
if (config_tok.heading.level == 3) {
/* Change output file */
char *path = trim_ws(config_tok.heading.title);
FILE *new_out_fp = fopen(path, "w+");
if (!new_out_fp) {
printf("Bad output file: %s\n", config_tok.heading.title);
/* Skip links until a new output file is given */
good_output_file = false;
continue;
}
out_fp = new_out_fp;
good_output_file = true;
}
} else if (good_output_file && token_is_good_link(config_tok)) {
/* Make requests and output links */
/* Get gemlog index page */
struct gemini_response resp;
enum gemini_result r = gemini_request(config_tok.link.url, &opts, &resp);
if (bad_response(r, &resp)) {
fprintf(stderr, "Could not access %s.\n", config_tok.link.url);
/* Go to next link in config file */
continue;
}
char *base_url = get_url_without_filename(config_tok.link.url);
char *abs_url;
/* Parse gemlog index page */
struct gemini_parser p;
gemini_parser_init(&p, resp.bio);
struct gemini_token tok;
while (gemini_parser_next(&p, &tok) == 0) {
if (token_is_good_link(tok)) {
abs_url = get_absolute_url(tok.link.url, base_url);
fprintf(out_fp, "=> %s", abs_url);
if (config_tok.link.text)
fprintf(out_fp, " %s", config_tok.link.text);
if (config_tok.link.text && tok.link.text)
fprintf(out_fp, ":");
if (tok.link.text)
fprintf(out_fp, " %s", tok.link.text);
fprintf(out_fp, "\n");
free(abs_url);
}
}
gemini_token_finish(&tok);
gemini_parser_finish(&p);
gemini_response_finish(&resp);
}
}
fclose(out_fp);
gemini_token_finish(&config_tok);
gemini_parser_finish(&config_parser);
gemini_response_finish(&config_resp);
gemini_tofu_finish(&cfg.tofu);
SSL_CTX_free(opts.ssl_ctx);
return 0;
}