diff --git a/Makefile b/Makefile index 2304c9f..383d2fa 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,11 @@ CFLAGS += -D_GNU_SOURCE -DPREFIX=\"$(PREFIX)\" -DCONFDIR=\"$(CONFDIR)\" -DGPLAC LDFLAGS ?= LDFLAGS += $(shell pkg-config --libs libcurl libssl libcrypto) MIMETYPES = +WITH_TITAN ?= 1 +ifeq ($(WITH_TITAN),1) + CFLAGS += -DGPLACES_WITH_TITAN + MIMETYPES := $(MIMETYPES);x-scheme-handler/titan +endif WITH_GOPHER ?= 1 ifeq ($(WITH_GOPHER),1) CFLAGS += -DGPLACES_WITH_GOPHER @@ -63,7 +68,7 @@ all: $(BIN) gplacesrc gplaces.desktop $(BIN): $(OBJ) $(CC) $(CFLAGS) -o $(BIN) $(OBJ) $(LDFLAGS) -gplaces.o: gplaces.c gopher.c gophers.c spartan.c finger.c guppy.c tcp.c socket.c +gplaces.o: gplaces.c titan.c gopher.c gophers.c spartan.c finger.c guppy.c tcp.c socket.c gplaces.desktop: gplaces.desktop.in @sed "s~^MimeType=.*~&$(MIMETYPES)~" $< > $@ diff --git a/README.md b/README.md index 91e6f9e..8397dbb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ gplaces is originally a Gemini port of the delve Gopher client by Sebastian Stei * sh-style history with $XDG_DATA_HOME/gplaces_history or ~/.gplaces_history * UTF-8 word wrapping * configurable external pager +* optional Titan support * optional Gopher support * optional gophers:// (Gopher+TLS+TOFU) support * optional Spartan support diff --git a/com.github.dimkr.gplaces.appdata.xml b/com.github.dimkr.gplaces.appdata.xml index 58bc9ac..7bbe78e 100644 --- a/com.github.dimkr.gplaces.appdata.xml +++ b/com.github.dimkr.gplaces.appdata.xml @@ -18,7 +18,7 @@
  • Slightly prettified Gemtext output
  • Configurable handlers for various file types
  • Quick navigation to recently viewed pages
  • -
  • Support for other "small internet" protocols: Spartan, Gopher, Finger and Guppy
  • +
  • Support for other "small internet" protocols: Titan, Spartan, Gopher, Finger and Guppy
  • gplaces should be fairly straightforward to use for anyone used to the command-line and man pages. Type "readme" for a short introduction. diff --git a/finger.c b/finger.c index dad88cf..e362d9b 100644 --- a/finger.c +++ b/finger.c @@ -2,7 +2,7 @@ ================================================================================ gplaces - a simple terminal Gemini client - Copyright (C) 2022, 2023 Dima Krasner + Copyright (C) 2022 - 2024 Dima Krasner 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 @@ -19,12 +19,13 @@ ================================================================================ */ -static void *finger_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *finger_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { static char buffer[1024 + 3]; /* path\r\n\0 */ char *user = NULL; int fd = -1, len = 0; (void)sel; + (void)redirs; (void)ask; switch (curl_url_get(url->cu, CURLUPART_USER, &user, 0)) { @@ -48,4 +49,4 @@ static void *finger_download(const Selector *sel, URL *url, char **mime, Parser } -const Protocol finger = {"finger", "79", tcp_read, tcp_peek, socket_error, tcp_close, finger_download}; +const Protocol finger = {"finger", "79", tcp_read, tcp_peek, socket_error, tcp_close, finger_download, set_fragment}; diff --git a/gopher.c b/gopher.c index 79ec2d2..b894eab 100644 --- a/gopher.c +++ b/gopher.c @@ -2,7 +2,7 @@ ================================================================================ gplaces - a simple terminal Gemini client - Copyright (C) 2022, 2023 Dima Krasner + Copyright (C) 2022 - 2024 Dima Krasner 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 @@ -58,17 +58,17 @@ static void parse_gophermap_line(char *line, int *pre, Selector **sel, SelectorL /*============================================================================*/ static char *gopher_request(const Selector *sel, URL *url, int ask, int *len, size_t skip) { static char buffer[1024 + 3]; /* path\r\n\0 */ - char *input = NULL, *query = NULL; + char *input = NULL, *fragment = NULL; const char *path, *end; if (url->path[0] == '/' && url->path[1] == '7') { - switch (curl_url_get(url->cu, CURLUPART_QUERY, &query, CURLU_URLDECODE)) { - case CURLUE_OK: input = query; break; - case CURLUE_NO_QUERY: break; + switch (curl_url_get(url->cu, CURLUPART_FRAGMENT, &fragment, 0)) { + case CURLUE_OK: input = fragment; break; + case CURLUE_NO_FRAGMENT: break; default: return NULL; } if (input == NULL) { - if (!ask || (input = bestline(color ? "\33[35mQuery>\33[0m " : "Query> ")) == NULL || !set_input(url, input)) return NULL; + if (!ask || (input = bestline(color ? "\33[35mQuery>\33[0m " : "Query> ")) == NULL || !set_fragment(url, input)) return NULL; if (interactive) { bestlineHistoryAdd(input); bestlineHistoryAdd(url->url); } *len = snprintf(buffer, sizeof(buffer), "%s\t%s\r\n", sel->rawurl + skip + strcspn(sel->rawurl + skip, "/") + 2, input); } else { @@ -76,8 +76,8 @@ static char *gopher_request(const Selector *sel, URL *url, int ask, int *len, si if ((end = strrchr(path, '?')) == NULL) *len = snprintf(buffer, sizeof(buffer), "%s\t%s\r\n", path, input); else *len = snprintf(buffer, sizeof(buffer), "%.*s\t%s\r\n", (int)(end - path), path, input); } - if (input != query) free(input); - curl_free(query); + if (input != fragment) free(input); + curl_free(fragment); } else if (url->path[0] == '/' && url->path[1] != '\0') *len = snprintf(buffer, sizeof(buffer), "%s\r\n", sel->rawurl + skip + strcspn(sel->rawurl + skip, "/") + 2); else *len = snprintf(buffer, sizeof(buffer), "%s\r\n", sel->rawurl + skip + strcspn(sel->rawurl + skip, "/")); @@ -124,10 +124,12 @@ static void gopher_type(void *c, const URL *url, char **mime, Parser *parser) { } -static void *gopher_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *gopher_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { char *buffer; int fd = -1, len; + (void)redirs; + if ((buffer = gopher_request(sel, url, ask, &len, 9)) == NULL || (fd = socket_connect(url, SOCK_STREAM)) == -1) goto fail; if (sendall(fd, buffer, len, MSG_NOSIGNAL) != len) { if (errno == EAGAIN || errno == EWOULDBLOCK) error(0, "cannot send request to `%s`:`%s`: cancelled", url->host, url->port); @@ -142,4 +144,4 @@ static void *gopher_download(const Selector *sel, URL *url, char **mime, Parser } -const Protocol gopher = {"gopher", "70", tcp_read, tcp_peek, socket_error, tcp_close, gopher_download}; +const Protocol gopher = {"gopher", "70", tcp_read, tcp_peek, socket_error, tcp_close, gopher_download, set_fragment}; diff --git a/gophers.c b/gophers.c index 94b2d6f..3d65481 100644 --- a/gophers.c +++ b/gophers.c @@ -2,7 +2,7 @@ ================================================================================ gplaces - a simple terminal Gemini client - Copyright (C) 2022, 2023 Dima Krasner + Copyright (C) 2022 - 2024 Dima Krasner 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 @@ -19,12 +19,14 @@ ================================================================================ */ -static void *gophers_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *gophers_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { char *buffer; SSL_CTX *ctx = NULL; SSL *ssl = NULL; int len, err; + (void)redirs; + if ((ctx = SSL_CTX_new(TLS_client_method())) == NULL) return NULL; SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); @@ -43,4 +45,4 @@ static void *gophers_download(const Selector *sel, URL *url, char **mime, Parser } -const Protocol gophers = {"gophers", "70", ssl_read, ssl_peek, ssl_error, ssl_close, gophers_download}; +const Protocol gophers = {"gophers", "70", ssl_read, ssl_peek, ssl_error, ssl_close, gophers_download, set_fragment}; diff --git a/gplaces.c b/gplaces.c index 475de68..5736084 100644 --- a/gplaces.c +++ b/gplaces.c @@ -81,7 +81,8 @@ typedef struct Protocol { int (*peek)(void *, void *, int); int (*error)(const URL *, void *, int); void (*close)(void *); - void *(*download)(const Selector *, URL *, char **mime, Parser *, int ask); + void *(*download)(const Selector *, URL *, char **mime, Parser *, unsigned int redirs, int ask); + int (*set_input)(URL *url, const char *input); } Protocol; struct Selector { @@ -128,6 +129,9 @@ typedef struct Help { /*============================================================================*/ const Protocol gemini; +#ifdef GPLACES_WITH_TITAN +const Protocol titan; +#endif #ifdef GPLACES_WITH_GOPHERS const Protocol gophers; #endif @@ -288,7 +292,7 @@ static void free_selectors(SelectorList *list) { } -static int set_input(URL *url, const char *input) { +static int set_query(URL *url, const char *input) { char *query, *tmp; if ((query = curl_easy_escape(NULL, input, 0)) == NULL) return 0; if (curl_url_set(url->cu, CURLUPART_QUERY, query, CURLU_NON_SUPPORT_SCHEME) != CURLUE_OK || curl_url_get(url->cu, CURLUPART_URL, &tmp, 0) != CURLUE_OK) { curl_free(query); return 0; } @@ -298,6 +302,16 @@ static int set_input(URL *url, const char *input) { } +#if defined(GPLACES_WITH_TITAN) || defined(GPLACES_WITH_GOPHER) || defined(GPLACES_WITH_GOPHERS) || defined(GPLACSE_WITH_FINGER) +static int set_fragment(URL *url, const char *input) { + char *tmp; + if (curl_url_set(url->cu, CURLUPART_FRAGMENT, input, CURLU_NON_SUPPORT_SCHEME) != CURLUE_OK || curl_url_get(url->cu, CURLUPART_URL, &tmp, 0) != CURLUE_OK) return 0; + curl_free(url->url); url->url = tmp; + return 1; +} +#endif + + static int parse_url(URL *url, const char *rawurl, const char *from, const char *input) { static char buffer[1024]; #if defined(GPLACES_USE_LIBIDN2) || defined(GPLACES_USE_LIBIDN) @@ -328,30 +342,14 @@ static int parse_url(URL *url, const char *rawurl, const char *from, const char valid: #endif - if (input != NULL && input[0] != '\0' && !set_input(url, input)) return 0; - else if ((input == NULL || input[0] == '\0') && curl_url_get(url->cu, CURLUPART_URL, &url->url, 0) != CURLUE_OK) return 0; - - if (curl_url_get(url->cu, CURLUPART_SCHEME, &url->scheme, 0) != CURLUE_OK || (!(file = (strcmp(url->scheme, "file") == 0)) && curl_url_get(url->cu, CURLUPART_HOST, &url->host, 0) != CURLUE_OK)) return 0; - -#if defined(GPLACES_USE_LIBIDN2) || defined(GPLACES_USE_LIBIDN) - #ifdef GPLACES_USE_LIBIDN2 - if (!file && (idn2_to_ascii_8z(url->host, &host, IDN2_NONTRANSITIONAL) == IDN2_OK || idn2_to_ascii_8z(url->host, &host, IDN2_TRANSITIONAL) == IDN2_OK)) { - #elif defined(GPLACES_USE_LIBIDN) - if (!file && idna_to_ascii_8z(url->host, &host, 0) == IDNA_SUCCESS) { - #endif - if (curl_url_set(url->cu, CURLUPART_HOST, host, 0) != CURLUE_OK) { free(host); return 0; } - free(host); - curl_free(url->host); url->host = NULL; - if (curl_url_get(url->cu, CURLUPART_HOST, &url->host, 0) != CURLUE_OK) return 0; - } -#endif - - if (curl_url_get(url->cu, CURLUPART_PATH, &url->path, 0) != CURLUE_OK) return 0; - - if (file) return 1; + if (curl_url_get(url->cu, CURLUPART_SCHEME, &url->scheme, 0) != CURLUE_OK) return 0; if (strcmp(url->scheme, "gemini") == 0) { url->proto = &gemini; +#ifdef GPLACES_WITH_TITAN + } else if (strcmp(url->scheme, "titan") == 0) { + url->proto = &titan; +#endif #ifdef GPLACES_WITH_GOPHER } else if (strcmp(url->scheme, "gopher") == 0) { url->proto = &gopher; @@ -374,6 +372,28 @@ static int parse_url(URL *url, const char *rawurl, const char *from, const char #endif } + if (input != NULL && input[0] != '\0' && !url->proto->set_input(url, input)) return 0; + else if ((input == NULL || input[0] == '\0') && curl_url_get(url->cu, CURLUPART_URL, &url->url, 0) != CURLUE_OK) return 0; + + if (!(file = (strcmp(url->scheme, "file")) == 0) && curl_url_get(url->cu, CURLUPART_HOST, &url->host, 0) != CURLUE_OK) return 0; + +#if defined(GPLACES_USE_LIBIDN2) || defined(GPLACES_USE_LIBIDN) + #ifdef GPLACES_USE_LIBIDN2 + if (!file && (idn2_to_ascii_8z(url->host, &host, IDN2_NONTRANSITIONAL) == IDN2_OK || idn2_to_ascii_8z(url->host, &host, IDN2_TRANSITIONAL) == IDN2_OK)) { + #elif defined(GPLACES_USE_LIBIDN) + if (!file && idna_to_ascii_8z(url->host, &host, 0) == IDNA_SUCCESS) { + #endif + if (curl_url_set(url->cu, CURLUPART_HOST, host, 0) != CURLUE_OK) { free(host); return 0; } + free(host); + curl_free(url->host); url->host = NULL; + if (curl_url_get(url->cu, CURLUPART_HOST, &url->host, 0) != CURLUE_OK) return 0; + } +#endif + + if (curl_url_get(url->cu, CURLUPART_PATH, &url->path, 0) != CURLUE_OK) return 0; + + if (file) return 1; + switch (curl_url_get(url->cu, CURLUPART_PORT, &url->port, 0)) { case CURLUE_OK: break; case CURLUE_NO_PORT: @@ -960,7 +980,7 @@ static int save_body(const URL *url, void *c, FILE *fp) { } -static int do_download(URL *url, SSL **body, char **mime, int ask) { +static int ssl_download(URL *url, SSL **body, char **mime, int request(const URL *, SSL *, void *), void *p, int ask) { static char crtpath[1024], keypath[1024], suffix[1024], buffer[1024], data[2 + 1 + 1024 + 2 + 1]; /* 99 meta\r\n\0 */ struct stat stbuf; const char *home; @@ -1015,8 +1035,7 @@ static int do_download(URL *url, SSL **body, char **mime, int ask) { if ((ssl = ssl_connect(url, ctx, ask)) == NULL) goto fail; - len = snprintf(buffer, sizeof(buffer), "%s\r\n", url->url); - if ((err = SSL_get_error(ssl, SSL_write(ssl, buffer, len >= (int)sizeof(buffer) ? (int)sizeof(buffer) - 1 : len))) != SSL_ERROR_NONE) { + if ((err = request(url, ssl, p)) != SSL_ERROR_NONE) { if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) error(0, "cannot send request to `%s`:`%s`: cancelled", url->host, url->port); else error(0, "cannot send request to `%s`:`%s`: error %d", url->host, url->port, err); goto fail; @@ -1047,7 +1066,7 @@ static int do_download(URL *url, SSL **body, char **mime, int ask) { if ((line = bestline(buffer)) == NULL) goto fail; if (data[1] != '1' && interactive) bestlineHistoryAdd(line); if (data[1] == '1') bestlineMaskModeDisable(); - if (!set_input(url, line)) { free(line); goto fail; } + if (!set_query(url, line)) { free(line); goto fail; } free(line); if (data[1] != '1' && interactive) bestlineHistoryAdd(url->url); break; @@ -1087,21 +1106,30 @@ static int do_download(URL *url, SSL **body, char **mime, int ask) { } -static void sigint(int sig) { - (void)sig; +/*============================================================================*/ +static int gemini_request(const URL *url, SSL *ssl, void *p) { + static char buffer[1024]; + int len; + + (void)p; + + len = snprintf(buffer, sizeof(buffer), "%s\r\n", url->url); + return SSL_get_error(ssl, SSL_write(ssl, buffer, len >= (int)sizeof(buffer) ? (int)sizeof(buffer) - 1 : len)); } -static void *gemini_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *gemini_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { SSL *ssl = NULL; - int status, redirs = 0; + int status = -1; (void)sel; do { - status = do_download(url, &ssl, mime, ask); + status = ssl_download(url, &ssl, mime, gemini_request, NULL, ask); if (status >= 20 && status <= 29) break; - } while ((status >= 10 && status <= 19) || (status >= 60 && status <= 69) || (status >= 30 && status <= 39 && ++redirs < 5)); + } while ((status >= 10 && status <= 19) || (status >= 60 && status <= 69) || (status >= 30 && status <= 39 && ++redirs < 5 && url->proto->download == gemini_download)); + + if (redirs < 5 && url->proto->download != gemini_download) return url->proto->download(sel, url, mime, parser, redirs, ask); if (ssl != NULL && strncmp(*mime, "text/gemini", 11) == 0) *parser = parse_gemtext_line; else if (ssl != NULL && (!interactive || strncmp(*mime, "text/plain", 10) == 0)) *parser = parse_plaintext_line; @@ -1111,10 +1139,13 @@ static void *gemini_download(const Selector *sel, URL *url, char **mime, Parser } -const Protocol gemini = {"gemini", "1965", ssl_read, ssl_peek, ssl_error, ssl_close, gemini_download}; +const Protocol gemini = {"gemini", "1965", ssl_read, ssl_peek, ssl_error, ssl_close, gemini_download, set_query}; /*============================================================================*/ +#ifdef GPLACES_WITH_TITAN + #include "titan.c" +#endif #if defined(GPLACES_WITH_GOPHER) || defined(GPLACES_WITH_SPARTAN) || defined(GPLACES_WITH_FINGER) || defined(GPLACES_WITH_GUPPY) #include "socket.c" #endif @@ -1174,7 +1205,7 @@ static void stream_to_handler(const Selector *sel, URL *url, const char *filenam if (pipe(fds) == -1) return; if (fcntl(fds[1], F_SETFD, FD_CLOEXEC) == 0 && (fp = fdopen(fds[1], "w")) != NULL) { setbuf(fp, NULL); - if ((c = url->proto->download(sel, url, &mime, &parser, 1)) != NULL) { + if ((c = url->proto->download(sel, url, &mime, &parser, 0, 1)) != NULL) { if ((handler = find_mime_handler(mime)) != NULL && (pid = start_handler(handler, filename, command, sizeof(command), sel, url, fds[0])) > 0) { close(fds[0]); fds[0] = -1; save_body(url, c, fp); @@ -1219,7 +1250,7 @@ static void download_to_file(const Selector *sel, URL *url, const char *def) { } if ((fp = fopen(filename, "wb")) == NULL) error(0, "cannot create file `%s`: %s", filename, strerror(errno)); else { - if ((c = url->proto->download(sel, url, &mime, &parser, 1)) != NULL) { + if ((c = url->proto->download(sel, url, &mime, &parser, 0, 1)) != NULL) { ret = save_body(url, c, fp); url->proto->close(c); } @@ -1266,7 +1297,7 @@ static SelectorList download_text(const Selector *sel, URL *url, int ask, int ha size_t parsed, length = 0, total = 0, prog = 0; int received, pre = 0, width, ok = 0, links = 0; - if (url->proto == NULL || (c = url->proto->download(sel, url, &mime, &parser, ask)) == NULL) goto out; + if (url->proto == NULL || (c = url->proto->download(sel, url, &mime, &parser, 0, ask)) == NULL) goto out; if (parser == NULL) { if (handle) save_and_handle(sel, url, c, mime); goto out; @@ -1795,6 +1826,11 @@ static void quit_client() { } +static void sigint(int sig) { + (void)sig; +} + + int main(int argc, char **argv) { struct sigaction sa = {.sa_handler = sigint}; diff --git a/guppy.c b/guppy.c index 4897796..ae5ab90 100644 --- a/guppy.c +++ b/guppy.c @@ -2,7 +2,7 @@ ================================================================================ gplaces - a simple terminal Gemini client - Copyright (C) 2022, 2023 Dima Krasner + Copyright (C) 2022 - 2024 Dima Krasner 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 @@ -115,7 +115,7 @@ static int do_guppy_download(URL *url, GuppySocket *s, char **mime, int ask) { free(chunk); if ((input = bestline(prompt)) == NULL) return 4; if (interactive) bestlineHistoryAdd(input); - if (!set_input(url, input)) { free(input); return 4; } + if (!set_query(url, input)) { free(input); return 4; } free(input); if (interactive) bestlineHistoryAdd(url->url); } else if (chunk->buffer[0] == '3') { @@ -170,9 +170,9 @@ static int do_guppy_download(URL *url, GuppySocket *s, char **mime, int ask) { } -static void *guppy_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *guppy_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { GuppySocket *s = NULL; - int status, redirs = 0; + int status; (void)sel; @@ -186,7 +186,9 @@ static void *guppy_download(const Selector *sel, URL *url, char **mime, Parser * status = do_guppy_download(url, s, mime, ask); /* stop on success, on error or when the redirect limit is exhausted */ if (status > 5) break; - } while (((status == 1) || (status == 3)) && ++redirs < 5); + } while (status == 1 || ((status == 3 && ++redirs < 5 && url->proto->download == guppy_download))); + + if (redirs < 5 && url->proto->download != guppy_download) { guppy_close(s); return url->proto->download(sel, url, mime, parser, redirs, ask); } if (status > 6 && strncmp(*mime, "text/gemini", 11) == 0) *parser = parse_gemtext_line; else if (status > 6 && strncmp(*mime, "text/plain", 10) == 0) *parser = parse_plaintext_line; @@ -270,4 +272,4 @@ static int guppy_read(void *c, void *buffer, int length) { } -const Protocol guppy = {"guppy", "6775", guppy_read, NULL, socket_error, guppy_close, guppy_download}; +const Protocol guppy = {"guppy", "6775", guppy_read, NULL, socket_error, guppy_close, guppy_download, set_query}; diff --git a/spartan.c b/spartan.c index 0d1270f..5b261a9 100644 --- a/spartan.c +++ b/spartan.c @@ -2,7 +2,7 @@ ================================================================================ gplaces - a simple terminal Gemini client - Copyright (C) 2022, 2023 Dima Krasner + Copyright (C) 2022 - 2024 Dima Krasner 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 @@ -76,11 +76,11 @@ static int do_spartan_download(URL *url, int *body, char **mime, const char *inp } -static void *spartan_download(const Selector *sel, URL *url, char **mime, Parser *parser, int ask) { +static void *spartan_download(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { char *input = NULL, *query = NULL; size_t inputlen = 0; static int fd = -1; - int status, redirs = 0; + int status; switch (curl_url_get(url->cu, CURLUPART_QUERY, &query, 0)) { case CURLUE_OK: input = query; break; @@ -88,7 +88,7 @@ static void *spartan_download(const Selector *sel, URL *url, char **mime, Parser default: return NULL; } if (sel->prompt && (input == NULL || *input == '\0')) { - if (!ask || (input = bestline(color ? "\33[35mData>\33[0m " : "Data> ")) == NULL || !set_input(url, input)) goto fail; + if (!ask || (input = bestline(color ? "\33[35mData>\33[0m " : "Data> ")) == NULL || !set_query(url, input)) goto fail; if (interactive) { bestlineHistoryAdd(input); bestlineHistoryAdd(url->url); } } if (input != NULL) inputlen = strlen(input); @@ -96,7 +96,9 @@ static void *spartan_download(const Selector *sel, URL *url, char **mime, Parser do { status = do_spartan_download(url, &fd, mime, input, inputlen, ask); if (status == 2) break; - } while (status == 3 && ++redirs < 5); + } while (status == 3 && ++redirs < 5 && url->proto->download == spartan_download); + + if (redirs < 5 && url->proto->download != spartan_download) return url->proto->download(sel, url, mime, parser, redirs, ask); if (fd != -1 && strncmp(*mime, "text/gemini", 11) == 0) *parser = parse_spartan_line; else if (fd != -1 && strncmp(*mime, "text/plain", 10) == 0) *parser = parse_plaintext_line; @@ -110,4 +112,4 @@ static void *spartan_download(const Selector *sel, URL *url, char **mime, Parser } -const Protocol spartan = {"spartan", "300", tcp_read, tcp_peek, socket_error, tcp_close, spartan_download}; +const Protocol spartan = {"spartan", "300", tcp_read, tcp_peek, socket_error, tcp_close, spartan_download, set_query}; diff --git a/titan.c b/titan.c new file mode 100644 index 0000000..8167368 --- /dev/null +++ b/titan.c @@ -0,0 +1,120 @@ +/* +================================================================================ + + gplaces - a simple terminal Gemini client + Copyright (C) 2024 Dima Krasner + + 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 . + +================================================================================ +*/ +typedef struct TitanParams { + char *url; + char *token; + const char *mime; + struct stat stbuf; + void *body; +} TitanParams; + + +/*============================================================================*/ +static int titan_request(const URL *url, SSL *ssl, void *p) { + static char buffer[1024]; + const TitanParams *params = (const TitanParams *)p; + int len, err; + + (void)url; + + len = snprintf(buffer, sizeof(buffer), params->token == NULL || *params->token == '\0' ? "%s;mime=%s;size=%zu\r\n" : "%s;mime=%s;size=%zu;token=%s\r\n", params->url, params->mime, params->stbuf.st_size, params->token); + if ((err = SSL_get_error(ssl, SSL_write(ssl, buffer, len >= (int)sizeof(buffer) ? (int)sizeof(buffer) - 1 : len))) != SSL_ERROR_NONE) return err; + + return params->stbuf.st_size > 0 ? SSL_get_error(ssl, SSL_write(ssl, params->body, params->stbuf.st_size)) : SSL_ERROR_NONE; +} + + +static void *titan_upload(const Selector *sel, URL *url, char **mime, Parser *parser, unsigned int redirs, int ask) { +#ifdef GPLACES_USE_LIBMAGIC + magic_t mag = NULL; +#else + char *tmp; +#define magic_close(x) do {} while (0) +#endif + CURLU *cu; + TitanParams params = {.mime = "application/octet-stream"}; + char *fragment, *path = NULL; + SSL *ssl = NULL; + int fd, status = -1; + + (void)sel; + + if ((cu = curl_url_dup(url->cu)) == NULL || curl_url_set(cu, CURLUPART_FRAGMENT, NULL, CURLU_NON_SUPPORT_SCHEME) != CURLUE_OK || curl_url_get(cu, CURLUPART_URL, ¶ms.url, 0) != CURLUE_OK) { curl_url_cleanup(cu); return NULL; } + curl_url_cleanup(cu); + + switch (curl_url_get(url->cu, CURLUPART_FRAGMENT, &fragment, 0)) { + case CURLUE_OK: path = fragment; break; + case CURLUE_NO_FRAGMENT: break; + default: return NULL; + } + + if (path == NULL || *path == '\0') { + if (!ask || (params.token = bestline("Token> ")) == NULL) { curl_free(params.url); return NULL; } + if (interactive) bestlineHistoryAdd(params.token); + if ((path = bestline("File> ")) == NULL) { free(params.token); curl_free(params.url); return NULL; } + if (interactive) bestlineHistoryAdd(path); + } + + if ((fd = open(path, O_RDONLY)) == -1) { error(0, "cannot open `%s`: %s", path, strerror(errno)); free(path); free(params.token); curl_free(params.url); return NULL; } + if (fstat(fd, ¶ms.stbuf) == -1) { error(0, "cannot open `%s`: %s", path, strerror(errno)); close(fd); free(path); free(params.token); curl_free(params.url); return NULL; } + if (params.stbuf.st_size > 0 && (params.body = mmap(NULL, params.stbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) { error(0, "cannot open `%s`: %s", path, strerror(errno)); close(fd); free(path); free(params.token); curl_free(params.url); return NULL; } + + if (params.stbuf.st_size > 0) { +#ifdef GPLACES_USE_LIBMAGIC + if ((mag = magic_open(MAGIC_MIME_TYPE | MAGIC_NO_CHECK_COMPRESS | MAGIC_ERROR)) == NULL) { munmap(params.body, params.stbuf.st_size); close(fd); free(path); free(params.token); curl_free(params.url); return NULL; } + if (magic_load(mag, NULL) != 0) { munmap(params.body, params.stbuf.st_size); close(fd); free(path); free(params.token); magic_close(mag); curl_free(params.url); return NULL; } + if ((params.mime = magic_buffer(mag, params.body, params.stbuf.st_size)) == NULL) { error(0, "cannot open `%s`: %s", path, magic_error(mag)); munmap(params.body, params.stbuf.st_size); close(fd); free(path); free(params.token); magic_close(mag); curl_free(params.url); return NULL; } +#else + if ((tmp = bestline("File type> ")) == NULL) { munmap(params.body, params.stbuf.st_size); close(fd); free(path); free(params.token); curl_free(params.url); return NULL; } + if (interactive) bestlineHistoryAdd(tmp); + params.mime = tmp; +#endif + } + + do { + status = ssl_download(url, &ssl, mime, titan_request, ¶ms, ask); + if (status >= 20 && status <= 29) break; + } while ((status >= 10 && status <= 19) || (status >= 60 && status <= 69) || (status >= 30 && status <= 39 && ++redirs < 5 && url->proto->download == titan_upload)); + + if (params.stbuf.st_size > 0) munmap(params.body, params.stbuf.st_size); + close(fd); + free(params.token); + if (path != fragment) free(path); +#ifdef GPLACES_USE_LIBMAGIC + if (mag != NULL) magic_close(mag); +#else + free(tmp); +#endif + curl_free(fragment); + curl_free(params.url); + + if (redirs < 5 && url->proto->download != titan_upload) return url->proto->download(sel, url, mime, parser, redirs, ask); + + if (ssl != NULL && strncmp(*mime, "text/gemini", 11) == 0) *parser = parse_gemtext_line; + else if (ssl != NULL && (!interactive || strncmp(*mime, "text/plain", 10) == 0)) *parser = parse_plaintext_line; + + if (redirs == 5) error(0, "too many redirects from `%s`", url->url); + return ssl; +} + + +const Protocol titan = {"titan", "1965", ssl_read, ssl_peek, ssl_error, ssl_close, titan_upload, set_fragment};