commit 756fea6c3dd6884a5002c5a5e353c63a9fd13c92 parent 30bfda2dd4bee37b1f585575999ae12996abae46 Author: René Wagner <rwa@clttr.info> Date: Sun, 4 Jul 2021 08:59:16 +0200 update 2021-07-04 sources for cgmnlm, orrg and gmnifaq are now browsable on gemini thanks to gmnigit Diffstat:
293 files changed, 30997 insertions(+), 62 deletions(-)
diff --git a/archive.gmi b/archive.gmi @@ -1,6 +1,10 @@ # shortlog archive @ gmn.clttr.info /en/ ## 2021 +### 2021-04-09 tmux, job, ... +After several attempts over the last years which all failed i finally managed to get familiar with tmux. A heavily modified binding config inspired by vim helped a lot. tmux is now a daily driver for me - finally. +Did some checks on the motorcycles to have them ready for a ride as soon as springs returns to my part of europe. Due to some other duties that are more present during spring and summer updates related to my various projects may slow down a bit. Additionally im currently thinking about switching to another team in my company - after for 4 years with my current team there's a feeling of "something new might be good". However no decision taken yet. + ### 2021-03-26 alt text in cgmnlm cgmnlm now features a new 'a' command that toggles the display of preformatted text or the associated alt text (if available). diff --git a/cgmnlm.gmi b/cgmnlm.gmi @@ -28,6 +28,7 @@ I created this project for my own use, it includes the following changes that wo * e[N] command to open a link in default external program (requires `xdg-open`) * t[N] command to download the content behind a link to a temporary file * a command to toggle between preformatted text and the associated alt text +* u command to navigate 1 path element up ### colors The actual colors used depend on your terminal palette: @@ -45,11 +46,11 @@ The actual colors used depend on your terminal palette: <url> go to url [N] Follow Nth link (where N is a number) p[N] Show URL of Nth link (where N is a number) -e Send current URL in the browser to external default program -e[N] Send URL of Nth link in external default program +e[N] Send URL of Nth link or current URL when N is ommited to external default program t[N] Download content of Nth link to a temporary file b Back (in the page history) f Forward (in the page history) +u one path element up H View all page history m Save bookmark M Browse bookmarks @@ -65,7 +66,8 @@ q Quit ``` ## how to get -=> https://src.clttr.info/rwa/cgmnlm source of 'cgmnlm' (including build instructions) +=> ./sources/cgmnlm.git/ browse the source of cgmnlm on gemini +=> https://src.clttr.info/rwa/cgmnlm source code & issue tracker on src.clttr.info => https://aur.archlinux.org/packages/cgmnlm-git/ ArchLinux AUR package Drop me a note if you are interested in packaging or have demand for a specific package. diff --git a/feeds.gmi b/feeds.gmi @@ -1,25 +0,0 @@ -# interesting feeds @ gmn.clttr.info /en/ - -List of feeds i read more or less regularly. The feeds are converted to gemini using orrg. -=> https://src.clttr.info/rwa/orrg online rss (& atom) feed reader for gemini - -The feeds are sorted by name, no other rating intended. -Mostly english and german language. - -## something about "the world" -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fgraslutscher.de%2Ffeed Graslutschers Blog -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fpedelecmonitor.wordpress.com%2Ffeed%2F Pedelec Monitor - -## tech related blogs -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fblog.codeberg.org%2Ffeeds%2Fall.atom.xml codeberg news -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fdrewdevault.com%2Fblog%2Findex.xml Drew DeVault's Blog -=> gemini://orrg.clttr.info/orrg.pl?gemini:%2F%2Fdrewdevault.com%2Ffeed.xml Drew DeVault's Geminispace Blog -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fblog.fefe.de%2Frss.xml fefe's blog -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fwww.haiku-os.org%2Findex.xml Haiku Project -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fkopfkrieg.org%2Frss%2F Kopfkrieg's Blog -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fwww.kuketz-blog.de%2Ffeed%2F Kuketz IT-Security Blog - -## tech news -=> gemini://orrg.clttr.info/orrg.pl?https:%2F%2Fwww.phoronix.com%2Frss.php Phoronix News Feed - -=> ./index.gmi [home] diff --git a/index.gmi b/index.gmi @@ -6,43 +6,44 @@ You'll mostly find tech and coding related stuff here. => ./de/ Hier entlang zur deutschsprachigen Sektion. ## 🗩 shortlog (latest 3 updates) +### 2021-07-03 cgmnlm usage update +cgmnlm saw 2 small updates recently which should make browsing capsules easier: +* the history page has now hints what needs to be typed to jump to this page +* as more capsules seems to skip the "home" link on pages, i've implemented the "u" command to move one path element up + +=> ./sources/cgmnlm.git/ Additionally the source of cgmnlm is now available for browsing via gemini + ### 2021-06-02 codeberg A few days ago i joined the codeberg non-profit org which runs the gitea-based codeberg software forge. -Although i still run my own gitea instance, i think it's a great opportunity help a community-driven alternative to the big players and allow other people host their software repos, +Although i still run my own gitea instance, i think it's a great opportunity help a community-driven alternative to the big players and allow other people host their software repos. => https://codeberg.org Codeberg ### 2021-05-08 transit and stuff -=> ../transit/ transit has seen some additions, check out the these interesting capsules +=> ./transit/ transit has seen some additions, check out these interesting capsules No big news on other storys currently. For the moment I decided to stay with my team at the job. At home we are quite busy with gardening - the previous owners didn't spend much time in keeping the garden in a good shape so we need to put some effort into it to catch up. Besides that i'm looking forward to get out and visit some new places on the motorcycle. -### 2021-04-09 tmux, job, ... -After several attempts over the last years which all failed i finally managed to get familiar with tmux. A heavily modified binding config inspired by vim helped a lot. tmux is now a daily driver for me - finally. -Did some checks on the motorcycles to have them ready for a ride as soon as springs returns to my part of europe. Due to some other duties that are more present during spring and summer updates related to my various projects may slow down a bit. Additionally im currently thinking about switching to another team in my company - after for 4 years with my current team there's a feeling of "something new might be good". However no decision taken yet. - => ./archive.gmi shortlog archive ## 🛈 info & articles -=> ../transit/ transit - my personal gemini feed +=> ./transit/ transit - my personal gemini feed => gemini://geminispace.info geminispace.info - public search provider for gemini -=> gemini://when.willgemini.support when.willgemini.support - a sarcastic answer to the evergrowing "proposals" on the mailinglist -=> ./linkdump.gmi gemini link dump => ./hosting.gmi (outdated) my self-hosting setup -=> ./feeds.gmi some stuff from the web ## 🗐 coding ### gemini related => ./cgmnlm.gmi cgmnlm, a colorful gemini line-mode client -=> https://src.clttr.info/rwa/cgmnlm source code & issue tracker for cgmnlm Perl maybe considered oldschool by many, but it is still very good in text processing - which is the very core of gemini. Therefore it is obvious to use perl for cgi with gemini. => gemini://orrg.clttr.info orrg, online rss feed renderer for gemini -=> https://src.clttr.info/rwa/orrg/ source of orrg +=> ./sources/orrg.git/ source of orrg => gemini://gmndemo.clttr.info/faq/ gmnifaq, a dynamic FAQ-engine for gemini capsule -=> https://src.clttr.info/rwa/gmnifaq/ source of gmnifaq +=> ./sources/gmnifaq.git/ source of gmnifaq + +=> https://src.clttr.info/explore/repos?q=gemini&topic=1 my gemini related projects on src.clttr.info ### photography related As a amateur photographer i created some open source tools which support my development workflow @@ -63,10 +64,10 @@ I run a few public sites on the "ordinary web": Besides "tech" my spare time is mostly dedicated to my wife and our dogs, motorcycling and photography. Sometimes even some casual gaming. ### 🖃 contact -=> mailto:rwa@clttr.info eMail -=> https://nerdculture.de/@schwurbel my fediverse account @ nerdculture,de +=> mailto:rwa@clttr.info eMail +=> https://src.clttr.info/rwa/ profile on my private Gitea instance => https://scl.clttr.info/profile/rwa fediverse account @ scl.clttr.info -=> https://fotowolke.net/rwa My photo showcase +=> https://fotowolke.net/rwa personal photo showcase ```licence _________________________ diff --git a/linkdump.gmi b/linkdump.gmi @@ -1,18 +0,0 @@ -# linkdump @ gmn.clttr.info /en/ - -Unsorted dump of links with stuff all around gemini space - -## various gemini stuff -=> gemini://rawtext.club:1965/~sloum/spacewalk.gmi Spacewalk - updates from around geminispace -=> gemini://geminispace.info geminispace.info, a public search provider for gemini -=> https://github.com/kr1sp1n/awesome-gemini awesome-gemini collection of software (pieces) fOR GEmini -=> gemini://medusae.space the medusae gemini directory -=> gemini://geddit.glv.one geddit, interactive link collection -=> gemini://tilde.pink/~kubikpixel/browsers-gemini.gmi gemini client collection by kubikpixel - -## mirrors -=> gemini://wp.glv.one Wikipedia -=> gemini://dw.schettler.net Deutsche Welle -=> gemini://dioskouroi.xyz/top HackerNews - -=> ./index.gmi [home] diff --git a/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch b/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch @@ -0,0 +1,285 @@ +diff --git a/Makefile b/Makefile +index 69a241a8825a6a7aa979eb2ae95a26faaf3a0532..3d4df602cd7f7ab4f5a45b47dee0d47729f0739c 100644 +--- a/Makefile ++++ b/Makefile +@@ -8,6 +8,10 @@ gmni: $(gmni_objects) + @printf 'CCLD\t$@\n' + @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) + ++gmnlm: $(gmnlm_objects) ++ @printf 'CCLD\t$@\n' ++ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) ++ + doc/gmni.1: doc/gmni.scd + + .SUFFIXES: .c .o .scd .1 +diff --git a/config.sh b/config.sh +index b93815ada4a25ec508e7a86cc79b9e9be3eba428..52931ab241c3177285b56258bf9b67ac4b63a7ea 100644 +--- a/config.sh ++++ b/config.sh +@@ -134,6 +134,7 @@ + all: ${all} + EOF + gmni >>"$outdir"/config.mk ++ gmnlm >>"$outdir"/config.mk + echo done + + touch $outdir/cppcache +diff --git a/configure b/configure +index aca6e8a1eaa9a6271f03eb8863640d0d93cdf435..151bdae8c12d9ad07d3a5240d7c554f4c62664c8 100755 +--- a/configure ++++ b/configure +@@ -7,10 +7,18 @@ genrules gmni \ + src/client.c \ + src/escape.c \ + src/gmni.c \ ++ src/url.c ++} ++ ++gmnlm() { ++ genrules gmnlm \ ++ src/client.c \ ++ src/escape.c \ ++ src/gmnlm.c \ + src/parser.c \ + src/url.c + } + +-all="gmni" ++all="gmni gmnlm" + + run_configure +diff --git a/src/gmni.c b/src/gmni.c +index 75c6c5afb6e7f1f286d314d93f2b44ef8414afb3..dc0c5c7f61679369fe4fadafd0508147868cc3c3 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -15,7 +15,7 @@ #include <unistd.h> + #include "gmni.h" + + static void +-usage(char *argv_0) ++usage(const char *argv_0) + { + fprintf(stderr, + "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", +diff --git a/src/gmnlm.c b/src/gmnlm.c +new file mode 100644 +index 0000000000000000000000000000000000000000..bc3f647d42372678b4180f539df8637d0ba69a12 +--- /dev/null ++++ b/src/gmnlm.c +@@ -0,0 +1,215 @@ ++#include <assert.h> ++#include <ctype.h> ++#include <getopt.h> ++#include <openssl/bio.h> ++#include <openssl/err.h> ++#include <stdbool.h> ++#include <stdio.h> ++#include <string.h> ++#include <sys/ioctl.h> ++#include <termios.h> ++#include <unistd.h> ++#include "gmni.h" ++#include "url.h" ++ ++struct link { ++ char *url; ++ struct link *next; ++}; ++ ++static void ++usage(const char *argv_0) ++{ ++ fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); ++} ++ ++static bool ++set_url(struct Curl_URL *url, char *new_url) ++{ ++ if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { ++ fprintf(stderr, "Error: invalid URL\n"); ++ return false; ++ } ++ return true; ++} ++ ++static char * ++trim_ws(char *in) ++{ ++ for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { ++ in[i] = 0; ++ } ++ for (; *in && isspace(*in); ++in); ++ return in; ++} ++ ++static void ++display_gemini(FILE *tty, struct gemini_response *resp, ++ struct link **next, bool pagination) ++{ ++ int nlinks = 0; ++ struct gemini_parser p; ++ gemini_parser_init(&p, resp->bio); ++ ++ struct winsize ws; ++ ioctl(fileno(tty), TIOCGWINSZ, &ws); ++ ++ int row = 0, col = 0; ++ struct gemini_token tok; ++ while (gemini_parser_next(&p, &tok) == 0) { ++ switch (tok.token) { ++ case GEMINI_TEXT: ++ // TODO: word wrap ++ col += fprintf(tty, " %s\n", trim_ws(tok.text)); ++ break; ++ case GEMINI_LINK: ++ (void)next; // TODO: Record links ++ col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( ++ tok.link.text ? tok.link.text : tok.link.url)); ++ break; ++ case GEMINI_PREFORMATTED: ++ continue; // TODO ++ case GEMINI_HEADING: ++ for (int n = tok.heading.level; n; --n) { ++ col += fprintf(tty, "#"); ++ } ++ col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); ++ break; ++ case GEMINI_LIST_ITEM: ++ // TODO: Option to disable Unicode ++ col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); ++ break; ++ case GEMINI_QUOTE: ++ // TODO: Option to disable Unicode ++ col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); ++ break; ++ } ++ ++ while (col >= ws.ws_col) { ++ col -= ws.ws_col; ++ ++row; ++ } ++ ++row; ++ col = 0; ++ ++ if (pagination && row >= ws.ws_row - 1) { ++ fprintf(tty, "[Enter for more, or q to stop] "); ++ ++ size_t n = 0; ++ char *l = NULL; ++ if (getline(&l, &n, tty) == -1) { ++ return; ++ } ++ if (strcmp(l, "q\n") == 0) { ++ return; ++ } ++ ++ free(l); ++ row = col = 0; ++ } ++ } ++ ++ gemini_parser_finish(&p); ++} ++ ++int ++main(int argc, char *argv[]) ++{ ++ bool pagination = true; ++ ++ bool have_url = false; ++ struct Curl_URL *url = curl_url(); ++ ++ FILE *tty = fopen("/dev/tty", "w+"); ++ ++ int c; ++ while ((c = getopt(argc, argv, "hP")) != -1) { ++ switch (c) { ++ case 'P': ++ pagination = false; ++ break; ++ case 'h': ++ usage(argv[0]); ++ return 0; ++ default: ++ fprintf(stderr, "fatal: unknown flag %c\n", c); ++ return 1; ++ } ++ } ++ ++ if (optind == argc - 1) { ++ set_url(url, argv[optind]); ++ have_url = true; ++ } else if (optind < argc - 1) { ++ usage(argv[0]); ++ return 1; ++ } ++ ++ SSL_load_error_strings(); ++ ERR_load_crypto_strings(); ++ struct gemini_options opts = { ++ .ssl_ctx = SSL_CTX_new(TLS_method()), ++ }; ++ ++ bool run = true; ++ struct gemini_response resp; ++ while (run) { ++ assert(have_url); // TODO ++ ++ struct link *links; ++ static char prompt[4096]; ++ ++ char *plain_url; ++ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); ++ assert(uc == CURLUE_OK); // Invariant ++ ++ snprintf(prompt, sizeof(prompt), "\n\t%s\n" ++ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" ++ "=> ", plain_url); ++ ++ enum gemini_result res = gemini_request(plain_url, &opts, &resp); ++ if (res != GEMINI_OK) { ++ fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp)); ++ assert(0); // TODO: Prompt ++ } ++ ++ switch (gemini_response_class(resp.status)) { ++ case GEMINI_STATUS_CLASS_INPUT: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_REDIRECT: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: ++ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: ++ fprintf(stderr, "Server returned %s %d %s\n", ++ resp.status / 10 == 4 ? ++ "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ resp.status, resp.meta); ++ break; ++ case GEMINI_STATUS_CLASS_SUCCESS: ++ display_gemini(tty, &resp, &links, pagination); ++ break; ++ } ++ ++ gemini_response_finish(&resp); ++ ++ fprintf(tty, "%s", prompt); ++ size_t l = 0; ++ char *in = NULL; ++ ssize_t n = getline(&in, &l, tty); ++ if (n == -1 && feof(tty)) { ++ break; ++ } ++ ++ if (strcmp(in, "q\n") == 0) { ++ run = false; ++ } ++ ++ free(in); ++ } ++ ++ SSL_CTX_free(opts.ssl_ctx); ++ curl_url_cleanup(url); ++ return 0; ++} diff --git a/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch b/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch @@ -0,0 +1,22 @@ +diff --git a/src/gmni.c b/src/gmni.c +index e8b25f9060c021bda099aa97fc2ec5657aeb2672..4f563ed19e0edc52086f55e4057a91f0802a1f93 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -20,7 +20,7 @@ static void + usage(const char *argv_0) + { + fprintf(stderr, +- "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", ++ "usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n", + argv_0); + } + +@@ -86,7 +86,7 @@ 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); ++ "Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint); + break; + case TOFU_FINGERPRINT_MISMATCH: + fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch b/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch @@ -0,0 +1,12 @@ +diff --git a/src/gmni.c b/src/gmni.c +index ab0908de5ec9682865504e940a5489a43c41b12e..bdaa9baaca75e5f167add9a0ebed73cc60eaa647 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -147,6 +147,7 @@ + new_url = gemini_input_url(url, input); + free(url); + url = new_url; ++ assert(url); + goto next; + case 3: // REDIRECT + free(url); diff --git a/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch b/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch @@ -0,0 +1,751 @@ +diff --git a/configure b/configure +index 7412cf545fa96e6be169c0055778b83a06b557f5..44db11c4765077677a1f506f034c846cc736ab8c 100755 +--- a/configure ++++ b/configure +@@ -7,7 +7,9 @@ genrules gmni \ + src/client.c \ + src/escape.c \ + src/gmni.c \ +- src/url.c ++ src/tofu.c \ ++ src/url.c \ ++ src/util.c + } + + gmnlm() { +@@ -16,6 +18,7 @@ src/client.c \ + src/escape.c \ + src/gmnlm.c \ + src/parser.c \ ++ src/tofu.c \ + src/url.c \ + src/util.c + } +diff --git a/doc/gmni.scd b/doc/gmni.scd +index 0d84d4d974f2c38b426f6ae85d228cdd4847cbda..2c1dc54272aaf3a4cbd6b1c13e29323f418f199b 100644 +--- a/doc/gmni.scd ++++ b/doc/gmni.scd +@@ -6,7 +6,7 @@ gmni - Gemini client + + # SYNPOSIS + +-*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ ++*gmni* [-46lLiIN] [-j _mode_] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ + + # DESCRIPTION + +@@ -51,6 +51,11 @@ this behavior. + + *-L* + Follow redirects. ++ ++*-j* _mode_ ++ Sets the TOFU (trust on first use) configuration, which controls if the ++ client shall trust new certificates. _mode_ can be one of *always*, ++ *once*, or *fail*. + + *-i* + Print the response status and meta text to stdout. +diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd +index c5e7bf7f6b189f984e01ea2a942f47acb993f7e7..b11f3612e044ad0667d8c4d306445ae86e9f73d4 100644 +--- a/doc/gmnlm.scd ++++ b/doc/gmnlm.scd +@@ -6,13 +6,18 @@ gmnlm - Gemini line-mode browser + + # SYNPOSIS + +-*gmnlm* [-PU] _gemini://..._ ++*gmnlm* [-PU] [-j _mode_] _gemini://..._ + + # DESCRIPTION + + *gmnlm* is an interactive line-mode Gemini browser. + + # OPTIONS ++ ++*-j* _mode_ ++ Sets the TOFU (trust on first use) configuration, which controls if the ++ client shall trust new certificates. _mode_ can be one of *always*, ++ *once*, or *fail*. + + *-P* + Disable pagination. +diff --git a/include/gmni.h b/include/gmni.h +index 4240c6231010ebb86aead2d49700fe2e3d00b65c..7e27b489d71fd3a43ca60292b17d56cab3caa5f8 100644 +--- a/include/gmni.h ++++ b/include/gmni.h +@@ -13,6 +13,7 @@ GEMINI_ERR_NOT_GEMINI, + GEMINI_ERR_RESOLVE, + GEMINI_ERR_CONNECT, + GEMINI_ERR_SSL, ++ GEMINI_ERR_SSL_VERIFY, + GEMINI_ERR_IO, + GEMINI_ERR_PROTOCOL, + }; +@@ -64,10 +65,6 @@ struct gemini_options { + // If NULL, an SSL context will be created. If unset, the ssl field + // must also be NULL. + SSL_CTX *ssl_ctx; +- +- // If NULL, an SSL connection will be established. If set, it is +- // presumed that the caller pre-established the SSL connection. +- SSL *ssl; + + // If ai_family != AF_UNSPEC (the default value on most systems), the + // client will connect to this address and skip name resolution. +diff --git a/include/tofu.h b/include/tofu.h +new file mode 100644 +index 0000000000000000000000000000000000000000..29aa9bc21567868cafb25a09dbc25ea0685ab01c +--- /dev/null ++++ b/include/tofu.h +@@ -0,0 +1,48 @@ ++#ifndef GEMINI_TOFU_H ++#define GEMINI_TOFU_H ++#include <limits.h> ++#include <openssl/ssl.h> ++#include <openssl/x509.h> ++#include <time.h> ++ ++enum tofu_error { ++ TOFU_VALID, ++ // Expired, wrong CN, etc. ++ TOFU_INVALID_CERT, ++ // Cert is valid but we haven't seen it before ++ TOFU_UNTRUSTED_CERT, ++ // Cert is valid but we already trust another cert for this host ++ TOFU_FINGERPRINT_MISMATCH, ++}; ++ ++enum tofu_action { ++ TOFU_ASK, ++ TOFU_FAIL, ++ TOFU_TRUST_ONCE, ++ TOFU_TRUST_ALWAYS, ++}; ++ ++struct known_host { ++ char *host, *fingerprint; ++ time_t expires; ++ int lineno; ++ struct known_host *next; ++}; ++ ++// Called when the user needs to be prompted to agree to trust an unknown ++// 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 { ++ char known_hosts_path[PATH_MAX+1]; ++ struct known_host *known_hosts; ++ int lineno; ++ 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); ++ ++#endif +diff --git a/src/client.c b/src/client.c +index d8b67d7b9f47b4473ba6eb278853ea0565739f09..07460f917b153b0d368eb2a98f368aa6a5df56a4 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -95,6 +95,7 @@ { + assert(url); + assert(resp); + resp->meta = NULL; ++ resp->bio = NULL; + if (strlen(url) > 1024) { + return GEMINI_ERR_INVALID_URL; + } +@@ -110,7 +111,7 @@ res = GEMINI_ERR_INVALID_URL; + goto cleanup; + } + +- char *scheme; ++ char *scheme, *host; + if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { + res = GEMINI_ERR_INVALID_URL; + goto cleanup; +@@ -120,6 +121,10 @@ res = GEMINI_ERR_NOT_GEMINI; + goto cleanup; + } + } ++ if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { ++ res = GEMINI_ERR_INVALID_URL; ++ goto cleanup; ++ } + + if (options && options->ssl_ctx) { + resp->ssl_ctx = options->ssl_ctx; +@@ -127,42 +132,54 @@ SSL_CTX_up_ref(options->ssl_ctx); + } else { + 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; + BIO *sbio = BIO_new(BIO_f_ssl()); +- if (options && options->ssl) { +- resp->ssl = options->ssl; +- SSL_up_ref(resp->ssl); +- BIO_set_ssl(sbio, resp->ssl, 0); +- resp->fd = -1; +- } else { +- res = gemini_connect(uri, options, resp, &resp->fd); +- if (res != GEMINI_OK) { +- goto cleanup; +- } ++ res = gemini_connect(uri, options, resp, &resp->fd); ++ if (res != GEMINI_OK) { ++ goto cleanup; ++ } ++ ++ resp->ssl = SSL_new(resp->ssl_ctx); ++ assert(resp->ssl); ++ SSL_set_connect_state(resp->ssl); ++ if ((r = SSL_set1_host(resp->ssl, host)) != 1) { ++ goto ssl_error; ++ } ++ if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { ++ goto ssl_error; ++ } ++ if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { ++ goto ssl_error; ++ } ++ if ((r = SSL_connect(resp->ssl)) != 1) { ++ goto ssl_error; ++ } ++ ++ X509 *cert = SSL_get_peer_certificate(resp->ssl); ++ if (!cert) { ++ resp->status = X509_V_ERR_UNSPECIFIED; ++ res = GEMINI_ERR_SSL_VERIFY; ++ goto cleanup; ++ } ++ X509_free(cert); + +- resp->ssl = SSL_new(resp->ssl_ctx); +- assert(resp->ssl); +- int r = SSL_set_fd(resp->ssl, resp->fd); +- if (r != 1) { +- resp->status = r; +- res = GEMINI_ERR_SSL; +- goto cleanup; +- } +- r = SSL_connect(resp->ssl); +- if (r != 1) { +- resp->status = r; +- res = GEMINI_ERR_SSL; +- goto cleanup; +- } +- BIO_set_ssl(sbio, resp->ssl, 0); ++ long vr = SSL_get_verify_result(resp->ssl); ++ if (vr != X509_V_OK) { ++ resp->status = vr; ++ res = GEMINI_ERR_SSL_VERIFY; ++ goto cleanup; + } + ++ BIO_set_ssl(sbio, resp->ssl, 0); ++ + resp->bio = BIO_new(BIO_f_buffer()); + BIO_push(resp->bio, sbio); + + char req[1024 + 3]; +- int r = snprintf(req, sizeof(req), "%s\r\n", url); ++ r = snprintf(req, sizeof(req), "%s\r\n", url); + assert(r > 0); + + r = BIO_puts(sbio, req); +@@ -199,6 +216,10 @@ + cleanup: + curl_url_cleanup(uri); + return res; ++ssl_error: ++ res = GEMINI_ERR_SSL; ++ resp->status = r; ++ goto cleanup; + } + + void +@@ -248,6 +269,8 @@ case GEMINI_ERR_SSL: + return ERR_error_string( + SSL_get_error(resp->ssl, resp->status), + NULL); ++ case GEMINI_ERR_SSL_VERIFY: ++ return X509_verify_cert_error_string(resp->status); + case GEMINI_ERR_IO: + return "I/O error"; + case GEMINI_ERR_PROTOCOL: +diff --git a/src/gmni.c b/src/gmni.c +index dc0c5c7f61679369fe4fadafd0508147868cc3c3..c13e0cd55623f557a8dc676d69aea54661b11faf 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -13,6 +13,7 @@ #include <sys/types.h> + #include <termios.h> + #include <unistd.h> + #include "gmni.h" ++#include "tofu.h" + + static void + usage(const char *argv_0) +@@ -57,6 +58,55 @@ } + return input; + } + ++struct tofu_config { ++ struct gemini_tofu tofu; ++ enum tofu_action action; ++}; ++ ++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", 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; ++} ++ + int + main(int argc, char *argv[]) + { +@@ -71,7 +121,6 @@ enum input_mode { + INPUT_READ, + INPUT_SUPPRESS, + }; +- + enum input_mode input_mode = INPUT_READ; + FILE *input_source = stdin; + +@@ -82,9 +131,11 @@ struct addrinfo hints = {0}; + struct gemini_options opts = { + .hints = &hints, + }; ++ struct tofu_config cfg; ++ cfg.action = TOFU_ASK; + + int c; +- while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { ++ while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:")) != -1) { + switch (c) { + case '4': + hints.ai_family = AF_INET; +@@ -115,6 +166,18 @@ break; + case 'h': + usage(argv[0]); + return 0; ++ case 'j': ++ if (strcmp(optarg, "fail") == 0) { ++ cfg.action = TOFU_FAIL; ++ } else if (strcmp(optarg, "once") == 0) { ++ cfg.action = TOFU_TRUST_ONCE; ++ } else if (strcmp(optarg, "always") == 0) { ++ cfg.action = TOFU_TRUST_ALWAYS; ++ } else { ++ usage(argv[0]); ++ return 1; ++ } ++ break; + case 'l': + linefeed = false; + break; +@@ -153,6 +216,8 @@ } + + 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); + + bool exit = false; + char *url = strdup(argv[optind]); +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 69b9a75ca1caab6f7e5f74685e550539be149ab6..41284df2235e869f49e542f5e1f2dcc240deb684 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -13,6 +13,7 @@ #include <sys/ioctl.h> + #include <termios.h> + #include <unistd.h> + #include "gmni.h" ++#include "tofu.h" + #include "url.h" + #include "util.h" + +@@ -29,6 +30,8 @@ + struct browser { + bool pagination, unicode; + struct gemini_options opts; ++ struct gemini_tofu tofu; ++ enum tofu_action tofu_mode; + + FILE *tty; + char *plain_url; +@@ -657,22 +660,113 @@ + return false; + } + ++static enum tofu_action ++tofu_callback(enum tofu_error error, const char *fingerprint, ++ struct known_host *host, void *data) ++{ ++ struct browser *browser = data; ++ if (browser->tofu_mode != TOFU_ASK) { ++ return browser->tofu_mode; ++ } ++ ++ static char prompt[8192]; ++ switch (error) { ++ case TOFU_VALID: ++ assert(0); // Invariant ++ case TOFU_INVALID_CERT: ++ snprintf(prompt, sizeof(prompt), ++ "The server presented an invalid certificate. If you choose to proceed, " ++ "you should not disclose personal information or trust the contents of the page.\n" ++ "trust [o]nce; [a]bort\n" ++ "=> "); ++ break; ++ case TOFU_UNTRUSTED_CERT: ++ snprintf(prompt, sizeof(prompt), ++ "The certificate offered by this server is of unknown trust. " ++ "Its fingerprint is: \n" ++ "%s\n\n" ++ "If you knew the fingerprint to expect in advance, verify that this matches.\n" ++ "Otherwise, it should be safe to trust this certificate.\n\n" ++ "[t]rust always; trust [o]nce; [a]bort\n" ++ "=> ", fingerprint); ++ break; ++ case TOFU_FINGERPRINT_MISMATCH: ++ snprintf(prompt, sizeof(prompt), ++ "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, ++ browser->tofu.known_hosts_path, host->lineno); ++ return TOFU_FAIL; ++ } ++ ++ bool prompting = true; ++ while (prompting) { ++ fprintf(browser->tty, "%s", prompt); ++ ++ size_t sz = 0; ++ char *line = NULL; ++ if (getline(&line, &sz, browser->tty) == -1) { ++ free(line); ++ return TOFU_FAIL; ++ } ++ if (line[1] != '\n') { ++ free(line); ++ continue; ++ } ++ ++ char c = line[0]; ++ free(line); ++ ++ switch (c) { ++ case 't': ++ if (error == TOFU_INVALID_CERT) { ++ break; ++ } ++ return TOFU_TRUST_ALWAYS; ++ case 'o': ++ return TOFU_TRUST_ONCE; ++ case 'a': ++ return TOFU_FAIL; ++ } ++ } ++ ++ return TOFU_FAIL; ++} ++ + int + main(int argc, char *argv[]) + { + struct browser browser = { + .pagination = true, ++ .tofu_mode = TOFU_ASK, + .unicode = true, + .url = curl_url(), + .tty = fopen("/dev/tty", "w+"), + }; + + int c; +- while ((c = getopt(argc, argv, "hPU")) != -1) { ++ while ((c = getopt(argc, argv, "hj:PU")) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return 0; ++ case 'j': ++ if (strcmp(optarg, "fail") == 0) { ++ browser.tofu_mode = TOFU_FAIL; ++ } else if (strcmp(optarg, "once") == 0) { ++ browser.tofu_mode = TOFU_TRUST_ONCE; ++ } else if (strcmp(optarg, "always") == 0) { ++ browser.tofu_mode = TOFU_TRUST_ALWAYS; ++ } else { ++ usage(argv[0]); ++ return 1; ++ } ++ break; + case 'P': + browser.pagination = false; + break; +@@ -695,6 +789,8 @@ + SSL_load_error_strings(); + ERR_load_crypto_strings(); + browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); ++ gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, ++ &tofu_callback, &browser); + + struct gemini_response resp; + browser.running = true; +diff --git a/src/tofu.c b/src/tofu.c +new file mode 100644 +index 0000000000000000000000000000000000000000..e8efeaf69fcb9bd890711f76959e86efa75cfec6 +--- /dev/null ++++ b/src/tofu.c +@@ -0,0 +1,201 @@ ++#include <assert.h> ++#include <errno.h> ++#include <libgen.h> ++#include <limits.h> ++#include <openssl/asn1.h> ++#include <openssl/evp.h> ++#include <openssl/ssl.h> ++#include <openssl/x509.h> ++#include <stdio.h> ++#include <string.h> ++#include <time.h> ++#include "tofu.h" ++#include "util.h" ++ ++static int ++verify_callback(X509_STORE_CTX *ctx, void *data) ++{ ++ // Gemini clients handle TLS verification differently from the rest of ++ // the internet. We use a TOFU system, so trust is based on two factors: ++ // ++ // - Is the certificate valid at the time of the request? ++ // - Has the user trusted this certificate yet? ++ // ++ // If the answer to the latter is "no", then we give the user an ++ // opportunity to explicitly agree to trust the certificate before ++ // rejecting it. ++ // ++ // If you're reading this code with the intent to re-use it, think ++ // twice. ++ // ++ // TODO: Check that the subject name is valid for the requested URL. ++ struct gemini_tofu *tofu = (struct gemini_tofu *)data; ++ X509 *cert = X509_STORE_CTX_get0_cert(ctx); ++ ++ int rc; ++ int day, sec; ++ const ASN1_TIME *notBefore = X509_get0_notBefore(cert); ++ const ASN1_TIME *notAfter = X509_get0_notAfter(cert); ++ if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { ++ rc = X509_V_ERR_UNSPECIFIED; ++ goto invalid_cert; ++ } ++ if (day > 0 || sec > 0) { ++ rc = X509_V_ERR_CERT_NOT_YET_VALID; ++ goto invalid_cert; ++ } ++ if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { ++ rc = X509_V_ERR_UNSPECIFIED; ++ goto invalid_cert; ++ } ++ if (day < 0 || sec < 0) { ++ rc = X509_V_ERR_CERT_HAS_EXPIRED; ++ goto invalid_cert; ++ } ++ ++ unsigned char md[256 / 8]; ++ const EVP_MD *sha512 = EVP_sha512(); ++ unsigned int len = sizeof(md); ++ rc = X509_digest(cert, sha512, md, &len); ++ assert(rc == 1); ++ ++ char fingerprint[256 / 8 * 3]; ++ for (size_t i = 0; i < sizeof(md); ++i) { ++ snprintf(&fingerprint[i * 3], 4, "%02X%s", ++ md[i], i + 1 == sizeof(md) ? "" : ":"); ++ } ++ ++ SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, ++ SSL_get_ex_data_X509_STORE_CTX_idx()); ++ const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); ++ if (!servername) { ++ rc = X509_V_ERR_HOSTNAME_MISMATCH; ++ goto invalid_cert; ++ } ++ ++ time_t now; ++ time(&now); ++ ++ enum tofu_error error = TOFU_UNTRUSTED_CERT; ++ struct known_host *host = tofu->known_hosts; ++ while (host) { ++ if (host->expires < now) { ++ goto next; ++ } ++ if (strcmp(host->host, servername) != 0) { ++ goto next; ++ } ++ if (strcmp(host->fingerprint, fingerprint) == 0) { ++ // Valid match in known hosts ++ return 0; ++ } ++ error = TOFU_FINGERPRINT_MISMATCH; ++ break; ++next: ++ host = host->next; ++ } ++ ++ rc = X509_V_ERR_CERT_UNTRUSTED; ++ ++callback: ++ switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { ++ case TOFU_ASK: ++ assert(0); // Invariant ++ case TOFU_FAIL: ++ X509_STORE_CTX_set_error(ctx, rc); ++ break; ++ case TOFU_TRUST_ONCE: ++ // No further action necessary ++ return 0; ++ case TOFU_TRUST_ALWAYS:; ++ FILE *f = fopen(tofu->known_hosts_path, "a"); ++ if (!f) { ++ fprintf(stderr, "Error opening %s for writing: %s\n", ++ tofu->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 %ld\n", servername, ++ "SHA-512", fingerprint, expires); ++ fclose(f); ++ ++ host = calloc(1, sizeof(struct known_host)); ++ host->host = strdup(servername); ++ host->fingerprint = strdup(fingerprint); ++ host->expires = expires; ++ host->lineno = ++tofu->lineno; ++ host->next = tofu->known_hosts; ++ tofu->known_hosts = host; ++ return 0; ++ } ++ ++ X509_STORE_CTX_set_error(ctx, rc); ++ return 0; ++ ++invalid_cert: ++ error = TOFU_INVALID_CERT; ++ goto callback; ++} ++ ++ ++void ++gemini_tofu_init(struct gemini_tofu *tofu, ++ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) ++{ ++ const struct pathspec paths[] = { ++ {.var = "GMNIDATA", .path = "/%s"}, ++ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, ++ {.var = "HOME", .path = "/.local/share/gmni/%s"} ++ }; ++ const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); ++ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), ++ path_fmt, "known_hosts"); ++ ++ if (mkdirs(dirname(tofu->known_hosts_path), 0755) != 0) { ++ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), ++ path_fmt, "known_hosts"); ++ fprintf(stderr, "Error creating directory %s: %s\n", ++ dirname(tofu->known_hosts_path), strerror(errno)); ++ return; ++ } ++ ++ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), ++ path_fmt, "known_hosts"); ++ ++ tofu->callback = cb; ++ tofu->cb_data = cb_data; ++ SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); ++ ++ FILE *f = fopen(tofu->known_hosts_path, "r"); ++ if (!f) { ++ return; ++ } ++ size_t n = 0; ++ char *line = NULL; ++ while (getline(&line, &n, f) != -1) { ++ struct known_host *host = calloc(1, sizeof(struct known_host)); ++ char *tok = strtok(line, " "); ++ assert(tok); ++ host->host = strdup(tok); ++ ++ tok = strtok(NULL, " "); ++ assert(tok); ++ if (strcmp(tok, "SHA-512") != 0) { ++ free(host); ++ continue; ++ } ++ ++ tok = strtok(NULL, " "); ++ assert(tok); ++ host->fingerprint = strdup(tok); ++ ++ tok = strtok(NULL, " "); ++ assert(tok); ++ host->expires = strtoul(tok, NULL, 10); ++ ++ host->next = tofu->known_hosts; ++ tofu->known_hosts = host; ++ } ++} diff --git a/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch b/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch @@ -0,0 +1,14 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index c70a4126acc5337a768c3a70513aecc1b0e16668..95b85fedd9fb84b50bf1c8a89be94daaeb99a4a6 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -730,6 +730,9 @@ case '\r': + if (!s[i+1]) break; + /* fallthrough */ + default: ++ // skip unicode continuation bytes ++ if ((s[i] & 0xc0) == 0x80) break; ++ + if (iscntrl(s[i])) { + s[i] = '.'; + } diff --git a/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch b/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch @@ -0,0 +1,49 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 129324718d13f8311f38cc3c80b7f0f3ccb3bd09..b7cc12dbbb391dd3e72e5e30d21a7e7957872f60 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -41,6 +41,14 @@ PROMPT_QUIT, + PROMPT_ANSWERED, + }; + ++const char *help_msg = ++ "The following commands are available:\n\n" ++ "q: Quit\n" ++ "N: Follow Nth link (where N is a number)\n" ++ "b: Back (in the page history)\n" ++ "f: Forward (in the page history)\n" ++ ; ++ + static void + usage(const char *argv_0) + { +@@ -100,6 +108,11 @@ if (strcmp(in, "q\n") == 0) { + result = PROMPT_QUIT; + goto exit; + } ++ if (strcmp(in, "?\n") == 0) { ++ fprintf(browser->tty, "%s", help_msg); ++ result = PROMPT_AGAIN; ++ goto exit; ++ } + if (strcmp(in, "b\n") == 0) { + if (!browser->history->prev) { + fprintf(stderr, "At beginning of history\n"); +@@ -291,7 +304,7 @@ + if (browser->pagination && row >= ws.ws_row - 4) { + char prompt[4096]; + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; or type a URL\n" ++ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" + "(more) => ", resp->meta, browser->plain_url, + browser->history->prev ? "[b]ack; " : "", + browser->history->next ? "[f]orward; " : ""); +@@ -500,7 +513,7 @@ goto next; + } + + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[N]: follow Nth link; %s%s[q]uit; or type a URL\n" ++ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" + "=> ", + resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", + browser.plain_url, diff --git a/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch b/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch @@ -0,0 +1,66 @@ +diff --git a/src/gmni.c b/src/gmni.c +index 5b1f37522e93da360e74ba92ccb00c530bd41463..6e27b2f859c692f37fb483184cda245a7e979cbe 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -71,16 +71,20 @@ enum input_mode { + INPUT_READ, + INPUT_SUPPRESS, + }; ++ + enum input_mode input_mode = INPUT_READ; + FILE *input_source = stdin; ++ + bool follow_redirects = false, linefeed = true; ++ int max_redirect = 5; ++ + struct addrinfo hints = {0}; + struct gemini_options opts = { + .hints = &hints, + }; + + int c; +- while ((c = getopt(argc, argv, "46d:D:E:hlLiIN")) != -1) { ++ while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { + switch (c) { + case '4': + hints.ai_family = AF_INET; +@@ -127,6 +131,15 @@ break; + case 'N': + input_mode = INPUT_SUPPRESS; + break; ++ case 'R':; ++ char *endptr; ++ errno = 0; ++ max_redirect = strtoul(optarg, &endptr, 10); ++ if (*endptr || errno != 0) { ++ fprintf(stderr, "Error: -R expects numeric argument\n"); ++ return 1; ++ } ++ break; + default: + fprintf(stderr, "fatal: unknown flag %c\n", c); + return 1; +@@ -144,7 +157,7 @@ + bool exit = false; + char *url = strdup(argv[optind]); + +- int ret = 0; ++ int ret = 0, nredir = 0; + while (!exit) { + struct gemini_response resp; + enum gemini_result r = gemini_request(url, &opts, &resp); +@@ -177,6 +190,14 @@ free(url); + url = new_url; + goto next; + case GEMINI_STATUS_CLASS_REDIRECT: ++ if (++nredir >= max_redirect) { ++ fprintf(stderr, ++ "Error: maximum redirects (%d) exceeded", ++ max_redirect); ++ exit = true; ++ goto next; ++ } ++ + free(url); + url = strdup(resp.meta); + if (!follow_redirects) { diff --git a/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch b/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 199572c363f9e50acdd052111350f3926d6a350d..5d45ffe7372f71ab32461bf71a590cc494269e96 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -246,7 +246,7 @@ open_bookmarks(browser); + result = PROMPT_ANSWERED; + goto exit; + case '/': +- if (in[1]) break; ++ if (!in[1]) break; + if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { + static char buf[1024]; + r = regerror(r, &browser->regex, buf, sizeof(buf)); diff --git a/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch b/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch @@ -0,0 +1,12 @@ +diff --git a/src/parser.c b/src/parser.c +index 2f78e4641c08b86f5dc50cc9776ea80ec7f9aead..04501b6b644af28abe7843076ae593b7165912b2 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -113,6 +113,7 @@ if (end && end + 1 < p->buf + p->bufln) { + size_t len = end - p->buf + 1; + memmove(p->buf, end + 1, p->bufln - len); + p->bufln -= len; ++ p->buf[p->bufln] = 0; + } else { + p->buf[0] = 0; + p->bufln = 0; diff --git a/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch b/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index da99a84580894312ba86b92e6abd02cfdfc80fd4..26654324b4fb845633f7db2b2ac01e5e3b4635a1 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -493,7 +493,7 @@ col += fprintf(out, " "); + } + break; + case GEMINI_QUOTE: +- col += fprintf(out, "%s ", ++ col += fprintf(out, " %s ", + browser->unicode ? "┃" : ">"); + if (text == NULL) { + text = trim_ws(tok.quote_text); diff --git a/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch b/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch @@ -0,0 +1,15 @@ +diff --git a/src/tofu.c b/src/tofu.c +index 45e1275d568cfcb4aacfe7f86788b76fff97916b..4eeb7fd0eb8df7423fe8fd8f106e2fe433fa8716 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -147,8 +147,8 @@ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) + { + const struct pathspec paths[] = { + {.var = "GMNIDATA", .path = "/%s"}, +- {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, +- {.var = "HOME", .path = "/.local/share/gmni/%s"} ++ {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, ++ {.var = "HOME", .path = "/.local/share/gemini/%s"} + }; + char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); + snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), diff --git a/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch b/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch @@ -0,0 +1,28 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index a021a97298bb41bff8cdbbceca172f561b64c7a5..8e85d091e54e3c3c11dbd6980ae660be6838c7c8 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -349,7 +349,7 @@ case 0: + close(pfd[1]); + dup2(pfd[0], STDIN_FILENO); + close(pfd[0]); +- execlp("sh", "sh", "-c", cmd); ++ execlp("sh", "sh", "-c", cmd, NULL); + perror("exec"); + _exit(1); + } +diff --git a/src/tofu.c b/src/tofu.c +index b9100c77fd61c71ce561f2194b991eee7130e689..ba5493352968b563ad76e0ecd25bab6370d2f6e5 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -124,8 +124,8 @@ }; + struct tm expires_tm; + ASN1_TIME_to_tm(notAfter, &expires_tm); + time_t expires = mktime(&expires_tm); +- fprintf(f, "%s %s %s %ld\n", servername, +- "SHA-512", fingerprint, expires); ++ fprintf(f, "%s %s %s %jd\n", servername, ++ "SHA-512", fingerprint, (intmax_t)expires); + fclose(f); + + host = calloc(1, sizeof(struct known_host)); diff --git a/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch b/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch @@ -0,0 +1,34 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 930ab19abe717b186070151a184b2a8e98542f77..1292bb6b8f526f923e9783ba8c0a014c2743af05 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -1106,7 +1106,7 @@ "=> ", host, fingerprint); + free(host); + break; + case TOFU_FINGERPRINT_MISMATCH: +- snprintf(prompt, sizeof(prompt), ++ fprintf(browser->tty, + "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" +diff --git a/src/tofu.c b/src/tofu.c +index ba5493352968b563ad76e0ecd25bab6370d2f6e5..48395c08cdbf68b31c5defd15b360f1eb2897a3f 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -188,6 +188,7 @@ if (!f) { + return; + } + n = 0; ++ int lineno = 1; + char *line = NULL; + while (getline(&line, &n, f) != -1) { + struct known_host *host = calloc(1, sizeof(struct known_host)); +@@ -210,6 +211,8 @@ + tok = strtok(NULL, " "); + assert(tok); + host->expires = strtoul(tok, NULL, 10); ++ ++ host->lineno = lineno++; + + host->next = tofu->known_hosts; + tofu->known_hosts = host; diff --git a/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch b/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch @@ -0,0 +1,562 @@ +diff --git a/.gitignore b/.gitignore +index 6ec8d5188d32b997f3a91f8a11f64f4e79b43770..4a40b62507e740b789fdb6d2d4a63c41e3b102c8 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -4,3 +4,5 @@ gmni + gmnlm + *.1 + *.o ++*.a ++*.pc +diff --git a/Makefile b/Makefile +index 5ac44dbda9d629823f99124f67ace8125e9bc697..22fed5d19ec4f989ead26d22d3d1887614e4927c 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,6 +1,7 @@ + .POSIX: + .SUFFIXES: + OUTDIR=.build ++VERSION=0.0.0 + include $(OUTDIR)/config.mk + include $(OUTDIR)/cppcache + +@@ -12,9 +13,26 @@ gmnlm: $(gmnlm_objects) + @printf 'CCLD\t$@\n' + @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) + ++libgmni.a: $(libgmni.a_objects) ++ @printf 'AR\t$@\n' ++ @$(AR) -rcs $@ $(libgmni.a_objects) ++ + doc/gmni.1: doc/gmni.scd + doc/gmnlm.1: doc/gmnlm.scd + ++libgmni.pc: ++ @printf 'GEN\t$@\n' ++ @printf 'prefix=%s\n' "$(PREFIX)" > $@ ++ @printf 'exec_prefix=$${prefix}\n' >> $@ ++ @printf 'includedir=$${prefix}/include\n' >> $@ ++ @printf 'libdir=$${prefix}/lib\n' >> $@ ++ @printf 'Name: libgmni\n' >> $@ ++ @printf 'Version: %s\n' "$(VERSION)" >> $@ ++ @printf 'Description: The gmni client library\n' >> $@ ++ @printf 'Requires: libssl libcrypto\n' >> $@ ++ @printf 'Cflags: -I$${includedir}/gmni\n' >> $@ ++ @printf 'Libs: -L$${libdir} -lgmni\n' >> $@ ++ + .SUFFIXES: .c .o .scd .1 + + .c.o: +@@ -22,7 +40,7 @@ @printf 'CC\t$@\n' + @touch $(OUTDIR)/cppcache + @grep $< $(OUTDIR)/cppcache >/dev/null || \ + $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache +- @$(CC) -c $(CFLAGS) -o $@ $< ++ @$(CC) -c -fPIC $(CFLAGS) -o $@ $< + + .scd.1: + @printf 'SCDOC\t$@\n' +@@ -31,7 +49,7 @@ + docs: doc/gmni.1 doc/gmnlm.1 + + clean: +- @rm -f gmni gmnlm doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) ++ @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) + + distclean: clean + @rm -rf "$(OUTDIR)" +@@ -41,6 +59,11 @@ mkdir -p $(BINDIR) + mkdir -p $(MANDIR)/man1 + install -Dm755 gmni $(BINDIR)/gmni + install -Dm755 gmnlm $(BINDIR)/gmnlm ++ install -Dm755 libgmni.a $(LIBDIR)/libgmni.a ++ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h ++ install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h ++ install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h ++ install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig + install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 + install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 + +diff --git a/config.sh b/config.sh +index b03dfffbc5976e69d2d878dbf3b6c545ebcad377..a6963622d4b41d6e25bb4fe230c2513849c077d3 100644 +--- a/config.sh ++++ b/config.sh +@@ -134,6 +134,7 @@ OUTDIR=${outdir} + _INSTDIR=\$(DESTDIR)\$(PREFIX) + BINDIR?=${BINDIR:-\$(_INSTDIR)/bin} + LIBDIR?=${LIBDIR:-\$(_INSTDIR)/lib} ++ INCLUDEDIR?=${INCLUDEDIR:-\$(_INSTDIR)/include} + MANDIR?=${MANDIR:-\$(_INSTDIR)/share/man} + CACHE=\$(OUTDIR)/cache + CFLAGS=${CFLAGS} +@@ -146,7 +147,7 @@ EOF + + for target in $all + do +- $target >>"$outdir"/config.mk ++ ${target//./_} >>"$outdir"/config.mk + done + echo done + +diff --git a/configure b/configure +index 44db11c4765077677a1f506f034c846cc736ab8c..e82a0e27e27c9873c8dac92fd3385e65bf511648 100755 +--- a/configure ++++ b/configure +@@ -23,6 +23,21 @@ src/url.c \ + src/util.c + } + +-all="gmni gmnlm" ++libgmni_a() { ++ genrules libgmni.a \ ++ src/client.c \ ++ src/escape.c \ ++ src/tofu.c \ ++ src/url.c \ ++ src/util.c \ ++ src/parser.c ++} ++ ++libgmni_pc() { ++ : ++} ++ ++all="gmni gmnlm libgmni.a libgmni.pc" ++ + + run_configure +diff --git a/include/gmni.h b/include/gmni.h +deleted file mode 100644 +index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..0000000000000000000000000000000000000000 +--- a/include/gmni.h ++++ /dev/null +@@ -1,164 +0,0 @@ +-#ifndef GEMINI_CLIENT_H +-#define GEMINI_CLIENT_H +-#include <netdb.h> +-#include <openssl/ssl.h> +-#include <stdbool.h> +-#include <sys/socket.h> +- +-enum gemini_result { +- GEMINI_OK, +- GEMINI_ERR_OOM, +- GEMINI_ERR_INVALID_URL, +- GEMINI_ERR_NOT_GEMINI, +- GEMINI_ERR_RESOLVE, +- GEMINI_ERR_CONNECT, +- GEMINI_ERR_SSL, +- GEMINI_ERR_SSL_VERIFY, +- GEMINI_ERR_IO, +- GEMINI_ERR_PROTOCOL, +-}; +- +-enum gemini_status { +- GEMINI_STATUS_INPUT = 10, +- GEMINI_STATUS_SENSITIVE_INPUT = 11, +- GEMINI_STATUS_SUCCESS = 20, +- GEMINI_STATUS_REDIRECT_TEMPORARY = 30, +- GEMINI_STATUS_REDIRECT_PERMANENT = 31, +- GEMINI_STATUS_TEMPORARY_FAILURE = 40, +- GEMINI_STATUS_SERVER_UNAVAILABLE = 41, +- GEMINI_STATUS_CGI_ERROR = 42, +- GEMINI_STATUS_PROXY_ERROR = 43, +- GEMINI_STATUS_SLOW_DOWN = 44, +- GEMINI_STATUS_PERMANENT_FAILURE = 50, +- GEMINI_STATUS_NOT_FOUND = 51, +- GEMINI_STATUS_GONE = 52, +- GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, +- GEMINI_STATUS_BAD_REQUEST = 59, +- GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, +- GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, +- GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, +-}; +- +-enum gemini_status_class { +- GEMINI_STATUS_CLASS_INPUT = 10, +- GEMINI_STATUS_CLASS_SUCCESS = 20, +- GEMINI_STATUS_CLASS_REDIRECT = 30, +- GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, +- GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, +- GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, +-}; +- +-struct gemini_response { +- enum gemini_status status; +- char *meta; +- +- // Response body may be read from here if appropriate: +- BIO *bio; +- +- // Connection state +- SSL_CTX *ssl_ctx; +- SSL *ssl; +- int fd; +-}; +- +-struct gemini_options { +- // If NULL, an SSL context will be created. If unset, the ssl field +- // must also be NULL. +- SSL_CTX *ssl_ctx; +- +- // 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; +- +- // If non-NULL, these hints are provided to getaddrinfo. Useful, for +- // example, to force IPv4/IPv6. +- struct addrinfo *hints; +-}; +- +-// Requests the specified URL via the gemini protocol. If options is non-NULL, +-// it may specify some additional configuration to adjust client behavior. +-// +-// Returns a value indicating the success of the request. +-// +-// 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_response *resp); +- +-// Must be called after gemini_request in order to free up the resources +-// allocated during the request. +-void gemini_response_finish(struct gemini_response *resp); +- +-// Returns a user-friendly string describing an error. +-const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); +- +-// Returns the given URL with the input response set to the specified value. +-// The caller must free the string. +-char *gemini_input_url(const char *url, const char *input); +- +-// Returns the general response class (i.e. with the second digit set to zero) +-// of the given Gemini status code. +-enum gemini_status_class gemini_response_class(enum gemini_status status); +- +-enum gemini_tok { +- GEMINI_TEXT, +- GEMINI_LINK, +- GEMINI_PREFORMATTED_BEGIN, +- GEMINI_PREFORMATTED_END, +- GEMINI_PREFORMATTED_TEXT, +- GEMINI_HEADING, +- GEMINI_LIST_ITEM, +- GEMINI_QUOTE, +-}; +- +-struct gemini_token { +- enum gemini_tok token; +- +- // The token field determines which of the union members is valid. +- union { +- char *text; +- +- struct { +- char *text; +- char *url; // May be NULL +- } link; +- +- char *preformatted; +- +- struct { +- char *title; +- int level; // 1, 2, or 3 +- } heading; +- +- char *list_item; +- char *quote_text; +- }; +-}; +- +-struct gemini_parser { +- BIO *f; +- 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); +- +-// Finishes this text/gemini parser and frees up its resources. +-void gemini_parser_finish(struct gemini_parser *p); +- +-// Reads the next token from a text/gemini file. +-// +-// Returns 0 on success, 1 on EOF, and -1 on failure. +-// +-// Caller must call gemini_token_finish before exiting or re-using the token +-// parameter. +-int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); +- +-// Must be called after gemini_next to free up resources for the next token. +-void gemini_token_finish(struct gemini_token *token); +- +-#endif +diff --git a/include/tofu.h b/include/tofu.h +deleted file mode 100644 +index a88167ba0fb6606b2b170e5005c55131f1861972..0000000000000000000000000000000000000000 +--- a/include/tofu.h ++++ /dev/null +@@ -1,49 +0,0 @@ +-#ifndef GEMINI_TOFU_H +-#define GEMINI_TOFU_H +-#include <limits.h> +-#include <openssl/ssl.h> +-#include <openssl/x509.h> +-#include <time.h> +- +-enum tofu_error { +- TOFU_VALID, +- // Expired, wrong CN, etc. +- TOFU_INVALID_CERT, +- // Cert is valid but we haven't seen it before +- TOFU_UNTRUSTED_CERT, +- // Cert is valid but we already trust another cert for this host +- TOFU_FINGERPRINT_MISMATCH, +-}; +- +-enum tofu_action { +- TOFU_ASK, +- TOFU_FAIL, +- TOFU_TRUST_ONCE, +- TOFU_TRUST_ALWAYS, +-}; +- +-struct known_host { +- char *host, *fingerprint; +- time_t expires; +- int lineno; +- struct known_host *next; +-}; +- +-// Called when the user needs to be prompted to agree to trust an unknown +-// 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 { +- char known_hosts_path[PATH_MAX+1]; +- struct known_host *known_hosts; +- int lineno; +- 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_finish(struct gemini_tofu *tofu); +- +-#endif +diff --git a/include/url.h b/include/url.h +deleted file mode 100644 +index 155fd55740dbe47a498062713aca217ab259734f..0000000000000000000000000000000000000000 +--- a/include/url.h ++++ /dev/null +@@ -1,103 +0,0 @@ +-#ifndef URLAPI_H +-#define URLAPI_H +-/*************************************************************************** +- * _ _ ____ _ +- * Project ___| | | | _ \| | +- * / __| | | | |_) | | +- * | (__| |_| | _ <| |___ +- * \___|\___/|_| \_\_____| +- * +- * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. +- * +- * This software is licensed as described in the file COPYING, which +- * you should have received as part of this distribution. The terms +- * are also available at https://curl.haxx.se/docs/copyright.html. +- * +- * You may opt to use, copy, modify, merge, publish, distribute and/or sell +- * copies of the Software, and permit persons to whom the Software is +- * furnished to do so, under the terms of the COPYING file. +- * +- * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +- * KIND, either express or implied. +- * +- ***************************************************************************/ +- +-/* the error codes for the URL API */ +-typedef enum { +- CURLUE_OK, +- CURLUE_BAD_HANDLE, /* 1 */ +- CURLUE_BAD_PARTPOINTER, /* 2 */ +- CURLUE_MALFORMED_INPUT, /* 3 */ +- CURLUE_BAD_PORT_NUMBER, /* 4 */ +- CURLUE_UNSUPPORTED_SCHEME, /* 5 */ +- CURLUE_URLDECODE, /* 6 */ +- CURLUE_OUT_OF_MEMORY, /* 7 */ +- CURLUE_USER_NOT_ALLOWED, /* 8 */ +- CURLUE_UNKNOWN_PART, /* 9 */ +- CURLUE_NO_SCHEME, /* 10 */ +- CURLUE_NO_USER, /* 11 */ +- CURLUE_NO_PASSWORD, /* 12 */ +- CURLUE_NO_OPTIONS, /* 13 */ +- CURLUE_NO_HOST, /* 14 */ +- CURLUE_NO_PORT, /* 15 */ +- CURLUE_NO_QUERY, /* 16 */ +- CURLUE_NO_FRAGMENT /* 17 */ +-} CURLUcode; +- +-typedef enum { +- CURLUPART_URL, +- CURLUPART_SCHEME, +- CURLUPART_USER, +- CURLUPART_PASSWORD, +- CURLUPART_OPTIONS, +- CURLUPART_HOST, +- CURLUPART_PORT, +- CURLUPART_PATH, +- CURLUPART_QUERY, +- CURLUPART_FRAGMENT +-} CURLUPart; +- +-#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ +-#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ +-#define CURLU_URLDECODE (1<<6) /* URL decode on get */ +-#define CURLU_URLENCODE (1<<7) /* URL encode on set */ +-#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ +- +-typedef struct Curl_URL CURLU; +- +-/* +- * curl_url() creates a new CURLU handle and returns a pointer to it. +- * Must be freed with curl_url_cleanup(). +- */ +-struct Curl_URL *curl_url(void); +- +-/* +- * curl_url_cleanup() frees the CURLU handle and related resources used for +- * the URL parsing. It will not free strings previously returned with the URL +- * API. +- */ +-void curl_url_cleanup(struct Curl_URL *handle); +- +-/* +- * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new +- * handle must also be freed with curl_url_cleanup(). +- */ +-struct Curl_URL *curl_url_dup(struct Curl_URL *in); +- +-/* +- * curl_url_get() extracts a specific part of the URL from a CURLU +- * handle. Returns error code. The returned pointer MUST be freed with +- * free() afterwards. +- */ +-CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, +- char **part, unsigned int flags); +- +-/* +- * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns +- * error code. The passed in string will be copied. Passing a NULL instead of +- * a part string, clears that part. +- */ +-CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, +- const char *part, unsigned int flags); +- +-#endif +diff --git a/src/client.c b/src/client.c +index 398e133b182af7f2fbe907be83cd5ecdc1b3a671..8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -9,8 +9,8 @@ #include <string.h> + #include <sys/socket.h> + #include <sys/types.h> + #include <unistd.h> +-#include "gmni.h" +-#include "url.h" ++#include <gmni/gmni.h> ++#include <gmni/url.h> + + static enum gemini_result + gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, +diff --git a/src/gmni.c b/src/gmni.c +index 61f41e29c324d980bd3a1f270e78649cdcec88e0..e8b25f9060c021bda099aa97fc2ec5657aeb2672 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -12,8 +12,8 @@ #include <sys/socket.h> + #include <sys/types.h> + #include <termios.h> + #include <unistd.h> +-#include "gmni.h" +-#include "tofu.h" ++#include <gmni/gmni.h> ++#include <gmni/tofu.h> + #include "util.h" + + static void +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 245ec85fa59fbcabde7e49c7d4d6e978aefa5f9a..2fba84e1f3c9e312ac53417d1b873158ceaf8c73 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -14,9 +14,9 @@ #include <sys/stat.h> + #include <sys/wait.h> + #include <termios.h> + #include <unistd.h> +-#include "gmni.h" +-#include "tofu.h" +-#include "url.h" ++#include <gmni/gmni.h> ++#include <gmni/tofu.h> ++#include <gmni/url.h> + #include "util.h" + + struct link { +diff --git a/src/parser.c b/src/parser.c +index 579415150f842557b6faf4097a63aac9324928fb..ad2c0e6306872f8dde450063ba8815e2ee7e314a 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -5,7 +5,7 @@ #include <stdbool.h> + #include <stddef.h> + #include <stdlib.h> + #include <string.h> +-#include "gmni.h" ++#include <gmni/gmni.h> + + void + gemini_parser_init(struct gemini_parser *p, BIO *f) +diff --git a/src/tofu.c b/src/tofu.c +index 48a627fe131996070d991bcc54bb7bcb40fddce4..863efc644a691370ee58ef0fb92291e9471adeb9 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -10,8 +10,8 @@ #include <openssl/x509v3.h> + #include <stdio.h> + #include <string.h> + #include <time.h> +-#include "gmni.h" +-#include "tofu.h" ++#include <gmni/gmni.h> ++#include <gmni/tofu.h> + #include "util.h" + + static int +diff --git a/src/url.c b/src/url.c +index dabf45f234e1e0957007b94ef12ef1125250dfea..47741e4ab1f91d336e5aba8117bde01094e370c3 100644 +--- a/src/url.c ++++ b/src/url.c +@@ -31,7 +31,7 @@ #include <stdlib.h> + #include <string.h> + #include <strings.h> + #include "escape.h" +-#include "url.h" ++#include <gmni/url.h> + + /* Provided by gmni */ + static char * +diff --git a/src/util.c b/src/util.c +index 360c99ac95de9dcfce860610aeef85e8986e99c2..0a479af3ea734ce25d0806aa086e61bef6b8953b 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -7,7 +7,7 @@ #include <stdio.h> + #include <stdlib.h> + #include <string.h> + #include <sys/stat.h> +-#include "gmni.h" ++#include <gmni/gmni.h> + #include "util.h" + + static void diff --git a/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch b/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch @@ -0,0 +1,47 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 8841c4613e208e379c6cd768a65a9a3b631d3146..8fc92928ba0d8532a5f94f8e637d0b0aa025bc09 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -78,9 +78,13 @@ + static bool + set_url(struct browser *browser, char *new_url, struct history **history) + { ++ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { ++ fprintf(stderr, "Error: invalid URL\n"); ++ return false; ++ } + if (history) { + struct history *next = calloc(1, sizeof(struct history)); +- next->url = strdup(new_url); ++ curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); + next->prev = *history; + if (*history) { + if ((*history)->next) { +@@ -89,10 +93,6 @@ } + (*history)->next = next; + } + *history = next; +- } +- if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { +- fprintf(stderr, "Error: invalid URL\n"); +- return false; + } + return true; + } +@@ -153,8 +153,14 @@ result = PROMPT_ANSWERED; + } + goto exit_re; + case 'n': +- result = PROMPT_NEXT; +- goto exit_re; ++ if (browser->searching) { ++ result = PROMPT_NEXT; ++ goto exit_re; ++ } else { ++ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); ++ result = PROMPT_AGAIN; ++ goto exit; ++ } + case '?': + fprintf(browser->tty, "%s", help_msg); + result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch b/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch @@ -0,0 +1,365 @@ +diff --git a/include/tofu.h b/include/tofu.h +index 29aa9bc21567868cafb25a09dbc25ea0685ab01c..a88167ba0fb6606b2b170e5005c55131f1861972 100644 +--- a/include/tofu.h ++++ b/include/tofu.h +@@ -44,5 +44,6 @@ }; + + void gemini_tofu_init(struct gemini_tofu *tofu, + SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); ++void gemini_tofu_finish(struct gemini_tofu *tofu); + + #endif +diff --git a/src/client.c b/src/client.c +index 2d39f56cb8e45f7b708e5a5dea1ec6fa84e188c9..bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -118,11 +118,14 @@ goto cleanup; + } else { + if (strcmp(scheme, "gemini") != 0) { + res = GEMINI_ERR_NOT_GEMINI; ++ free(scheme); + goto cleanup; + } ++ free(scheme); + } + if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { + res = GEMINI_ERR_INVALID_URL; ++ free(host); + goto cleanup; + } + +@@ -139,6 +142,7 @@ int r; + BIO *sbio = BIO_new(BIO_f_ssl()); + res = gemini_connect(uri, options, resp, &resp->fd); + if (res != GEMINI_OK) { ++ free(host); + goto cleanup; + } + +@@ -146,11 +150,14 @@ resp->ssl = SSL_new(resp->ssl_ctx); + assert(resp->ssl); + SSL_set_connect_state(resp->ssl); + if ((r = SSL_set1_host(resp->ssl, host)) != 1) { ++ free(host); + goto ssl_error; + } + if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { ++ free(host); + goto ssl_error; + } ++ free(host); + if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { + goto ssl_error; + } +@@ -235,15 +242,16 @@ resp->fd = -1; + } + + if (resp->bio) { +- BIO_free(BIO_pop(resp->bio)); // ssl bio +- BIO_free(resp->bio); // buffered bio ++ BIO_free_all(resp->bio); + resp->bio = NULL; + } + + if (resp->ssl) { + SSL_free(resp->ssl); + } +- SSL_CTX_free(resp->ssl_ctx); ++ if (resp->ssl_ctx) { ++ SSL_CTX_free(resp->ssl_ctx); ++ } + free(resp->meta); + + resp->ssl = NULL; +diff --git a/src/gmni.c b/src/gmni.c +index c13e0cd55623f557a8dc676d69aea54661b11faf..4af98fbbe70b09ce5a7fce46863438b0487d8738 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -336,6 +336,8 @@ next: + gemini_response_finish(&resp); + } + ++ SSL_CTX_free(opts.ssl_ctx); + free(url); ++ gemini_tofu_finish(&cfg.tofu); + return ret; + } +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 5d45ffe7372f71ab32461bf71a590cc494269e96..a2717fb84c76fd50a000ceda540af54c60396252 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -83,6 +83,7 @@ if (!history) { + return; + } + history_free(history->next); ++ free(history->url); + free(history); + } + +@@ -92,6 +93,9 @@ { + if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { + fprintf(stderr, "Error: invalid URL\n"); + return false; ++ } ++ if (browser->plain_url != NULL) { ++ free(browser->plain_url); + } + curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); + if (history) { +@@ -130,17 +134,19 @@ + static void + save_bookmark(struct browser *browser) + { +- const char *path_fmt = get_data_pathfmt(); ++ char *path_fmt = get_data_pathfmt(); + static char path[PATH_MAX+1]; + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + if (mkdirs(dirname(path), 0755) != 0) { + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ free(path_fmt); + fprintf(stderr, "Error creating directory %s: %s\n", + dirname(path), strerror(errno)); + return; + } + + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ free(path_fmt); + FILE *f = fopen(path, "a"); + if (!f) { + fprintf(stderr, "Error opening %s for writing: %s\n", +@@ -150,7 +156,7 @@ } + + char *title = browser->page_title; + if (title) { +- title = trim_ws(browser->page_title); ++ title = trim_ws(strdup(browser->page_title)); + } + + fprintf(f, "=> %s%s%s\n", browser->plain_url, +@@ -159,6 +165,9 @@ fclose(f); + + fprintf(browser->tty, "Bookmark saved: %s\n", + title ? title : browser->plain_url); ++ if (title != NULL) { ++ free(title); ++ } + } + + static void +@@ -411,12 +420,14 @@ col += fprintf(out, " "); + } + break; + case GEMINI_PREFORMATTED_BEGIN: ++ gemini_token_finish(&tok); ++ /* fallthrough */ + case GEMINI_PREFORMATTED_END: + continue; // Not used + case GEMINI_PREFORMATTED_TEXT: + col += fprintf(out, "` "); + if (text == NULL) { +- text = tok.text; ++ text = tok.preformatted; + } + break; + case GEMINI_HEADING: +@@ -484,6 +495,9 @@ if (!text[0]) { + text = NULL; + } + } ++ if (text == NULL) { ++ gemini_token_finish(&tok); ++ } + + while (col >= ws.ws_col) { + col -= ws.ws_col; +@@ -510,8 +524,16 @@ case PROMPT_MORE: + break; + case PROMPT_QUIT: + browser->running = false; ++ if (text != NULL) { ++ gemini_token_finish(&tok); ++ } ++ gemini_parser_finish(&p); + return true; + case PROMPT_ANSWERED: ++ if (text != NULL) { ++ gemini_token_finish(&tok); ++ } ++ gemini_parser_finish(&p); + return true; + case PROMPT_NEXT: + searching = true; +@@ -523,6 +545,7 @@ row = col = 0; + } + } + ++ gemini_token_finish(&tok); + gemini_parser_finish(&p); + return false; + } +@@ -617,6 +640,7 @@ CURLUcode uc = curl_url_get(browser->url, + CURLUPART_SCHEME, &scheme, 0); + assert(uc == CURLUE_OK); // Invariant + if (strcmp(scheme, "file") == 0) { ++ free(scheme); + requesting = false; + + char *path; +@@ -630,6 +654,7 @@ + FILE *fp = fopen(path, "r"); + if (!fp) { + resp->status = GEMINI_STATUS_NOT_FOUND; ++ free(path); + break; + } + +@@ -643,9 +668,14 @@ resp->meta = strdup("text/plain"); + } else { + resp->meta = strdup("application/x-octet-stream"); + } ++ free(path); + resp->status = GEMINI_STATUS_SUCCESS; ++ resp->fd = -1; ++ resp->ssl = NULL; ++ resp->ssl_ctx = NULL; + return display_response(browser, resp); + } ++ free(scheme); + + enum gemini_result res = gemini_request(browser->plain_url, + &browser->opts, resp); +@@ -672,7 +702,7 @@ break; + case GEMINI_STATUS_CLASS_REDIRECT: + if (++nredir >= 5) { + requesting = false; +- fprintf(stderr, "Error: maximum redirects (5) exceeded"); ++ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); + break; + } + fprintf(stderr, "Following redirect to %s\n", resp->meta); +@@ -816,6 +846,7 @@ browser.unicode = false; + break; + default: + fprintf(stderr, "fatal: unknown flag %c\n", c); ++ curl_url_cleanup(browser.url); + return 1; + } + } +@@ -841,6 +872,7 @@ while (browser.running) { + static char prompt[4096]; + if (do_requests(&browser, &resp)) { + // Skip prompts ++ gemini_response_finish(&resp); + goto next; + } + +@@ -880,8 +912,16 @@ } + browser.links = NULL; + } + +- history_free(browser.history); ++ gemini_tofu_finish(&browser.tofu); ++ struct history *hist = browser.history; ++ while (hist && hist->prev) { ++ hist = hist->prev; ++ } ++ history_free(hist); + SSL_CTX_free(browser.opts.ssl_ctx); + curl_url_cleanup(browser.url); ++ free(browser.page_title); ++ free(browser.plain_url); ++ fclose(browser.tty); + return 0; + } +diff --git a/src/parser.c b/src/parser.c +index 5b0f01399d934f593cdf987e096dd2cfb134d114..2f78e4641c08b86f5dc50cc9776ea80ec7f9aead 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -35,15 +35,14 @@ { + memset(tok, 0, sizeof(*tok)); + + int eof = 0; +- while (!strstr(p->buf, "\n")) { +- if (p->bufln == p->bufsz) { ++ while (!strchr(p->buf, '\n')) { ++ while (p->bufln >= p->bufsz - 1) { + p->bufsz *= 2; +- char *buf = realloc(p->buf, p->bufsz); +- assert(buf); +- p->buf = buf; ++ p->buf = realloc(p->buf, p->bufsz); ++ assert(p->buf); + } + +- int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); ++ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); + if (n == -1) { + return -1; + } else if (n == 0) { +@@ -55,7 +54,7 @@ p->buf[p->bufln] = 0; + } + + char *end; +- if ((end = strstr(p->buf, "\n")) != NULL) { ++ if ((end = strchr(p->buf, '\n')) != NULL) { + *end = 0; + } + +diff --git a/src/tofu.c b/src/tofu.c +index 354e211c856451bdea120bbb1aed5917099eb285..45e1275d568cfcb4aacfe7f86788b76fff97916b 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -150,7 +150,7 @@ {.var = "GMNIDATA", .path = "/%s"}, + {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, + {.var = "HOME", .path = "/.local/share/gmni/%s"} + }; +- const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); ++ char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); + snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), + path_fmt, "known_hosts"); + +@@ -164,6 +164,7 @@ } + + snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), + path_fmt, "known_hosts"); ++ free(path_fmt); + + tofu->callback = cb; + tofu->cb_data = cb_data; +@@ -175,6 +176,7 @@ return; + } + size_t n = 0; + char *line = NULL; ++ tofu->known_hosts = NULL; + while (getline(&line, &n, f) != -1) { + struct known_host *host = calloc(1, sizeof(struct known_host)); + char *tok = strtok(line, " "); +@@ -184,6 +186,7 @@ + tok = strtok(NULL, " "); + assert(tok); + if (strcmp(tok, "SHA-512") != 0) { ++ free(host->host); + free(host); + continue; + } +@@ -198,5 +201,20 @@ host->expires = strtoul(tok, NULL, 10); + + host->next = tofu->known_hosts; + tofu->known_hosts = host; ++ } ++ free(line); ++ fclose(f); ++} ++ ++void ++gemini_tofu_finish(struct gemini_tofu *tofu) ++{ ++ struct known_host *host = tofu->known_hosts; ++ while (host) { ++ struct known_host *tmp = host; ++ host = host->next; ++ free(tmp->host); ++ free(tmp->fingerprint); ++ free(tmp); + } + } diff --git a/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch b/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch @@ -0,0 +1,24 @@ +diff --git a/src/url.c b/src/url.c +index 47e31b5fcbeeecb5bc3962585311b20e3518bb73..dabf45f234e1e0957007b94ef12ef1125250dfea 100644 +--- a/src/url.c ++++ b/src/url.c +@@ -1361,19 +1361,6 @@ bool free_part = false; + char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ + if(!enc) + return CURLUE_OUT_OF_MEMORY; +- if(plusencode) { +- /* space to plus */ +- i = part; +- for(o = enc; *i; ++o, ++i) +- *o = (*i == ' ') ? '+' : *i; +- *o = 0; /* zero terminate */ +- part = strdup(enc); +- if(!part) { +- free(enc); +- return CURLUE_OUT_OF_MEMORY; +- } +- free_part = true; +- } + for(i = part, o = enc; *i; i++) { + if(Curl_isunreserved(*i) || + ((*i == '/') && urlskipslash) || diff --git a/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch b/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch @@ -0,0 +1,93 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 9efe3e231d445d55ea43d444a72eaa79f595ac76..37b4db277d8fcd961eb1746294724783e638109c 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -87,11 +87,11 @@ "<Enter>\t\tread more lines (if available)\n" + "<url>\t\tgo to url\n" + "[N]\t\tFollow Nth link (where N is a number)\n" + "p[N]\t\tPrint URL of Nth link (where N is a number)\n" +- "e\t\tSend current URL of browser to external default program\n" +- "e[N]\t\tSend URL of Nth link in external default program\n" ++ "e[N]\t\tSend URL of Nth link or current URL when N is ommited to external default program\n" + "t[N]\t\tDownload content of Nth link to a temporary file\n" + "b[N]\t\tJump back N entries in history, N is optional, default 1\n" + "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" ++ "u\t\tone path element up\n" + "H\t\tView all page history\n" + "m\t\tSave bookmark\n" + "M\t\tBrowse bookmarks\n" +@@ -623,6 +623,7 @@ goto exit; + } + in[n - 1] = 0; // Remove LF + ++ char url[1024] = {0}; + int r; + switch (in[0]) { + case '\0': +@@ -664,6 +665,22 @@ } + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; ++ case 'u':; ++ int keep = 0; ++ int len = strlen(browser->plain_url); ++ for (int i=0; i<len; i++) ++ { ++ // ignore trailing / on uri path ++ if (browser->plain_url[i] == '/' && i != len-1) { ++ keep = i; ++ } ++ } ++ if (keep > 9) { ++ strncpy(url , browser->plain_url, keep+1); ++ set_url(browser, url, &browser->history); ++ } ++ result = PROMPT_ANSWERED; ++ goto exit; + case 'H': + if (in[1]) break; + struct history *cur = browser->history; +@@ -723,9 +740,8 @@ case 'p': + case 't': + if (!in[1]) { + if (in[0] == 'e') { +- char xdgopen[4096]; +- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); +- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); ++ snprintf(url, sizeof(url), "xdg-open %s", browser->plain_url); ++ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); + goto exit; + } else { + break; +@@ -743,13 +759,11 @@ fprintf(stderr, "Error: no such link.\n"); + } else { + fprintf(browser->tty, "=> %s\n", link->url); + if (in[0] == 'e') { +- char xdgopen[4096]; +- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); +- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); ++ snprintf(url, sizeof(url), "xdg-open %s", link->url); ++ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); + } + if (in[0] == 't') { + struct gemini_response resp; +- char url[1024] = {0}; + strncpy(&url[0], browser->plain_url, sizeof(link->url)-1); + set_url(browser, link->url, &browser->history); + // XXX: may affect history, do we care? +@@ -785,7 +799,6 @@ goto exit; + case 'd': + if (in[1] != '\0' && !isspace(in[1])) break; + struct gemini_response resp; +- char url[1024] = {0}; + strncpy(&url[0], browser->plain_url, sizeof(url)-1); + // XXX: may affect history, do we care? + enum gemini_result res = do_requests(browser, &resp); +@@ -831,7 +844,6 @@ + if (!link) { + fprintf(stderr, "Error: no such link.\n"); + } else if (endptr[0] == '|') { +- char url[1024] = {0}; + struct gemini_response resp; + strncpy(url, browser->plain_url, sizeof(url) - 1); + set_url(browser, link->url, &browser->history); diff --git a/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch b/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch @@ -0,0 +1,47 @@ +diff --git a/Makefile b/Makefile +index c5bc1ed76a0a61d70c2bfe3506a868f0d7eace5e..9d17d35eb67ee4b86a2d9219c3f1fce98695a521 100644 +--- a/Makefile ++++ b/Makefile +@@ -54,9 +54,8 @@ + distclean: clean + @rm -rf "$(OUTDIR)" + +-install: all ++install: all install_docs + mkdir -p $(BINDIR) +- mkdir -p $(MANDIR)/man1 + install -Dm755 gmni $(BINDIR)/gmni + install -Dm755 gmnlm $(BINDIR)/gmnlm + install -Dm755 libgmni.a $(LIBDIR)/libgmni.a +@@ -64,8 +63,6 @@ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h + install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h + install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h + install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc +- install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 +- install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 + + uninstall: + rm -f $(BINDIR)/gmni +diff --git a/config.sh b/config.sh +index 5bd34bb30dec02422855edbf37817f8276840211..ca5444c73b47daa204d9dd17a3949147385dedec 100644 +--- a/config.sh ++++ b/config.sh +@@ -125,6 +125,10 @@ if scdoc -v >/dev/null 2>&1 + then + echo yes + all="$all docs" ++ install_docs=" ++ mkdir -p \$(MANDIR)/man1 ++ install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 ++ install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" + else + echo no + fi +@@ -148,6 +152,7 @@ CFLAGS+=-DPREFIX='"\$(PREFIX)"' + CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' + + all: ${all} ++ install_docs: ${install_docs} + EOF + + for target in $(printf '%s\n' $all | tr '.' '_') diff --git a/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch b/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch @@ -0,0 +1,32 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index cbc04be66573d4980e24f00761c54d4ac8a1a88f..e80b38b6521ef0eccd5629a1f0b16d2406d8cac1 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -278,8 +278,10 @@ + if (browser->pagination && row >= ws.ws_row - 4) { + char prompt[4096]; + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" +- "(more) => ", resp->meta, browser->plain_url); ++ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit\n" ++ "(more) => ", resp->meta, browser->plain_url, ++ browser->history->prev ? "[b]ack; " : "", ++ browser->history->next ? "[f]orward; " : ""); + enum prompt_result result = PROMPT_AGAIN; + while (result == PROMPT_AGAIN) { + result = do_prompts(prompt, browser); +@@ -485,10 +487,12 @@ goto next; + } + + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" ++ "[N]: follow Nth link; %s%s[q]uit\n" + "=> ", + resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", +- browser.plain_url); ++ browser.plain_url, ++ browser.history->prev ? "[b]ack; " : "", ++ browser.history->next ? "[f]orward; " : ""); + gemini_response_finish(&resp); + + enum prompt_result result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch b/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch @@ -0,0 +1,13 @@ +diff --git a/config.sh b/config.sh +index 8d586cfde54fa3c3bc369ff0156ad5d55a79f383..424cbda346869c7476859d55a600c414c86bbb46 100644 +--- a/config.sh ++++ b/config.sh +@@ -73,7 +73,7 @@ *-Werror*) + werror="-Werror" + ;; + esac +- if $CC $werror "$@" -o /dev/null "$outdir"/check.c >/dev/null 2>&1 ++ if $CC $werror "$@" -o "$outdir"/check "$outdir"/check.c >/dev/null 2>&1 + then + append_cflags "$@" + else diff --git a/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch b/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch @@ -0,0 +1,12 @@ +diff --git a/README.md b/README.md +index e1b424367e49daad233793f663f6340849eb4286..9564df898ce60d9895b818f95ae020c39edb83af 100644 +--- a/README.md ++++ b/README.md +@@ -18,6 +18,7 @@ + It includes the following modifications: + - default 4 char indenting + - e[N] command to open a link in default external program (requires `xdg-open`) ++- t[N] command to download the content behind a link to a temporary file + - colored headings & links + + The actual colors used depend on your terminal palette: diff --git a/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch b/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch @@ -0,0 +1,92 @@ +diff --git a/Makefile b/Makefile +index 0070b703ea0af36743878d2264259bc04edb6b70..d6b576c49053adcc21557a8d88137101deeb5846 100644 +--- a/Makefile ++++ b/Makefile +@@ -4,11 +4,11 @@ OUTDIR=.build + include $(OUTDIR)/config.mk + include $(OUTDIR)/cppcache + +-gmnic: $(gmnic_objects) ++gmni: $(gmni_objects) + @printf 'CCLD\t$@\n' +- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) ++ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) + +-doc/gmnic.1: doc/gmnic.scd ++doc/gmni.1: doc/gmni.scd + + .SUFFIXES: .c .o .scd .1 + +@@ -23,10 +23,10 @@ .scd.1: + @printf 'SCDOC\t$@\n' + @$(SCDOC) < $< > $@ + +-docs: doc/gmnic.1 ++docs: doc/gmni.1 + + clean: +- @rm -f gmnic ++ @rm -f gmni doc/gmni.1 + + distclean: clean + @rm -rf "$(OUTDIR)" +diff --git a/configure b/configure +index 680b57fc9e319c2707e0cc2ded0bc22597544d78..407886859d8ca0b9cbcb085ecef89d004c68cde4 100755 +--- a/configure ++++ b/configure +@@ -3,13 +3,13 @@ srcdir=${SRCDIR:-$(dirname "$0")} + eval ". $srcdir/config.sh" + + gmni() { +- genrules gmnic \ ++ genrules gmni \ + src/client.c \ + src/escape.c \ +- src/gmnic.c \ ++ src/gmni.c \ + src/url.c + } + +-all="gmnic" ++all="gmni" + + run_configure +diff --git a/doc/gmnic.scd b/doc/gmni.scd +rename from doc/gmnic.scd +rename to doc/gmni.scd +index 9eec29cfddf50c2678ef7ae080275fbfbe3fcaa1..0d84d4d974f2c38b426f6ae85d228cdd4847cbda 100644 +--- a/doc/gmnic.scd ++++ b/doc/gmni.scd +@@ -1,16 +1,16 @@ +-gmnic(1) ++gmni(1) + + # NAME + +-gmnic - Gemini client ++gmni - Gemini client + + # SYNPOSIS + +-*gmnic* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ ++*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ + + # DESCRIPTION + +-*gmnic* executes a gemini request and, if successful, prints the response body ++*gmni* executes a gemini request and, if successful, prints the response body + to stdout. + + If an error is returned, information is printed to stderr and the process exits +@@ -45,7 +45,7 @@ accept a password, append ":" to the path and it will be intepreted as + an empty password. + + *-l* +- For *text/\** responses, gmnic normally adds a line feed if stdout is a ++ For *text/\** responses, *gmni* normally adds a line feed if stdout is a + TTY and the response body does not include one. This flag suppresses + this behavior. + +diff --git a/src/gmnic.c b/src/gmni.c +rename from src/gmnic.c +rename to src/gmni.c diff --git a/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch b/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch @@ -0,0 +1,66 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 15256bbd0dcd1c6899e8e91ba5228baf93ce289f..2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -192,6 +192,40 @@ } + } + + static char * ++get_input(const struct gemini_response *resp, FILE *source) ++{ ++ int r = 0; ++ struct termios attrs; ++ bool tty = fileno(source) != -1 && isatty(fileno(source)); ++ char *input = NULL; ++ if (tty) { ++ fprintf(stderr, "%s: ", resp->meta); ++ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { ++ r = tcgetattr(fileno(source), &attrs); ++ struct termios new_attrs; ++ r = tcgetattr(fileno(source), &new_attrs); ++ if (r != -1) { ++ new_attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &new_attrs); ++ } ++ } ++ } ++ size_t s = 0; ++ ssize_t n = getline(&input, &s, source); ++ if (n == -1) { ++ fprintf(stderr, "Error reading input: %s\n", ++ feof(source) ? "EOF" : strerror(ferror(source))); ++ return NULL; ++ } ++ input[n - 1] = '\0'; // Drop LF ++ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { ++ attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &attrs); ++ } ++ return input; ++} ++ ++static char * + do_requests(struct browser *browser, struct gemini_response *resp) + { + char *plain_url; +@@ -210,9 +244,19 @@ requesting = false; + break; + } + ++ char *input; + switch (gemini_response_class(resp->status)) { + case GEMINI_STATUS_CLASS_INPUT: +- assert(0); // TODO ++ input = get_input(resp, browser->tty); ++ if (!input) { ++ requesting = false; ++ break; ++ } ++ ++ char *new_url = gemini_input_url(plain_url, input); ++ assert(new_url); ++ set_url(browser, new_url, NULL); ++ break; + case GEMINI_STATUS_CLASS_REDIRECT: + if (++nredir >= 5) { + requesting = false; diff --git a/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch b/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch @@ -0,0 +1,114 @@ +diff --git a/README.md b/README.md +index d68381569be5736bac9887345b5c053e69393b5a..3cb5f020cab021c3b4ffd7eb98908c76c352e90d 100644 +--- a/README.md ++++ b/README.md +@@ -27,6 +27,8 @@ - default 4 char indenting + - k command to remove the bookmark for the current page + - e[N] command to open a link in default external program (requires `xdg-open`) + - t[N] command to download the content behind a link to a temporary file ++- b & f commands to navigate history can jump multiple entries at once ++- colored headings & links + + The actual colors used depend on your terminal palette: + - heading 1: light red +@@ -39,11 +41,13 @@ - preformatted text: light gray + + Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. + +-## Dependencies: ++## Usage + +-- A POSIX-like system and a C11 compiler +-- OpenSSL +-- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional, build only) ++See `gmni(1)`, `cgmnlm(1)`. ++ ++# Installation ++ ++* ArchLinux and derivates: https://aur.archlinux.org/packages/cgmnlm-git/ + + ## Compiling + +@@ -54,6 +58,9 @@ $ make + # make install + ``` + +-## Usage ++### Dependencies: ++ ++- A POSIX-like system and a C11 compiler ++- OpenSSL ++- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) + +-See `gmni(1)`, `cgmnlm(1)`. +diff --git a/src/gmnlm.c b/src/gmnlm.c +index cc429cdfe6d482e549afee2bd9b05520985e92f9..eaa34c055cbb92564ebd279fcf06189dc577360d 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -84,8 +84,8 @@ "[N]\t\tFollow Nth link (where N is a number)\n" + "p[N]\t\tPrint URL of Nth link (where N is a number)\n" + "e[N]\t\tSend URL of Nth link in external default program\n" + "t[N]\t\tDownload content of Nth link to a temporary file\n" +- "b\t\tBack (in the page history)\n" +- "f\t\tForward (in the page history)\n" ++ "b[N]\t\tJump back N entries in history, N is optional, default 1\n" ++ "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" + "H\t\tView all page history\n" + "m\t\tSave bookmark\n" + "M\t\tBrowse bookmarks\n" +@@ -546,6 +546,8 @@ + struct link *link = browser->links; + char *endptr = NULL; + int linksel = 0; ++ int historyhops = 1; ++ + char *in = NULL; + size_t l = 0; + ssize_t n = getline(&in, &l, browser->tty); +@@ -554,7 +556,7 @@ result = PROMPT_QUIT; + goto exit; + } + in[n - 1] = 0; // Remove LF +- ++ + int r; + switch (in[0]) { + case '\0': +@@ -565,25 +567,24 @@ if (in[1]) break; + result = PROMPT_QUIT; + goto exit; + case 'b': +- if (in[1]) break; +- if (!browser->history->prev) { +- fprintf(stderr, "At beginning of history\n"); +- result = PROMPT_AGAIN; +- goto exit; ++ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); ++ while (historyhops > 0) { ++ if (browser->history->prev) { ++ browser->history = browser->history->prev; ++ } ++ historyhops--; + } +- if (in[1]) break; +- browser->history = browser->history->prev; + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; + case 'f': +- if (in[1]) break; +- if (!browser->history->next) { +- fprintf(stderr, "At end of history\n"); +- result = PROMPT_AGAIN; +- goto exit; ++ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); ++ while (historyhops > 0) { ++ if (browser->history->next) { ++ browser->history = browser->history->next; ++ } ++ historyhops--; + } +- browser->history = browser->history->next; + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; diff --git a/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch b/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch @@ -0,0 +1,23 @@ +diff --git a/src/gmnic.c b/src/gmnic.c +index fd6add8a61546a662a3fe7bb4f3beb9848a19452..2a06a180ebd5478e25b97cacea8468c86d11df00 100644 +--- a/src/gmnic.c ++++ b/src/gmnic.c +@@ -17,7 +17,7 @@ static void + usage(char *argv_0) + { + fprintf(stderr, +- "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", ++ "usage: %s [-46lLiIN] [-C cert] [-d input] [-D path] gemini://...\n", + argv_0); + } + +@@ -209,7 +209,8 @@ w += x; + } + } + if (strncmp(resp.meta, "text/", 5) == 0 +- && linefeed && last != '\n') { ++ && linefeed && last != '\n' ++ && isatty(STDOUT_FILENO)) { + printf("\n"); + } + break; diff --git a/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch b/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch @@ -0,0 +1,12 @@ +diff --git a/README.md b/README.md +index 3cb5f020cab021c3b4ffd7eb98908c76c352e90d..3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043 100644 +--- a/README.md ++++ b/README.md +@@ -28,7 +28,6 @@ - k command to remove the bookmark for the current page + - e[N] command to open a link in default external program (requires `xdg-open`) + - t[N] command to download the content behind a link to a temporary file + - b & f commands to navigate history can jump multiple entries at once +-- colored headings & links + + The actual colors used depend on your terminal palette: + - heading 1: light red diff --git a/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch b/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch @@ -0,0 +1,12 @@ +diff --git a/src/parser.c b/src/parser.c +index eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c..b9db3d2000f1176df4a21300f7b806ed6a5ded75 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -52,7 +52,6 @@ p->bufln += n; + p->buf[p->bufln] = 0; + } + +- // TODO: Collapse multi-line text for the user-agent to wrap + char *end; + if ((end = strstr(p->buf, "\n")) != NULL) { + *end = 0; diff --git a/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch b/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch @@ -0,0 +1,22 @@ +diff --git a/config.sh b/config.sh +index 70c4489bf5fd48fc0eb636bf739a5d82dcff8663..ef533ec79238744458fb2e69b95e3d34333c0e5a 100644 +--- a/config.sh ++++ b/config.sh +@@ -5,7 +5,6 @@ AS=${AS:-as} + CC=${CC:-cc} + CFLAGS=${CFLAGS:-} + LD=${LD:-ld} +-LIBSSL= + + for arg + do +@@ -13,9 +12,6 @@ # TODO: Add args for install directories + case "$arg" in + --prefix=*) + PREFIX=${arg#*=} +- ;; +- --with-libssl=*) +- LIBSSL=${arg#*=} + ;; + esac + done diff --git a/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch b/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch @@ -0,0 +1,26 @@ +diff --git a/README.md b/README.md +index ce71012d402a0c975c73f6d4acd2922f32c16cbc..c269ec775d39d0c26445cfd39358829aae06b575 100644 +--- a/README.md ++++ b/README.md +@@ -5,13 +5,19 @@ + - A CLI utility (like curl): gmni + - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm + +-[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) +- + Dependencies: + + - A POSIX-like system and a C11 compiler + - OpenSSL + - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) ++ ++Features: ++ ++- Page history ++- Regex searches ++- Bookmarks ++ ++[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) + + ## Compiling + diff --git a/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch b/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch @@ -0,0 +1,18 @@ +diff --git a/Makefile b/Makefile +index 12ff4b45e4b043ae5c93629b502e42e8396b2b24..4dc668c893cfeb03020f992bf92c7ae46953d299 100644 +--- a/Makefile ++++ b/Makefile +@@ -6,11 +6,11 @@ include $(OUTDIR)/cppcache + + gmni: $(gmni_objects) + @printf 'CCLD\t$@\n' +- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) ++ $(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) + + gmnlm: $(gmnlm_objects) + @printf 'CCLD\t$@\n' +- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) ++ @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) + + doc/gmni.1: doc/gmni.scd + doc/gmnlm.1: doc/gmnlm.scd diff --git a/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch b/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch @@ -0,0 +1,30 @@ +diff --git a/.gitignore b/.gitignore +index d622c7ceba259e04d4a2e8c2ee4c91de45e13187..66d40426a4b715ec6bceb934219dff612ea2f011 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -1,7 +1,7 @@ + .build + build + /gmni +-gmnlm ++cgmnlm + *.1 + *.o + *.a +diff --git a/cgmnlm b/cgmnlm +deleted file mode 100755 +index a7237df4a6c75847b8bfaee4dc4d1df687975936..0000000000000000000000000000000000000000 +Binary files a/cgmnlm and /dev/null differ +diff --git a/src/cgmnlm.c b/src/cgmnlm.c +index cb0bc10b924eb6afa057d41158815b133fd9372f..60b275d1f8b8425206dd7000a221d60c7473ddff 100644 +--- a/src/cgmnlm.c ++++ b/src/cgmnlm.c +@@ -897,7 +897,7 @@ char *end = NULL; + if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { + *end = 0; + } +- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" ++ snprintf(prompt, sizeof(prompt), "%s at %s\n" + "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" + "(more) => ", resp->meta, browser->plain_url, + browser->searching ? "[n]ext result; " : "", diff --git a/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch b/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch @@ -0,0 +1,30 @@ +diff --git a/src/cgmnlm.c b/src/cgmnlm.c +index 60b275d1f8b8425206dd7000a221d60c7473ddff..38ac821cff06602aba88c5e1c414aeccf3eb1e1f 100644 +--- a/src/cgmnlm.c ++++ b/src/cgmnlm.c +@@ -733,9 +733,7 @@ default: + // skip unicode continuation bytes + if ((s[i] & 0xc0) == 0x80) break; + +- if (iscntrl(s[i])) { +- s[i] = '.'; +- } ++ if (iscntrl(s[i])) s[i] = '.'; + *col += 1; + break; + } +@@ -743,12 +741,10 @@ + if (*col >= ws->ws_col) { + int j = i--; + while (&s[i] != s && !isspace(s[i])) --i; +- if (&s[i] == s) { +- i = j; +- } ++ if (&s[i] == s) i = j; + char c = s[i]; + s[i] = 0; +- int n = fprintf(f, "%s\n", s); ++ int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1); + s[i] = c; + *row += 1; + *col = 0; diff --git a/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch b/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch @@ -0,0 +1,52 @@ +diff --git a/include/gmni.h b/include/gmni.h +index 42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6..4d46380f2ef13db72ebf2e1a3434865142b6bcbc 100644 +--- a/include/gmni.h ++++ b/include/gmni.h +@@ -8,6 +8,7 @@ enum gemini_result { + GEMINI_OK, + GEMINI_ERR_OOM, + GEMINI_ERR_INVALID_URL, ++ GEMINI_ERR_NOT_GEMINI, + GEMINI_ERR_RESOLVE, + GEMINI_ERR_CONNECT, + GEMINI_ERR_SSL, +diff --git a/src/client.c b/src/client.c +index f1674d534a9a5f2edcb6c7be678e17ad5724a9a7..34d25f3794f8069b15a688ebd89b7bf82fdd01e2 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -104,11 +104,24 @@ struct Curl_URL *uri = curl_url(); + if (!uri) { + return GEMINI_ERR_OOM; + } ++ ++ enum gemini_result res = GEMINI_OK; + if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { +- return GEMINI_ERR_INVALID_URL; ++ res = GEMINI_ERR_INVALID_URL; ++ goto cleanup; ++ } ++ ++ char *scheme; ++ if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { ++ res = GEMINI_ERR_INVALID_URL; ++ goto cleanup; ++ } else { ++ if (strcmp(scheme, "gemini") != 0) { ++ res = GEMINI_ERR_NOT_GEMINI; ++ goto cleanup; ++ } + } + +- enum gemini_result res = GEMINI_OK; + if (options && options->ssl_ctx) { + resp->ssl_ctx = options->ssl_ctx; + SSL_CTX_up_ref(options->ssl_ctx); +@@ -226,6 +239,8 @@ case GEMINI_ERR_OOM: + return "Out of memory"; + case GEMINI_ERR_INVALID_URL: + return "Invalid URL"; ++ case GEMINI_ERR_NOT_GEMINI: ++ return "Not a gemini URL"; + case GEMINI_ERR_RESOLVE: + return gai_strerror(resp->status); + case GEMINI_ERR_CONNECT: diff --git a/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch b/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch @@ -0,0 +1,141 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index dd8c8d200a949a24d030ce3429a4d20f1430fe97..94e9933e514438c633663ae8871d4fba3e64c349 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -17,15 +17,42 @@ char *url; + struct link *next; + }; + ++struct history { ++ char *url; ++ struct history *prev, *next; ++}; ++ + static void + usage(const char *argv_0) + { + fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); ++} ++ ++static void ++history_free(struct history *history) ++{ ++ if (!history) { ++ return; ++ } ++ history_free(history->next); ++ free(history); + } + + static bool +-set_url(struct Curl_URL *url, char *new_url) ++set_url(struct Curl_URL *url, char *new_url, struct history **history) + { ++ if (history) { ++ struct history *next = calloc(1, sizeof(struct history)); ++ next->url = strdup(new_url); ++ next->prev = *history; ++ if (*history) { ++ if ((*history)->next) { ++ history_free((*history)->next); ++ } ++ (*history)->next = next; ++ } ++ *history = next; ++ } + if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { + fprintf(stderr, "Error: invalid URL\n"); + return false; +@@ -138,8 +165,9 @@ return 1; + } + } + ++ struct history *history; + if (optind == argc - 1) { +- set_url(url, argv[optind]); ++ set_url(url, argv[optind], &history); + } else { + usage(argv[0]); + return 1; +@@ -161,9 +189,10 @@ char *plain_url; + CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); + assert(uc == CURLUE_OK); // Invariant + +- snprintf(prompt, sizeof(prompt), "\n\t%s\n" +- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " +- "[b]ack; [f]orward\n" ++ snprintf(prompt, sizeof(prompt), "\nat %s\n" ++ "[n]: follow Nth link; [o <url>]: open URL; " ++ "[b]ack; [f]orward; " ++ "[q]uit\n" + "=> ", plain_url); + + enum gemini_result res = gemini_request(plain_url, &opts, &resp); +@@ -201,11 +230,29 @@ size_t l = 0; + char *in = NULL; + ssize_t n = getline(&in, &l, tty); + if (n == -1 && feof(tty)) { +- prompting = run = false; ++ run = false; + break; + } + if (strcmp(in, "q\n") == 0) { +- prompting = run = false; ++ run = false; ++ break; ++ } ++ if (strcmp(in, "b\n") == 0) { ++ if (!history->prev) { ++ fprintf(stderr, "At beginning of history\n"); ++ continue; ++ } ++ history = history->prev; ++ set_url(url, history->url, NULL); ++ break; ++ } ++ if (strcmp(in, "f\n") == 0) { ++ if (!history->next) { ++ fprintf(stderr, "At end of history\n"); ++ continue; ++ } ++ history = history->next; ++ set_url(url, history->url, NULL); + break; + } + +@@ -221,23 +268,24 @@ + if (!link) { + fprintf(stderr, "Error: no such link.\n"); + } else { +- prompting = false; +- set_url(url, link->url); ++ set_url(url, link->url, &history); ++ break; + } + } ++ free(in); ++ } + +- link = links; +- while (link) { +- struct link *next = link->next; +- free(link->url); +- free(link); +- link = next; +- } +- +- free(in); ++ struct link *link = links; ++ while (link) { ++ struct link *next = link->next; ++ free(link->url); ++ free(link); ++ link = next; + } ++ links = NULL; + } + ++ history_free(history); + SSL_CTX_free(opts.ssl_ctx); + curl_url_cleanup(url); + return 0; diff --git a/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch b/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch @@ -0,0 +1,16 @@ +diff --git a/Makefile b/Makefile +index d6b576c49053adcc21557a8d88137101deeb5846..69a241a8825a6a7aa979eb2ae95a26faaf3a0532 100644 +--- a/Makefile ++++ b/Makefile +@@ -31,4 +31,10 @@ + distclean: clean + @rm -rf "$(OUTDIR)" + +-.PHONY: clean distclean docs ++install: all ++ mkdir -p $(BINDIR) ++ mkdir -p $(MANDIR)/man1 ++ install -Dm755 gmni $(BINDIR)/gmni ++ install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 ++ ++.PHONY: clean distclean docs install diff --git a/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch b/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch @@ -0,0 +1,21 @@ +diff --git a/src/tofu.c b/src/tofu.c +index e8efeaf69fcb9bd890711f76959e86efa75cfec6..354e211c856451bdea120bbb1aed5917099eb285 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -31,6 +31,7 @@ // + // TODO: Check that the subject name is valid for the requested URL. + struct gemini_tofu *tofu = (struct gemini_tofu *)data; + X509 *cert = X509_STORE_CTX_get0_cert(ctx); ++ struct known_host *host = NULL; + + int rc; + int day, sec; +@@ -77,7 +78,7 @@ time_t now; + time(&now); + + enum tofu_error error = TOFU_UNTRUSTED_CERT; +- struct known_host *host = tofu->known_hosts; ++ host = tofu->known_hosts; + while (host) { + if (host->expires < now) { + goto next; diff --git a/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch b/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5..c5b778095021a7592fce5c7e4a83c70be822790e 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -246,7 +246,7 @@ fclose(fi); + fclose(fo); + free(line); + if ( rename(tempfile, path) != 0) { +- fprintf(browser->tty, "Failed to udpate bookmarks: %s\n", strerror(errno)); ++ fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno)); + } + } + diff --git a/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch b/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch @@ -0,0 +1,183 @@ +diff --git a/Makefile b/Makefile +index 5e468e26f12fef2c066826e4e5662e51ca4dde42..00e31d3fcee294d181c736b75e8e2bc36bdc7053 100644 +--- a/Makefile ++++ b/Makefile +@@ -9,16 +9,16 @@ gmni: $(gmni_objects) + @printf 'CCLD\t$@\n' + @$(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) + +-gmnlm: $(gmnlm_objects) ++cgmnlm: $(cgmnlm_objects) + @printf 'CCLD\t$@\n' +- @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) ++ @$(CC) $(LDFLAGS) -o $@ $(cgmnlm_objects) $(LIBS) + + libgmni.a: $(libgmni.a_objects) + @printf 'AR\t$@\n' + @$(AR) -rcs $@ $(libgmni.a_objects) + + doc/gmni.1: doc/gmni.scd +-doc/gmnlm.1: doc/gmnlm.scd ++doc/cgmnlm.1: doc/cgmnlm.scd + + libgmni.pc: + @printf 'GEN\t$@\n' +@@ -46,10 +46,10 @@ .scd.1: + @printf 'SCDOC\t$@\n' + @$(SCDOC) < $< > $@ + +-docs: doc/gmni.1 doc/gmnlm.1 ++docs: doc/gmni.1 doc/cgmnlm.1 + + clean: +- @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) ++ @rm -f gmni cgmnlm libgmni.a libgmni.pc doc/gmni.1 doc/cgmnlm.1 $(cgmnlm_objects) $(gmni_objects) + + distclean: clean + @rm -rf "$(OUTDIR)" +@@ -60,7 +60,7 @@ mkdir -p $(LIBDIR) + mkdir -p $(INCLUDEDIR)/gmni + mkdir -p $(LIBDIR)/pkgconfig + install -m755 gmni $(BINDIR)/gmni +- install -m755 gmnlm $(BINDIR)/gmnlm ++ install -m755 cgmnlm $(BINDIR)/cgmnlm + install -m755 libgmni.a $(LIBDIR)/libgmni.a + install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h + install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h +@@ -69,11 +69,11 @@ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc + + uninstall: + rm -f $(BINDIR)/gmni +- rm -f $(BINDIR)/gmnlm ++ rm -f $(BINDIR)/cgmnlm + rm -f $(LIBDIR)/libgmni.a + rm -rf $(INCLUDEDIR)/gmni + rm -f $(LIBDIR)/pkgconfig/libgmni.pc + rm -f $(MANDIR)/man1/gmni.1 +- rm -f $(MANDIR)/man1/gmnlm.1 ++ rm -f $(MANDIR)/man1/cgmnlm.1 + + .PHONY: clean distclean docs install +diff --git a/README.md b/README.md +index c269ec775d39d0c26445cfd39358829aae06b575..9e1c9831127d8395d3fa8173f4ecb775c27e31b1 100644 +--- a/README.md ++++ b/README.md +@@ -1,23 +1,29 @@ +-# gmni - A Gemini client ++# cgmnlm - A colorful Gemini line mode client + + This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: + + - A CLI utility (like curl): gmni +-- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm +- +-Dependencies: +- +-- A POSIX-like system and a C11 compiler +-- OpenSSL +-- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) ++- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): cgmnlm + +-Features: ++## Features: + + - Page history + - Regex searches + - Bookmarks + +-[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) ++### Modifications compared to upstream ++ ++This project is of fork of https://git.sr.ht/~sircmpwn/gmni ++ ++It includes the following modifications: ++- default 4 char indenting ++- colored headings & links ++ ++## Dependencies: ++ ++- A POSIX-like system and a C11 compiler ++- OpenSSL ++- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) + + ## Compiling + +@@ -30,4 +36,4 @@ ``` + + ## Usage + +-See `gmni(1)`, `gmnlm(1)`. ++See `gmni(1)`, `cgmnlm(1)`. +diff --git a/cgmnlm b/cgmnlm +new file mode 100755 +index 0000000000000000000000000000000000000000..5c05cd3ecca44b577a75544d7e2119421a5da102 +Binary files /dev/null and b/cgmnlm differ +diff --git a/config.sh b/config.sh +index fd6a325bb21807ed6b6dc871b64479ebfe89a68d..8d586cfde54fa3c3bc369ff0156ad5d55a79f383 100644 +--- a/config.sh ++++ b/config.sh +@@ -128,7 +128,7 @@ all="$all docs" + install_docs=" + mkdir -p \$(MANDIR)/man1 + install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 +- install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" ++ install -m644 doc/cgmnlm.1 \$(MANDIR)/man1/cgmnlm.1" + else + echo no + fi +diff --git a/configure b/configure +index e82a0e27e27c9873c8dac92fd3385e65bf511648..aa761971fe95d70ed242977bed8dfd6d55a9f794 100755 +--- a/configure ++++ b/configure +@@ -12,11 +12,11 @@ src/url.c \ + src/util.c + } + +-gmnlm() { +- genrules gmnlm \ ++cgmnlm() { ++ genrules cgmnlm \ + src/client.c \ + src/escape.c \ +- src/gmnlm.c \ ++ src/cgmnlm.c \ + src/parser.c \ + src/tofu.c \ + src/url.c \ +@@ -37,7 +37,7 @@ libgmni_pc() { + : + } + +-all="gmni gmnlm libgmni.a libgmni.pc" ++all="gmni cgmnlm libgmni.a libgmni.pc" + + + run_configure +diff --git a/doc/gmnlm.scd b/doc/cgmnlm.scd +rename from doc/gmnlm.scd +rename to doc/cgmnlm.scd +index e0a368f8921253c76219790d35380848e486720d..1ed6e620cb39e04ade8b98fa9bab7e7c0bacc5e9 100644 +--- a/doc/gmnlm.scd ++++ b/doc/cgmnlm.scd +@@ -2,15 +2,15 @@ gmnlm(1) + + # NAME + +-gmnlm - Gemini line-mode browser ++cgmnlm - colored Gemini line-mode browser + + # SYNPOSIS + +-*gmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ ++*cgmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ + + # DESCRIPTION + +-*gmnlm* is an interactive line-mode Gemini browser. ++*cgmnlm* is an interactive line-mode Gemini browser. + + # OPTIONS + +diff --git a/src/gmnlm.c b/src/cgmnlm.c +rename from src/gmnlm.c +rename to src/cgmnlm.c diff --git a/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch b/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch @@ -0,0 +1,43 @@ +diff --git a/include/util.h b/include/util.h +index 8ec9ac5d94092a75813a3e083140fbb88079c29d..6c241f41a175c3d27390ecb6ca5b041bd637cb2b 100644 +--- a/include/util.h ++++ b/include/util.h +@@ -9,6 +9,7 @@ const char *path; + }; + + char *getpath(const struct pathspec *paths, size_t npaths); ++void posix_dirname(char *path, char *dname); + int mkdirs(char *path, mode_t mode); + int download_resp(FILE *out, struct gemini_response resp, const char *path, + char *url); +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 12a6c971b7ae7d18bcdafc3b2b81f55539340095..0ab21849e186a99c84d0f94c59ef6c821e91b97d 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -562,6 +562,13 @@ assert(host); + n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); + assert(n < sizeof(certpath)); + n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); ++ char dname[PATH_MAX + 1]; ++ posix_dirname(certpath, dname); ++ if (mkdirs(dname, 0755) != 0) { ++ fprintf(stderr, "Error creating directory %s: %s\n", ++ dname, strerror(errno)); ++ break; ++ } + assert(n < sizeof(keypath)); + fprintf(stderr, "The server requested a client certificate.\n" + "Presently, this process is not automated.\n" +diff --git a/src/util.c b/src/util.c +index 1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68..8441b584ff199ffd81472539a59acce5d0a18f42 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -11,7 +11,7 @@ #include <string.h> + #include <sys/stat.h> + #include "util.h" + +-static void ++void + posix_dirname(char *path, char *dname) + { + char p[PATH_MAX+1]; diff --git a/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch b/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch @@ -0,0 +1,18 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 0546486340b7299be3c97266dd534a1354bf31a3..d05180c7a74c88bd7ed8ba551a4a522bb8b74009 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -759,6 +759,13 @@ + FILE *fp = fopen(path, "r"); + if (!fp) { + 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; ++ resp->meta = NULL; ++ resp->fd = -1; + free(path); + break; + } diff --git a/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch b/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch @@ -0,0 +1,16 @@ +diff --git a/config.sh b/config.sh +index 55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe..5bd34bb30dec02422855edbf37817f8276840211 100644 +--- a/config.sh ++++ b/config.sh +@@ -85,6 +85,11 @@ find_library() { + name="$1" + pc="$2" + printf "Checking for %s... " "$name" ++ if ! command -v pkg-config >/dev/null ++ then ++ printf "ERROR: pkg-config not found\n" ++ return 1 ++ fi + if ! pkg-config "$pc" 2>/dev/null + then + printf "NOT FOUND\n" diff --git a/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch b/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch @@ -0,0 +1,30 @@ +diff --git a/README.md b/README.md +index 9aa989fe358ecccd9b984b1aff0df97c6c2ceec0..4b03a4782f0b97d8687b3360f20d9c8748f88ca3 100644 +--- a/README.md ++++ b/README.md +@@ -14,7 +14,7 @@ - basic Client Certificate support (no autocreation of client certs currently) + + ## Non-Features: + +-- no inlinig of any link type ++- no inlining of any link type + - no caching of page content + - no persistent history across sessions + +@@ -25,11 +25,11 @@ + It includes the following modifications: + - colored headings & links + - default 4 char indenting +-- s command to directly search in geminispace (via geminispace.info) +-- k command to remove the bookmark for the current page +-- e[N] command to open a link in default external program (requires `xdg-open`) +-- t[N] command to download the content behind a link to a temporary file +-- a command to switch between display of preformatted blocks and alt text (if available) ++- `s` command to directly search in geminispace (via geminispace.info) ++- `k` command to remove the bookmark for the current page ++- `e[N]` command to open a link in default external program (requires `xdg-open`) ++- `t[N]` command to download the content behind a link to a temporary file ++- `a` command to switch between display of preformatted blocks and alt text (if available) + + The actual colors used depend on your terminal palette: + - heading 1: light red diff --git a/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch b/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch @@ -0,0 +1,131 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 391d2b4a2a2dddd7e688a950bb852dfa0687cc32..cbc04be66573d4980e24f00761c54d4ac8a1a88f 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -139,10 +139,7 @@ + static char * + trim_ws(char *in) + { +- for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { +- in[i] = 0; +- } +- for (; *in && isspace(*in); ++in); ++ while (*in && isspace(*in)) ++in; + return in; + } + +@@ -203,61 +200,80 @@ struct link **next = &browser->links; + while (text != NULL || gemini_parser_next(&p, &tok) == 0) { + switch (tok.token) { + case GEMINI_TEXT: +- // TODO: Run other stuff through wrap() ++ col += fprintf(browser->tty, " "); + if (text == NULL) { + text = tok.text; + } +- +- do { ++ break; ++ case GEMINI_LINK: ++ if (text == NULL) { ++ col += fprintf(browser->tty, "%d) ", nlinks++); ++ text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); ++ *next = calloc(1, sizeof(struct link)); ++ (*next)->url = strdup(trim_ws(tok.link.url)); ++ next = &(*next)->next; ++ } else { + col += fprintf(browser->tty, " "); +- int w = wrap(browser->tty, text, &ws, &row, &col); +- text += w; +- if (row >= ws.ws_row - 4) { +- break; +- } +- } while (text[0]); +- +- if (!text[0]) { +- text = NULL; + } + break; +- case GEMINI_LINK: +- col += fprintf(browser->tty, "%d) %s\n", nlinks++, +- trim_ws(tok.link.text ? tok.link.text : tok.link.url)); +- *next = calloc(1, sizeof(struct link)); +- (*next)->url = strdup(trim_ws(tok.link.url)); +- next = &(*next)->next; +- break; + case GEMINI_PREFORMATTED: + continue; // TODO + case GEMINI_HEADING: +- for (int n = tok.heading.level; n; --n) { +- col += fprintf(browser->tty, "#"); +- } +- for (int n = 3 - tok.heading.level; n > 1; --n) { +- col += fprintf(browser->tty, " "); ++ if (text == NULL) { ++ for (int n = tok.heading.level; n; --n) { ++ col += fprintf(browser->tty, "#"); ++ } ++ switch (tok.heading.level) { ++ case 1: ++ col += fprintf(browser->tty, " "); ++ break; ++ case 2: ++ case 3: ++ col += fprintf(browser->tty, " "); ++ break; ++ } ++ text = trim_ws(tok.heading.title); ++ } else { ++ col += fprintf(browser->tty, " "); + } +- col += fprintf(browser->tty, " %s\n", +- trim_ws(tok.heading.title)); + break; + case GEMINI_LIST_ITEM: +- col += fprintf(browser->tty, " %s %s\n", +- browser->unicode ? "•" : "*", +- trim_ws(tok.list_item)); ++ if (text == NULL) { ++ col += fprintf(browser->tty, " %s ", ++ browser->unicode ? "•" : "*"); ++ text = trim_ws(tok.list_item); ++ } else { ++ col += fprintf(browser->tty, " "); ++ } + break; + case GEMINI_QUOTE: +- col += fprintf(browser->tty, " %s %s\n", +- browser->unicode ? "|" : "|", +- trim_ws(tok.quote_text)); ++ if (text == NULL) { ++ col += fprintf(browser->tty, " %s ", ++ browser->unicode ? "|" : "|"); ++ text = trim_ws(tok.quote_text); ++ } else { ++ col += fprintf(browser->tty, " "); ++ } + break; + } + ++ if (text) { ++ int w = wrap(browser->tty, text, &ws, &row, &col); ++ text += w; ++ if (text[0] && row < ws.ws_row - 4) { ++ continue; ++ } ++ ++ if (!text[0]) { ++ text = NULL; ++ } ++ } ++ + while (col >= ws.ws_col) { + col -= ws.ws_col; + ++row; + } +- ++row; +- col = 0; ++ ++row; col = 0; + + if (browser->pagination && row >= ws.ws_row - 4) { + char prompt[4096]; diff --git a/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch b/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch @@ -0,0 +1,36 @@ +diff --git a/README.md b/README.md +index 3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043..aa9f811a9bfd9cfb7b6654954e33fc62e868f8c4 100644 +--- a/README.md ++++ b/README.md +@@ -24,6 +24,7 @@ + It includes the following modifications: + - colored headings & links + - default 4 char indenting ++- s command to directly search in geminispace (via geminispace.info) + - k command to remove the bookmark for the current page + - e[N] command to open a link in default external program (requires `xdg-open`) + - t[N] command to download the content behind a link to a temporary file +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 2c35f76f8ee4b6dfa6af5bbe8af790a3e98afbbc..e28bf70afc5d6fd4c5c6eff85d85198d62a127e8 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -93,6 +93,7 @@ "m\t\tSave bookmark\n" + "M\t\tBrowse bookmarks\n" + "k\t\tRemove bookmark for current page\n" + "r\t\tReload the page\n" ++ "s\t\tSearch via geminispace.info\n" + "/<text>\t\tsearch for text (POSIX regular expression)\n" + "n\t\tjump to next search match\n" + "d <path>\tDownload page to <path>\n" +@@ -567,6 +568,11 @@ goto exit; + case 'q': + if (in[1]) break; + result = PROMPT_QUIT; ++ goto exit; ++ case 's': ++ if (in[1]) break; ++ set_url(browser, "gemini://geminispace.info/search", &browser->history); ++ result = PROMPT_ANSWERED; + goto exit; + case 'b': + if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch b/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch @@ -0,0 +1,248 @@ +diff --git a/configure b/configure +index 407886859d8ca0b9cbcb085ecef89d004c68cde4..aca6e8a1eaa9a6271f03eb8863640d0d93cdf435 100755 +--- a/configure ++++ b/configure +@@ -7,6 +7,7 @@ genrules gmni \ + src/client.c \ + src/escape.c \ + src/gmni.c \ ++ src/parser.c \ + src/url.c + } + +diff --git a/include/gmni.h b/include/gmni.h +index 4d46380f2ef13db72ebf2e1a3434865142b6bcbc..d78d1c8eb4e94507c363219573691e60d40e8ea8 100644 +--- a/include/gmni.h ++++ b/include/gmni.h +@@ -103,4 +103,64 @@ // Returns the general response class (i.e. with the second digit set to zero) + // of the given Gemini status code. + enum gemini_status_class gemini_response_class(enum gemini_status status); + ++enum gemini_tok { ++ GEMINI_TEXT, ++ GEMINI_LINK, ++ GEMINI_PREFORMATTED, ++ GEMINI_HEADING, ++ GEMINI_LIST_ITEM, ++ GEMINI_QUOTE, ++}; ++ ++struct gemini_token { ++ enum gemini_tok token; ++ ++ // The token field determines which of the union members is valid. ++ union { ++ char *text; ++ ++ struct { ++ char *text; ++ char *url; // May be NULL ++ } link; ++ ++ struct { ++ char *text; ++ char *alt_text; // May be NULL ++ } preformatted; ++ ++ struct { ++ char *title; ++ int level; // 1, 2, or 3 ++ } heading; ++ ++ char *list_item; ++ char *quote_text; ++ }; ++}; ++ ++struct gemini_parser { ++ BIO *f; ++ char *buf; ++ size_t bufsz; ++ size_t bufln; ++}; ++ ++// Initializes a text/gemini parser which reads from the specified BIO. ++void gemini_parser_init(struct gemini_parser *p, BIO *f); ++ ++// Finishes this text/gemini parser and frees up its resources. ++void gemini_parser_finish(struct gemini_parser *p); ++ ++// Reads the next token from a text/gemini file. ++// ++// Returns 0 on success, 1 on EOF, and -1 on failure. ++// ++// Caller must call gemini_token_finish before exiting or re-using the token ++// parameter. ++int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token); ++ ++// Must be called after gemini_next to free up resources for the next token. ++void gemini_token_finish(struct gemini_token *token); ++ + #endif +diff --git a/src/gmni.c b/src/gmni.c +index 6e27b2f859c692f37fb483184cda245a7e979cbe..75c6c5afb6e7f1f286d314d93f2b44ef8414afb3 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -233,9 +233,11 @@ case SHOW_HEADERS: + printf("%d %s\n", resp.status, resp.meta); + /* fallthrough */ + case OMIT_HEADERS: +- if (resp.status / 10 != 2) { ++ if (gemini_response_class(resp.status) != ++ GEMINI_STATUS_CLASS_SUCCESS) { + break; + } ++ + char last; + char buf[BUFSIZ]; + for (int n = 1; n > 0;) { +diff --git a/src/parser.c b/src/parser.c +new file mode 100644 +index 0000000000000000000000000000000000000000..ffcc28767be7d638d18609764999d004790aa9a2 +--- /dev/null ++++ b/src/parser.c +@@ -0,0 +1,144 @@ ++#include <assert.h> ++#include <ctype.h> ++#include <openssl/bio.h> ++#include <stddef.h> ++#include <stdlib.h> ++#include <string.h> ++#include "gmni.h" ++ ++void ++gemini_parser_init(struct gemini_parser *p, BIO *f) ++{ ++ p->f = f; ++ p->bufln = 0; ++ p->bufsz = BUFSIZ; ++ p->buf = malloc(p->bufsz + 1); ++ p->buf[0] = 0; ++ BIO_up_ref(p->f); ++} ++ ++void ++gemini_parser_finish(struct gemini_parser *p) ++{ ++ if (!p) { ++ return; ++ } ++ BIO_free(p->f); ++ free(p->buf); ++} ++ ++int ++gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok) ++{ ++ memset(tok, 0, sizeof(*tok)); ++ ++ int eof = 0; ++ while (!strstr(p->buf, "\n")) { ++ if (p->bufln == p->bufsz) { ++ p->bufsz *= 2; ++ char *buf = realloc(p->buf, p->bufsz); ++ assert(buf); ++ p->buf = buf; ++ } ++ ++ int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); ++ if (n == -1) { ++ return -1; ++ } else if (n == 0) { ++ eof = 1; ++ break; ++ } ++ p->bufln += n; ++ p->buf[p->bufln] = 0; ++ } ++ ++ // TODO: Collapse multi-line text for the user-agent to wrap ++ char *end; ++ if ((end = strstr(p->buf, "\n")) != NULL) { ++ *end = 0; ++ } ++ ++ // TODO: Provide whitespace trimming helper function ++ if (strncmp(p->buf, "=>", 2) == 0) { ++ tok->token = GEMINI_LINK; ++ int i = 2; ++ while (p->buf[i] && isspace(p->buf[i])) ++i; ++ tok->link.url = &p->buf[i]; ++ ++ for (; p->buf[i]; ++i) { ++ if (isspace(p->buf[i])) { ++ p->buf[i++] = 0; ++ while (isspace(p->buf[i])) ++i; ++ if (p->buf[i]) { ++ tok->link.text = strdup(&p->buf[i]); ++ } ++ break; ++ } ++ } ++ ++ tok->link.url = strdup(tok->link.url); ++ } else if (strncmp(p->buf, "```", 3) == 0) { ++ tok->token = GEMINI_PREFORMATTED; // TODO ++ tok->preformatted.text = strdup("<text>"); ++ tok->preformatted.alt_text = strdup("<alt-text>"); ++ } else if (p->buf[0] == '#') { ++ tok->token = GEMINI_HEADING; ++ int level = 1; ++ while (p->buf[level] == '#' && level < 3) { ++ ++level; ++ } ++ tok->heading.level = level; ++ tok->heading.title = strdup(&p->buf[level]); ++ } else if (p->buf[0] == '*') { ++ tok->token = GEMINI_LIST_ITEM; ++ tok->list_item = strdup(&p->buf[1]); ++ } else if (p->buf[0] == '>') { ++ tok->token = GEMINI_QUOTE; ++ tok->quote_text = strdup(&p->buf[1]); ++ } else { ++ tok->token = GEMINI_TEXT; ++ tok->text = strdup(p->buf); ++ } ++ ++ if (end && end + 1 < p->buf + p->bufln) { ++ size_t len = end - p->buf + 1; ++ memmove(p->buf, end + 1, p->bufln - len); ++ p->bufln -= len; ++ } else { ++ p->buf[0] = 0; ++ p->bufln = 0; ++ } ++ ++ return eof; ++} ++ ++void ++gemini_token_finish(struct gemini_token *tok) ++{ ++ if (!tok) { ++ return; ++ } ++ ++ switch (tok->token) { ++ case GEMINI_TEXT: ++ free(tok->text); ++ break; ++ case GEMINI_LINK: ++ free(tok->link.text); ++ free(tok->link.url); ++ break; ++ case GEMINI_PREFORMATTED: ++ free(tok->preformatted.text); ++ free(tok->preformatted.alt_text); ++ break; ++ case GEMINI_HEADING: ++ free(tok->heading.title); ++ break; ++ case GEMINI_LIST_ITEM: ++ free(tok->list_item); ++ break; ++ case GEMINI_QUOTE: ++ free(tok->quote_text); ++ break; ++ } ++} diff --git a/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch b/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmni.c b/src/gmni.c +index 000a56550a686928f84e2c301c9017a8c48b3d68..61f41e29c324d980bd3a1f270e78649cdcec88e0 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -315,7 +315,7 @@ ret = download_resp(stderr, resp, output_file, url); + break; + } + +- char last; ++ char last = 0; + char buf[BUFSIZ]; + for (int n = 1; n > 0;) { + n = BIO_read(resp.bio, buf, BUFSIZ); diff --git a/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch b/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch @@ -0,0 +1,12 @@ +diff --git a/src/tofu.c b/src/tofu.c +index 50a295870876265849eae7fdf0de926a3ad309fd..5b34850de9e17dcd59fc64a3227cd92bfb0a6cef 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -88,7 +88,6 @@ cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); + } + + enum tofu_error error = TOFU_UNTRUSTED_CERT; +- (void)error; + struct known_host *host = cc->store->known_hosts; + while (host) { + if (strcmp(host->host, cc->server_name) != 0) { diff --git a/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch b/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch @@ -0,0 +1,43 @@ +diff --git a/Makefile b/Makefile +index 9d17d35eb67ee4b86a2d9219c3f1fce98695a521..5e468e26f12fef2c066826e4e5662e51ca4dde42 100644 +--- a/Makefile ++++ b/Makefile +@@ -56,13 +56,16 @@ @rm -rf "$(OUTDIR)" + + install: all install_docs + mkdir -p $(BINDIR) +- install -Dm755 gmni $(BINDIR)/gmni +- install -Dm755 gmnlm $(BINDIR)/gmnlm +- install -Dm755 libgmni.a $(LIBDIR)/libgmni.a +- install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h +- install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h +- install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h +- install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc ++ mkdir -p $(LIBDIR) ++ mkdir -p $(INCLUDEDIR)/gmni ++ mkdir -p $(LIBDIR)/pkgconfig ++ install -m755 gmni $(BINDIR)/gmni ++ install -m755 gmnlm $(BINDIR)/gmnlm ++ install -m755 libgmni.a $(LIBDIR)/libgmni.a ++ install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h ++ install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h ++ install -m644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h ++ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc + + uninstall: + rm -f $(BINDIR)/gmni +diff --git a/config.sh b/config.sh +index ca5444c73b47daa204d9dd17a3949147385dedec..fd6a325bb21807ed6b6dc871b64479ebfe89a68d 100644 +--- a/config.sh ++++ b/config.sh +@@ -127,8 +127,8 @@ echo yes + all="$all docs" + install_docs=" + mkdir -p \$(MANDIR)/man1 +- install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 +- install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" ++ install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 ++ install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" + else + echo no + fi diff --git a/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch b/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch @@ -0,0 +1,12 @@ +diff --git a/.gitignore b/.gitignore +index 7b35cd8ef5366f89f8db24b1f62f0a1f2c3e336d..6ec8d5188d32b997f3a91f8a11f64f4e79b43770 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -1,5 +1,6 @@ + .build + build +-gmnic ++gmni ++gmnlm + *.1 + *.o diff --git a/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch b/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmni.c b/src/gmni.c +index a61ee7bcb3ce77da24b1db97d379c2003c61d055..171e1407ba2dfa3341f0693ae8d60bb9cedb6564 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -85,7 +85,7 @@ fprintf(stderr, + "The certificate offered by this server is of unknown trust. " + "Its fingerprint is: \n" + "%s\n\n" +- "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); ++ "Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint); + break; + case TOFU_FINGERPRINT_MISMATCH: + fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch b/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch @@ -0,0 +1,59 @@ +diff --git a/src/gmnic.c b/src/gmnic.c +index 794b94ba2235757864acb81194b8de88220cc6da..60d5c68530fffacbeea62c1a8b78497857e02a02 100644 +--- a/src/gmnic.c ++++ b/src/gmnic.c +@@ -34,9 +34,10 @@ INPUT_SUPPRESS, + }; + enum input_mode input_mode = INPUT_READ; + FILE *input_source = stdin; ++ bool linefeed = true; + + int c; +- while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { ++ while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { + switch (c) { + case '4': + assert(0); // TODO +@@ -67,6 +68,9 @@ break; + case 'h': + usage(argv[0]); + return 0; ++ case 'l': ++ linefeed = false; ++ break; + case 'L': + assert(0); // TODO: Follow redirects + break; +@@ -81,7 +85,7 @@ case 'N': + input_mode = INPUT_SUPPRESS; + break; + default: +- fprintf(stderr, "fatal: unknown flag %c", c); ++ fprintf(stderr, "fatal: unknown flag %c\n", c); + return 1; + } + } +@@ -167,8 +171,9 @@ case OMIT_HEADERS: + if (resp.status / 10 != 2) { + break; + } +- for (int n = 1; n > 0;) { +- char buf[BUFSIZ]; ++ char buf[BUFSIZ]; ++ int n; ++ for (n = 1; n > 0;) { + n = BIO_read(resp.bio, buf, BUFSIZ); + if (n == -1) { + fprintf(stderr, "Error: read\n"); +@@ -184,6 +189,11 @@ return 1; + } + w += x; + } ++ } ++ if (strncmp(resp.meta, "text/", 5) == 0 ++ && linefeed ++ && buf[n - 1] != '\n') { ++ printf("\n"); + } + break; + } diff --git a/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch b/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch @@ -0,0 +1,77 @@ +diff --git a/README.md b/README.md +index ea1f81f04aac1b93f73fca7d984b6e3f02415a2b..e1b424367e49daad233793f663f6340849eb4286 100644 +--- a/README.md ++++ b/README.md +@@ -17,6 +17,7 @@ This project is of fork of https://git.sr.ht/~sircmpwn/gmni + + It includes the following modifications: + - default 4 char indenting ++- e[N] command to open a link in default external program (requires `xdg-open`) + - colored headings & links + + The actual colors used depend on your terminal palette: +diff --git a/src/cgmnlm.c b/src/cgmnlm.c +index 6f0b8773c16001ab95edaec345879874483114a9..5bf7dfdbea9a60cd610d76b82bb8e9bc3d5ffa1d 100644 +--- a/src/cgmnlm.c ++++ b/src/cgmnlm.c +@@ -82,6 +82,7 @@ "The following commands are available:\n\n" + "q\tQuit\n" + "[N]\tFollow Nth link (where N is a number)\n" + "u[N]\tShow URL of Nth link (where N is a number)\n" ++ "e[N]\tSend URL of Nth link in external default program\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" + "H\tView all page history\n" +@@ -502,8 +503,11 @@ { + enum prompt_result result; + fprintf(browser->tty, "%s", prompt); + +- size_t l = 0; ++ struct link *link = browser->links; ++ char *endptr = NULL; ++ int linksel = 0; + char *in = NULL; ++ size_t l = 0; + ssize_t n = getline(&in, &l, browser->tty); + if (n == -1 && feof(browser->tty)) { + result = PROMPT_QUIT; +@@ -592,11 +596,10 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); + result = PROMPT_AGAIN; + goto exit; + } ++ case 'e': + case 'u': + if (!in[1]) break; +- struct link *link = browser->links; +- char *endptr; +- int linksel = (int)strtol(in+1, &endptr, 10); ++ linksel = (int)strtol(in+1, &endptr, 10); + if (!endptr[0] && linksel >= 0) { + while (linksel > 0 && link) { + link = link->next; +@@ -607,8 +610,12 @@ if (!link) { + fprintf(stderr, "Error: no such link.\n"); + } else { + fprintf(browser->tty, "=> %s\n", link->url); +- result = PROMPT_AGAIN; +- goto exit; ++ if (in[0] == 'e') { ++ char xdgopen[4096]; ++ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); ++ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); ++ } ++ fprintf(browser->tty, "\n"); + } + } else { + fprintf(stderr, "Error: invalid argument.\n"); +@@ -664,9 +671,7 @@ result = PROMPT_AGAIN; + goto exit; + } + +- struct link *link = browser->links; +- char *endptr; +- int linksel = (int)strtol(in, &endptr, 10); ++ linksel = (int)strtol(in, &endptr, 10); + if ((endptr[0] == '\0' || endptr[0] == '|') && linksel >= 0) { + while (linksel > 0 && link) { + link = link->next; diff --git a/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch b/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch @@ -0,0 +1,13 @@ +diff --git a/README.md b/README.md +index dfd2e1744003996f1e629c78e459dac70985a430..ce71012d402a0c975c73f6d4acd2922f32c16cbc 100644 +--- a/README.md ++++ b/README.md +@@ -5,7 +5,7 @@ + - A CLI utility (like curl): gmni + - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm + +-[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) ++[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) + + Dependencies: + diff --git a/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch b/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch @@ -0,0 +1,31 @@ +diff --git a/src/gmni.c b/src/gmni.c +index f3015ac679eba77397fb4aac5068f3a63e1cdbab..ff6c619f7c6cde57faa7398d4201d2cf867bf05a 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -290,6 +290,10 @@ exit = true; + goto next; + } + ++ if (header_mode != OMIT_HEADERS) { ++ printf("%d %s\n", resp.status, resp.meta); ++ } ++ + switch (gemini_response_class(resp.status)) { + case GEMINI_STATUS_CLASS_INPUT: + if (input_mode == INPUT_SUPPRESS) { +@@ -351,14 +355,7 @@ exit = true; + break; + } + +- switch (header_mode) { +- case ONLY_HEADERS: +- printf("%d %s\n", resp.status, resp.meta); +- break; +- case SHOW_HEADERS: +- printf("%d %s\n", resp.status, resp.meta); +- /* fallthrough */ +- case OMIT_HEADERS: ++ if (header_mode != ONLY_HEADERS) { + if (gemini_response_class(resp.status) != + GEMINI_STATUS_CLASS_SUCCESS) { + break; diff --git a/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch b/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch @@ -0,0 +1,14 @@ +diff --git a/src/gmni.c b/src/gmni.c +index 245ba76363b58e9c9ddbf87464d48cfb38463892..a61ee7bcb3ce77da24b1db97d379c2003c61d055 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -84,7 +84,8 @@ case TOFU_UNTRUSTED_CERT: + fprintf(stderr, + "The certificate offered by this server is of unknown trust. " + "Its fingerprint is: \n" +- "%s\n\n", fingerprint); ++ "%s\n\n" ++ "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); + break; + case TOFU_FINGERPRINT_MISMATCH: + fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch b/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch @@ -0,0 +1,13 @@ +diff --git a/src/client.c b/src/client.c +index 8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4..a8a12f3cfbe1e98fd2045eec257cfaf95c319910 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -212,7 +212,7 @@ } + + char *endptr; + resp->status = (enum gemini_status)strtol(buf, &endptr, 10); +- if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { ++ if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) { + fprintf(stderr, "invalid status\n"); + res = GEMINI_ERR_PROTOCOL; + goto cleanup; diff --git a/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch b/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch @@ -0,0 +1,331 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51..3598e3fb1770aca2bd646d7a3f2bf642b54c2ab9 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -27,9 +27,18 @@ bool pagination; + struct gemini_options opts; + + FILE *tty; ++ char *plain_url; + struct Curl_URL *url; + struct link *links; + struct history *history; ++ bool running; ++}; ++ ++enum prompt_result { ++ PROMPT_AGAIN, ++ PROMPT_MORE, ++ PROMPT_QUIT, ++ PROMPT_ANSWERED, + }; + + static void +@@ -70,6 +79,63 @@ } + return true; + } + ++static enum prompt_result ++do_prompts(const char *prompt, struct browser *browser) ++{ ++ fprintf(browser->tty, "%s", prompt); ++ ++ size_t l = 0; ++ char *in = NULL; ++ ssize_t n = getline(&in, &l, browser->tty); ++ if (n == -1 && feof(browser->tty)) { ++ return PROMPT_QUIT; ++ } ++ if (strcmp(in, "\n") == 0) { ++ return PROMPT_MORE; ++ } ++ if (strcmp(in, "q\n") == 0) { ++ return PROMPT_QUIT; ++ } ++ if (strcmp(in, "b\n") == 0) { ++ if (!browser->history->prev) { ++ fprintf(stderr, "At beginning of history\n"); ++ return PROMPT_AGAIN; ++ } ++ browser->history = browser->history->prev; ++ set_url(browser, browser->history->url, NULL); ++ return PROMPT_ANSWERED; ++ } ++ if (strcmp(in, "f\n") == 0) { ++ if (!browser->history->next) { ++ fprintf(stderr, "At end of history\n"); ++ return PROMPT_AGAIN; ++ } ++ browser->history = browser->history->next; ++ set_url(browser, browser->history->url, NULL); ++ return PROMPT_ANSWERED; ++ } ++ ++ struct link *link = browser->links; ++ char *endptr; ++ int linksel = (int)strtol(in, &endptr, 10); ++ if (endptr[0] == '\n' && linksel >= 0) { ++ while (linksel > 0 && link) { ++ link = link->next; ++ --linksel; ++ } ++ ++ if (!link) { ++ fprintf(stderr, "Error: no such link.\n"); ++ } else { ++ set_url(browser, link->url, &browser->history); ++ return PROMPT_ANSWERED; ++ } ++ } ++ free(in); ++ ++ return PROMPT_AGAIN; ++} ++ + static char * + trim_ws(char *in) + { +@@ -80,7 +146,7 @@ for (; *in && isspace(*in); ++in); + return in; + } + +-static void ++static bool + display_gemini(struct browser *browser, struct gemini_response *resp) + { + // TODO: Strip ANSI escape sequences +@@ -136,29 +202,36 @@ } + ++row; + col = 0; + +- // TODO: It would be nice if we could follow links from this +- // prompt +- if (browser->pagination && row >= ws.ws_row - 1) { +- fprintf(browser->tty, "[Enter for more, or q to stop] "); ++ if (browser->pagination && row >= ws.ws_row - 4) { ++ char prompt[4096]; ++ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" ++ "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" ++ "(more) => ", resp->meta, browser->plain_url); ++ enum prompt_result result = PROMPT_AGAIN; ++ while (result == PROMPT_AGAIN) { ++ result = do_prompts(prompt, browser); ++ } + +- size_t n = 0; +- char *l = NULL; +- if (getline(&l, &n, browser->tty) == -1) { +- return; +- } +- if (strcmp(l, "q\n") == 0) { +- return; ++ switch (result) { ++ case PROMPT_AGAIN: ++ case PROMPT_MORE: ++ break; ++ case PROMPT_QUIT: ++ browser->running = false; ++ return true; ++ case PROMPT_ANSWERED: ++ return true; + } + +- free(l); + row = col = 0; + } + } + + gemini_parser_finish(&p); ++ return false; + } + +-static void ++static bool + display_plaintext(struct browser *browser, struct gemini_response *resp) + { + // TODO: Strip ANSI escape sequences +@@ -175,20 +248,20 @@ } + } + + (void)row; (void)col; // TODO: generalize pagination ++ return false; + } + +-static void ++static bool + display_response(struct browser *browser, struct gemini_response *resp) + { + if (strcmp(resp->meta, "text/gemini") == 0 + || strncmp(resp->meta, "text/gemini;", 12) == 0) { +- display_gemini(browser, resp); +- return; ++ return display_gemini(browser, resp); + } + if (strncmp(resp->meta, "text/", 5) == 0) { +- display_plaintext(browser, resp); +- return; ++ return display_plaintext(browser, resp); + } ++ assert(0); // TODO: Deal with other mimetypes + } + + static char * +@@ -225,19 +298,19 @@ } + return input; + } + +-static char * ++// Returns true to skip prompting ++static bool + do_requests(struct browser *browser, struct gemini_response *resp) + { +- char *plain_url; + int nredir = 0; + bool requesting = true; + while (requesting) { + CURLUcode uc = curl_url_get(browser->url, +- CURLUPART_URL, &plain_url, 0); ++ CURLUPART_URL, &browser->plain_url, 0); + assert(uc == CURLUE_OK); // Invariant + +- enum gemini_result res = gemini_request( +- plain_url, &browser->opts, resp); ++ enum gemini_result res = gemini_request(browser->plain_url, ++ &browser->opts, resp); + if (res != GEMINI_OK) { + fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); + requesting = false; +@@ -253,7 +326,8 @@ requesting = false; + break; + } + +- char *new_url = gemini_input_url(plain_url, input); ++ char *new_url = gemini_input_url( ++ browser->plain_url, input); + assert(new_url); + set_url(browser, new_url, NULL); + break; +@@ -278,8 +352,7 @@ resp->status, resp->meta); + break; + case GEMINI_STATUS_CLASS_SUCCESS: + requesting = false; +- display_response(browser, resp); +- break; ++ return display_response(browser, resp); + } + + if (requesting) { +@@ -287,63 +360,7 @@ gemini_response_finish(resp); + } + } + +- return plain_url; +-} +- +-static bool +-do_prompts(const char *prompt, struct browser *browser) +-{ +- bool prompting = true; +- while (prompting) { +- fprintf(browser->tty, "%s", prompt); +- +- size_t l = 0; +- char *in = NULL; +- ssize_t n = getline(&in, &l, browser->tty); +- if (n == -1 && feof(browser->tty)) { +- return false; +- } +- if (strcmp(in, "q\n") == 0) { +- return false; +- } +- if (strcmp(in, "b\n") == 0) { +- if (!browser->history->prev) { +- fprintf(stderr, "At beginning of history\n"); +- continue; +- } +- browser->history = browser->history->prev; +- set_url(browser, browser->history->url, NULL); +- break; +- } +- if (strcmp(in, "f\n") == 0) { +- if (!browser->history->next) { +- fprintf(stderr, "At end of history\n"); +- continue; +- } +- browser->history = browser->history->next; +- set_url(browser, browser->history->url, NULL); +- break; +- } +- +- struct link *link = browser->links; +- char *endptr; +- int linksel = (int)strtol(in, &endptr, 10); +- if (endptr[0] == '\n' && linksel >= 0) { +- while (linksel > 0 && link) { +- link = link->next; +- --linksel; +- } +- +- if (!link) { +- fprintf(stderr, "Error: no such link.\n"); +- } else { +- set_url(browser, link->url, &browser->history); +- break; +- } +- } +- free(in); +- } +- return true; ++ return false; + } + + int +@@ -381,24 +398,38 @@ SSL_load_error_strings(); + ERR_load_crypto_strings(); + browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); + +- bool run = true; + struct gemini_response resp; +- while (run) { ++ browser.running = true; ++ while (browser.running) { + static char prompt[4096]; +- char *plain_url = do_requests(&browser, &resp); ++ if (do_requests(&browser, &resp)) { ++ // Skip prompts ++ goto next; ++ } + +- snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" +- "[n]: follow Nth link; [o <url>]: open URL; " +- "[b]ack; [f]orward; " +- "[q]uit\n" ++ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" ++ "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" + "=> ", +- resp.status == GEMINI_STATUS_SUCCESS ? " " : "", + resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", +- plain_url); ++ browser.plain_url); + gemini_response_finish(&resp); + +- run = do_prompts(prompt, &browser); ++ enum prompt_result result = PROMPT_AGAIN; ++ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { ++ result = do_prompts(prompt, &browser); ++ } ++ switch (result) { ++ case PROMPT_AGAIN: ++ case PROMPT_MORE: ++ assert(0); ++ case PROMPT_QUIT: ++ browser.running = false; ++ break; ++ case PROMPT_ANSWERED: ++ break; ++ } + ++next:; + struct link *link = browser.links; + while (link) { + struct link *next = link->next; diff --git a/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch b/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch @@ -0,0 +1,135 @@ +diff --git a/include/gmni.h b/include/gmni.h +index d78d1c8eb4e94507c363219573691e60d40e8ea8..4240c6231010ebb86aead2d49700fe2e3d00b65c 100644 +--- a/include/gmni.h ++++ b/include/gmni.h +@@ -2,6 +2,7 @@ #ifndef GEMINI_CLIENT_H + #define GEMINI_CLIENT_H + #include <netdb.h> + #include <openssl/ssl.h> ++#include <stdbool.h> + #include <sys/socket.h> + + enum gemini_result { +@@ -106,7 +107,9 @@ + enum gemini_tok { + GEMINI_TEXT, + GEMINI_LINK, +- GEMINI_PREFORMATTED, ++ GEMINI_PREFORMATTED_BEGIN, ++ GEMINI_PREFORMATTED_END, ++ GEMINI_PREFORMATTED_TEXT, + GEMINI_HEADING, + GEMINI_LIST_ITEM, + GEMINI_QUOTE, +@@ -124,10 +127,7 @@ char *text; + char *url; // May be NULL + } link; + +- struct { +- char *text; +- char *alt_text; // May be NULL +- } preformatted; ++ char *preformatted; + + struct { + char *title; +@@ -144,6 +144,7 @@ BIO *f; + char *buf; + size_t bufsz; + size_t bufln; ++ bool preformatted; + }; + + // Initializes a text/gemini parser which reads from the specified BIO. +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 5e5d828f6f246e0baa3917177acd8cb6482387f8..8841c4613e208e379c6cd768a65a9a3b631d3146 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -278,8 +278,15 @@ } else { + col += fprintf(out, " "); + } + break; +- case GEMINI_PREFORMATTED: +- continue; // TODO ++ case GEMINI_PREFORMATTED_BEGIN: ++ case GEMINI_PREFORMATTED_END: ++ continue; // Not used ++ case GEMINI_PREFORMATTED_TEXT: ++ col += fprintf(out, "` "); ++ if (text == NULL) { ++ text = tok.text; ++ } ++ break; + case GEMINI_HEADING: + if (text == NULL) { + for (int n = tok.heading.level; n; --n) { +diff --git a/src/parser.c b/src/parser.c +index b9db3d2000f1176df4a21300f7b806ed6a5ded75..5b0f01399d934f593cdf987e096dd2cfb134d114 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -1,6 +1,7 @@ + #include <assert.h> + #include <ctype.h> + #include <openssl/bio.h> ++#include <stdbool.h> + #include <stddef.h> + #include <stdlib.h> + #include <string.h> +@@ -15,6 +16,7 @@ p->bufsz = BUFSIZ; + p->buf = malloc(p->bufsz + 1); + p->buf[0] = 0; + BIO_up_ref(p->f); ++ p->preformatted = false; + } + + void +@@ -57,7 +59,15 @@ if ((end = strstr(p->buf, "\n")) != NULL) { + *end = 0; + } + +- if (strncmp(p->buf, "=>", 2) == 0) { ++ if (p->preformatted) { ++ if (strncmp(p->buf, "```", 3) == 0) { ++ tok->token = GEMINI_PREFORMATTED_END; ++ p->preformatted = false; ++ } else { ++ tok->token = GEMINI_PREFORMATTED_TEXT; ++ tok->preformatted = strdup(p->buf); ++ } ++ } else if (strncmp(p->buf, "=>", 2) == 0) { + tok->token = GEMINI_LINK; + int i = 2; + while (p->buf[i] && isspace(p->buf[i])) ++i; +@@ -76,9 +86,11 @@ } + + tok->link.url = strdup(tok->link.url); + } else if (strncmp(p->buf, "```", 3) == 0) { +- tok->token = GEMINI_PREFORMATTED; // TODO +- tok->preformatted.text = strdup("<text>"); +- tok->preformatted.alt_text = strdup("<alt-text>"); ++ tok->token = GEMINI_PREFORMATTED_BEGIN; ++ if (p->buf[3]) { ++ tok->preformatted = strdup(&p->buf[3]); ++ } ++ p->preformatted = true; + } else if (p->buf[0] == '#') { + tok->token = GEMINI_HEADING; + int level = 1; +@@ -125,9 +137,14 @@ case GEMINI_LINK: + free(tok->link.text); + free(tok->link.url); + break; +- case GEMINI_PREFORMATTED: +- free(tok->preformatted.text); +- free(tok->preformatted.alt_text); ++ case GEMINI_PREFORMATTED_BEGIN: ++ free(tok->preformatted); ++ break; ++ case GEMINI_PREFORMATTED_TEXT: ++ free(tok->preformatted); ++ break; ++ case GEMINI_PREFORMATTED_END: ++ // Nothing to free + break; + case GEMINI_HEADING: + free(tok->heading.title); diff --git a/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch b/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch @@ -0,0 +1,33 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index f218f0a43c1d32223e6bb73ecc5069c5cd0cf21a..92e866a2a58a222f56993e57849c348ead658ab4 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -157,7 +157,7 @@ } + + char *title = browser->page_title; + if (title) { +- title = trim_ws(strdup(browser->page_title)); ++ title = trim_ws(browser->page_title); + } + + fprintf(f, "=> %s%s%s\n", browser->plain_url, +@@ -166,17 +166,15 @@ fclose(f); + + fprintf(browser->tty, "Bookmark saved: %s\n", + title ? title : browser->plain_url); +- if (title != NULL) { +- free(title); +- } + } + + static void + open_bookmarks(struct browser *browser) + { +- const char *path_fmt = get_data_pathfmt(); ++ char *path_fmt = get_data_pathfmt(); + static char path[PATH_MAX+1]; + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ free(path_fmt); + static char url[PATH_MAX+1+7]; + snprintf(url, sizeof(url), "file://%s", path); + set_url(browser, url, &browser->history); diff --git a/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch b/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch @@ -0,0 +1,21 @@ +diff --git a/src/client.c b/src/client.c +index 34d25f3794f8069b15a688ebd89b7bf82fdd01e2..d8b67d7b9f47b4473ba6eb278853ea0565739f09 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -59,7 +59,7 @@ { + struct addrinfo *addr; + enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); + if (res != GEMINI_OK) { +- goto cleanup; ++ return res; + } + + struct addrinfo *rp; +@@ -79,7 +79,6 @@ res = GEMINI_ERR_CONNECT; + return res; + } + +-cleanup: + if (!options || !options->addr) { + freeaddrinfo(addr); + } diff --git a/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch b/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch @@ -0,0 +1,456 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 273423b8261a9608c89505b4e154899806f994f4..15256bbd0dcd1c6899e8e91ba5228baf93ce289f 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -22,6 +22,16 @@ char *url; + struct history *prev, *next; + }; + ++struct browser { ++ bool pagination; ++ struct gemini_options opts; ++ ++ FILE *tty; ++ struct Curl_URL *url; ++ struct link *links; ++ struct history *history; ++}; ++ + static void + usage(const char *argv_0) + { +@@ -39,7 +49,7 @@ free(history); + } + + static bool +-set_url(struct Curl_URL *url, char *new_url, struct history **history) ++set_url(struct browser *browser, char *new_url, struct history **history) + { + if (history) { + struct history *next = calloc(1, sizeof(struct history)); +@@ -53,7 +63,7 @@ (*history)->next = next; + } + *history = next; + } +- if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { ++ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { + fprintf(stderr, "Error: invalid URL\n"); + return false; + } +@@ -71,27 +81,29 @@ return in; + } + + static void +-display_gemini(FILE *tty, struct gemini_response *resp, +- struct link **next, bool pagination) ++display_gemini(struct browser *browser, struct gemini_response *resp) + { ++ // TODO: Strip ANSI escape sequences + int nlinks = 0; + struct gemini_parser p; + gemini_parser_init(&p, resp->bio); + + struct winsize ws; +- ioctl(fileno(tty), TIOCGWINSZ, &ws); ++ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); + + int row = 0, col = 0; + struct gemini_token tok; ++ struct link **next = &browser->links; + while (gemini_parser_next(&p, &tok) == 0) { + switch (tok.token) { + case GEMINI_TEXT: + // TODO: word wrap +- col += fprintf(tty, " %s\n", trim_ws(tok.text)); ++ col += fprintf(browser->tty, " %s\n", ++ trim_ws(tok.text)); + break; + case GEMINI_LINK: +- col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( +- tok.link.text ? tok.link.text : tok.link.url)); ++ col += fprintf(browser->tty, "[%d] %s\n", nlinks++, ++ trim_ws(tok.link.text ? tok.link.text : tok.link.url)); + *next = calloc(1, sizeof(struct link)); + (*next)->url = strdup(trim_ws(tok.link.url)); + next = &(*next)->next; +@@ -100,17 +112,20 @@ case GEMINI_PREFORMATTED: + continue; // TODO + case GEMINI_HEADING: + for (int n = tok.heading.level; n; --n) { +- col += fprintf(tty, "#"); ++ col += fprintf(browser->tty, "#"); + } +- col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); ++ col += fprintf(browser->tty, " %s\n", ++ trim_ws(tok.heading.title)); + break; + case GEMINI_LIST_ITEM: + // TODO: Option to disable Unicode +- col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); ++ col += fprintf(browser->tty, " • %s\n", ++ trim_ws(tok.list_item)); + break; + case GEMINI_QUOTE: + // TODO: Option to disable Unicode +- col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); ++ col += fprintf(browser->tty, " | %s\n", ++ trim_ws(tok.quote_text)); + break; + } + +@@ -121,12 +136,14 @@ } + ++row; + col = 0; + +- if (pagination && row >= ws.ws_row - 1) { +- fprintf(tty, "[Enter for more, or q to stop] "); ++ // TODO: It would be nice if we could follow links from this ++ // prompt ++ if (browser->pagination && row >= ws.ws_row - 1) { ++ fprintf(browser->tty, "[Enter for more, or q to stop] "); + + size_t n = 0; + char *l = NULL; +- if (getline(&l, &n, tty) == -1) { ++ if (getline(&l, &n, browser->tty) == -1) { + return; + } + if (strcmp(l, "q\n") == 0) { +@@ -142,52 +159,163 @@ gemini_parser_finish(&p); + } + + static void +-display_plaintext(FILE *tty, struct gemini_response *resp, bool pagination) ++display_plaintext(struct browser *browser, struct gemini_response *resp) + { ++ // TODO: Strip ANSI escape sequences + struct winsize ws; + int row = 0, col = 0; +- ioctl(fileno(tty), TIOCGWINSZ, &ws); ++ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); + + char buf[BUFSIZ]; + int n; + while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { + while (n) { +- n -= fwrite(buf, 1, n, tty); ++ n -= fwrite(buf, 1, n, browser->tty); + } + } + +- (void)pagination; (void)row; (void)col; // TODO: generalize pagination ++ (void)row; (void)col; // TODO: generalize pagination + } + + static void +-display_response(FILE *tty, struct gemini_response *resp, +- struct link **next, bool pagination) ++display_response(struct browser *browser, struct gemini_response *resp) + { + if (strcmp(resp->meta, "text/gemini") == 0 + || strncmp(resp->meta, "text/gemini;", 12) == 0) { +- display_gemini(tty, resp, next, pagination); ++ display_gemini(browser, resp); + return; + } + if (strncmp(resp->meta, "text/", 5) == 0) { +- display_plaintext(tty, resp, pagination); ++ display_plaintext(browser, resp); + return; + } + } + ++static char * ++do_requests(struct browser *browser, struct gemini_response *resp) ++{ ++ char *plain_url; ++ int nredir = 0; ++ bool requesting = true; ++ while (requesting) { ++ CURLUcode uc = curl_url_get(browser->url, ++ CURLUPART_URL, &plain_url, 0); ++ assert(uc == CURLUE_OK); // Invariant ++ ++ enum gemini_result res = gemini_request( ++ plain_url, &browser->opts, resp); ++ if (res != GEMINI_OK) { ++ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); ++ requesting = false; ++ break; ++ } ++ ++ switch (gemini_response_class(resp->status)) { ++ case GEMINI_STATUS_CLASS_INPUT: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_REDIRECT: ++ if (++nredir >= 5) { ++ requesting = false; ++ fprintf(stderr, "Error: maximum redirects (5) exceeded"); ++ break; ++ } ++ fprintf(stderr, "Following redirect to %s\n", resp->meta); ++ set_url(browser, resp->meta, NULL); ++ break; ++ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: ++ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: ++ requesting = false; ++ fprintf(stderr, "Server returned %s %d %s\n", ++ resp->status / 10 == 4 ? ++ "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ resp->status, resp->meta); ++ break; ++ case GEMINI_STATUS_CLASS_SUCCESS: ++ requesting = false; ++ display_response(browser, resp); ++ break; ++ } ++ ++ if (requesting) { ++ gemini_response_finish(resp); ++ } ++ } ++ ++ return plain_url; ++} ++ ++static bool ++do_prompts(const char *prompt, struct browser *browser) ++{ ++ bool prompting = true; ++ while (prompting) { ++ fprintf(browser->tty, "%s", prompt); ++ ++ size_t l = 0; ++ char *in = NULL; ++ ssize_t n = getline(&in, &l, browser->tty); ++ if (n == -1 && feof(browser->tty)) { ++ return false; ++ } ++ if (strcmp(in, "q\n") == 0) { ++ return false; ++ } ++ if (strcmp(in, "b\n") == 0) { ++ if (!browser->history->prev) { ++ fprintf(stderr, "At beginning of history\n"); ++ continue; ++ } ++ browser->history = browser->history->prev; ++ set_url(browser, browser->history->url, NULL); ++ break; ++ } ++ if (strcmp(in, "f\n") == 0) { ++ if (!browser->history->next) { ++ fprintf(stderr, "At end of history\n"); ++ continue; ++ } ++ browser->history = browser->history->next; ++ set_url(browser, browser->history->url, NULL); ++ break; ++ } ++ ++ struct link *link = browser->links; ++ char *endptr; ++ int linksel = (int)strtol(in, &endptr, 10); ++ if (endptr[0] == '\n' && linksel >= 0) { ++ while (linksel > 0 && link) { ++ link = link->next; ++ --linksel; ++ } ++ ++ if (!link) { ++ fprintf(stderr, "Error: no such link.\n"); ++ } else { ++ set_url(browser, link->url, &browser->history); ++ break; ++ } ++ } ++ free(in); ++ } ++ return true; ++} ++ + int + main(int argc, char *argv[]) + { +- bool pagination = true; +- +- struct Curl_URL *url = curl_url(); +- +- FILE *tty = fopen("/dev/tty", "w+"); ++ struct browser browser = { ++ .pagination = true, ++ .url = curl_url(), ++ .tty = fopen("/dev/tty", "w+"), ++ }; + + int c; + while ((c = getopt(argc, argv, "hP")) != -1) { + switch (c) { + case 'P': +- pagination = false; ++ browser.pagination = false; + break; + case 'h': + usage(argv[0]); +@@ -198,9 +326,8 @@ return 1; + } + } + +- struct history *history; + if (optind == argc - 1) { +- set_url(url, argv[optind], &history); ++ set_url(&browser, argv[optind], &browser.history); + } else { + usage(argv[0]); + return 1; +@@ -208,65 +335,13 @@ } + + SSL_load_error_strings(); + ERR_load_crypto_strings(); +- struct gemini_options opts = { +- .ssl_ctx = SSL_CTX_new(TLS_method()), +- }; ++ browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); + + bool run = true; + struct gemini_response resp; + while (run) { +- struct link *links; + static char prompt[4096]; +- char *plain_url; +- +- int nredir = 0; +- bool requesting = true; +- while (requesting) { +- CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); +- assert(uc == CURLUE_OK); // Invariant +- +- enum gemini_result res = gemini_request( +- plain_url, &opts, &resp); +- if (res != GEMINI_OK) { +- fprintf(stderr, "Error: %s\n", +- gemini_strerr(res, &resp)); +- requesting = false; +- break; +- } +- +- switch (gemini_response_class(resp.status)) { +- case GEMINI_STATUS_CLASS_INPUT: +- assert(0); // TODO +- case GEMINI_STATUS_CLASS_REDIRECT: +- if (++nredir >= 5) { +- requesting = false; +- fprintf(stderr, "Error: maximum redirects (5) exceeded"); +- break; +- } +- fprintf(stderr, +- "Following redirect to %s\n", resp.meta); +- set_url(url, resp.meta, NULL); +- break; +- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: +- assert(0); // TODO +- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: +- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: +- requesting = false; +- fprintf(stderr, "Server returned %s %d %s\n", +- resp.status / 10 == 4 ? +- "TEMPORARY FAILURE" : "PERMANENT FALIURE", +- resp.status, resp.meta); +- break; +- case GEMINI_STATUS_CLASS_SUCCESS: +- requesting = false; +- display_response(tty, &resp, &links, pagination); +- break; +- } +- +- if (requesting) { +- gemini_response_finish(&resp); +- } +- } ++ char *plain_url = do_requests(&browser, &resp); + + snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" + "[n]: follow Nth link; [o <url>]: open URL; " +@@ -276,74 +351,22 @@ "=> ", + resp.status == GEMINI_STATUS_SUCCESS ? " " : "", + resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", + plain_url); +- + gemini_response_finish(&resp); + +- bool prompting = true; +- while (prompting) { +- fprintf(tty, "%s", prompt); ++ run = do_prompts(prompt, &browser); + +- size_t l = 0; +- char *in = NULL; +- ssize_t n = getline(&in, &l, tty); +- if (n == -1 && feof(tty)) { +- run = false; +- break; +- } +- if (strcmp(in, "q\n") == 0) { +- run = false; +- break; +- } +- if (strcmp(in, "b\n") == 0) { +- if (!history->prev) { +- fprintf(stderr, "At beginning of history\n"); +- continue; +- } +- history = history->prev; +- set_url(url, history->url, NULL); +- break; +- } +- if (strcmp(in, "f\n") == 0) { +- if (!history->next) { +- fprintf(stderr, "At end of history\n"); +- continue; +- } +- history = history->next; +- set_url(url, history->url, NULL); +- break; +- } +- +- struct link *link = links; +- char *endptr; +- int linksel = (int)strtol(in, &endptr, 10); +- if (endptr[0] == '\n' && linksel >= 0) { +- while (linksel > 0 && link) { +- link = link->next; +- --linksel; +- } +- +- if (!link) { +- fprintf(stderr, "Error: no such link.\n"); +- } else { +- set_url(url, link->url, &history); +- break; +- } +- } +- free(in); +- } +- +- struct link *link = links; ++ struct link *link = browser.links; + while (link) { + struct link *next = link->next; + free(link->url); + free(link); + link = next; + } +- links = NULL; ++ browser.links = NULL; + } + +- history_free(history); +- SSL_CTX_free(opts.ssl_ctx); +- curl_url_cleanup(url); ++ history_free(browser.history); ++ SSL_CTX_free(browser.opts.ssl_ctx); ++ curl_url_cleanup(browser.url); + return 0; + } diff --git a/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch b/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch @@ -0,0 +1,61 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 25afbaf0ce2621c4559fd34a035a9f944485aa3d..7fba942066c9d9870f78311f050ef19dc42f7ce1 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -10,6 +10,7 @@ #include <stdbool.h> + #include <stdio.h> + #include <string.h> + #include <sys/ioctl.h> ++#include <sys/stat.h> + #include <termios.h> + #include <unistd.h> + #include "gmni.h" +@@ -52,6 +53,17 @@ PROMPT_QUIT, + PROMPT_ANSWERED, + PROMPT_NEXT, + }; ++ ++const char *default_bookmarks = ++ "# Welcome to gmni\n\n" ++ "Links:\n\n" ++ // TODO: sub out the URL for the appropriate geminispace version once ++ // sr.ht supports gemini ++ "=> https://sr.ht/~sircmpwn/gmni The gmni browser\n" ++ "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" ++ "This file can be found at %s and may be edited at your pleasure.\n\n" ++ "Bookmarks:\n" ++ ; + + const char *help_msg = + "The following commands are available:\n\n" +@@ -175,6 +187,20 @@ char *path_fmt = get_data_pathfmt(); + static char path[PATH_MAX+1]; + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); ++ ++ struct stat buf; ++ if (stat(path, &buf) == -1 && errno == ENOENT) { ++ // TOCTOU, but we almost certainly don't care ++ FILE *f = fopen(path, "a"); ++ if (f == NULL) { ++ fprintf(stderr, "Error opening %s for writing: %s\n", ++ path, strerror(errno)); ++ return; ++ } ++ fprintf(f, default_bookmarks, path); ++ fclose(f); ++ } ++ + static char url[PATH_MAX+1+7]; + snprintf(url, sizeof(url), "file://%s", path); + set_url(browser, url, &browser->history); +@@ -867,8 +893,7 @@ if (!set_url(&browser, argv[optind], &browser.history)) { + return 1; + } + } else { +- usage(argv[0]); +- return 1; ++ open_bookmarks(&browser); + } + + SSL_load_error_strings(); diff --git a/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch b/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch @@ -0,0 +1,20 @@ +diff --git a/src/tofu.c b/src/tofu.c +index de3746563ed3e0c515a5c52a053f3a535fda5e09..af5d9f20d78558b3ff7dc9253bef53f2deef1d25 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -175,13 +175,14 @@ tofu->callback = cb; + tofu->cb_data = cb_data; + SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); + ++ tofu->known_hosts = NULL; ++ + FILE *f = fopen(tofu->known_hosts_path, "r"); + if (!f) { + return; + } + size_t n = 0; + char *line = NULL; +- tofu->known_hosts = NULL; + while (getline(&line, &n, f) != -1) { + struct known_host *host = calloc(1, sizeof(struct known_host)); + char *tok = strtok(line, " "); diff --git a/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch b/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch @@ -0,0 +1,15 @@ +diff --git a/src/gmni.c b/src/gmni.c +index 4af98fbbe70b09ce5a7fce46863438b0487d8738..245ba76363b58e9c9ddbf87464d48cfb38463892 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -315,8 +315,8 @@ last = buf[n - 1]; + } + ssize_t w = 0; + while (w < (ssize_t)n) { +- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); +- if (x == -1) { ++ ssize_t x = fwrite(&buf[w], 1, n - w, stdout); ++ if (ferror(stdout)) { + fprintf(stderr, "Error: write: %s\n", + strerror(errno)); + return 1; diff --git a/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch b/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch @@ -0,0 +1,243 @@ +diff --git a/configure b/configure +index 151bdae8c12d9ad07d3a5240d7c554f4c62664c8..7412cf545fa96e6be169c0055778b83a06b557f5 100755 +--- a/configure ++++ b/configure +@@ -16,7 +16,8 @@ src/client.c \ + src/escape.c \ + src/gmnlm.c \ + src/parser.c \ +- src/url.c ++ src/url.c \ ++ src/util.c + } + + all="gmni gmnlm" +diff --git a/include/util.h b/include/util.h +new file mode 100644 +index 0000000000000000000000000000000000000000..cf731bfdc75a750df6f18873f48dd859786587c7 +--- /dev/null ++++ b/include/util.h +@@ -0,0 +1,12 @@ ++#ifndef GEMINI_UTIL_H ++#define GEMINI_UTIL_H ++ ++struct pathspec { ++ const char *var; ++ const char *path; ++}; ++ ++char *getpath(const struct pathspec *paths, size_t npaths); ++int mkdirs(char *path, mode_t mode); ++ ++#endif +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 4b844200eeb4318d694566d7a84eb3e64c7c7668..c5d5da36ffad8772315928e8a69b785d994511e4 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -12,6 +12,7 @@ #include <termios.h> + #include <unistd.h> + #include "gmni.h" + #include "url.h" ++#include "util.h" + + struct link { + char *url; +@@ -29,6 +30,7 @@ struct gemini_options opts; + + FILE *tty; + char *plain_url; ++ char *page_title; + struct Curl_URL *url; + struct link *links; + struct history *history; +@@ -52,6 +54,8 @@ "q\tQuit\n" + "N\tFollow Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" ++ "m\tSave bookmark\n" ++ "M\tBrowse bookmarks\n" + "\n" + "Other commands include:\n\n" + "<Enter>\tread more lines\n" +@@ -98,6 +102,63 @@ } + return true; + } + ++static char * ++get_data_pathfmt() ++{ ++ const struct pathspec paths[] = { ++ {.var = "GMNIDATA", .path = "/%s"}, ++ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, ++ {.var = "HOME", .path = "/.local/share/gmni/%s"} ++ }; ++ return getpath(paths, sizeof(paths) / sizeof(paths[0])); ++} ++ ++static char * ++trim_ws(char *in) ++{ ++ while (*in && isspace(*in)) ++in; ++ return in; ++} ++ ++static void ++save_bookmark(struct browser *browser) ++{ ++ const char *path_fmt = get_data_pathfmt(); ++ static char path[PATH_MAX+1]; ++ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ mkdirs(path, 0755); ++ ++ FILE *f = fopen(path, "a"); ++ if (!f) { ++ fprintf(stderr, "Error opening %s for writing: %s\n", ++ path, strerror(errno)); ++ return; ++ } ++ ++ char *title = browser->page_title; ++ if (title) { ++ title = trim_ws(browser->page_title); ++ } ++ ++ fprintf(f, "=> %s%s%s\n", browser->plain_url, ++ title ? " " : "", title ? title : ""); ++ fclose(f); ++ ++ fprintf(browser->tty, "Bookmark saved: %s\n", ++ title ? title : browser->plain_url); ++} ++ ++static void ++open_bookmarks(struct browser *browser) ++{ ++ const char *path_fmt = get_data_pathfmt(); ++ static char path[PATH_MAX+1]; ++ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ static char url[PATH_MAX+1+7]; ++ snprintf(url, sizeof(url), "file://%s", path); ++ set_url(browser, url, &browser->history); ++} ++ + static enum prompt_result + do_prompts(const char *prompt, struct browser *browser) + { +@@ -134,6 +195,16 @@ browser->history = browser->history->prev; + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; ++ case 'm': ++ if (in[1]) break; ++ save_bookmark(browser); ++ result = PROMPT_AGAIN; ++ goto exit; ++ case 'M': ++ if (in[1]) break; ++ open_bookmarks(browser); ++ result = PROMPT_ANSWERED; ++ goto exit; + case 'f': + if (in[1]) break; + if (!browser->history->next) { +@@ -205,13 +276,6 @@ free(in); + return result; + } + +-static char * +-trim_ws(char *in) +-{ +- while (*in && isspace(*in)) ++in; +- return in; +-} +- + static int + wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) + { +@@ -258,6 +322,8 @@ { + int nlinks = 0; + struct gemini_parser p; + gemini_parser_init(&p, resp->bio); ++ free(browser->page_title); ++ browser->page_title = NULL; + + struct winsize ws; + ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); +@@ -302,6 +368,9 @@ text = tok.text; + } + break; + case GEMINI_HEADING: ++ if (!browser->page_title) { ++ browser->page_title = strdup(tok.heading.title); ++ } + if (text == NULL) { + for (int n = tok.heading.level; n; --n) { + col += fprintf(out, "#"); +diff --git a/src/util.c b/src/util.c +new file mode 100644 +index 0000000000000000000000000000000000000000..26e7283a26cc8e593bb3e3fd53d4b7a9bc646e3f +--- /dev/null ++++ b/src/util.c +@@ -0,0 +1,62 @@ ++#include <assert.h> ++#include <errno.h> ++#include <libgen.h> ++#include <limits.h> ++#include <stdlib.h> ++#include <string.h> ++#include <sys/stat.h> ++#include "util.h" ++ ++static void ++posix_dirname(char *path, char *dname) ++{ ++ char p[PATH_MAX+1]; ++ char *t; ++ ++ assert(strlen(path) <= PATH_MAX); ++ ++ strcpy(p, path); ++ t = dirname(path); ++ memmove(dname, t, strlen(t) + 1); ++ ++ /* restore the path if dirname worked in-place */ ++ if (t == path && path != dname) { ++ strcpy(path, p); ++ } ++} ++ ++/** Make directory and all of its parents */ ++int ++mkdirs(char *path, mode_t mode) ++{ ++ char dname[PATH_MAX + 1]; ++ posix_dirname(path, dname); ++ if (strcmp(dname, "/") == 0) { ++ return 0; ++ } ++ if (mkdirs(dname, mode) != 0) { ++ return -1; ++ } ++ if (mkdir(path, mode) != 0 && errno != EEXIST) { ++ return -1; ++ } ++ errno = 0; ++ return 0; ++} ++ ++char * ++getpath(const struct pathspec *paths, size_t npaths) { ++ for (size_t i = 0; i < npaths; i++) { ++ const char *var = ""; ++ if (paths[i].var) { ++ var = getenv(paths[i].var); ++ } ++ if (var) { ++ char *out = calloc(1, ++ strlen(var) + strlen(paths[i].path) + 1); ++ strcat(strcat(out, var), paths[i].path); ++ return out; ++ } ++ } ++ return NULL; ++} diff --git a/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch b/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch @@ -0,0 +1,22 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 07b699134c0c71a20d4780148ad74cb246634549..da99a84580894312ba86b92e6abd02cfdfc80fd4 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -457,7 +457,6 @@ /* fallthrough */ + case GEMINI_PREFORMATTED_END: + continue; // Not used + case GEMINI_PREFORMATTED_TEXT: +- col += fprintf(out, "` "); + if (text == NULL) { + text = tok.preformatted; + } +@@ -494,7 +493,8 @@ col += fprintf(out, " "); + } + break; + case GEMINI_QUOTE: +- col += fprintf(out, "> "); ++ col += fprintf(out, "%s ", ++ browser->unicode ? "┃" : ">"); + if (text == NULL) { + text = trim_ws(tok.quote_text); + } diff --git a/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch b/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch @@ -0,0 +1,62 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 41284df2235e869f49e542f5e1f2dcc240deb684..756520c728baad52206866a3af64b0ffebbac3cd 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -59,6 +59,7 @@ "q\tQuit\n" + "N\tFollow Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" ++ "H\tView all page history\n" + "m\tSave bookmark\n" + "M\tBrowse bookmarks\n" + "\n" +@@ -206,16 +207,6 @@ browser->history = browser->history->prev; + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; +- case 'm': +- if (in[1]) break; +- save_bookmark(browser); +- result = PROMPT_AGAIN; +- goto exit; +- case 'M': +- if (in[1]) break; +- open_bookmarks(browser); +- result = PROMPT_ANSWERED; +- goto exit; + case 'f': + if (in[1]) break; + if (!browser->history->next) { +@@ -225,6 +216,32 @@ goto exit; + } + browser->history = browser->history->next; + set_url(browser, browser->history->url, NULL); ++ result = PROMPT_ANSWERED; ++ goto exit; ++ case 'H': ++ if (in[1]) break; ++ struct history *cur = browser->history; ++ while (cur->prev) cur = cur->prev; ++ while (cur != browser->history) { ++ fprintf(browser->tty, " %s\n", cur->url); ++ cur = cur->next; ++ } ++ fprintf(browser->tty, "* %s\n", cur->url); ++ cur = cur->next; ++ while (cur) { ++ fprintf(browser->tty, " %s\n", cur->url); ++ cur = cur->next; ++ } ++ result = PROMPT_AGAIN; ++ goto exit; ++ case 'm': ++ if (in[1]) break; ++ save_bookmark(browser); ++ result = PROMPT_AGAIN; ++ goto exit; ++ case 'M': ++ if (in[1]) break; ++ open_bookmarks(browser); + result = PROMPT_ANSWERED; + goto exit; + case '/': diff --git a/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch b/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmni.c b/src/gmni.c +index 4f563ed19e0edc52086f55e4057a91f0802a1f93..1c3e5f2bebddf5b606d11acd8924562cabde9a6f 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -287,7 +287,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: + if (header_mode == OMIT_HEADERS) { + fprintf(stderr, "%s: %d %s\n", + resp.status / 10 == 4 ? +- "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ "TEMPORARY FAILURE" : "PERMANENT FAILURE", + resp.status, resp.meta); + } + exit = true; diff --git a/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch b/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch @@ -0,0 +1,31 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 81923101e320b8517405ef2ed7efdbc626241201..6d77264cabb0db317ddf0807953d327f2379d9be 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -168,7 +168,7 @@ col += fprintf(browser->tty, " %s\n", + trim_ws(tok.text)); + break; + case GEMINI_LINK: +- col += fprintf(browser->tty, "[%d] %s\n", nlinks++, ++ col += fprintf(browser->tty, "%d) %s\n", nlinks++, + trim_ws(tok.link.text ? tok.link.text : tok.link.url)); + *next = calloc(1, sizeof(struct link)); + (*next)->url = strdup(trim_ws(tok.link.url)); +@@ -205,7 +205,7 @@ + if (browser->pagination && row >= ws.ws_row - 4) { + char prompt[4096]; + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" ++ "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" + "(more) => ", resp->meta, browser->plain_url); + enum prompt_result result = PROMPT_AGAIN; + while (result == PROMPT_AGAIN) { +@@ -412,7 +412,7 @@ goto next; + } + + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" ++ "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" + "=> ", + resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", + browser.plain_url); diff --git a/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch b/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch @@ -0,0 +1,28 @@ +diff --git a/src/client.c b/src/client.c +index bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e..18b5115371bc6b13b9b796a85f7ad4f826eef973 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -236,11 +236,6 @@ if (!resp) { + return; + } + +- if (resp->fd != -1) { +- close(resp->fd); +- resp->fd = -1; +- } +- + if (resp->bio) { + BIO_free_all(resp->bio); + resp->bio = NULL; +@@ -253,6 +248,11 @@ if (resp->ssl_ctx) { + SSL_CTX_free(resp->ssl_ctx); + } + free(resp->meta); ++ ++ if (resp->fd != -1) { ++ close(resp->fd); ++ resp->fd = -1; ++ } + + resp->ssl = NULL; + resp->ssl_ctx = NULL; diff --git a/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch b/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch @@ -0,0 +1,16 @@ +diff --git a/configure b/configure +index aa761971fe95d70ed242977bed8dfd6d55a9f794..70a19b193d6e33b41b5cc7ef13511bb0f5b4e076 100755 +--- a/configure ++++ b/configure +@@ -16,7 +16,7 @@ cgmnlm() { + genrules cgmnlm \ + src/client.c \ + src/escape.c \ +- src/cgmnlm.c \ ++ src/gmnlm.c \ + src/parser.c \ + src/tofu.c \ + src/url.c \ +diff --git a/src/cgmnlm.c b/src/gmnlm.c +rename from src/cgmnlm.c +rename to src/gmnlm.c diff --git a/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch b/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch @@ -0,0 +1,62 @@ +diff --git a/Makefile b/Makefile +index 3d4df602cd7f7ab4f5a45b47dee0d47729f0739c..12ff4b45e4b043ae5c93629b502e42e8396b2b24 100644 +--- a/Makefile ++++ b/Makefile +@@ -13,6 +13,7 @@ @printf 'CCLD\t$@\n' + @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) + + doc/gmni.1: doc/gmni.scd ++doc/gmnlm.1: doc/gmnlm.scd + + .SUFFIXES: .c .o .scd .1 + +@@ -27,10 +28,10 @@ .scd.1: + @printf 'SCDOC\t$@\n' + @$(SCDOC) < $< > $@ + +-docs: doc/gmni.1 ++docs: doc/gmni.1 doc/gmnlm.1 + + clean: +- @rm -f gmni doc/gmni.1 ++ @rm -f gmni doc/gmni.1 doc/gmnlm.1 + + distclean: clean + @rm -rf "$(OUTDIR)" +@@ -39,6 +40,8 @@ install: all + mkdir -p $(BINDIR) + mkdir -p $(MANDIR)/man1 + install -Dm755 gmni $(BINDIR)/gmni ++ install -Dm755 gmnlm $(BINDIR)/gmnlm + install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 ++ install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 + + .PHONY: clean distclean docs install +diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd +new file mode 100644 +index 0000000000000000000000000000000000000000..c5e7bf7f6b189f984e01ea2a942f47acb993f7e7 +--- /dev/null ++++ b/doc/gmnlm.scd +@@ -0,0 +1,22 @@ ++gmnlm(1) ++ ++# NAME ++ ++gmnlm - Gemini line-mode browser ++ ++# SYNPOSIS ++ ++*gmnlm* [-PU] _gemini://..._ ++ ++# DESCRIPTION ++ ++*gmnlm* is an interactive line-mode Gemini browser. ++ ++# OPTIONS ++ ++*-P* ++ Disable pagination. ++ ++*-U* ++ Disable conservative use of Unicode symbols to render Gemini layout ++ features. diff --git a/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch b/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch @@ -0,0 +1,21 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index cb7e2843798f66f596e071823d9e41153a555fcc..c70a4126acc5337a768c3a70513aecc1b0e16668 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -89,6 +89,7 @@ "B\tBrowse bookmarks\n" + "r\tReload the page\n" + "d <path>\tDownload page to <path>\n" + "|<prog>\tPipe page into program\n" ++ "[N]|<prog>\tPipe content of Nth link into program\n" + "\n" + "Other commands include:\n\n" + "<Enter>\tread more lines\n" +@@ -791,7 +792,7 @@ } + break; + case GEMINI_LINK: + if (text == NULL) { +- col += fprintf(out, "%2d> %s", nlinks++, ANSI_COLOR_CYAN); ++ col += fprintf(out, "%2d) %s", nlinks++, ANSI_COLOR_CYAN); + text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); + *next = calloc(1, sizeof(struct link)); + (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch b/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch @@ -0,0 +1,39 @@ +diff --git a/include/client.h b/include/client.h +index 40729073bcaa2c0a172d3375662ac91cef0ea550..671de12e7ee53fbbfc03407760723d2a66458c2d 100644 +--- a/include/client.h ++++ b/include/client.h +@@ -39,11 +39,8 @@ enum gemini_result { + GEMINI_OK, + GEMINI_ERR_OOM, + GEMINI_ERR_INVALID_URL, +- // status is set to the return value from getaddrinfo + GEMINI_ERR_RESOLVE, +- // status is set to errno + GEMINI_ERR_CONNECT, +- // use SSL_get_error(resp->ssl, resp->status) to get details + GEMINI_ERR_SSL, + GEMINI_ERR_IO, + GEMINI_ERR_PROTOCOL, +@@ -52,17 +49,16 @@ + // Requests the specified URL via the gemini protocol. If options is non-NULL, + // it may specify some additional configuration to adjust client behavior. + // +-// Returns a value indicating the success of the request. If GEMINI_OK is +-// returned, the response details shall be written to the gemini_response +-// argument. ++// Returns a value indicating the success of the request. ++// ++// 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_response *resp); + + // Must be called after gemini_request in order to free up the resources +-// allocated during the request. If you intend to re-use the SSL_CTX provided by +-// gemini_options, set the ctx pointer to NULL before calling +-// gemini_response_finish. ++// allocated during the request. + void gemini_response_finish(struct gemini_response *resp); + + // Returns a user-friendly string describing an error. diff --git a/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch b/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch @@ -0,0 +1,20 @@ +diff --git a/config.sh b/config.sh +index a6963622d4b41d6e25bb4fe230c2513849c077d3..55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe 100644 +--- a/config.sh ++++ b/config.sh +@@ -144,11 +144,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' + + all: ${all} + EOF +- +- for target in $all ++ ++ for target in $(printf '%s\n' $all | tr '.' '_') + do +- ${target//./_} >>"$outdir"/config.mk +- done ++ $target ++ done >>"$outdir"/config.mk + echo done + + touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch b/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch @@ -0,0 +1,13 @@ +diff --git a/src/parser.c b/src/parser.c +index 04501b6b644af28abe7843076ae593b7165912b2..579415150f842557b6faf4097a63aac9324928fb 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -46,7 +46,7 @@ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); + if (n == -1) { + return -1; + } else if (n == 0) { +- eof = 1; ++ eof = p->bufln == 0; + break; + } + p->bufln += n; diff --git a/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch b/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch @@ -0,0 +1,22 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 07d0000babe7cd51876ead407e83c3062adfda56..07b699134c0c71a20d4780148ad74cb246634549 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -69,7 +69,7 @@ const char *help_msg = + "The following commands are available:\n\n" + "q\tQuit\n" + "N\tFollow Nth link (where N is a number)\n" +- "p N\tShow URL of Nth link (where N is a number)\n" ++ "p[N]\tShow URL of Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" + "H\tView all page history\n" +@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; + goto exit; + } + case 'p': +- if (!isspace(in[1])) break; ++ if (!in[1]) break; + struct link *link = browser->links; + char *endptr; + int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch b/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch @@ -0,0 +1,53 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index ef4c97950495e369e4fd3b1256c185fdb01f8cf1..8cafaea17de972c240031b0fe6ab88e3621bd871 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -179,8 +179,7 @@ + n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); + assert(n < sizeof(path)); +- strncpy(dname, path, sizeof(dname)-1); +- dirname(dname); ++ posix_dirname(path, dname); + if (mkdirs(dname, 0755) != 0) { + fprintf(stderr, "Error creating directory %s: %s\n", + dname, strerror(errno)); +@@ -262,8 +261,7 @@ n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); + + assert(n < sizeof(path)); +- strncpy(dname, path, sizeof(dname)-1); +- dirname(dname); ++ posix_dirname(path, dname); + if (mkdirs(dname, 0755) != 0) { + fprintf(stderr, "Error creating directory %s: %s\n", + dname, strerror(errno)); +diff --git a/src/tofu.c b/src/tofu.c +index 54183a79278de45bfbe5a1f8ddbcbf10be1c01c4..0923aed3f3d9ac02a7bc6b81db965d6924e4af74 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -177,20 +177,15 @@ + n = snprintf(tofu->known_hosts_path, + sizeof(tofu->known_hosts_path), + path_fmt, "known_hosts"); ++ free(path_fmt); + assert(n < sizeof(tofu->known_hosts_path)); + +- strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); ++ posix_dirname(tofu->known_hosts_path, dname); + if (mkdirs(dname, 0755) != 0) { +- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), +- path_fmt, "known_hosts"); +- fprintf(stderr, "Error creating directory %s: %s\n", +- dirname(tofu->known_hosts_path), strerror(errno)); ++ fprintf(stderr, "Error creating directory %s: %s\n", dname, ++ strerror(errno)); + return; + } +- +- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), +- path_fmt, "known_hosts"); +- free(path_fmt); + + tofu->callback = cb; + tofu->cb_data = cb_data; diff --git a/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch b/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch @@ -0,0 +1,281 @@ +diff --git a/include/client.h b/include/client.h +index f711eea91c3842ac29a884b38e0d7a0ab99cb7fb..40729073bcaa2c0a172d3375662ac91cef0ea550 100644 +--- a/include/client.h ++++ b/include/client.h +@@ -68,4 +68,8 @@ + // Returns a user-friendly string describing an error. + const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); + ++// Returns the given URL with the input response set to the specified value. ++// The caller must free the string. ++char *gemini_input_url(const char *url, const char *input); ++ + #endif +diff --git a/src/client.c b/src/client.c +index 67671ccca2a25a460681bdffacb981c7f26c1407..c252c9d9ab1783dd462343ce346cc74611e9ae85 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -176,7 +176,7 @@ } + + char *endptr; + resp->status = (int)strtol(buf, &endptr, 10); +- if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { ++ if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { + res = GEMINI_ERR_PROTOCOL; + goto cleanup; + } +@@ -195,14 +195,25 @@ { + if (!resp) { + return; + } ++ + if (resp->fd != -1) { + close(resp->fd); ++ resp->fd = -1; + } +- BIO_free(BIO_pop(resp->bio)); // ssl bio +- BIO_free(resp->bio); // buffered bio ++ ++ if (resp->bio) { ++ BIO_free(BIO_pop(resp->bio)); // ssl bio ++ BIO_free(resp->bio); // buffered bio ++ resp->bio = NULL; ++ } ++ + SSL_free(resp->ssl); + SSL_CTX_free(resp->ssl_ctx); + free(resp->meta); ++ ++ resp->ssl = NULL; ++ resp->ssl_ctx = NULL; ++ resp->meta = NULL; + } + + const char * +@@ -230,3 +241,26 @@ return "Protocol error"; + } + assert(0); + } ++ ++char * ++gemini_input_url(const char *url, const char *input) ++{ ++ char *new_url = NULL; ++ struct Curl_URL *uri = curl_url(); ++ if (!uri) { ++ return NULL; ++ } ++ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { ++ goto cleanup; ++ } ++ if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) { ++ goto cleanup; ++ } ++ if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) { ++ new_url = NULL; ++ goto cleanup; ++ } ++cleanup: ++ curl_url_cleanup(uri); ++ return new_url; ++} +diff --git a/src/gmnic.c b/src/gmnic.c +index 014211dc3784ba3eadcf1885ef7bcf6fe84d8462..794b94ba2235757864acb81194b8de88220cc6da 100644 +--- a/src/gmnic.c ++++ b/src/gmnic.c +@@ -26,10 +26,17 @@ OMIT_HEADERS, + SHOW_HEADERS, + ONLY_HEADERS, + }; +- enum header_mode headers = OMIT_HEADERS; ++ enum header_mode header_mode = OMIT_HEADERS; ++ ++ enum input_mode { ++ INPUT_READ, ++ INPUT_SUPPRESS, ++ }; ++ enum input_mode input_mode = INPUT_READ; ++ FILE *input_source = stdin; + + int c; +- while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { ++ while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { + switch (c) { + case '4': + assert(0); // TODO +@@ -41,7 +48,21 @@ case 'C': + assert(0); // TODO: Client certificates + break; + case 'd': +- assert(0); // TODO: Input ++ input_mode = INPUT_READ; ++ input_source = fmemopen(optarg, strlen(optarg), "r"); ++ break; ++ case 'D': ++ input_mode = INPUT_READ; ++ if (strcmp(optarg, "-") == 0) { ++ input_source = stdin; ++ } else { ++ input_source = fopen(optarg, "r"); ++ if (!input_source) { ++ fprintf(stderr, "Error: open %s: %s", ++ optarg, strerror(errno)); ++ return 1; ++ } ++ } + break; + case 'h': + usage(argv[0]); +@@ -50,10 +71,14 @@ case 'L': + assert(0); // TODO: Follow redirects + break; + case 'i': +- headers = SHOW_HEADERS; ++ header_mode = SHOW_HEADERS; + break; + case 'I': +- headers = ONLY_HEADERS; ++ header_mode = ONLY_HEADERS; ++ input_mode = INPUT_SUPPRESS; ++ break; ++ case 'N': ++ input_mode = INPUT_SUPPRESS; + break; + default: + fprintf(stderr, "fatal: unknown flag %c", c); +@@ -69,43 +94,105 @@ + SSL_load_error_strings(); + ERR_load_crypto_strings(); + +- struct gemini_response resp; +- enum gemini_result r = gemini_request(argv[optind], NULL, &resp); +- if (r != GEMINI_OK) { +- fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); +- gemini_response_finish(&resp); +- return (int)r; +- } ++ bool exit = false; ++ char *url = strdup(argv[optind]); ++ ++ int ret = 0; ++ while (!exit) { ++ struct gemini_response resp; ++ enum gemini_result r = gemini_request(url, NULL, &resp); ++ if (r != GEMINI_OK) { ++ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); ++ ret = (int)r; ++ exit = true; ++ goto next; ++ } ++ ++ char *new_url, *input = NULL; ++ switch (resp.status / 10) { ++ case 1: // INPUT ++ if (input_mode == INPUT_SUPPRESS) { ++ exit = true; ++ break; ++ } ++ ++ if (fileno(input_source) != -1 && ++ isatty(fileno(input_source))) { ++ fprintf(stderr, "%s: ", resp.meta); ++ } + +- switch (headers) { +- case ONLY_HEADERS: +- printf("%d %s\n", resp.status, resp.meta); +- break; +- case SHOW_HEADERS: +- printf("%d %s\n", resp.status, resp.meta); +- /* fallthrough */ +- case OMIT_HEADERS: +- for (int n = 1; n > 0;) { +- char buf[BUFSIZ]; +- n = BIO_read(resp.bio, buf, BUFSIZ); ++ size_t s = 0; ++ ssize_t n = getline(&input, &s, input_source); + if (n == -1) { +- fprintf(stderr, "Error: read\n"); +- return 1; ++ fprintf(stderr, "Error reading input: %s\n", ++ feof(input_source) ? "EOF" : ++ strerror(ferror(input_source))); ++ r = 1; ++ exit = true; ++ break; ++ } ++ input[n - 1] = '\0'; // Drop LF ++ ++ new_url = gemini_input_url(url, input); ++ free(url); ++ url = new_url; ++ goto next; ++ case 3: // REDIRECT ++ assert(0); // TODO ++ case 6: // CLIENT CERTIFICATE REQUIRED ++ assert(0); // TODO ++ case 4: // TEMPORARY FAILURE ++ case 5: // PERMANENT FAILURE ++ if (header_mode == OMIT_HEADERS) { ++ fprintf(stderr, "%s: %d %s\n", ++ resp.status / 10 == 4 ? ++ "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ resp.status, resp.meta); + } +- ssize_t w = 0; +- while (w < (ssize_t)n) { +- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); +- if (x == -1) { +- fprintf(stderr, "Error: write: %s\n", +- strerror(errno)); ++ exit = true; ++ break; ++ case 2: // SUCCESS ++ exit = true; ++ break; ++ } ++ ++ switch (header_mode) { ++ case ONLY_HEADERS: ++ printf("%d %s\n", resp.status, resp.meta); ++ break; ++ case SHOW_HEADERS: ++ printf("%d %s\n", resp.status, resp.meta); ++ /* fallthrough */ ++ case OMIT_HEADERS: ++ if (resp.status / 10 != 2) { ++ break; ++ } ++ for (int n = 1; n > 0;) { ++ char buf[BUFSIZ]; ++ n = BIO_read(resp.bio, buf, BUFSIZ); ++ if (n == -1) { ++ fprintf(stderr, "Error: read\n"); + return 1; + } +- w += x; ++ ssize_t w = 0; ++ while (w < (ssize_t)n) { ++ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); ++ if (x == -1) { ++ fprintf(stderr, "Error: write: %s\n", ++ strerror(errno)); ++ return 1; ++ } ++ w += x; ++ } + } ++ break; + } +- break; ++ ++next: ++ gemini_response_finish(&resp); + } + +- gemini_response_finish(&resp); +- return 0; ++ (void)input_mode; ++ free(url); ++ return ret; + } diff --git a/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch b/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch @@ -0,0 +1,115 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index bc3f647d42372678b4180f539df8637d0ba69a12..dd8c8d200a949a24d030ce3429a4d20f1430fe97 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -63,9 +63,11 @@ // TODO: word wrap + col += fprintf(tty, " %s\n", trim_ws(tok.text)); + break; + case GEMINI_LINK: +- (void)next; // TODO: Record links + col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( + tok.link.text ? tok.link.text : tok.link.url)); ++ *next = calloc(1, sizeof(struct link)); ++ (*next)->url = strdup(trim_ws(tok.link.url)); ++ next = &(*next)->next; + break; + case GEMINI_PREFORMATTED: + continue; // TODO +@@ -117,7 +119,6 @@ main(int argc, char *argv[]) + { + bool pagination = true; + +- bool have_url = false; + struct Curl_URL *url = curl_url(); + + FILE *tty = fopen("/dev/tty", "w+"); +@@ -139,8 +140,7 @@ } + + if (optind == argc - 1) { + set_url(url, argv[optind]); +- have_url = true; +- } else if (optind < argc - 1) { ++ } else { + usage(argv[0]); + return 1; + } +@@ -154,8 +154,6 @@ + bool run = true; + struct gemini_response resp; + while (run) { +- assert(have_url); // TODO +- + struct link *links; + static char prompt[4096]; + +@@ -164,7 +162,8 @@ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); + assert(uc == CURLUE_OK); // Invariant + + snprintf(prompt, sizeof(prompt), "\n\t%s\n" +- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" ++ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " ++ "[b]ack; [f]orward\n" + "=> ", plain_url); + + enum gemini_result res = gemini_request(plain_url, &opts, &resp); +@@ -194,19 +193,49 @@ } + + gemini_response_finish(&resp); + +- fprintf(tty, "%s", prompt); +- size_t l = 0; +- char *in = NULL; +- ssize_t n = getline(&in, &l, tty); +- if (n == -1 && feof(tty)) { +- break; +- } ++ bool prompting = true; ++ while (prompting) { ++ fprintf(tty, "%s", prompt); ++ ++ size_t l = 0; ++ char *in = NULL; ++ ssize_t n = getline(&in, &l, tty); ++ if (n == -1 && feof(tty)) { ++ prompting = run = false; ++ break; ++ } ++ if (strcmp(in, "q\n") == 0) { ++ prompting = run = false; ++ break; ++ } ++ ++ struct link *link = links; ++ char *endptr; ++ int linksel = (int)strtol(in, &endptr, 10); ++ if (endptr[0] == '\n' && linksel >= 0) { ++ while (linksel > 0 && link) { ++ link = link->next; ++ --linksel; ++ } + +- if (strcmp(in, "q\n") == 0) { +- run = false; +- } ++ if (!link) { ++ fprintf(stderr, "Error: no such link.\n"); ++ } else { ++ prompting = false; ++ set_url(url, link->url); ++ } ++ } + +- free(in); ++ link = links; ++ while (link) { ++ struct link *next = link->next; ++ free(link->url); ++ free(link); ++ link = next; ++ } ++ ++ free(in); ++ } + } + + SSL_CTX_free(opts.ssl_ctx); diff --git a/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch b/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch @@ -0,0 +1,116 @@ +diff --git a/src/client.c b/src/client.c +index 86152d6183462636cca4eecbd47824569a688cb1..f1674d534a9a5f2edcb6c7be678e17ad5724a9a7 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -175,7 +175,7 @@ goto cleanup; + } + + char *endptr; +- resp->status = (int)strtol(buf, &endptr, 10); ++ resp->status = (enum gemini_status)strtol(buf, &endptr, 10); + if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { + res = GEMINI_ERR_PROTOCOL; + goto cleanup; +@@ -268,5 +268,5 @@ + enum gemini_status_class + gemini_response_class(enum gemini_status status) + { +- return status / 10; ++ return status / 10 * 10; + } +diff --git a/src/gmni.c b/src/gmni.c +index b4efdc0235dac0279ce284f2c6b19f680a191647..5b1f37522e93da360e74ba92ccb00c530bd41463 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -10,6 +10,7 @@ #include <stdlib.h> + #include <string.h> + #include <sys/socket.h> + #include <sys/types.h> ++#include <termios.h> + #include <unistd.h> + #include "gmni.h" + +@@ -19,6 +20,41 @@ { + fprintf(stderr, + "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", + argv_0); ++} ++ ++static char * ++get_input(const struct gemini_response *resp, FILE *source) ++{ ++ int r = 0; ++ struct termios attrs; ++ bool tty = fileno(source) != -1 && isatty(fileno(source)); ++ char *input = NULL; ++ if (tty) { ++ fprintf(stderr, "%s: ", resp->meta); ++ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { ++ r = tcgetattr(fileno(source), &attrs); ++ struct termios new_attrs; ++ r = tcgetattr(fileno(source), &new_attrs); ++ if (r != -1) { ++ new_attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &new_attrs); ++ } ++ } ++ } ++ size_t s = 0; ++ ssize_t n = getline(&input, &s, source); ++ if (n == -1) { ++ fprintf(stderr, "Error reading input: %s\n", ++ feof(source) ? "EOF" : ++ strerror(ferror(source))); ++ return NULL; ++ } ++ input[n - 1] = '\0'; // Drop LF ++ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { ++ attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &attrs); ++ } ++ return input; + } + + int +@@ -119,7 +155,6 @@ exit = true; + goto next; + } + +- char *new_url, *input = NULL; + switch (gemini_response_class(resp.status)) { + case GEMINI_STATUS_CLASS_INPUT: + if (input_mode == INPUT_SUPPRESS) { +@@ -127,27 +162,19 @@ exit = true; + break; + } + +- if (fileno(input_source) != -1 && +- isatty(fileno(input_source))) { +- fprintf(stderr, "%s: ", resp.meta); +- } +- +- size_t s = 0; +- ssize_t n = getline(&input, &s, input_source); +- if (n == -1) { +- fprintf(stderr, "Error reading input: %s\n", +- feof(input_source) ? "EOF" : +- strerror(ferror(input_source))); ++ char *input = get_input(&resp, input_source); ++ if (!input) { + r = 1; + exit = true; + break; + } +- input[n - 1] = '\0'; // Drop LF + +- new_url = gemini_input_url(url, input); ++ char *new_url = gemini_input_url(url, input); ++ assert(new_url); ++ ++ free(input); + free(url); + url = new_url; +- assert(url); + goto next; + case GEMINI_STATUS_CLASS_REDIRECT: + free(url); diff --git a/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch b/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch @@ -0,0 +1,20 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index cec330b44ae6e9252e92fbd1a5d74e7592b84e2a..9efe3e231d445d55ea43d444a72eaa79f595ac76 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -475,7 +475,6 @@ } + } else { + browser->opts.client_cert = NULL; + } +- free(host); + } + + while (requesting) { +@@ -600,6 +599,7 @@ if (client_cert.key) { + free(client_cert.key); + } + free(scheme); ++ free(host); + return res; + } + diff --git a/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch b/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch @@ -0,0 +1,87 @@ +diff --git a/README.md b/README.md +index fa803f99158582d982ef3a01d6b87471fb27f0cb..9aa989fe358ecccd9b984b1aff0df97c6c2ceec0 100644 +--- a/README.md ++++ b/README.md +@@ -29,7 +29,7 @@ - s command to directly search in geminispace (via geminispace.info) + - k command to remove the bookmark for the current page + - e[N] command to open a link in default external program (requires `xdg-open`) + - t[N] command to download the content behind a link to a temporary file +-- b & f commands to navigate history can jump multiple entries at once ++- a command to switch between display of preformatted blocks and alt text (if available) + + The actual colors used depend on your terminal palette: + - heading 1: light red +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 8cafaea17de972c240031b0fe6ab88e3621bd871..77d03cb16e1e27c440ecb1c5b7d154db50659dc6 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -44,7 +44,7 @@ struct history *prev, *next; + }; + + struct browser { +- bool pagination, unicode; ++ bool pagination, unicode, alttext; + int max_width; + struct gemini_options opts; + struct gemini_tofu tofu; +@@ -103,6 +103,7 @@ "n\t\tjump to next search match\n" + "d <path>\tDownload page to <path>\n" + "|<prog>\t\tPipe page into program\n" + "[N]|<prog>\tPipe content of Nth link into program\n" ++ "a\t\ttoggle usage of alt text instead of preformatted text\n" + "q\t\tQuit\n" + "\n" + ; +@@ -636,6 +637,11 @@ if (in[1]) break; + set_url(browser, "gemini://geminispace.info/search", &browser->history); + result = PROMPT_ANSWERED; + goto exit; ++ case 'a': ++ browser->alttext = !browser->alttext; ++ fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n", browser->alttext ? "ENABLED" : "DISABLED"); ++ result = PROMPT_AGAIN; ++ goto exit; + case 'b': + if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); + while (historyhops > 0) { +@@ -939,6 +945,7 @@ + fprintf(out, "\n"); + char *text = NULL; + int row = 0, col = 0; ++ bool no_alttext; + struct gemini_token tok; + struct link **next = &browser->links; + while (text != NULL || gemini_parser_next(&p, &tok) == 0) { +@@ -962,13 +969,21 @@ col += fprintf(out, " "); + } + break; + case GEMINI_PREFORMATTED_BEGIN: +- gemini_token_finish(&tok); ++ if (text == NULL && browser->alttext) { ++ if (tok.preformatted == NULL) { ++ no_alttext = true; ++ } else { ++ fprintf(out, " A %s", ANSI_COLOR_GRAY); ++ text = tok.preformatted; ++ } ++ } ++ break; + /* fallthrough */ + case GEMINI_PREFORMATTED_END: + continue; // Not used + case GEMINI_PREFORMATTED_TEXT: +- if (text == NULL) { +- fprintf(out, " %s", ANSI_COLOR_GRAY); ++ if (text == NULL && (!browser->alttext || no_alttext)) { ++ fprintf(out, " P %s", ANSI_COLOR_GRAY); + text = tok.preformatted; + } + break; +@@ -1241,6 +1256,7 @@ main(int argc, char *argv[]) + { + struct browser browser = { + .pagination = true, ++ .alttext = false, + .tofu_mode = TOFU_ASK, + .unicode = true, + .url = curl_url(), diff --git a/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch b/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch @@ -0,0 +1,46 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index d22f64ce27dfcf61eed80ed09b2d072efb8d71e0..6ad61939f8dcd6f6cf530f5615e442fcfb92ffde 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -71,13 +71,13 @@ + const char *help_msg = + "The following commands are available:\n\n" + "q\tQuit\n" +- "N\tFollow Nth link (where N is a number)\n" +- "p[N]\tShow URL of Nth link (where N is a number)\n" ++ "[N]\tFollow Nth link (where N is a number)\n" ++ "u[N]\tShow URL of Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" + "H\tView all page history\n" +- "m\tSave bookmark\n" +- "M\tBrowse bookmarks\n" ++ "a\tSave bookmark\n" ++ "B\tBrowse bookmarks\n" + "r\tReload the page\n" + "d <path>\tDownload page to <path>\n" + "|<prog>\tPipe page into program\n" +@@ -549,12 +549,12 @@ cur = cur->next; + } + result = PROMPT_AGAIN; + goto exit; +- case 'm': ++ case 'a': + if (in[1]) break; + save_bookmark(browser); + result = PROMPT_AGAIN; + goto exit; +- case 'M': ++ case 'B': + if (in[1]) break; + open_bookmarks(browser); + result = PROMPT_ANSWERED; +@@ -582,7 +582,7 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); + result = PROMPT_AGAIN; + goto exit; + } +- case 'p': ++ case 'u': + if (!in[1]) break; + struct link *link = browser->links; + char *endptr; diff --git a/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch b/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch @@ -0,0 +1,13 @@ +diff --git a/README.md b/README.md +index 842e238162c33eec93dad8b10abb7bfe21af447b..dfd2e1744003996f1e629c78e459dac70985a430 100644 +--- a/README.md ++++ b/README.md +@@ -5,7 +5,7 @@ + - A CLI utility (like curl): gmni + - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm + +-[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) ++[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) + + Dependencies: + diff --git a/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch b/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch @@ -0,0 +1,120 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 2375151c7f5fced09a80e41d26809730827f8925..4b844200eeb4318d694566d7a84eb3e64c7c7668 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -82,6 +82,7 @@ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { + fprintf(stderr, "Error: invalid URL\n"); + return false; + } ++ curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); + if (history) { + struct history *next = calloc(1, sizeof(struct history)); + curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); +@@ -118,19 +119,23 @@ case '\0': + result = PROMPT_MORE; + goto exit; + case 'q': ++ if (in[1]) break; + result = PROMPT_QUIT; + goto exit; + case 'b': ++ if (in[1]) break; + if (!browser->history->prev) { + fprintf(stderr, "At beginning of history\n"); + result = PROMPT_AGAIN; + goto exit; + } ++ if (in[1]) break; + browser->history = browser->history->prev; + set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; + case 'f': ++ if (in[1]) break; + if (!browser->history->next) { + fprintf(stderr, "At end of history\n"); + result = PROMPT_AGAIN; +@@ -141,6 +146,7 @@ set_url(browser, browser->history->url, NULL); + result = PROMPT_ANSWERED; + goto exit; + case '/': ++ if (in[1]) break; + if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { + static char buf[1024]; + r = regerror(r, &browser->regex, buf, sizeof(buf)); +@@ -153,6 +159,7 @@ result = PROMPT_ANSWERED; + } + goto exit_re; + case 'n': ++ if (in[1]) break; + if (browser->searching) { + result = PROMPT_NEXT; + goto exit_re; +@@ -162,6 +169,7 @@ result = PROMPT_AGAIN; + goto exit; + } + case '?': ++ if (in[1]) break; + fprintf(browser->tty, "%s", help_msg); + result = PROMPT_AGAIN; + goto exit; +@@ -465,6 +473,17 @@ } + return input; + } + ++static bool ++has_suffix(char *str, char *suff) ++{ ++ size_t suffl = strlen(suff); ++ size_t strl = strlen(str); ++ if (strl < suffl) { ++ return false; ++ } ++ return strcmp(&str[strl - suffl], suff) == 0; ++} ++ + // Returns true to skip prompting + static bool + do_requests(struct browser *browser, struct gemini_response *resp) +@@ -472,9 +491,40 @@ { + int nredir = 0; + bool requesting = true; + while (requesting) { ++ char *scheme; + CURLUcode uc = curl_url_get(browser->url, +- CURLUPART_URL, &browser->plain_url, 0); ++ CURLUPART_SCHEME, &scheme, 0); + assert(uc == CURLUE_OK); // Invariant ++ if (strcmp(scheme, "file") == 0) { ++ requesting = false; ++ ++ char *path; ++ uc = curl_url_get(browser->url, ++ CURLUPART_PATH, &path, 0); ++ if (uc != CURLUE_OK) { ++ resp->status = GEMINI_STATUS_BAD_REQUEST; ++ break; ++ } ++ ++ FILE *fp = fopen(path, "r"); ++ if (!fp) { ++ resp->status = GEMINI_STATUS_NOT_FOUND; ++ 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")) { ++ resp->meta = strdup("text/plain"); ++ } else { ++ resp->meta = strdup("application/x-octet-stream"); ++ } ++ resp->status = GEMINI_STATUS_SUCCESS; ++ return display_response(browser, resp); ++ } + + enum gemini_result res = gemini_request(browser->plain_url, + &browser->opts, resp); diff --git a/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch b/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch @@ -0,0 +1,28 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 37b4db277d8fcd961eb1746294724783e638109c..3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -684,15 +684,19 @@ goto exit; + case 'H': + if (in[1]) break; + struct history *cur = browser->history; +- while (cur->prev) cur = cur->prev; ++ int hist_count = 0; ++ while (cur->prev) { ++ cur = cur->prev; ++ hist_count++; ++ } + while (cur != browser->history) { +- fprintf(browser->tty, " %s\n", cur->url); ++ fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url); + cur = cur->next; + } +- fprintf(browser->tty, "* %s\n", cur->url); ++ fprintf(browser->tty, "* %s\n", cur->url); + cur = cur->next; + while (cur) { +- fprintf(browser->tty, " %s\n", cur->url); ++ fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url); + cur = cur->next; + } + result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch b/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 9f76188252a30e95868b99d64d57369ac03a7e1c..23b8e6054c6824ebfe4aba19404d7eeb2c3dab35 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -471,7 +471,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: + requesting = false; + fprintf(stderr, "Server returned %s %d %s\n", + resp->status / 10 == 4 ? +- "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ "TEMPORARY FAILURE" : "PERMANENT FAILURE", + resp->status, resp->meta); + break; + case GEMINI_STATUS_CLASS_SUCCESS: diff --git a/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch b/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch @@ -0,0 +1,35 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 23b8e6054c6824ebfe4aba19404d7eeb2c3dab35..c5419243f983c878abff1cfb36633d648f6449f2 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -159,7 +159,7 @@ size_t n; + + n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + assert(n < sizeof(path)); +- strncpy(dname, dirname(path), sizeof(dname)); ++ strncpy(dname, dirname(path), sizeof(dname)-1); + if (mkdirs(dname, 0755) != 0) { + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); +@@ -200,7 +200,7 @@ size_t n; + + n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + assert(n < sizeof(path)); +- strncpy(dname, dirname(path), sizeof(dname)); ++ strncpy(dname, dirname(path), sizeof(dname)-1); + if (mkdirs(dname, 0755) != 0) { + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); +diff --git a/src/tofu.c b/src/tofu.c +index 16548a1cbcebc8c4cdda4a57a4655a04e7f390a3..b9100c77fd61c71ce561f2194b991eee7130e689 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -164,7 +164,7 @@ n = snprintf(tofu->known_hosts_path, 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)); ++ strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); + if (mkdirs(dname, 0755) != 0) { + snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), + path_fmt, "known_hosts"); diff --git a/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch b/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch @@ -0,0 +1,14 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index def42ac8b1af405da29681d29cd7b9d07b727f11..07d0000babe7cd51876ead407e83c3062adfda56 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -380,6 +380,9 @@ assert(0); // Not supposed to happen + case '\t': + *col = *col + (8 - *col % 8); + break; ++ case '\r': ++ if (!s[i+1]) break; ++ /* fallthrough */ + default: + if (iscntrl(s[i])) { + s[i] = '.'; diff --git a/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch b/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch @@ -0,0 +1,197 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 26654324b4fb845633f7db2b2ac01e5e3b4635a1..99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -35,6 +35,7 @@ struct gemini_tofu tofu; + enum tofu_action tofu_mode; + + FILE *tty; ++ char *meta; + char *plain_url; + char *page_title; + struct Curl_URL *url; +@@ -206,6 +207,52 @@ snprintf(url, sizeof(url), "file://%s", path); + set_url(browser, url, &browser->history); + } + ++static void ++print_media_parameters(FILE *out, char *params) ++{ ++ if (params == NULL) { ++ fprintf(out, "No media parameters\n"); ++ return; ++ } ++ for (char *param = strtok(params, ";"); param; ++ param = strtok(NULL, ";")) { ++ char *value = strchr(param, '='); ++ if (value == NULL) { ++ fprintf(out, "Invalid media type parameter '%s'\n", ++ trim_ws(param)); ++ continue; ++ } ++ *value = 0; ++ fprintf(out, "%s: ", trim_ws(param)); ++ *value++ = '='; ++ if (*value != '"') { ++ fprintf(out, "%s\n", value); ++ continue; ++ } ++ while (value++) { ++ switch (*value) { ++ case '\0': ++ if ((value = strtok(NULL, ";")) != NULL) { ++ fprintf(out, ";%c", *value); ++ } ++ break; ++ case '"': ++ value = NULL; ++ break; ++ case '\\': ++ if (value[1] == '\0') { ++ break; ++ } ++ value++; ++ /* fallthrough */ ++ default: ++ putc(*value, out); ++ } ++ } ++ putc('\n', out); ++ } ++} ++ + static enum prompt_result + do_prompts(const char *prompt, struct browser *browser) + { +@@ -329,6 +376,12 @@ case 'r': + if (in[1]) break; + result = PROMPT_ANSWERED; + goto exit; ++ case 'i': ++ if (in[1]) break; ++ print_media_parameters(browser->tty, browser->meta ++ ? strchr(browser->meta, ';') : NULL); ++ result = PROMPT_AGAIN; ++ goto exit; + case '?': + if (in[1]) break; + fprintf(browser->tty, "%s", help_msg); +@@ -539,12 +592,19 @@ ++row; col = 0; + + if (browser->pagination && row >= ws.ws_row - 4) { + char prompt[4096]; ++ char *end = NULL; ++ if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { ++ *end = 0; ++ } + snprintf(prompt, sizeof(prompt), "\n%s at %s\n" + "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" + "(more) => ", resp->meta, browser->plain_url, + browser->searching ? "[n]ext result; " : "", + browser->history->prev ? "[b]ack; " : "", + browser->history->next ? "[f]orward; " : ""); ++ if (end != NULL) { ++ *end = ';'; ++ } + enum prompt_result result = PROMPT_AGAIN; + while (result == PROMPT_AGAIN) { + result = do_prompts(prompt, browser); +@@ -714,6 +774,7 @@ &browser->opts, resp); + if (res != GEMINI_OK) { + fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); + requesting = false; ++ resp->status = 70 + res; + break; + } + +@@ -858,6 +919,7 @@ .tofu_mode = TOFU_ASK, + .unicode = true, + .url = curl_url(), + .tty = fopen("/dev/tty", "w+"), ++ .meta = NULL, + }; + + int c; +@@ -909,38 +971,45 @@ struct gemini_response resp; + browser.running = true; + while (browser.running) { + static char prompt[4096]; +- if (do_requests(&browser, &resp)) { +- // Skip prompts +- gemini_response_finish(&resp); +- goto next; ++ bool skip_prompt = do_requests(&browser, &resp); ++ if (browser.meta) { ++ free(browser.meta); + } +- +- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" +- "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" +- "=> ", +- resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", +- browser.plain_url, +- browser.history->prev ? "[b]ack; " : "", +- browser.history->next ? "[f]orward; " : ""); ++ browser.meta = resp.status == GEMINI_STATUS_SUCCESS ++ ? strdup(resp.meta) : NULL; + gemini_response_finish(&resp); ++ if (!skip_prompt) { ++ char *end = NULL; ++ if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { ++ *end = 0; ++ } ++ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" ++ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" ++ "=> ", browser.meta ? browser.meta ++ : "[request failed]", browser.plain_url, ++ browser.history->prev ? "[b]ack; " : "", ++ browser.history->next ? "[f]orward; " : ""); ++ if (end != NULL) { ++ *end = ';'; ++ } + +- enum prompt_result result = PROMPT_AGAIN; +- while (result == PROMPT_AGAIN || result == PROMPT_MORE) { +- result = do_prompts(prompt, &browser); +- } +- switch (result) { +- case PROMPT_AGAIN: +- case PROMPT_MORE: +- assert(0); +- case PROMPT_QUIT: +- browser.running = false; +- break; +- case PROMPT_ANSWERED: +- case PROMPT_NEXT: +- break; ++ enum prompt_result result = PROMPT_AGAIN; ++ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { ++ result = do_prompts(prompt, &browser); ++ } ++ switch (result) { ++ case PROMPT_AGAIN: ++ case PROMPT_MORE: ++ assert(0); ++ case PROMPT_QUIT: ++ browser.running = false; ++ break; ++ case PROMPT_ANSWERED: ++ case PROMPT_NEXT: ++ break; ++ } + } + +-next:; + struct link *link = browser.links; + while (link) { + struct link *next = link->next; +@@ -961,6 +1030,9 @@ SSL_CTX_free(browser.opts.ssl_ctx); + curl_url_cleanup(browser.url); + free(browser.page_title); + free(browser.plain_url); ++ if (browser.meta != NULL) { ++ free(browser.meta); ++ } + fclose(browser.tty); + return 0; + } diff --git a/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch b/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch @@ -0,0 +1,30 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 489a6833f75c9e6a13562c84dc8e3ded24be23c4..930ab19abe717b186070151a184b2a8e98542f77 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -82,6 +82,7 @@ "<Enter>\t\tread more lines (if available)\n" + "<url>\t\tgo to url\n" + "[N]\t\tFollow Nth link (where N is a number)\n" + "p[N]\t\tPrint URL of Nth link (where N is a number)\n" ++ "e\t\tSend current URL of browser to external default program\n" + "e[N]\t\tSend URL of Nth link in external default program\n" + "t[N]\t\tDownload content of Nth link to a temporary file\n" + "b[N]\t\tJump back N entries in history, N is optional, default 1\n" +@@ -645,7 +646,16 @@ } + case 'e': + case 'p': + case 't': +- if (!in[1]) break; ++ if (!in[1]) { ++ if (in[0] == 'e') { ++ char xdgopen[4096]; ++ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); ++ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); ++ goto exit; ++ } else { ++ break; ++ } ++ } + linksel = (int)strtol(in+1, &endptr, 10); + if (!endptr[0] && linksel >= 0) { + while (linksel > 0 && link) { diff --git a/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch b/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch @@ -0,0 +1,13 @@ +diff --git a/Makefile b/Makefile +index 22fed5d19ec4f989ead26d22d3d1887614e4927c..c686cc60d215bc9e6032b429a1bc8babc6509a2f 100644 +--- a/Makefile ++++ b/Makefile +@@ -40,7 +40,7 @@ @printf 'CC\t$@\n' + @touch $(OUTDIR)/cppcache + @grep $< $(OUTDIR)/cppcache >/dev/null || \ + $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache +- @$(CC) -c -fPIC $(CFLAGS) -o $@ $< ++ @$(CC) -c $(CFLAGS) -o $@ $< + + .scd.1: + @printf 'SCDOC\t$@\n' diff --git a/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch b/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch @@ -0,0 +1,12 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 77d03cb16e1e27c440ecb1c5b7d154db50659dc6..041096cc5f0117632f80b0427b9a916aaa6401c2 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -1085,6 +1085,7 @@ + switch (result) { + case PROMPT_AGAIN: + case PROMPT_MORE: ++ printf("\n"); + break; + case PROMPT_QUIT: + browser->running = false; diff --git a/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch b/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch @@ -0,0 +1,50 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6..85917dfa97c034af41b9ab15e45139d6b95709de 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -834,7 +834,7 @@ } + + static enum tofu_action + tofu_callback(enum tofu_error error, const char *fingerprint, +- struct known_host *host, void *data) ++ struct known_host *khost, void *data) + { + struct browser *browser = data; + if (browser->tofu_mode != TOFU_ASK) { +@@ -852,15 +852,22 @@ "you should not disclose personal information or trust the contents of the page.\n" + "trust [o]nce; [a]bort\n" + "=> "); + break; +- case TOFU_UNTRUSTED_CERT: ++ case TOFU_UNTRUSTED_CERT:; ++ char *host; ++ if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { ++ fprintf(stderr, "Error: invalid URL %s\n", ++ browser->plain_url); ++ return TOFU_FAIL; ++ } + snprintf(prompt, sizeof(prompt), +- "The certificate offered by this server is of unknown trust. " ++ "The certificate offered by %s is of unknown trust. " + "Its fingerprint is: \n" + "%s\n\n" + "If you knew the fingerprint to expect in advance, verify that this matches.\n" + "Otherwise, it should be safe to trust this certificate.\n\n" + "[t]rust always; trust [o]nce; [a]bort\n" +- "=> ", fingerprint); ++ "=> ", host, fingerprint); ++ free(host); + break; + case TOFU_FINGERPRINT_MISMATCH: + snprintf(prompt, sizeof(prompt), +@@ -871,8 +878,8 @@ "%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, +- browser->tofu.known_hosts_path, host->lineno); ++ fingerprint, khost->fingerprint, ++ browser->tofu.known_hosts_path, khost->lineno); + return TOFU_FAIL; + } + diff --git a/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch b/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch @@ -0,0 +1,164 @@ +diff --git a/include/client.h b/include/gmni.h +rename from include/client.h +rename to include/gmni.h +index 671de12e7ee53fbbfc03407760723d2a66458c2d..42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6 100644 +--- a/include/client.h ++++ b/include/gmni.h +@@ -4,8 +4,49 @@ #include <netdb.h> + #include <openssl/ssl.h> + #include <sys/socket.h> + ++enum gemini_result { ++ GEMINI_OK, ++ GEMINI_ERR_OOM, ++ GEMINI_ERR_INVALID_URL, ++ GEMINI_ERR_RESOLVE, ++ GEMINI_ERR_CONNECT, ++ GEMINI_ERR_SSL, ++ GEMINI_ERR_IO, ++ GEMINI_ERR_PROTOCOL, ++}; ++ ++enum gemini_status { ++ GEMINI_STATUS_INPUT = 10, ++ GEMINI_STATUS_SENSITIVE_INPUT = 11, ++ GEMINI_STATUS_SUCCESS = 20, ++ GEMINI_STATUS_REDIRECT_TEMPORARY = 30, ++ GEMINI_STATUS_REDIRECT_PERMANENT = 31, ++ GEMINI_STATUS_TEMPORARY_FAILURE = 40, ++ GEMINI_STATUS_SERVER_UNAVAILABLE = 41, ++ GEMINI_STATUS_CGI_ERROR = 42, ++ GEMINI_STATUS_PROXY_ERROR = 43, ++ GEMINI_STATUS_SLOW_DOWN = 44, ++ GEMINI_STATUS_PERMANENT_FAILURE = 50, ++ GEMINI_STATUS_NOT_FOUND = 51, ++ GEMINI_STATUS_GONE = 52, ++ GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, ++ GEMINI_STATUS_BAD_REQUEST = 59, ++ GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, ++ GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, ++ GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, ++}; ++ ++enum gemini_status_class { ++ GEMINI_STATUS_CLASS_INPUT = 10, ++ GEMINI_STATUS_CLASS_SUCCESS = 20, ++ GEMINI_STATUS_CLASS_REDIRECT = 30, ++ GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, ++ GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, ++ GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, ++}; ++ + struct gemini_response { +- int status; ++ enum gemini_status status; + char *meta; + + // Response body may be read from here if appropriate: +@@ -35,17 +76,6 @@ // example, to force IPv4/IPv6. + struct addrinfo *hints; + }; + +-enum gemini_result { +- GEMINI_OK, +- GEMINI_ERR_OOM, +- GEMINI_ERR_INVALID_URL, +- GEMINI_ERR_RESOLVE, +- GEMINI_ERR_CONNECT, +- GEMINI_ERR_SSL, +- GEMINI_ERR_IO, +- GEMINI_ERR_PROTOCOL, +-}; +- + // Requests the specified URL via the gemini protocol. If options is non-NULL, + // it may specify some additional configuration to adjust client behavior. + // +@@ -67,5 +97,9 @@ + // Returns the given URL with the input response set to the specified value. + // The caller must free the string. + char *gemini_input_url(const char *url, const char *input); ++ ++// Returns the general response class (i.e. with the second digit set to zero) ++// of the given Gemini status code. ++enum gemini_status_class gemini_response_class(enum gemini_status status); + + #endif +diff --git a/src/client.c b/src/client.c +index 59fa1381ab66496900acb4321f22c6ec3675db5e..86152d6183462636cca4eecbd47824569a688cb1 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -9,7 +9,7 @@ #include <string.h> + #include <sys/socket.h> + #include <sys/types.h> + #include <unistd.h> +-#include "client.h" ++#include "gmni.h" + #include "url.h" + + static enum gemini_result +@@ -264,3 +264,9 @@ cleanup: + curl_url_cleanup(uri); + return new_url; + } ++ ++enum gemini_status_class ++gemini_response_class(enum gemini_status status) ++{ ++ return status / 10; ++} +diff --git a/src/gmni.c b/src/gmni.c +index bdaa9baaca75e5f167add9a0ebed73cc60eaa647..b4efdc0235dac0279ce284f2c6b19f680a191647 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -11,7 +11,7 @@ #include <string.h> + #include <sys/socket.h> + #include <sys/types.h> + #include <unistd.h> +-#include "client.h" ++#include "gmni.h" + + static void + usage(char *argv_0) +@@ -120,8 +120,8 @@ goto next; + } + + char *new_url, *input = NULL; +- switch (resp.status / 10) { +- case 1: // INPUT ++ switch (gemini_response_class(resp.status)) { ++ case GEMINI_STATUS_CLASS_INPUT: + if (input_mode == INPUT_SUPPRESS) { + exit = true; + break; +@@ -149,7 +149,7 @@ free(url); + url = new_url; + assert(url); + goto next; +- case 3: // REDIRECT ++ case GEMINI_STATUS_CLASS_REDIRECT: + free(url); + url = strdup(resp.meta); + if (!follow_redirects) { +@@ -160,10 +160,10 @@ } + exit = true; + } + goto next; +- case 6: // CLIENT CERTIFICATE REQUIRED ++ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: + assert(0); // TODO +- case 4: // TEMPORARY FAILURE +- case 5: // PERMANENT FAILURE ++ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: ++ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: + if (header_mode == OMIT_HEADERS) { + fprintf(stderr, "%s: %d %s\n", + resp.status / 10 == 4 ? +@@ -172,7 +172,7 @@ resp.status, resp.meta); + } + exit = true; + break; +- case 2: // SUCCESS ++ case GEMINI_STATUS_CLASS_SUCCESS: + exit = true; + break; + } diff --git a/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch b/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch @@ -0,0 +1,42 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 02d8829598902d93f1dea3d68073f609573370c1..199572c363f9e50acdd052111350f3926d6a350d 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -57,6 +57,7 @@ const char *help_msg = + "The following commands are available:\n\n" + "q\tQuit\n" + "N\tFollow Nth link (where N is a number)\n" ++ "p[N]\tShow URL of Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" + "H\tView all page history\n" +@@ -267,6 +268,29 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); + result = PROMPT_AGAIN; + goto exit; + } ++ case 'p': ++ if (!in[1]) break; ++ struct link *link = browser->links; ++ char *endptr; ++ int linksel = (int)strtol(in+1, &endptr, 10); ++ if (!endptr[0] && linksel >= 0) { ++ while (linksel > 0 && link) { ++ link = link->next; ++ --linksel; ++ } ++ ++ if (!link) { ++ fprintf(stderr, "Error: no such link.\n"); ++ } else { ++ fprintf(browser->tty, "=> %s\n", link->url); ++ result = PROMPT_AGAIN; ++ goto exit; ++ } ++ } else { ++ fprintf(stderr, "Error: invalid argument.\n"); ++ } ++ result = PROMPT_AGAIN; ++ goto exit; + case '?': + if (in[1]) break; + fprintf(browser->tty, "%s", help_msg); diff --git a/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch b/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch @@ -0,0 +1,445 @@ +diff --git a/configure b/configure +index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..7b55a2580018cc69b2f52c9bc53832cc9faadc4f 100755 +--- a/configure ++++ b/configure +@@ -4,6 +4,7 @@ eval ". $srcdir/config.sh" + + gmni() { + genrules gmni \ ++ src/certs.c \ + src/client.c \ + src/escape.c \ + src/gmni.c \ +diff --git a/doc/gmni.scd b/doc/gmni.scd +index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644 +--- a/doc/gmni.scd ++++ b/doc/gmni.scd +@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a + second request is performed with the contents of _path_ as the user + input. + +-*-E* _path_[:_password_] +- Sets the path to the client certificate to use (and optionally a +- password). If the filename contains ":" but the certificate does not +- accept a password, append ":" to the path and it will be intepreted as +- an empty password. ++*-E* _path_:_key_ ++ Sets the path to the client certificate and private key file to use, ++ both PEM encoded. + + *-l* + For *text/\** responses, *gmni* normally adds a line feed if stdout is a +diff --git a/include/gmni/certs.h b/include/gmni/certs.h +new file mode 100644 +index 0000000000000000000000000000000000000000..22e226d6b4252dddd8526970cec0a947f12242d1 +--- /dev/null ++++ b/include/gmni/certs.h +@@ -0,0 +1,27 @@ ++#ifndef GEMINI_CERTS_H ++#define GEMINI_CERTS_H ++#include <bearssl.h> ++#include <stdio.h> ++ ++struct gmni_options; ++ ++struct gmni_client_certificate { ++ br_x509_certificate *chain; ++ size_t nchain; ++ struct gmni_private_key *key; ++}; ++ ++struct gmni_private_key { ++ int type; ++ union { ++ br_rsa_private_key rsa; ++ br_ec_private_key ec; ++ }; ++ unsigned char data[]; ++}; ++ ++// Returns nonzero on failure and sets errno ++int gmni_ccert_load(struct gmni_client_certificate *cert, ++ FILE *certin, FILE *skin); ++ ++#endif +diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h +index 16bef51024275bbb6ade9c90a11ae68876df387a..22295e20fb36d492a979c060de8dcdca14df5a34 100644 +--- a/include/gmni/gmni.h ++++ b/include/gmni/gmni.h +@@ -1,6 +1,6 @@ + #ifndef GEMINI_CLIENT_H + #define GEMINI_CLIENT_H +-#include <bearssl_ssl.h> ++#include <bearssl.h> + #include <netdb.h> + #include <stdbool.h> + #include <sys/socket.h> +@@ -61,6 +61,8 @@ br_ssl_client_context *sc; + int fd; + }; + ++struct gmni_client_certificate; ++ + 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. +@@ -69,6 +71,10 @@ + // If non-NULL, these hints are provided to getaddrinfo. Useful, for + // example, to force IPv4/IPv6. + struct addrinfo *hints; ++ ++ // If non-NULL, this will be used as the client certificate for the ++ // request. The other fields must be set as well. ++ struct gmni_client_certificate *client_cert; + }; + + struct gemini_tofu; +diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h +index a0981a5296421541a766846bc36c733dab1dfd1a..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644 +--- a/include/gmni/tofu.h ++++ b/include/gmni/tofu.h +@@ -1,6 +1,6 @@ + #ifndef GEMINI_TOFU_H + #define GEMINI_TOFU_H +-#include <bearssl_x509.h> ++#include <bearssl.h> + #include <limits.h> + + enum tofu_error { +diff --git a/src/certs.c b/src/certs.c +new file mode 100644 +index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014 +--- /dev/null ++++ b/src/certs.c +@@ -0,0 +1,156 @@ ++#include <assert.h> ++#include <bearssl.h> ++#include <errno.h> ++#include <gmni/certs.h> ++#include <gmni/gmni.h> ++#include <stdio.h> ++#include <stdlib.h> ++ ++static void ++crt_append(void *ctx, const void *src, size_t len) ++{ ++ br_x509_certificate *crt = (br_x509_certificate *)ctx; ++ crt->data = realloc(crt->data, crt->data_len + len); ++ assert(crt->data); ++ memcpy(&crt->data[crt->data_len], src, len); ++ crt->data_len += len; ++} ++ ++static void ++key_append(void *ctx, const void *src, size_t len) ++{ ++ br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; ++ br_skey_decoder_push(skctx, src, len); ++} ++ ++int ++gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) ++{ ++ // TODO: Better error propagation to caller ++ static unsigned char buf[BUFSIZ]; ++ ++ br_pem_decoder_context pemdec; ++ br_pem_decoder_init(&pemdec); ++ ++ cert->chain = NULL; ++ cert->nchain = 0; ++ ++ static const char *certname = "CERTIFICATE"; ++ while (!feof(certin)) { ++ size_t n = fread(&buf, 1, sizeof(buf), certin); ++ if (ferror(certin)) { ++ goto error; ++ } ++ size_t q = 0; ++ while (q < n) { ++ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); ++ switch (br_pem_decoder_event(&pemdec)) { ++ case BR_PEM_BEGIN_OBJ: ++ if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { ++ break; ++ } ++ cert->chain = realloc(cert->chain, ++ sizeof(br_x509_certificate) * (cert->nchain + 1)); ++ memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); ++ br_pem_decoder_setdest(&pemdec, &crt_append, ++ &cert->chain[cert->nchain]); ++ ++cert->nchain; ++ break; ++ case BR_PEM_END_OBJ: ++ break; ++ case BR_PEM_ERROR: ++ fprintf(stderr, "Error decoding PEM certificate\n"); ++ errno = EINVAL; ++ goto error; ++ } ++ } ++ } ++ ++ if (cert->nchain == 0) { ++ fprintf(stderr, "No certificates found in provided client certificate file\n"); ++ errno = EINVAL; ++ goto error; ++ } ++ ++ br_skey_decoder_context skdec = {0}; ++ br_skey_decoder_init(&skdec); ++ br_pem_decoder_init(&pemdec); ++ ++ // TODO: Better validation of PEM file ++ while (!feof(skin)) { ++ size_t n = fread(&buf, 1, sizeof(buf), skin); ++ if (ferror(skin)) { ++ goto error; ++ } ++ size_t q = 0; ++ while (q < n) { ++ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); ++ switch (br_pem_decoder_event(&pemdec)) { ++ case BR_PEM_BEGIN_OBJ: ++ br_pem_decoder_setdest(&pemdec, &key_append, &skdec); ++ break; ++ case BR_PEM_END_OBJ: ++ // no-op ++ break; ++ case BR_PEM_ERROR: ++ fprintf(stderr, "Error decoding PEM private key\n"); ++ errno = EINVAL; ++ goto error; ++ } ++ } ++ } ++ ++ int err = br_skey_decoder_last_error(&skdec); ++ if (err != 0) { ++ fprintf(stderr, "Error loading private key: %d\n", err); ++ errno = EINVAL; ++ goto error; ++ } ++ switch (br_skey_decoder_key_type(&skdec)) { ++ struct gmni_private_key *k; ++ const br_ec_private_key *ec; ++ const br_rsa_private_key *rsa; ++ case BR_KEYTYPE_RSA: ++ rsa = br_skey_decoder_get_rsa(&skdec); ++ cert->key = k = malloc(sizeof(*k) ++ + rsa->plen + rsa->qlen ++ + rsa->dplen + rsa->dqlen ++ + rsa->iqlen); ++ assert(k); ++ k->type = BR_KEYTYPE_RSA; ++ k->rsa = *rsa; ++ k->rsa.p = k->data; ++ k->rsa.q = k->rsa.p + k->rsa.plen; ++ k->rsa.dp = k->rsa.q + k->rsa.qlen; ++ k->rsa.dq = k->rsa.dp + k->rsa.dplen; ++ k->rsa.iq = k->rsa.dq + k->rsa.dqlen; ++ memcpy(k->rsa.p, rsa->p, rsa->plen); ++ memcpy(k->rsa.q, rsa->q, rsa->qlen); ++ memcpy(k->rsa.dp, rsa->dp, rsa->dplen); ++ memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); ++ memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); ++ break; ++ case BR_KEYTYPE_EC: ++ ec = br_skey_decoder_get_ec(&skdec); ++ cert->key = k = malloc(sizeof(*k) + ec->xlen); ++ assert(k); ++ k->type = BR_KEYTYPE_EC; ++ k->ec.curve = ec->curve; ++ k->ec.x = k->data; ++ k->ec.xlen = ec->xlen; ++ memcpy(k->ec.x, ec->x, ec->xlen); ++ break; ++ default: ++ assert(0); ++ } ++ ++ fclose(certin); ++ fclose(skin); ++ return 0; ++ ++error: ++ fclose(certin); ++ fclose(skin); ++ free(cert->chain); ++ return 1; ++} +diff --git a/src/client.c b/src/client.c +index e402cc97d96a904a2a8e9e2db40345b222cb85ae..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -1,13 +1,14 @@ + #include <assert.h> + #include <errno.h> + #include <netdb.h> +-#include <bearssl_ssl.h> ++#include <bearssl.h> + #include <stdlib.h> + #include <stdio.h> + #include <string.h> + #include <sys/socket.h> + #include <sys/types.h> + #include <unistd.h> ++#include <gmni/certs.h> + #include <gmni/gmni.h> + #include <gmni/tofu.h> + #include <gmni/url.h> +@@ -169,7 +170,26 @@ } + + // TODO: session reuse + resp->sc = &tofu->sc; ++ if (options->client_cert) { ++ struct gmni_client_certificate *cert = options->client_cert; ++ struct gmni_private_key *key = cert->key; ++ switch (key->type) { ++ case BR_KEYTYPE_RSA: ++ br_ssl_client_set_single_rsa(resp->sc, ++ cert->chain, cert->nchain, &key->rsa, ++ br_rsa_pkcs1_sign_get_default()); ++ break; ++ case BR_KEYTYPE_EC: ++ br_ssl_client_set_single_ec(resp->sc, ++ cert->chain, cert->nchain, &key->ec, ++ BR_KEYTYPE_SIGN, 0, ++ br_ec_get_default(), ++ br_ecdsa_sign_asn1_get_default()); ++ break; ++ } ++ } + br_ssl_client_reset(resp->sc, host, 0); ++ + br_sslio_init(&resp->body, &resp->sc->eng, + sock_read, &resp->fd, sock_write, &resp->fd); + +diff --git a/src/gmni.c b/src/gmni.c +index a8321d06c367128706d19ac283a53e55d95d0a92..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644 +--- a/src/gmni.c ++++ b/src/gmni.c +@@ -1,5 +1,5 @@ + #include <assert.h> +-#include <bearssl_ssl.h> ++#include <bearssl.h> + #include <errno.h> + #include <getopt.h> + #include <netdb.h> +@@ -11,6 +11,7 @@ #include <sys/socket.h> + #include <sys/types.h> + #include <termios.h> + #include <unistd.h> ++#include <gmni/certs.h> + #include <gmni/gmni.h> + #include <gmni/tofu.h> + #include <gmni/url.h> +@@ -109,6 +110,45 @@ + return action; + } + ++static struct gmni_client_certificate * ++load_client_cert(char *argv_0, char *path) ++{ ++ char *certpath = strtok(path, ":"); ++ if (!certpath) { ++ usage(argv_0); ++ exit(1); ++ } ++ ++ FILE *certf = fopen(certpath, "r"); ++ if (!certf) { ++ fprintf(stderr, "Failed to open certificate: %s\n", ++ strerror(errno)); ++ exit(1); ++ } ++ ++ char *keypath = strtok(NULL, ":"); ++ if (!keypath) { ++ usage(argv_0); ++ exit(1); ++ } ++ ++ FILE *keyf = fopen(keypath, "r"); ++ if (!keyf) { ++ fprintf(stderr, "Failed to open certificate: %s\n", ++ strerror(errno)); ++ exit(1); ++ } ++ ++ struct gmni_client_certificate *cert = ++ calloc(1, sizeof(struct gmni_client_certificate)); ++ if (gmni_ccert_load(cert, certf, keyf) != 0) { ++ fprintf(stderr, "Failed to load client certificate: %s\n", ++ strerror(errno)); ++ exit(1); ++ } ++ return cert; ++} ++ + int + main(int argc, char *argv[]) + { +@@ -165,7 +205,7 @@ } + } + break; + case 'E': +- assert(0); // TODO: Client certificates ++ opts.client_cert = load_client_cert(argv[0], optarg); + break; + case 'h': + usage(argv[0]); +@@ -226,7 +266,7 @@ + bool exit = false; + struct Curl_URL *url = curl_url(); + +- if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { ++ if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { + // TODO: Better error + fprintf(stderr, "Error: invalid URL\n"); + return 1; +@@ -238,8 +278,8 @@ char *buf; + curl_url_get(url, CURLUPART_URL, &buf, 0); + + struct gemini_response resp; +- enum gemini_result r = gemini_request( +- buf, &opts, &cfg.tofu, &resp); ++ enum gemini_result r = gemini_request(buf, ++ &opts, &cfg.tofu, &resp); + + free(buf); + +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 0ea492bb85fe024bd250c304808dcb8f0a915f90..aeb0c834d49c799fcdae54acfcbd3925dcedd392 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -1,5 +1,5 @@ + #include <assert.h> +-#include <bearssl_ssl.h> ++#include <bearssl.h> + #include <ctype.h> + #include <errno.h> + #include <fcntl.h> +diff --git a/src/tofu.c b/src/tofu.c +index 570bd41c885cd9bb007b58bffca957ace6916bea..0acdf33a50bb4957741f4789472ffe56035a7159 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -1,6 +1,5 @@ + #include <assert.h> +-#include <bearssl_hash.h> +-#include <bearssl_x509.h> ++#include <bearssl.h> + #include <errno.h> + #include <gmni/gmni.h> + #include <gmni/tofu.h> +diff --git a/src/util.c b/src/util.c +index 780d0e8803ab9179683bd9a92cbead05624f2681..1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -1,5 +1,5 @@ + #include <assert.h> +-#include <bearssl_ssl.h> ++#include <bearssl.h> + #include <errno.h> + #include <gmni/gmni.h> + #include <libgen.h> diff --git a/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch b/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch @@ -0,0 +1,22 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 7fba942066c9d9870f78311f050ef19dc42f7ce1..def42ac8b1af405da29681d29cd7b9d07b727f11 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -69,7 +69,7 @@ const char *help_msg = + "The following commands are available:\n\n" + "q\tQuit\n" + "N\tFollow Nth link (where N is a number)\n" +- "p[N]\tShow URL of Nth link (where N is a number)\n" ++ "p N\tShow URL of Nth link (where N is a number)\n" + "b\tBack (in the page history)\n" + "f\tForward (in the page history)\n" + "H\tView all page history\n" +@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; + goto exit; + } + case 'p': +- if (!in[1]) break; ++ if (!isspace(in[1])) break; + struct link *link = browser->links; + char *endptr; + int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch b/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch @@ -0,0 +1,14 @@ +diff --git a/src/tofu.c b/src/tofu.c +index aefac17cf61674d5c40d9554d0cb9089b1519ba4..50a295870876265849eae7fdf0de926a3ad309fd 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -63,6 +63,9 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { + cc->err = err; + return; + } ++ if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { ++ return; ++ } + cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); + br_sha512_out(&cc->sha512, &cc->hash); + } diff --git a/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch b/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch @@ -0,0 +1,13 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index a0bc81f851a69ec34c7e9baf643c10728632c7e2..4cd601913caa3fb307de28cfbe861689f933eaf5 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -88,7 +88,7 @@ "b\t\tBack (in the page history)\n" + "f\t\tForward (in the page history)\n" + "H\t\tView all page history\n" + "a\t\tSave bookmark\n" +- "s\tRemove bookmark for current URL\n" ++ "s\t\tRemove bookmark for current URL\n" + "B\t\tBrowse bookmarks\n" + "r\t\tReload the page\n" + "/<text>\t\tsearch for text (POSIX regular expression)\n" diff --git a/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch b/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch @@ -0,0 +1,31 @@ +diff --git a/src/tofu.c b/src/tofu.c +index 5b34850de9e17dcd59fc64a3227cd92bfb0a6cef..570bd41c885cd9bb007b58bffca957ace6916bea 100644 +--- a/src/tofu.c ++++ b/src/tofu.c +@@ -25,7 +25,7 @@ static void + xt_start_cert(const br_x509_class **ctx, uint32_t length) + { + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; +- if (cc->err != 0) { ++ if (cc->err != 0 || cc->pkey) { + return; + } + if (length == 0) { +@@ -40,7 +40,7 @@ static void + xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) + { + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; +- if (cc->err != 0) { ++ if (cc->err != 0 || cc->pkey) { + return; + } + br_x509_decoder_push(&cc->decoder, buf, len); +@@ -63,7 +63,7 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { + cc->err = err; + return; + } +- if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { ++ if (br_x509_decoder_isCA(&cc->decoder)) { + return; + } + cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); diff --git a/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch b/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch @@ -0,0 +1,221 @@ +diff --git a/include/client.h b/include/client.h +index dbd73234311331ea52650d59dbd9cf535b73298b..f711eea91c3842ac29a884b38e0d7a0ab99cb7fb 100644 +--- a/include/client.h ++++ b/include/client.h +@@ -46,6 +46,7 @@ GEMINI_ERR_CONNECT, + // use SSL_get_error(resp->ssl, resp->status) to get details + GEMINI_ERR_SSL, + GEMINI_ERR_IO, ++ GEMINI_ERR_PROTOCOL, + }; + + // Requests the specified URL via the gemini protocol. If options is non-NULL, +@@ -63,5 +64,8 @@ // allocated during the request. If you intend to re-use the SSL_CTX provided by + // gemini_options, set the ctx pointer to NULL before calling + // gemini_response_finish. + void gemini_response_finish(struct gemini_response *resp); ++ ++// Returns a user-friendly string describing an error. ++const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); + + #endif +diff --git a/src/client.c b/src/client.c +index 5f2debb52c310f88e82ad83ca2251ba233d6a704..67671ccca2a25a460681bdffacb981c7f26c1407 100644 +--- a/src/client.c ++++ b/src/client.c +@@ -2,6 +2,7 @@ #include <assert.h> + #include <errno.h> + #include <netdb.h> + #include <openssl/bio.h> ++#include <openssl/err.h> + #include <openssl/ssl.h> + #include <stdlib.h> + #include <string.h> +@@ -168,6 +169,21 @@ res = GEMINI_ERR_IO; + goto cleanup; + } + ++ if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { ++ res = GEMINI_ERR_PROTOCOL; ++ goto cleanup; ++ } ++ ++ char *endptr; ++ resp->status = (int)strtol(buf, &endptr, 10); ++ if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { ++ res = GEMINI_ERR_PROTOCOL; ++ goto cleanup; ++ } ++ resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); ++ strncpy(resp->meta, &endptr[1], r - 5); ++ resp->meta[r - 5] = '\0'; ++ + cleanup: + curl_url_cleanup(uri); + return res; +@@ -188,3 +204,29 @@ SSL_free(resp->ssl); + SSL_CTX_free(resp->ssl_ctx); + free(resp->meta); + } ++ ++const char * ++gemini_strerr(enum gemini_result r, struct gemini_response *resp) ++{ ++ switch (r) { ++ case GEMINI_OK: ++ return "OK"; ++ case GEMINI_ERR_OOM: ++ return "Out of memory"; ++ case GEMINI_ERR_INVALID_URL: ++ return "Invalid URL"; ++ case GEMINI_ERR_RESOLVE: ++ 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); ++ case GEMINI_ERR_IO: ++ return "I/O error"; ++ case GEMINI_ERR_PROTOCOL: ++ return "Protocol error"; ++ } ++ assert(0); ++} +diff --git a/src/gmnic.c b/src/gmnic.c +index 7b2eb183ebfe67dce0aaf8c6010bf6785b503df6..014211dc3784ba3eadcf1885ef7bcf6fe84d8462 100644 +--- a/src/gmnic.c ++++ b/src/gmnic.c +@@ -1,9 +1,13 @@ + #include <assert.h> ++#include <errno.h> + #include <getopt.h> ++#include <openssl/bio.h> + #include <openssl/err.h> + #include <stdbool.h> + #include <stdio.h> + #include <stdlib.h> ++#include <string.h> ++#include <unistd.h> + #include "client.h" + + static void +@@ -17,11 +21,15 @@ + int + main(int argc, char *argv[]) + { +- bool headers = false, follow_redirect = false; +- char *certificate = NULL, *input = NULL; ++ enum header_mode { ++ OMIT_HEADERS, ++ SHOW_HEADERS, ++ ONLY_HEADERS, ++ }; ++ enum header_mode headers = OMIT_HEADERS; + + int c; +- while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { ++ while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { + switch (c) { + case '4': + assert(0); // TODO +@@ -30,25 +38,29 @@ case '6': + assert(0); // TODO + break; + case 'C': +- certificate = optarg; ++ assert(0); // TODO: Client certificates + break; + case 'd': +- input = optarg; ++ assert(0); // TODO: Input + break; + case 'h': + usage(argv[0]); + return 0; + case 'L': +- follow_redirect = true; ++ assert(0); // TODO: Follow redirects ++ break; ++ case 'i': ++ headers = SHOW_HEADERS; + break; + case 'I': +- headers = true; ++ headers = ONLY_HEADERS; + break; + default: + fprintf(stderr, "fatal: unknown flag %c", c); + return 1; + } + } ++ + if (optind != argc - 1) { + usage(argv[0]); + return 1; +@@ -59,33 +71,41 @@ ERR_load_crypto_strings(); + + struct gemini_response resp; + enum gemini_result r = gemini_request(argv[optind], NULL, &resp); +- switch (r) { +- case GEMINI_OK: +- printf("OK\n"); +- break; +- case GEMINI_ERR_OOM: +- printf("OOM\n"); +- break; +- case GEMINI_ERR_INVALID_URL: +- printf("INVALID_URL\n"); +- break; +- case GEMINI_ERR_RESOLVE: +- printf("RESOLVE\n"); +- break; +- case GEMINI_ERR_CONNECT: +- printf("CONNECT\n"); ++ if (r != GEMINI_OK) { ++ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); ++ gemini_response_finish(&resp); ++ return (int)r; ++ } ++ ++ switch (headers) { ++ case ONLY_HEADERS: ++ printf("%d %s\n", resp.status, resp.meta); + break; +- case GEMINI_ERR_SSL: +- fprintf(stderr, "SSL error: %s\n", ERR_error_string( +- SSL_get_error(resp.ssl, resp.status), NULL)); ++ case SHOW_HEADERS: ++ printf("%d %s\n", resp.status, resp.meta); ++ /* fallthrough */ ++ case OMIT_HEADERS: ++ for (int n = 1; n > 0;) { ++ char buf[BUFSIZ]; ++ n = BIO_read(resp.bio, buf, BUFSIZ); ++ if (n == -1) { ++ fprintf(stderr, "Error: read\n"); ++ return 1; ++ } ++ ssize_t w = 0; ++ while (w < (ssize_t)n) { ++ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); ++ if (x == -1) { ++ fprintf(stderr, "Error: write: %s\n", ++ strerror(errno)); ++ return 1; ++ } ++ w += x; ++ } ++ } + break; + } + + gemini_response_finish(&resp); +- +- (void)headers; +- (void)follow_redirect; +- (void)certificate; +- (void)input; + return 0; + } diff --git a/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch b/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch @@ -0,0 +1,12 @@ +diff --git a/src/parser.c b/src/parser.c +index ffcc28767be7d638d18609764999d004790aa9a2..eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c 100644 +--- a/src/parser.c ++++ b/src/parser.c +@@ -58,7 +58,6 @@ if ((end = strstr(p->buf, "\n")) != NULL) { + *end = 0; + } + +- // TODO: Provide whitespace trimming helper function + if (strncmp(p->buf, "=>", 2) == 0) { + tok->token = GEMINI_LINK; + int i = 2; diff --git a/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch b/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch @@ -0,0 +1,363 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index d05180c7a74c88bd7ed8ba551a4a522bb8b74009..9bc95fad89c0f92bc5a68fdd40d272b021c3cc25 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -263,6 +263,167 @@ putc('\n', out); + } + } + ++static char * ++get_input(const struct gemini_response *resp, FILE *source) ++{ ++ int r = 0; ++ struct termios attrs; ++ bool tty = fileno(source) != -1 && isatty(fileno(source)); ++ char *input = NULL; ++ if (tty) { ++ fprintf(stderr, "%s: ", resp->meta); ++ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { ++ r = tcgetattr(fileno(source), &attrs); ++ struct termios new_attrs; ++ r = tcgetattr(fileno(source), &new_attrs); ++ if (r != -1) { ++ new_attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &new_attrs); ++ } ++ } ++ } ++ size_t s = 0; ++ ssize_t n = getline(&input, &s, source); ++ if (n == -1) { ++ fprintf(stderr, "Error reading input: %s\n", ++ feof(source) ? "EOF" : strerror(ferror(source))); ++ return NULL; ++ } ++ input[n - 1] = '\0'; // Drop LF ++ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { ++ attrs.c_lflag &= ~ECHO; ++ tcsetattr(fileno(source), TCSANOW, &attrs); ++ } ++ return input; ++} ++ ++static bool ++has_suffix(char *str, char *suff) ++{ ++ size_t suffl = strlen(suff); ++ size_t strl = strlen(str); ++ if (strl < suffl) { ++ return false; ++ } ++ return strcmp(&str[strl - suffl], suff) == 0; ++} ++ ++static enum gemini_result ++do_requests(struct browser *browser, struct gemini_response *resp) ++{ ++ int nredir = 0; ++ bool requesting = true; ++ enum gemini_result res; ++ while (requesting) { ++ char *scheme; ++ CURLUcode uc = curl_url_get(browser->url, ++ CURLUPART_SCHEME, &scheme, 0); ++ assert(uc == CURLUE_OK); // Invariant ++ if (strcmp(scheme, "file") == 0) { ++ free(scheme); ++ requesting = false; ++ ++ char *path; ++ uc = curl_url_get(browser->url, ++ CURLUPART_PATH, &path, 0); ++ if (uc != CURLUE_OK) { ++ resp->status = GEMINI_STATUS_BAD_REQUEST; ++ break; ++ } ++ ++ FILE *fp = fopen(path, "r"); ++ if (!fp) { ++ 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; ++ 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")) { ++ resp->meta = strdup("text/plain"); ++ } else { ++ resp->meta = strdup("application/x-octet-stream"); ++ } ++ free(path); ++ resp->status = GEMINI_STATUS_SUCCESS; ++ resp->fd = -1; ++ resp->ssl = NULL; ++ resp->ssl_ctx = NULL; ++ return GEMINI_OK; ++ } ++ free(scheme); ++ ++ res = gemini_request(browser->plain_url, &browser->opts, resp); ++ if (res != GEMINI_OK) { ++ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); ++ requesting = false; ++ resp->status = 70 + res; ++ break; ++ } ++ ++ char *input; ++ switch (gemini_response_class(resp->status)) { ++ case GEMINI_STATUS_CLASS_INPUT: ++ input = get_input(resp, browser->tty); ++ if (!input) { ++ requesting = false; ++ break; ++ } ++ if (input[0] == '\0' && browser->history->prev) { ++ free(input); ++ browser->history = browser->history->prev; ++ set_url(browser, browser->history->url, NULL); ++ break; ++ } ++ ++ char *new_url = gemini_input_url( ++ browser->plain_url, input); ++ free(input); ++ assert(new_url); ++ set_url(browser, new_url, NULL); ++ free(new_url); ++ break; ++ case GEMINI_STATUS_CLASS_REDIRECT: ++ if (++nredir >= 5) { ++ requesting = false; ++ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); ++ break; ++ } ++ set_url(browser, resp->meta, NULL); ++ break; ++ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: ++ assert(0); // TODO ++ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: ++ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: ++ requesting = false; ++ fprintf(stderr, "Server returned %s %d %s\n", ++ resp->status / 10 == 4 ? ++ "TEMPORARY FAILURE" : "PERMANENT FALIURE", ++ resp->status, resp->meta); ++ break; ++ case GEMINI_STATUS_CLASS_SUCCESS: ++ return res; ++ } ++ ++ if (requesting) { ++ gemini_response_finish(resp); ++ } ++ } ++ ++ return res; ++} ++ + static enum prompt_result + do_prompts(const char *prompt, struct browser *browser) + { +@@ -678,6 +839,9 @@ + static bool + display_response(struct browser *browser, struct gemini_response *resp) + { ++ if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { ++ return false; ++ } + if (strcmp(resp->meta, "text/gemini") == 0 + || strncmp(resp->meta, "text/gemini;", 12) == 0) { + return display_gemini(browser, resp); +@@ -688,170 +852,6 @@ } + assert(0); // TODO: Deal with other mimetypes + } + +-static char * +-get_input(const struct gemini_response *resp, FILE *source) +-{ +- int r = 0; +- struct termios attrs; +- bool tty = fileno(source) != -1 && isatty(fileno(source)); +- char *input = NULL; +- if (tty) { +- fprintf(stderr, "%s: ", resp->meta); +- if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { +- r = tcgetattr(fileno(source), &attrs); +- struct termios new_attrs; +- r = tcgetattr(fileno(source), &new_attrs); +- if (r != -1) { +- new_attrs.c_lflag &= ~ECHO; +- tcsetattr(fileno(source), TCSANOW, &new_attrs); +- } +- } +- } +- size_t s = 0; +- ssize_t n = getline(&input, &s, source); +- if (n == -1) { +- fprintf(stderr, "Error reading input: %s\n", +- feof(source) ? "EOF" : strerror(ferror(source))); +- return NULL; +- } +- input[n - 1] = '\0'; // Drop LF +- if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { +- attrs.c_lflag &= ~ECHO; +- tcsetattr(fileno(source), TCSANOW, &attrs); +- } +- return input; +-} +- +-static bool +-has_suffix(char *str, char *suff) +-{ +- size_t suffl = strlen(suff); +- size_t strl = strlen(str); +- if (strl < suffl) { +- return false; +- } +- return strcmp(&str[strl - suffl], suff) == 0; +-} +- +-// Returns true to skip prompting +-static bool +-do_requests(struct browser *browser, struct gemini_response *resp) +-{ +- int nredir = 0; +- bool requesting = true; +- while (requesting) { +- char *scheme; +- CURLUcode uc = curl_url_get(browser->url, +- CURLUPART_SCHEME, &scheme, 0); +- assert(uc == CURLUE_OK); // Invariant +- if (strcmp(scheme, "file") == 0) { +- free(scheme); +- requesting = false; +- +- char *path; +- uc = curl_url_get(browser->url, +- CURLUPART_PATH, &path, 0); +- if (uc != CURLUE_OK) { +- resp->status = GEMINI_STATUS_BAD_REQUEST; +- break; +- } +- +- FILE *fp = fopen(path, "r"); +- if (!fp) { +- 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; +- 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")) { +- resp->meta = strdup("text/plain"); +- } else { +- resp->meta = strdup("application/x-octet-stream"); +- } +- free(path); +- resp->status = GEMINI_STATUS_SUCCESS; +- resp->fd = -1; +- resp->ssl = NULL; +- resp->ssl_ctx = NULL; +- return display_response(browser, resp); +- } +- free(scheme); +- +- enum gemini_result res = gemini_request(browser->plain_url, +- &browser->opts, resp); +- if (res != GEMINI_OK) { +- fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); +- requesting = false; +- resp->status = 70 + res; +- break; +- } +- +- char *input; +- switch (gemini_response_class(resp->status)) { +- case GEMINI_STATUS_CLASS_INPUT: +- input = get_input(resp, browser->tty); +- if (!input) { +- requesting = false; +- break; +- } +- if (input[0] == '\0' && browser->history->prev) { +- free(input); +- browser->history = browser->history->prev; +- set_url(browser, browser->history->url, NULL); +- break; +- } +- +- char *new_url = gemini_input_url( +- browser->plain_url, input); +- free(input); +- assert(new_url); +- set_url(browser, new_url, NULL); +- free(new_url); +- break; +- case GEMINI_STATUS_CLASS_REDIRECT: +- if (++nredir >= 5) { +- requesting = false; +- fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); +- break; +- } +- fprintf(stderr, "Following redirect to %s\n", resp->meta); +- set_url(browser, resp->meta, NULL); +- break; +- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: +- assert(0); // TODO +- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: +- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: +- requesting = false; +- fprintf(stderr, "Server returned %s %d %s\n", +- resp->status / 10 == 4 ? +- "TEMPORARY FAILURE" : "PERMANENT FALIURE", +- resp->status, resp->meta); +- break; +- case GEMINI_STATUS_CLASS_SUCCESS: +- requesting = false; +- return display_response(browser, resp); +- } +- +- if (requesting) { +- gemini_response_finish(resp); +- } +- } +- +- return false; +-} +- + static enum tofu_action + tofu_callback(enum tofu_error error, const char *fingerprint, + struct known_host *khost, void *data) +@@ -1001,7 +1001,8 @@ struct gemini_response resp; + browser.running = true; + while (browser.running) { + static char prompt[4096]; +- bool skip_prompt = do_requests(&browser, &resp); ++ bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK ++ && display_response(&browser, &resp); + if (browser.meta) { + free(browser.meta); + } diff --git a/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch b/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch @@ -0,0 +1,108 @@ +diff --git a/README.md b/README.md +index 80695023b563475331438ce368d70f77ff6f24ca..eabc77ea5a6dd0f73bd908fa5a52b03fd53ddcce 100644 +--- a/README.md ++++ b/README.md +@@ -28,7 +28,7 @@ - heading 3: light green + - gemini link on same capsule: light cyan + - gemini link to another capsule: dark cyan + - non-gemini link: light magenta +-- quote: light gray ++- preformatted text: light gray + + Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. + +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 8e85d091e54e3c3c11dbd6980ae660be6838c7c8..66db54c7e0cf7dd6a1f58872621cc28e69944134 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -23,7 +23,7 @@ #define ANSI_COLOR_RED "\x1b[91m" + #define ANSI_COLOR_GREEN "\x1b[92m" + #define ANSI_COLOR_YELLOW "\x1b[93m" + #define ANSI_COLOR_BLUE "\x1b[94m" +-#define ANSI_COLOR_MAGENTA "\x1b[95m" ++#define ANSI_COLOR_MAGENTA "\x1b[35m" + #define ANSI_COLOR_CYAN "\x1b[36m" + #define ANSI_COLOR_LCYAN "\x1b[96m" + #define ANSI_COLOR_GRAY "\x1b[37m" +@@ -70,7 +70,7 @@ + const char *default_bookmarks = + "# Welcome to cgmnlm\n\n" + "Links:\n\n" +- "=> https://src.clttr.info/rwa/cgmnlm The cgmnlm browser\n" ++ "=> https://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" + "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" + "This file can be found at %s and may be edited at your pleasure.\n\n" + "Bookmarks:\n" +@@ -809,20 +809,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { + repeat: + switch (tok.token) { + case GEMINI_TEXT: +- col += fprintf(out, " "); ++ col += fprintf(out, " "); + if (text == NULL) { + text = tok.text; + } + break; + case GEMINI_LINK: + if (text == NULL) { +- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); ++ col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); + text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); + *next = calloc(1, sizeof(struct link)); + (*next)->url = strdup(trim_ws(tok.link.url)); + next = &(*next)->next; + } else { +- col += fprintf(out, " "); ++ col += fprintf(out, " "); + } + break; + case GEMINI_PREFORMATTED_BEGIN: +@@ -832,7 +832,7 @@ case GEMINI_PREFORMATTED_END: + continue; // Not used + case GEMINI_PREFORMATTED_TEXT: + if (text == NULL) { +- fprintf(out, " "); ++ fprintf(out, " %s", ANSI_COLOR_GRAY); + text = tok.preformatted; + } + break; +@@ -843,32 +843,31 @@ } + if (text == NULL) { + switch (tok.heading.level) { + case 1: +- col += fprintf(out, "%s%s", " # ", ANSI_COLOR_RED); ++ col += fprintf(out, " # %s", ANSI_COLOR_RED); + break; + case 2: +- col += fprintf(out, "%s%s", " ## ", ANSI_COLOR_YELLOW); ++ col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); + break; + case 3: +- col += fprintf(out, "%s%s", "### ", ANSI_COLOR_GREEN); ++ col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); + break; + } + text = trim_ws(tok.heading.title); + } else { +- col += fprintf(out, " "); ++ col += fprintf(out, " "); + } + break; + case GEMINI_LIST_ITEM: + if (text == NULL) { +- col += fprintf(out, " %s ", ++ col += fprintf(out, " %s ", + browser->unicode ? "•" : "*"); + text = trim_ws(tok.list_item); + } else { +- col += fprintf(out, " "); ++ col += fprintf(out, " "); + } + break; + case GEMINI_QUOTE: +- col += fprintf(out, " %s%s %s", ANSI_COLOR_RESET, +- browser->unicode ? "┃" : ">", ANSI_COLOR_GRAY); ++ col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); + if (text == NULL) { + text = trim_ws(tok.quote_text); + } diff --git a/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch b/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch @@ -0,0 +1,27 @@ +diff --git a/config.sh b/config.sh +index 52931ab241c3177285b56258bf9b67ac4b63a7ea..2a94bc6feada62bcfda616fa573d0a12db0a4502 100644 +--- a/config.sh ++++ b/config.sh +@@ -88,6 +88,8 @@ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" + LIBS="$LIBS $(pkg-config --libs "$pc")" + } + ++docs() { true; } ++ + run_configure() { + mkdir -p $outdir + +@@ -133,8 +135,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' + + all: ${all} + EOF +- gmni >>"$outdir"/config.mk +- gmnlm >>"$outdir"/config.mk ++ ++ for target in $all ++ do ++ $target >>"$outdir"/config.mk ++ done + echo done + + touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch b/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch @@ -0,0 +1,28 @@ +diff --git a/README.md b/README.md +new file mode 100644 +index 0000000000000000000000000000000000000000..c08d5b2e62d4ec740ffda4acfccbc198ad9726d4 +--- /dev/null ++++ b/README.md +@@ -0,0 +1,22 @@ ++# gmni - A Gemini client ++ ++This is a [Gemini](https://gemini.circumlunar.space/) client. ++ ++Dependencies: ++ ++- A POSIX-like system and a C11 compiler ++- OpenSSL ++- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) ++ ++## Compiling ++ ++``` ++$ mkdir build && cd build ++$ ../configure ++$ make ++# make install ++``` ++ ++## Usage ++ ++See `gmni(1)` for the CLI usage. diff --git a/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch b/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch @@ -0,0 +1,20 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index b4f20de66d50fe9f0287c74b9a9292fe8ddd48d3..0546486340b7299be3c97266dd534a1354bf31a3 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -188,6 +188,15 @@ { + char *path_fmt = get_data_pathfmt(); + static char path[PATH_MAX+1]; + snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ if (mkdirs(dirname(path), 0755) != 0) { ++ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); ++ free(path_fmt); ++ fprintf(stderr, "Error creating directory %s: %s\n", ++ dirname(path), strerror(errno)); ++ return; ++ } ++ ++ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); + free(path_fmt); + + struct stat buf; diff --git a/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch b/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch @@ -0,0 +1,84 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 5d6ce3cc5ff97884bb10c2e9e7f6e870b5d2f54a..e6910607646fdd4f4d5a04a052715eda35147c30 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -146,6 +146,45 @@ for (; *in && isspace(*in); ++in); + return in; + } + ++static int ++wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) ++{ ++ if (!s[0]) { ++ fprintf(f, "\n"); ++ return 0; ++ } ++ for (int i = 0; s[i]; ++i) { ++ // TODO: Other control sequences, and eat ANSI escapes before ++ // they become a problem ++ switch (s[i]) { ++ case '\n': ++ assert(0); // Not supposed to happen ++ case '\t': ++ *col = *col + (8 - *col % 8); ++ break; ++ default: ++ *col += 1; ++ break; ++ } ++ ++ if (*col >= ws->ws_col) { ++ int j = i--; ++ while (&s[i] != s && !isspace(s[i])) --i; ++ if (&s[i] == s) { ++ i = j; ++ } ++ char c = s[i]; ++ s[i] = 0; ++ int n = fprintf(f, "%s\n", s); ++ s[i] = c; ++ *row += 1; ++ *col = 0; ++ return n; ++ } ++ } ++ return fprintf(f, "%s\n", s) - 1; ++} ++ + static bool + display_gemini(struct browser *browser, struct gemini_response *resp) + { +@@ -157,15 +196,29 @@ + struct winsize ws; + ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); + ++ char *text = NULL; + int row = 0, col = 0; + struct gemini_token tok; + struct link **next = &browser->links; +- while (gemini_parser_next(&p, &tok) == 0) { ++ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { + switch (tok.token) { + case GEMINI_TEXT: +- // TODO: word wrap +- col += fprintf(browser->tty, " %s\n", +- trim_ws(tok.text)); ++ if (text == NULL) { ++ text = tok.text; ++ } ++ ++ do { ++ col += fprintf(browser->tty, " "); ++ int w = wrap(browser->tty, text, &ws, &row, &col); ++ text += w; ++ if (row >= ws.ws_row - 4) { ++ break; ++ } ++ } while (text[0]); ++ ++ if (!text[0]) { ++ text = NULL; ++ } + break; + case GEMINI_LINK: + col += fprintf(browser->tty, "%d) %s\n", nlinks++, diff --git a/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch b/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch @@ -0,0 +1,37 @@ +diff --git a/README.md b/README.md +index 9564df898ce60d9895b818f95ae020c39edb83af..80695023b563475331438ce368d70f77ff6f24ca 100644 +--- a/README.md ++++ b/README.md +@@ -25,7 +25,8 @@ The actual colors used depend on your terminal palette: + - heading 1: light red + - heading 2: light yellow + - heading 3: light green +-- gemini link: light cyan ++- gemini link on same capsule: light cyan ++- gemini link to another capsule: dark cyan + - non-gemini link: light magenta + - quote: light gray + +diff --git a/src/cgmnlm.c b/src/cgmnlm.c +index 39be8ad24bdf97f87c3e51f4b5ce92e8bf1b421b..a021a97298bb41bff8cdbbceca172f561b64c7a5 100644 +--- a/src/cgmnlm.c ++++ b/src/cgmnlm.c +@@ -24,7 +24,8 @@ #define ANSI_COLOR_GREEN "\x1b[92m" + #define ANSI_COLOR_YELLOW "\x1b[93m" + #define ANSI_COLOR_BLUE "\x1b[94m" + #define ANSI_COLOR_MAGENTA "\x1b[95m" +-#define ANSI_COLOR_CYAN "\x1b[96m" ++#define ANSI_COLOR_CYAN "\x1b[36m" ++#define ANSI_COLOR_LCYAN "\x1b[96m" + #define ANSI_COLOR_GRAY "\x1b[37m" + #define ANSI_COLOR_RESET "\x1b[0m" + +@@ -815,7 +816,7 @@ } + break; + case GEMINI_LINK: + if (text == NULL) { +- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA); ++ col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); + text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); + *next = calloc(1, sizeof(struct link)); + (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch b/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch @@ -0,0 +1,19 @@ +diff --git a/src/util.c b/src/util.c +index 573e8a717efbe843fce003126cb932928b15f716..2f62c29ce40ac012993e1d208d65491b56d204aa 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -19,13 +19,8 @@ + assert(strlen(path) <= PATH_MAX); + + strcpy(p, path); +- t = dirname(path); ++ t = dirname(p); + memmove(dname, t, strlen(t) + 1); +- +- /* restore the path if dirname worked in-place */ +- if (t == path && path != dname) { +- strcpy(path, p); +- } + } + + /** Make directory and all of its parents */ diff --git a/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch b/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch @@ -0,0 +1,2412 @@ +diff --git a/config.sh b/config.sh +index fc1134c0c98d9f57cb2cfdbb3b9d6faf35001dfd..70c4489bf5fd48fc0eb636bf739a5d82dcff8663 100644 +--- a/config.sh ++++ b/config.sh +@@ -5,6 +5,7 @@ AS=${AS:-as} + CC=${CC:-cc} + CFLAGS=${CFLAGS:-} + LD=${LD:-ld} ++LIBSSL= + + for arg + do +@@ -12,6 +13,9 @@ # TODO: Add args for install directories + case "$arg" in + --prefix=*) + PREFIX=${arg#*=} ++ ;; ++ --with-libssl=*) ++ LIBSSL=${arg#*=} + ;; + esac + done +@@ -72,12 +76,27 @@ return 1 + fi + } + ++find_library() { ++ name="$1" ++ pc="$2" ++ printf "Checking for %s... " "$name" ++ if ! pkg-config "$pc" 2>/dev/null ++ then ++ printf "NOT FOUND\n" ++ printf "Tried pkg-config %s\n" "$pc" ++ return 1 ++ fi ++ printf "OK\n" ++ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" ++ LIBS="$LIBS $(pkg-config --libs "$pc")" ++} ++ + run_configure() { + mkdir -p $outdir + + for flag in -g -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -Werror -pedantic + do +- printf "Checking for $flag... " ++ printf "Checking for %s... " "$flag" + if test_cflags "$flag" + then + echo yes +@@ -86,9 +105,13 @@ echo no + fi + done + ++ find_library OpenSSL libssl ++ find_library OpenSSL libcrypto ++ + printf "Creating $outdir/config.mk... " + cat <<-EOF > "$outdir"/config.mk + CC=$CC ++ LIBS=$LIBS + PREFIX=${PREFIX:-/usr/local} + OUTDIR=${outdir} + _INSTDIR=\$(DESTDIR)\$(PREFIX) +diff --git a/configure b/configure +index 7b1a48b785b7ab1a8811cb36ea9ee00a68fc9ff8..680b57fc9e319c2707e0cc2ded0bc22597544d78 100755 +--- a/configure ++++ b/configure +@@ -4,7 +4,10 @@ eval ". $srcdir/config.sh" + + gmni() { + genrules gmnic \ +- src/gmnic.c ++ src/client.c \ ++ src/escape.c \ ++ src/gmnic.c \ ++ src/url.c + } + + all="gmnic" +diff --git a/include/client.h b/include/client.h +new file mode 100644 +index 0000000000000000000000000000000000000000..dbd73234311331ea52650d59dbd9cf535b73298b +--- /dev/null ++++ b/include/client.h +@@ -0,0 +1,67 @@ ++#ifndef GEMINI_CLIENT_H ++#define GEMINI_CLIENT_H ++#include <netdb.h> ++#include <openssl/ssl.h> ++#include <sys/socket.h> ++ ++struct gemini_response { ++ int status; ++ char *meta; ++ ++ // Response body may be read from here if appropriate: ++ BIO *bio; ++ ++ // Connection state ++ SSL_CTX *ssl_ctx; ++ SSL *ssl; ++ int fd; ++}; ++ ++struct gemini_options { ++ // If NULL, an SSL context will be created. If unset, the ssl field ++ // must also be NULL. ++ SSL_CTX *ssl_ctx; ++ ++ // If NULL, an SSL connection will be established. If set, it is ++ // presumed that the caller pre-established the SSL connection. ++ SSL *ssl; ++ ++ // 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; ++ ++ // If non-NULL, these hints are provided to getaddrinfo. Useful, for ++ // example, to force IPv4/IPv6. ++ struct addrinfo *hints; ++}; ++ ++enum gemini_result { ++ GEMINI_OK, ++ GEMINI_ERR_OOM, ++ GEMINI_ERR_INVALID_URL, ++ // status is set to the return value from getaddrinfo ++ GEMINI_ERR_RESOLVE, ++ // status is set to errno ++ GEMINI_ERR_CONNECT, ++ // use SSL_get_error(resp->ssl, resp->status) to get details ++ GEMINI_ERR_SSL, ++ GEMINI_ERR_IO, ++}; ++ ++// Requests the specified URL via the gemini protocol. If options is non-NULL, ++// it may specify some additional configuration to adjust client behavior. ++// ++// Returns a value indicating the success of the request. If GEMINI_OK is ++// returned, the response details shall be written to the gemini_response ++// argument. ++enum gemini_result gemini_request(const char *url, ++ struct gemini_options *options, ++ struct gemini_response *resp); ++ ++// Must be called after gemini_request in order to free up the resources ++// allocated during the request. If you intend to re-use the SSL_CTX provided by ++// gemini_options, set the ctx pointer to NULL before calling ++// gemini_response_finish. ++void gemini_response_finish(struct gemini_response *resp); ++ ++#endif +diff --git a/include/escape.h b/include/escape.h +new file mode 100644 +index 0000000000000000000000000000000000000000..a1184ba3141b2992b0b18e44105bdc4474a7d8c9 +--- /dev/null ++++ b/include/escape.h +@@ -0,0 +1,175 @@ ++#ifndef ESCAPE_H ++#define ESCAPE_H ++/*************************************************************************** ++ * _ _ ____ _ ++ * Project ___| | | | _ \| | ++ * / __| | | | |_) | | ++ * | (__| |_| | _ <| |___ ++ * \___|\___/|_| \_\_____| ++ * ++ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. ++ * ++ * This software is licensed as described in the file COPYING, which ++ * you should have received as part of this distribution. The terms ++ * are also available at https://curl.haxx.se/docs/copyright.html. ++ * ++ * You may opt to use, copy, modify, merge, publish, distribute and/or sell ++ * copies of the Software, and permit persons to whom the Software is ++ * furnished to do so, under the terms of the COPYING file. ++ * ++ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY ++ * KIND, either express or implied. ++ * ++ ***************************************************************************/ ++ ++/* from curl.h */ ++typedef enum { ++ CURLE_OK = 0, ++ CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ ++ CURLE_FAILED_INIT, /* 2 */ ++ CURLE_URL_MALFORMAT, /* 3 */ ++ CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for ++ 7.17.0, reused in April 2011 for 7.21.5] */ ++ CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ ++ CURLE_COULDNT_RESOLVE_HOST, /* 6 */ ++ CURLE_COULDNT_CONNECT, /* 7 */ ++ CURLE_WEIRD_SERVER_REPLY, /* 8 */ ++ CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server ++ due to lack of access - when login fails ++ this is not returned. */ ++ CURLE_FTP_ACCEPT_FAILED, /* 10 - [was obsoleted in April 2006 for ++ 7.15.4, reused in Dec 2011 for 7.24.0]*/ ++ CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ ++ CURLE_FTP_ACCEPT_TIMEOUT, /* 12 - timeout occurred accepting server ++ [was obsoleted in August 2007 for 7.17.0, ++ reused in Dec 2011 for 7.24.0]*/ ++ CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ ++ CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ ++ CURLE_FTP_CANT_GET_HOST, /* 15 */ ++ CURLE_HTTP2, /* 16 - A problem in the http2 framing layer. ++ [was obsoleted in August 2007 for 7.17.0, ++ reused in July 2014 for 7.38.0] */ ++ CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ ++ CURLE_PARTIAL_FILE, /* 18 */ ++ CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ ++ CURLE_OBSOLETE20, /* 20 - NOT USED */ ++ CURLE_QUOTE_ERROR, /* 21 - quote command failure */ ++ CURLE_HTTP_RETURNED_ERROR, /* 22 */ ++ CURLE_WRITE_ERROR, /* 23 */ ++ CURLE_OBSOLETE24, /* 24 - NOT USED */ ++ CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ ++ CURLE_READ_ERROR, /* 26 - couldn't open/read from file */ ++ CURLE_OUT_OF_MEMORY, /* 27 */ ++ /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error ++ instead of a memory allocation error if CURL_DOES_CONVERSIONS ++ is defined ++ */ ++ CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ ++ CURLE_OBSOLETE29, /* 29 - NOT USED */ ++ CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ ++ CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ ++ CURLE_OBSOLETE32, /* 32 - NOT USED */ ++ CURLE_RANGE_ERROR, /* 33 - RANGE "command" didn't work */ ++ CURLE_HTTP_POST_ERROR, /* 34 */ ++ CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ ++ CURLE_BAD_DOWNLOAD_RESUME, /* 36 - couldn't resume download */ ++ CURLE_FILE_COULDNT_READ_FILE, /* 37 */ ++ CURLE_LDAP_CANNOT_BIND, /* 38 */ ++ CURLE_LDAP_SEARCH_FAILED, /* 39 */ ++ CURLE_OBSOLETE40, /* 40 - NOT USED */ ++ CURLE_FUNCTION_NOT_FOUND, /* 41 - NOT USED starting with 7.53.0 */ ++ CURLE_ABORTED_BY_CALLBACK, /* 42 */ ++ CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ ++ CURLE_OBSOLETE44, /* 44 - NOT USED */ ++ CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ ++ CURLE_OBSOLETE46, /* 46 - NOT USED */ ++ CURLE_TOO_MANY_REDIRECTS, /* 47 - catch endless re-direct loops */ ++ CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ ++ CURLE_TELNET_OPTION_SYNTAX, /* 49 - Malformed telnet option */ ++ CURLE_OBSOLETE50, /* 50 - NOT USED */ ++ CURLE_OBSOLETE51, /* 51 - NOT USED */ ++ CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ ++ CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ ++ CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as ++ default */ ++ CURLE_SEND_ERROR, /* 55 - failed sending network data */ ++ CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ ++ CURLE_OBSOLETE57, /* 57 - NOT IN USE */ ++ CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ ++ CURLE_SSL_CIPHER, /* 59 - couldn't use specified cipher */ ++ CURLE_PEER_FAILED_VERIFICATION, /* 60 - peer's certificate or fingerprint ++ wasn't verified fine */ ++ CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ ++ CURLE_LDAP_INVALID_URL, /* 62 - Invalid LDAP URL */ ++ CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ ++ CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ ++ CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind ++ that failed */ ++ CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ ++ CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not ++ accepted and we failed to login */ ++ CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ ++ CURLE_TFTP_PERM, /* 69 - permission problem on server */ ++ CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ ++ CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ ++ CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ ++ CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ ++ CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ ++ CURLE_CONV_FAILED, /* 75 - conversion failed */ ++ CURLE_CONV_REQD, /* 76 - caller must register conversion ++ callbacks using curl_easy_setopt options ++ CURLOPT_CONV_FROM_NETWORK_FUNCTION, ++ CURLOPT_CONV_TO_NETWORK_FUNCTION, and ++ CURLOPT_CONV_FROM_UTF8_FUNCTION */ ++ CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing ++ or wrong format */ ++ CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ ++ CURLE_SSH, /* 79 - error from the SSH layer, somewhat ++ generic so the error message will be of ++ interest when this has happened */ ++ ++ CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL ++ connection */ ++ CURLE_AGAIN, /* 81 - socket is not ready for send/recv, ++ wait till it's ready and try again (Added ++ in 7.18.2) */ ++ CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or ++ wrong format (Added in 7.19.0) */ ++ CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in ++ 7.19.0) */ ++ CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ ++ CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ ++ CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ ++ CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ ++ CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ ++ CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the ++ session will be queued */ ++ CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not ++ match */ ++ CURLE_SSL_INVALIDCERTSTATUS, /* 91 - invalid certificate status */ ++ CURLE_HTTP2_STREAM, /* 92 - stream error in HTTP/2 framing layer ++ */ ++ CURLE_RECURSIVE_API_CALL, /* 93 - an api function was called from ++ inside a callback */ ++ CURL_LAST /* never use! */ ++} CURLcode; ++ ++/* Escape and unescape URL encoding in strings. The functions return a new ++ * allocated string or NULL if an error occurred. */ ++ ++bool Curl_isunreserved(unsigned char in); ++CURLcode Curl_urldecode(const char *string, size_t length, ++ char **ostring, size_t *olen, ++ bool reject_crlf); ++ ++char *curl_easy_escape(const char *string, int length); ++ ++char *curl_escape(const char *string, int length); ++ ++char *curl_easy_unescape(const char *string, ++ int length, int *outlength); ++ ++char *curl_unescape(const char *string, int length); ++ ++ ++#endif /* HEADER_CURL_ESCAPE_H */ +diff --git a/include/url.h b/include/url.h +new file mode 100644 +index 0000000000000000000000000000000000000000..155fd55740dbe47a498062713aca217ab259734f +--- /dev/null ++++ b/include/url.h +@@ -0,0 +1,103 @@ ++#ifndef URLAPI_H ++#define URLAPI_H ++/*************************************************************************** ++ * _ _ ____ _ ++ * Project ___| | | | _ \| | ++ * / __| | | | |_) | | ++ * | (__| |_| | _ <| |___ ++ * \___|\___/|_| \_\_____| ++ * ++ * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. ++ * ++ * This software is licensed as described in the file COPYING, which ++ * you should have received as part of this distribution. The terms ++ * are also available at https://curl.haxx.se/docs/copyright.html. ++ * ++ * You may opt to use, copy, modify, merge, publish, distribute and/or sell ++ * copies of the Software, and permit persons to whom the Software is ++ * furnished to do so, under the terms of the COPYING file. ++ * ++ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY ++ * KIND, either express or implied. ++ * ++ ***************************************************************************/ ++ ++/* the error codes for the URL API */ ++typedef enum { ++ CURLUE_OK, ++ CURLUE_BAD_HANDLE, /* 1 */ ++ CURLUE_BAD_PARTPOINTER, /* 2 */ ++ CURLUE_MALFORMED_INPUT, /* 3 */ ++ CURLUE_BAD_PORT_NUMBER, /* 4 */ ++ CURLUE_UNSUPPORTED_SCHEME, /* 5 */ ++ CURLUE_URLDECODE, /* 6 */ ++ CURLUE_OUT_OF_MEMORY, /* 7 */ ++ CURLUE_USER_NOT_ALLOWED, /* 8 */ ++ CURLUE_UNKNOWN_PART, /* 9 */ ++ CURLUE_NO_SCHEME, /* 10 */ ++ CURLUE_NO_USER, /* 11 */ ++ CURLUE_NO_PASSWORD, /* 12 */ ++ CURLUE_NO_OPTIONS, /* 13 */ ++ CURLUE_NO_HOST, /* 14 */ ++ CURLUE_NO_PORT, /* 15 */ ++ CURLUE_NO_QUERY, /* 16 */ ++ CURLUE_NO_FRAGMENT /* 17 */ ++} CURLUcode; ++ ++typedef enum { ++ CURLUPART_URL, ++ CURLUPART_SCHEME, ++ CURLUPART_USER, ++ CURLUPART_PASSWORD, ++ CURLUPART_OPTIONS, ++ CURLUPART_HOST, ++ CURLUPART_PORT, ++ CURLUPART_PATH, ++ CURLUPART_QUERY, ++ CURLUPART_FRAGMENT ++} CURLUPart; ++ ++#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ ++#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ ++#define CURLU_URLDECODE (1<<6) /* URL decode on get */ ++#define CURLU_URLENCODE (1<<7) /* URL encode on set */ ++#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ ++ ++typedef struct Curl_URL CURLU; ++ ++/* ++ * curl_url() creates a new CURLU handle and returns a pointer to it. ++ * Must be freed with curl_url_cleanup(). ++ */ ++struct Curl_URL *curl_url(void); ++ ++/* ++ * curl_url_cleanup() frees the CURLU handle and related resources used for ++ * the URL parsing. It will not free strings previously returned with the URL ++ * API. ++ */ ++void curl_url_cleanup(struct Curl_URL *handle); ++ ++/* ++ * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new ++ * handle must also be freed with curl_url_cleanup(). ++ */ ++struct Curl_URL *curl_url_dup(struct Curl_URL *in); ++ ++/* ++ * curl_url_get() extracts a specific part of the URL from a CURLU ++ * handle. Returns error code. The returned pointer MUST be freed with ++ * free() afterwards. ++ */ ++CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, ++ char **part, unsigned int flags); ++ ++/* ++ * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns ++ * error code. The passed in string will be copied. Passing a NULL instead of ++ * a part string, clears that part. ++ */ ++CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, ++ const char *part, unsigned int flags); ++ ++#endif +diff --git a/src/client.c b/src/client.c +new file mode 100644 +index 0000000000000000000000000000000000000000..5f2debb52c310f88e82ad83ca2251ba233d6a704 +--- /dev/null ++++ b/src/client.c +@@ -0,0 +1,190 @@ ++#include <assert.h> ++#include <errno.h> ++#include <netdb.h> ++#include <openssl/bio.h> ++#include <openssl/ssl.h> ++#include <stdlib.h> ++#include <string.h> ++#include <sys/socket.h> ++#include <sys/types.h> ++#include <unistd.h> ++#include "client.h" ++#include "url.h" ++ ++static enum gemini_result ++gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, ++ struct gemini_response *resp, struct addrinfo **addr) ++{ ++ int port = 1965; ++ char *uport; ++ if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) { ++ port = (int)strtol(uport, NULL, 10); ++ free(uport); ++ } ++ ++ if (options && options->addr->ai_family != AF_UNSPEC) { ++ *addr = options->addr; ++ } else { ++ struct addrinfo hints = {0}; ++ if (options && options->hints) { ++ hints = *options->hints; ++ } else { ++ hints.ai_family = AF_UNSPEC; ++ hints.ai_socktype = SOCK_STREAM; ++ } ++ ++ char pbuf[7]; ++ snprintf(pbuf, sizeof(pbuf), "%d", port); ++ ++ char *domain; ++ CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0); ++ assert(uc == CURLUE_OK); ++ ++ int r = getaddrinfo(domain, pbuf, &hints, addr); ++ free(domain); ++ if (r != 0) { ++ resp->status = r; ++ return GEMINI_ERR_RESOLVE; ++ } ++ } ++ ++ return GEMINI_OK; ++} ++ ++static enum gemini_result ++gemini_connect(struct Curl_URL *uri, struct gemini_options *options, ++ struct gemini_response *resp, int *sfd) ++{ ++ struct addrinfo *addr; ++ enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); ++ if (res != GEMINI_OK) { ++ goto cleanup; ++ } ++ ++ struct addrinfo *rp; ++ for (rp = addr; rp != NULL; rp = rp->ai_next) { ++ *sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); ++ if (*sfd == -1) { ++ continue; ++ } ++ if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) { ++ break; ++ } ++ close(*sfd); ++ } ++ if (rp == NULL) { ++ resp->status = errno; ++ res = GEMINI_ERR_CONNECT; ++ return res; ++ } ++ ++cleanup: ++ if (!options || !options->addr) { ++ freeaddrinfo(addr); ++ } ++ return res; ++} ++ ++#define GEMINI_META_MAXLEN 1024 ++#define GEMINI_STATUS_MAXLEN 2 ++ ++enum gemini_result ++gemini_request(const char *url, struct gemini_options *options, ++ struct gemini_response *resp) ++{ ++ assert(url); ++ assert(resp); ++ resp->meta = NULL; ++ if (strlen(url) > 1024) { ++ return GEMINI_ERR_INVALID_URL; ++ } ++ ++ struct Curl_URL *uri = curl_url(); ++ if (!uri) { ++ return GEMINI_ERR_OOM; ++ } ++ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { ++ return GEMINI_ERR_INVALID_URL; ++ } ++ ++ enum gemini_result res = GEMINI_OK; ++ if (options && options->ssl_ctx) { ++ resp->ssl_ctx = options->ssl_ctx; ++ SSL_CTX_up_ref(options->ssl_ctx); ++ } else { ++ resp->ssl_ctx = SSL_CTX_new(TLS_method()); ++ assert(resp->ssl_ctx); ++ } ++ ++ BIO *sbio = BIO_new(BIO_f_ssl()); ++ if (options && options->ssl) { ++ resp->ssl = options->ssl; ++ SSL_up_ref(resp->ssl); ++ BIO_set_ssl(sbio, resp->ssl, 0); ++ resp->fd = -1; ++ } else { ++ res = gemini_connect(uri, options, resp, &resp->fd); ++ if (res != GEMINI_OK) { ++ goto cleanup; ++ } ++ ++ resp->ssl = SSL_new(resp->ssl_ctx); ++ assert(resp->ssl); ++ int r = SSL_set_fd(resp->ssl, resp->fd); ++ if (r != 1) { ++ resp->status = r; ++ res = GEMINI_ERR_SSL; ++ goto cleanup; ++ } ++ r = SSL_connect(resp->ssl); ++ if (r != 1) { ++ resp->status = r; ++ res = GEMINI_ERR_SSL; ++ goto cleanup; ++ } ++ BIO_set_ssl(sbio, resp->ssl, 0); ++ } ++ ++ resp->bio = BIO_new(BIO_f_buffer()); ++ BIO_push(resp->bio, sbio); ++ ++ char req[1024 + 3]; ++ int r = snprintf(req, sizeof(req), "%s\r\n", url); ++ assert(r > 0); ++ ++ r = BIO_puts(sbio, req); ++ if (r == -1) { ++ res = GEMINI_ERR_IO; ++ goto cleanup; ++ } ++ assert(r == (int)strlen(req)); ++ ++ char buf[GEMINI_META_MAXLEN ++ + GEMINI_STATUS_MAXLEN ++ + 2 /* CRLF */ + 1 /* NUL */]; ++ r = BIO_gets(resp->bio, buf, sizeof(buf)); ++ if (r == -1) { ++ res = GEMINI_ERR_IO; ++ goto cleanup; ++ } ++ ++cleanup: ++ curl_url_cleanup(uri); ++ return res; ++} ++ ++void ++gemini_response_finish(struct gemini_response *resp) ++{ ++ if (!resp) { ++ return; ++ } ++ if (resp->fd != -1) { ++ close(resp->fd); ++ } ++ BIO_free(BIO_pop(resp->bio)); // ssl bio ++ BIO_free(resp->bio); // buffered bio ++ SSL_free(resp->ssl); ++ SSL_CTX_free(resp->ssl_ctx); ++ free(resp->meta); ++} +diff --git a/src/escape.c b/src/escape.c +new file mode 100644 +index 0000000000000000000000000000000000000000..d083da699d96f01a94a6ce8a86e41f0a47c54ffe +--- /dev/null ++++ b/src/escape.c +@@ -0,0 +1,213 @@ ++/*************************************************************************** ++ * _ _ ____ _ ++ * Project ___| | | | _ \| | ++ * / __| | | | |_) | | ++ * | (__| |_| | _ <| |___ ++ * \___|\___/|_| \_\_____| ++ * ++ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. ++ * ++ * This software is licensed as described in the file COPYING, which ++ * you should have received as part of this distribution. The terms ++ * are also available at https://curl.haxx.se/docs/copyright.html. ++ * ++ * You may opt to use, copy, modify, merge, publish, distribute and/or sell ++ * copies of the Software, and permit persons to whom the Software is ++ * furnished to do so, under the terms of the COPYING file. ++ * ++ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY ++ * KIND, either express or implied. ++ * ++ ***************************************************************************/ ++ ++/* Escape and unescape URL encoding in strings. The functions return a new ++ * allocated string or NULL if an error occurred. */ ++#include <ctype.h> ++#include <limits.h> ++#include <stdbool.h> ++#include <stddef.h> ++#include <stdio.h> ++#include <stdlib.h> ++#include <string.h> ++#include "escape.h" ++ ++/* Portable character check (remember EBCDIC). Do not use isalnum() because ++ its behavior is altered by the current locale. ++ See https://tools.ietf.org/html/rfc3986#section-2.3 ++*/ ++bool Curl_isunreserved(unsigned char in) ++{ ++ switch(in) { ++ case '0': case '1': case '2': case '3': case '4': ++ case '5': case '6': case '7': case '8': case '9': ++ case 'a': case 'b': case 'c': case 'd': case 'e': ++ case 'f': case 'g': case 'h': case 'i': case 'j': ++ case 'k': case 'l': case 'm': case 'n': case 'o': ++ case 'p': case 'q': case 'r': case 's': case 't': ++ case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': ++ case 'A': case 'B': case 'C': case 'D': case 'E': ++ case 'F': case 'G': case 'H': case 'I': case 'J': ++ case 'K': case 'L': case 'M': case 'N': case 'O': ++ case 'P': case 'Q': case 'R': case 'S': case 'T': ++ case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': ++ case '-': case '.': case '_': case '~': ++ return true; ++ default: ++ break; ++ } ++ return false; ++} ++ ++/* for ABI-compatibility with previous versions */ ++char *curl_escape(const char *string, int inlength) ++{ ++ return curl_easy_escape(string, inlength); ++} ++ ++/* for ABI-compatibility with previous versions */ ++char *curl_unescape(const char *string, int length) ++{ ++ return curl_easy_unescape(string, length, NULL); ++} ++ ++char *curl_easy_escape(const char *string, int inlength) ++{ ++ size_t alloc; ++ char *ns; ++ char *testing_ptr = NULL; ++ size_t newlen; ++ size_t strindex = 0; ++ size_t length; ++ ++ if(inlength < 0) ++ return NULL; ++ ++ alloc = (inlength?(size_t)inlength:strlen(string)) + 1; ++ newlen = alloc; ++ ++ ns = malloc(alloc); ++ if(!ns) ++ return NULL; ++ ++ length = alloc-1; ++ while(length--) { ++ unsigned char in = *string; /* we need to treat the characters unsigned */ ++ ++ if(Curl_isunreserved(in)) ++ /* just copy this */ ++ ns[strindex++] = in; ++ else { ++ /* encode it */ ++ newlen += 2; /* the size grows with two, since this'll become a %XX */ ++ if(newlen > alloc) { ++ alloc *= 2; ++ testing_ptr = realloc(ns, alloc); ++ if(!testing_ptr) ++ return NULL; ++ ns = testing_ptr; ++ } ++ ++ snprintf(&ns[strindex], 4, "%%%02X", in); ++ ++ strindex += 3; ++ } ++ string++; ++ } ++ ns[strindex] = 0; /* terminate it */ ++ return ns; ++} ++ ++/* ++ * Curl_urldecode() URL decodes the given string. ++ * ++ * Optionally detects control characters (byte codes lower than 32) in the ++ * data and rejects such data. ++ * ++ * Returns a pointer to a malloced string in *ostring with length given in ++ * *olen. If length == 0, the length is assumed to be strlen(string). ++ */ ++CURLcode Curl_urldecode(const char *string, size_t length, ++ char **ostring, size_t *olen, ++ bool reject_ctrl) ++{ ++ size_t alloc = (length?length:strlen(string)) + 1; ++ char *ns = malloc(alloc); ++ size_t strindex = 0; ++ unsigned long hex; ++ ++ if(!ns) ++ return CURLE_OUT_OF_MEMORY; ++ ++ while(--alloc > 0) { ++ unsigned char in = *string; ++ if(('%' == in) && (alloc > 2) && ++ isxdigit(string[1]) && isxdigit(string[2])) { ++ /* this is two hexadecimal digits following a '%' */ ++ char hexstr[3]; ++ char *ptr; ++ hexstr[0] = string[1]; ++ hexstr[1] = string[2]; ++ hexstr[2] = 0; ++ ++ hex = strtoul(hexstr, &ptr, 16); ++ ++ in = (unsigned char)hex; /* this long is never bigger than 255 anyway */ ++ ++ string += 2; ++ alloc -= 2; ++ } ++ ++ if(reject_ctrl && (in < 0x20)) { ++ free(ns); ++ return CURLE_URL_MALFORMAT; ++ } ++ ++ ns[strindex++] = in; ++ string++; ++ } ++ ns[strindex] = 0; /* terminate it */ ++ ++ if(olen) ++ /* store output size */ ++ *olen = strindex; ++ ++ /* store output string */ ++ *ostring = ns; ++ ++ return CURLE_OK; ++} ++ ++/* ++ * Unescapes the given URL escaped string of given length. Returns a ++ * pointer to a malloced string with length given in *olen. ++ * If length == 0, the length is assumed to be strlen(string). ++ * If olen == NULL, no output length is stored. ++ */ ++char *curl_easy_unescape(const char *string, int length, int *olen) ++{ ++ char *str = NULL; ++ if(length >= 0) { ++ size_t inputlen = length; ++ size_t outputlen; ++ CURLcode res = Curl_urldecode(string, inputlen, &str, &outputlen, false); ++ if(res) ++ return NULL; ++ ++ if(olen) { ++ if(outputlen <= (size_t) INT_MAX) ++ *olen = (int)outputlen; ++ else ++ /* too large to return in an int, fail! */ ++ free(str); ++ } ++ } ++ return str; ++} ++ ++/* For operating systems/environments that use different malloc/free ++ systems for the app and for this library, we provide a free that uses ++ the library's memory system */ ++void curl_free(void *p) ++{ ++ free(p); ++} +diff --git a/src/gmnic.c b/src/gmnic.c +index 35a49493cc6af2486d055c9bde5a576942c2e40a..7b2eb183ebfe67dce0aaf8c6010bf6785b503df6 100644 +--- a/src/gmnic.c ++++ b/src/gmnic.c +@@ -1,8 +1,91 @@ ++#include <assert.h> ++#include <getopt.h> ++#include <openssl/err.h> ++#include <stdbool.h> + #include <stdio.h> ++#include <stdlib.h> ++#include "client.h" ++ ++static void ++usage(char *argv_0) ++{ ++ fprintf(stderr, ++ "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", ++ argv_0); ++} + + int +-main(int argc, char *argv[]) { +- (void)argc; (void)argv; +- printf("Hello, world!\n"); ++main(int argc, char *argv[]) ++{ ++ bool headers = false, follow_redirect = false; ++ char *certificate = NULL, *input = NULL; ++ ++ int c; ++ while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { ++ switch (c) { ++ case '4': ++ assert(0); // TODO ++ break; ++ case '6': ++ assert(0); // TODO ++ break; ++ case 'C': ++ certificate = optarg; ++ break; ++ case 'd': ++ input = optarg; ++ break; ++ case 'h': ++ usage(argv[0]); ++ return 0; ++ case 'L': ++ follow_redirect = true; ++ break; ++ case 'I': ++ headers = true; ++ break; ++ default: ++ fprintf(stderr, "fatal: unknown flag %c", c); ++ return 1; ++ } ++ } ++ if (optind != argc - 1) { ++ usage(argv[0]); ++ return 1; ++ } ++ ++ SSL_load_error_strings(); ++ ERR_load_crypto_strings(); ++ ++ struct gemini_response resp; ++ enum gemini_result r = gemini_request(argv[optind], NULL, &resp); ++ switch (r) { ++ case GEMINI_OK: ++ printf("OK\n"); ++ break; ++ case GEMINI_ERR_OOM: ++ printf("OOM\n"); ++ break; ++ case GEMINI_ERR_INVALID_URL: ++ printf("INVALID_URL\n"); ++ break; ++ case GEMINI_ERR_RESOLVE: ++ printf("RESOLVE\n"); ++ break; ++ case GEMINI_ERR_CONNECT: ++ printf("CONNECT\n"); ++ break; ++ case GEMINI_ERR_SSL: ++ fprintf(stderr, "SSL error: %s\n", ERR_error_string( ++ SSL_get_error(resp.ssl, resp.status), NULL)); ++ break; ++ } ++ ++ gemini_response_finish(&resp); ++ ++ (void)headers; ++ (void)follow_redirect; ++ (void)certificate; ++ (void)input; + return 0; + } +diff --git a/src/url.c b/src/url.c +new file mode 100644 +index 0000000000000000000000000000000000000000..47e31b5fcbeeecb5bc3962585311b20e3518bb73 +--- /dev/null ++++ b/src/url.c +@@ -0,0 +1,1448 @@ ++/*************************************************************************** ++ * _ _ ____ _ ++ * Project ___| | | | _ \| | ++ * / __| | | | |_) | | ++ * | (__| |_| | _ <| |___ ++ * \___|\___/|_| \_\_____| ++ * ++ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. ++ * ++ * This software is licensed as described in the file COPYING, which ++ * you should have received as part of this distribution. The terms ++ * are also available at https://curl.haxx.se/docs/copyright.html. ++ * ++ * You may opt to use, copy, modify, merge, publish, distribute and/or sell ++ * copies of the Software, and permit persons to whom the Software is ++ * furnished to do so, under the terms of the COPYING file. ++ * ++ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY ++ * KIND, either express or implied. ++ * ++ ***************************************************************************/ ++ ++#define MAX_SCHEME_LEN 8 ++ ++#include <assert.h> ++#include <ctype.h> ++#include <stdarg.h> ++#include <stdbool.h> ++#include <stdio.h> ++#include <stdlib.h> ++#include <string.h> ++#include <strings.h> ++#include "escape.h" ++#include "url.h" ++ ++/* Provided by gmni */ ++static char * ++aprintf(const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ int n = vsnprintf(NULL, 0, fmt, ap); ++ va_end(ap); ++ ++ char *strp = calloc(n + 1, 1); ++ assert(strp); ++ ++ va_start(ap, fmt); ++ n = vsnprintf(strp, n + 1, fmt, ap); ++ va_end(ap); ++ return strp; ++} ++ ++/* via lib/dotdot.c */ ++char *Curl_dedotdotify(const char *input) ++{ ++ size_t inlen = strlen(input); ++ char *clone; ++ size_t clen = inlen; /* the length of the cloned input */ ++ char *out = malloc(inlen + 1); ++ char *outptr; ++ char *orgclone; ++ char *queryp; ++ if(!out) ++ return NULL; /* out of memory */ ++ ++ *out = 0; /* zero terminates, for inputs like "./" */ ++ ++ /* get a cloned copy of the input */ ++ clone = strdup(input); ++ if(!clone) { ++ free(out); ++ return NULL; ++ } ++ orgclone = clone; ++ outptr = out; ++ ++ if(!*clone) { ++ /* zero length string, return that */ ++ free(out); ++ return clone; ++ } ++ ++ /* ++ * To handle query-parts properly, we must find it and remove it during the ++ * dotdot-operation and then append it again at the end to the output ++ * string. ++ */ ++ queryp = strchr(clone, '?'); ++ if(queryp) ++ *queryp = 0; ++ ++ do { ++ ++ /* A. If the input buffer begins with a prefix of "../" or "./", then ++ remove that prefix from the input buffer; otherwise, */ ++ ++ if(!strncmp("./", clone, 2)) { ++ clone += 2; ++ clen -= 2; ++ } ++ else if(!strncmp("../", clone, 3)) { ++ clone += 3; ++ clen -= 3; ++ } ++ ++ /* B. if the input buffer begins with a prefix of "/./" or "/.", where ++ "." is a complete path segment, then replace that prefix with "/" in ++ the input buffer; otherwise, */ ++ else if(!strncmp("/./", clone, 3)) { ++ clone += 2; ++ clen -= 2; ++ } ++ else if(!strcmp("/.", clone)) { ++ clone[1]='/'; ++ clone++; ++ clen -= 1; ++ } ++ ++ /* C. if the input buffer begins with a prefix of "/../" or "/..", where ++ ".." is a complete path segment, then replace that prefix with "/" in ++ the input buffer and remove the last segment and its preceding "/" (if ++ any) from the output buffer; otherwise, */ ++ ++ else if(!strncmp("/../", clone, 4)) { ++ clone += 3; ++ clen -= 3; ++ /* remove the last segment from the output buffer */ ++ while(outptr > out) { ++ outptr--; ++ if(*outptr == '/') ++ break; ++ } ++ *outptr = 0; /* zero-terminate where it stops */ ++ } ++ else if(!strcmp("/..", clone)) { ++ clone[2]='/'; ++ clone += 2; ++ clen -= 2; ++ /* remove the last segment from the output buffer */ ++ while(outptr > out) { ++ outptr--; ++ if(*outptr == '/') ++ break; ++ } ++ *outptr = 0; /* zero-terminate where it stops */ ++ } ++ ++ /* D. if the input buffer consists only of "." or "..", then remove ++ that from the input buffer; otherwise, */ ++ ++ else if(!strcmp(".", clone) || !strcmp("..", clone)) { ++ *clone = 0; ++ *out = 0; ++ } ++ ++ else { ++ /* E. move the first path segment in the input buffer to the end of ++ the output buffer, including the initial "/" character (if any) and ++ any subsequent characters up to, but not including, the next "/" ++ character or the end of the input buffer. */ ++ ++ do { ++ *outptr++ = *clone++; ++ clen--; ++ } while(*clone && (*clone != '/')); ++ *outptr = 0; ++ } ++ ++ } while(*clone); ++ ++ if(queryp) { ++ size_t qlen; ++ /* There was a query part, append that to the output. The 'clone' string ++ may now have been altered so we copy from the original input string ++ from the correct index. */ ++ size_t oindex = queryp - orgclone; ++ qlen = strlen(&input[oindex]); ++ memcpy(outptr, &input[oindex], qlen + 1); /* include the end zero byte */ ++ } ++ ++ free(orgclone); ++ return out; ++} ++ ++/* via lib/url.c */ ++CURLcode Curl_parse_login_details(const char *login, const size_t len, ++ char **userp, char **passwdp, ++ char **optionsp) ++{ ++ CURLcode result = CURLE_OK; ++ char *ubuf = NULL; ++ char *pbuf = NULL; ++ char *obuf = NULL; ++ const char *psep = NULL; ++ const char *osep = NULL; ++ size_t ulen; ++ size_t plen; ++ size_t olen; ++ ++ /* Attempt to find the password separator */ ++ if(passwdp) { ++ psep = strchr(login, ':'); ++ ++ /* Within the constraint of the login string */ ++ if(psep >= login + len) ++ psep = NULL; ++ } ++ ++ /* Attempt to find the options separator */ ++ if(optionsp) { ++ osep = strchr(login, ';'); ++ ++ /* Within the constraint of the login string */ ++ if(osep >= login + len) ++ osep = NULL; ++ } ++ ++ /* Calculate the portion lengths */ ++ ulen = (psep ? ++ (size_t)(osep && psep > osep ? osep - login : psep - login) : ++ (osep ? (size_t)(osep - login) : len)); ++ plen = (psep ? ++ (osep && osep > psep ? (size_t)(osep - psep) : ++ (size_t)(login + len - psep)) - 1 : 0); ++ olen = (osep ? ++ (psep && psep > osep ? (size_t)(psep - osep) : ++ (size_t)(login + len - osep)) - 1 : 0); ++ ++ /* Allocate the user portion buffer */ ++ if(userp && ulen) { ++ ubuf = malloc(ulen + 1); ++ if(!ubuf) ++ result = CURLE_OUT_OF_MEMORY; ++ } ++ ++ /* Allocate the password portion buffer */ ++ if(!result && passwdp && plen) { ++ pbuf = malloc(plen + 1); ++ if(!pbuf) { ++ free(ubuf); ++ result = CURLE_OUT_OF_MEMORY; ++ } ++ } ++ ++ /* Allocate the options portion buffer */ ++ if(!result && optionsp && olen) { ++ obuf = malloc(olen + 1); ++ if(!obuf) { ++ free(pbuf); ++ free(ubuf); ++ result = CURLE_OUT_OF_MEMORY; ++ } ++ } ++ ++ if(!result) { ++ /* Store the user portion if necessary */ ++ if(ubuf) { ++ memcpy(ubuf, login, ulen); ++ ubuf[ulen] = '\0'; ++ free(*userp); ++ *userp = ubuf; ++ } ++ ++ /* Store the password portion if necessary */ ++ if(pbuf) { ++ memcpy(pbuf, psep + 1, plen); ++ pbuf[plen] = '\0'; ++ free(*passwdp); ++ *passwdp = pbuf; ++ } ++ ++ /* Store the options portion if necessary */ ++ if(obuf) { ++ memcpy(obuf, osep + 1, olen); ++ obuf[olen] = '\0'; ++ free(*optionsp); ++ *optionsp = obuf; ++ } ++ } ++ ++ return result; ++} ++ ++/* Internal representation of CURLU. Point to URL-encoded strings. */ ++struct Curl_URL { ++ char *scheme; ++ char *user; ++ char *password; ++ char *options; /* IMAP only? */ ++ char *host; ++ char *port; ++ char *path; ++ char *query; ++ char *fragment; ++ ++ char *scratch; /* temporary scratch area */ ++ long portnum; /* the numerical version */ ++}; ++ ++#define DEFAULT_SCHEME "https" ++ ++static void free_urlhandle(struct Curl_URL *u) ++{ ++ free(u->scheme); ++ free(u->user); ++ free(u->password); ++ free(u->options); ++ free(u->host); ++ free(u->port); ++ free(u->path); ++ free(u->query); ++ free(u->fragment); ++ free(u->scratch); ++} ++ ++/* move the full contents of one handle onto another and ++ free the original */ ++static void mv_urlhandle(struct Curl_URL *from, ++ struct Curl_URL *to) ++{ ++ free_urlhandle(to); ++ *to = *from; ++ free(from); ++} ++ ++/* ++ * Find the separator at the end of the host name, or the '?' in cases like ++ * http://www.url.com?id=2380 ++ */ ++static const char *find_host_sep(const char *url) ++{ ++ const char *sep; ++ const char *query; ++ ++ /* Find the start of the hostname */ ++ sep = strstr(url, "//"); ++ if(!sep) ++ sep = url; ++ else ++ sep += 2; ++ ++ query = strchr(sep, '?'); ++ sep = strchr(sep, '/'); ++ ++ if(!sep) ++ sep = url + strlen(url); ++ ++ if(!query) ++ query = url + strlen(url); ++ ++ return sep < query ? sep : query; ++} ++ ++/* ++ * Decide in an encoding-independent manner whether a character in an ++ * URL must be escaped. The same criterion must be used in strlen_url() ++ * and strcpy_url(). ++ */ ++static bool urlchar_needs_escaping(int c) ++{ ++ return !(iscntrl(c) || isspace(c) || isgraph(c)); ++} ++ ++/* ++ * strlen_url() returns the length of the given URL if the spaces within the ++ * URL were properly URL encoded. ++ * URL encoding should be skipped for host names, otherwise IDN resolution ++ * will fail. ++ */ ++size_t Curl_strlen_url(const char *url, bool relative) ++{ ++ const unsigned char *ptr; ++ size_t newlen = 0; ++ bool left = true; /* left side of the ? */ ++ const unsigned char *host_sep = (const unsigned char *) url; ++ ++ if(!relative) ++ host_sep = (const unsigned char *) find_host_sep(url); ++ ++ for(ptr = (unsigned char *)url; *ptr; ptr++) { ++ ++ if(ptr < host_sep) { ++ ++newlen; ++ continue; ++ } ++ ++ switch(*ptr) { ++ case '?': ++ left = false; ++ /* FALLTHROUGH */ ++ default: ++ if(urlchar_needs_escaping(*ptr)) ++ newlen += 2; ++ newlen++; ++ break; ++ case ' ': ++ if(left) ++ newlen += 3; ++ else ++ newlen++; ++ break; ++ } ++ } ++ return newlen; ++} ++ ++/* strcpy_url() copies a url to a output buffer and URL-encodes the spaces in ++ * the source URL accordingly. ++ * URL encoding should be skipped for host names, otherwise IDN resolution ++ * will fail. ++ */ ++void Curl_strcpy_url(char *output, const char *url, bool relative) ++{ ++ /* we must add this with whitespace-replacing */ ++ bool left = true; ++ const unsigned char *iptr; ++ char *optr = output; ++ const unsigned char *host_sep = (const unsigned char *) url; ++ ++ if(!relative) ++ host_sep = (const unsigned char *) find_host_sep(url); ++ ++ for(iptr = (unsigned char *)url; /* read from here */ ++ *iptr; /* until zero byte */ ++ iptr++) { ++ ++ if(iptr < host_sep) { ++ *optr++ = *iptr; ++ continue; ++ } ++ ++ switch(*iptr) { ++ case '?': ++ left = false; ++ /* FALLTHROUGH */ ++ default: ++ if(urlchar_needs_escaping(*iptr)) { ++ snprintf(optr, 4, "%%%02x", *iptr); ++ optr += 3; ++ } ++ else ++ *optr++=*iptr; ++ break; ++ case ' ': ++ if(left) { ++ *optr++='%'; /* add a '%' */ ++ *optr++='2'; /* add a '2' */ ++ *optr++='0'; /* add a '0' */ ++ } ++ else ++ *optr++='+'; /* add a '+' here */ ++ break; ++ } ++ } ++ *optr = 0; /* zero terminate output buffer */ ++ ++} ++ ++/* ++ * Returns true if the given URL is absolute (as opposed to relative) within ++ * the buffer size. Returns the scheme in the buffer if true and 'buf' is ++ * non-NULL. ++ */ ++bool Curl_is_absolute_url(const char *url, char *buf, size_t buflen) ++{ ++ size_t i; ++ for(i = 0; i < buflen && url[i]; ++i) { ++ char s = url[i]; ++ if((s == ':') && (url[i + 1] == '/')) { ++ if(buf) ++ buf[i] = 0; ++ return true; ++ } ++ /* RFC 3986 3.1 explains: ++ scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ++ */ ++ else if(isalnum(s) || (s == '+') || (s == '-') || (s == '.') ) { ++ if(buf) ++ buf[i] = (char)tolower(s); ++ } ++ else ++ break; ++ } ++ return false; ++} ++ ++/* ++ * Concatenate a relative URL to a base URL making it absolute. ++ * URL-encodes any spaces. ++ * The returned pointer must be freed by the caller unless NULL ++ * (returns NULL on out of memory). ++ */ ++char *Curl_concat_url(const char *base, const char *relurl) ++{ ++ /*** ++ TRY to append this new path to the old URL ++ to the right of the host part. Oh crap, this is doomed to cause ++ problems in the future... ++ */ ++ char *newest; ++ char *protsep; ++ char *pathsep; ++ size_t newlen; ++ bool host_changed = false; ++ ++ const char *useurl = relurl; ++ size_t urllen; ++ ++ /* we must make our own copy of the URL to play with, as it may ++ point to read-only data */ ++ char *url_clone = strdup(base); ++ ++ if(!url_clone) ++ return NULL; /* skip out of this NOW */ ++ ++ /* protsep points to the start of the host name */ ++ protsep = strstr(url_clone, "//"); ++ if(!protsep) ++ protsep = url_clone; ++ else ++ protsep += 2; /* pass the slashes */ ++ ++ if('/' != relurl[0]) { ++ int level = 0; ++ ++ /* First we need to find out if there's a ?-letter in the URL, ++ and cut it and the right-side of that off */ ++ pathsep = strchr(protsep, '?'); ++ if(pathsep) ++ *pathsep = 0; ++ ++ /* we have a relative path to append to the last slash if there's one ++ available, or if the new URL is just a query string (starts with a ++ '?') we append the new one at the end of the entire currently worked ++ out URL */ ++ if(useurl[0] != '?') { ++ pathsep = strrchr(protsep, '/'); ++ if(pathsep) ++ *pathsep = 0; ++ } ++ ++ /* Check if there's any slash after the host name, and if so, remember ++ that position instead */ ++ pathsep = strchr(protsep, '/'); ++ if(pathsep) ++ protsep = pathsep + 1; ++ else ++ protsep = NULL; ++ ++ /* now deal with one "./" or any amount of "../" in the newurl ++ and act accordingly */ ++ ++ if((useurl[0] == '.') && (useurl[1] == '/')) ++ useurl += 2; /* just skip the "./" */ ++ ++ while((useurl[0] == '.') && ++ (useurl[1] == '.') && ++ (useurl[2] == '/')) { ++ level++; ++ useurl += 3; /* pass the "../" */ ++ } ++ ++ if(protsep) { ++ while(level--) { ++ /* cut off one more level from the right of the original URL */ ++ pathsep = strrchr(protsep, '/'); ++ if(pathsep) ++ *pathsep = 0; ++ else { ++ *protsep = 0; ++ break; ++ } ++ } ++ } ++ } ++ else { ++ /* We got a new absolute path for this server */ ++ ++ if((relurl[0] == '/') && (relurl[1] == '/')) { ++ /* the new URL starts with //, just keep the protocol part from the ++ original one */ ++ *protsep = 0; ++ useurl = &relurl[2]; /* we keep the slashes from the original, so we ++ skip the new ones */ ++ host_changed = true; ++ } ++ else { ++ /* cut off the original URL from the first slash, or deal with URLs ++ without slash */ ++ pathsep = strchr(protsep, '/'); ++ if(pathsep) { ++ /* When people use badly formatted URLs, such as ++ "http://www.url.com?dir=/home/daniel" we must not use the first ++ slash, if there's a ?-letter before it! */ ++ char *sep = strchr(protsep, '?'); ++ if(sep && (sep < pathsep)) ++ pathsep = sep; ++ *pathsep = 0; ++ } ++ else { ++ /* There was no slash. Now, since we might be operating on a badly ++ formatted URL, such as "http://www.url.com?id=2380" which doesn't ++ use a slash separator as it is supposed to, we need to check for a ++ ?-letter as well! */ ++ pathsep = strchr(protsep, '?'); ++ if(pathsep) ++ *pathsep = 0; ++ } ++ } ++ } ++ ++ /* If the new part contains a space, this is a mighty stupid redirect ++ but we still make an effort to do "right". To the left of a '?' ++ letter we replace each space with %20 while it is replaced with '+' ++ on the right side of the '?' letter. ++ */ ++ newlen = Curl_strlen_url(useurl, !host_changed); ++ ++ urllen = strlen(url_clone); ++ ++ newest = malloc(urllen + 1 + /* possible slash */ ++ newlen + 1 /* zero byte */); ++ ++ if(!newest) { ++ free(url_clone); /* don't leak this */ ++ return NULL; ++ } ++ ++ /* copy over the root url part */ ++ memcpy(newest, url_clone, urllen); ++ ++ /* check if we need to append a slash */ ++ if(('/' == useurl[0]) || (protsep && !*protsep) || ('?' == useurl[0])) ++ ; ++ else ++ newest[urllen++]='/'; ++ ++ /* then append the new piece on the right side */ ++ Curl_strcpy_url(&newest[urllen], useurl, !host_changed); ++ ++ free(url_clone); ++ ++ return newest; ++} ++ ++/* ++ * parse_hostname_login() ++ * ++ * Parse the login details (user name, password and options) from the URL and ++ * strip them out of the host name ++ * ++ */ ++static CURLUcode parse_hostname_login(struct Curl_URL *u, ++ char **hostname, ++ unsigned int flags) ++{ ++ CURLUcode result = CURLUE_OK; ++ CURLcode ccode; ++ char *userp = NULL; ++ char *passwdp = NULL; ++ char *optionsp = NULL; ++ ++ /* At this point, we're hoping all the other special cases have ++ * been taken care of, so conn->host.name is at most ++ * [user[:password][;options]]@]hostname ++ * ++ * We need somewhere to put the embedded details, so do that first. ++ */ ++ ++ char *ptr = strchr(*hostname, '@'); ++ char *login = *hostname; ++ ++ if(!ptr) ++ goto out; ++ ++ /* We will now try to extract the ++ * possible login information in a string like: ++ * ftp://user:password@ftp.my.site:8021/README */ ++ *hostname = ++ptr; ++ ++ /* We could use the login information in the URL so extract it. Only parse ++ options if the handler says we should. Note that 'h' might be NULL! */ ++ ccode = Curl_parse_login_details(login, ptr - login - 1, ++ &userp, &passwdp, NULL); ++ if(ccode) { ++ result = CURLUE_MALFORMED_INPUT; ++ goto out; ++ } ++ ++ if(userp) { ++ if(flags & CURLU_DISALLOW_USER) { ++ /* Option DISALLOW_USER is set and url contains username. */ ++ result = CURLUE_USER_NOT_ALLOWED; ++ goto out; ++ } ++ ++ u->user = userp; ++ } ++ ++ if(passwdp) ++ u->password = passwdp; ++ ++ if(optionsp) ++ u->options = optionsp; ++ ++ return CURLUE_OK; ++ out: ++ ++ free(userp); ++ free(passwdp); ++ free(optionsp); ++ ++ return result; ++} ++ ++static CURLUcode parse_port(struct Curl_URL *u, char *hostname) ++{ ++ char *portptr; ++ char endbracket; ++ int len; ++ ++ if((1 == sscanf(hostname, "[%*45[0123456789abcdefABCDEF:.%%]%c%n", ++ &endbracket, &len)) && ++ (']' == endbracket)) { ++ /* this is a RFC2732-style specified IP-address */ ++ portptr = &hostname[len]; ++ if(*portptr) { ++ if(*portptr != ':') ++ return CURLUE_MALFORMED_INPUT; ++ } ++ else ++ portptr = NULL; ++ } ++ else ++ portptr = strchr(hostname, ':'); ++ ++ if(portptr) { ++ char *rest; ++ long port; ++ char portbuf[7]; ++ ++ if(!isdigit(portptr[1])) ++ return CURLUE_BAD_PORT_NUMBER; ++ ++ port = strtol(portptr + 1, &rest, 10); /* Port number must be decimal */ ++ ++ if((port <= 0) || (port > 0xffff)) ++ /* Single unix standard says port numbers are 16 bits long, but we don't ++ treat port zero as OK. */ ++ return CURLUE_BAD_PORT_NUMBER; ++ ++ if(rest[0]) ++ return CURLUE_BAD_PORT_NUMBER; ++ ++ if(rest != &portptr[1]) { ++ *portptr++ = '\0'; /* cut off the name there */ ++ *rest = 0; ++ /* generate a new to get rid of leading zeroes etc */ ++ snprintf(portbuf, sizeof(portbuf), "%ld", port); ++ u->portnum = port; ++ u->port = strdup(portbuf); ++ if(!u->port) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ else { ++ /* Browser behavior adaptation. If there's a colon with no digits after, ++ just cut off the name there which makes us ignore the colon and just ++ use the default port. Firefox and Chrome both do that. */ ++ *portptr = '\0'; ++ } ++ } ++ ++ return CURLUE_OK; ++} ++ ++/* scan for byte values < 31 or 127 */ ++static CURLUcode junkscan(char *part) ++{ ++ char badbytes[]={ ++ /* */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, ++ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, ++ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, ++ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, ++ 0x7f, ++ 0x00 /* zero terminate */ ++ }; ++ if(part) { ++ size_t n = strlen(part); ++ size_t nfine = strcspn(part, badbytes); ++ if(nfine != n) ++ /* since we don't know which part is scanned, return a generic error ++ code */ ++ return CURLUE_MALFORMED_INPUT; ++ } ++ return CURLUE_OK; ++} ++ ++static CURLUcode hostname_check(char *hostname, unsigned int flags) ++{ ++ const char *l = NULL; /* accepted characters */ ++ size_t len; ++ size_t hlen = strlen(hostname); ++ (void)flags; ++ ++ if(hostname[0] == '[') { ++ hostname++; ++ l = "0123456789abcdefABCDEF::.%"; ++ hlen -= 2; ++ } ++ ++ if(l) { ++ /* only valid letters are ok */ ++ len = strspn(hostname, l); ++ if(hlen != len) ++ /* hostname with bad content */ ++ return CURLUE_MALFORMED_INPUT; ++ } ++ else { ++ /* letters from the second string is not ok */ ++ len = strcspn(hostname, " "); ++ if(hlen != len) ++ /* hostname with bad content */ ++ return CURLUE_MALFORMED_INPUT; ++ } ++ return CURLUE_OK; ++} ++ ++#define HOSTNAME_END(x) (((x) == '/') || ((x) == '?') || ((x) == '#')) ++ ++static CURLUcode seturl(const char *url, struct Curl_URL *u, unsigned int flags) ++{ ++ char *path; ++ bool path_alloced = false; ++ char *hostname; ++ char *query = NULL; ++ char *fragment = NULL; ++ CURLUcode result; ++ bool url_has_scheme = false; ++ char schemebuf[MAX_SCHEME_LEN]; ++ char *schemep = NULL; ++ size_t schemelen = 0; ++ size_t urllen; ++ ++ if(!url) ++ return CURLUE_MALFORMED_INPUT; ++ ++ /************************************************************* ++ * Parse the URL. ++ ************************************************************/ ++ /* allocate scratch area */ ++ urllen = strlen(url); ++ path = u->scratch = malloc(urllen * 2 + 2); ++ if(!path) ++ return CURLUE_OUT_OF_MEMORY; ++ ++ hostname = &path[urllen + 1]; ++ hostname[0] = 0; ++ ++ if(Curl_is_absolute_url(url, schemebuf, sizeof(schemebuf))) { ++ url_has_scheme = true; ++ schemelen = strlen(schemebuf); ++ } ++ ++ /* handle the file: scheme */ ++ if(url_has_scheme && strcasecmp(schemebuf, "file") == 0) { ++ /* path has been allocated large enough to hold this */ ++ strcpy(path, &url[5]); ++ ++ hostname = NULL; /* no host for file: URLs */ ++ u->scheme = strdup("file"); ++ if(!u->scheme) ++ return CURLUE_OUT_OF_MEMORY; ++ ++ /* Extra handling URLs with an authority component (i.e. that start with ++ * "file://") ++ * ++ * We allow omitted hostname (e.g. file:/<path>) -- valid according to ++ * RFC 8089, but not the (current) WHAT-WG URL spec. ++ */ ++ if(path[0] == '/' && path[1] == '/') { ++ /* swallow the two slashes */ ++ char *ptr = &path[2]; ++ path = ptr; ++ } ++ } ++ else { ++ /* clear path */ ++ const char *p; ++ const char *hostp; ++ size_t len; ++ path[0] = 0; ++ ++ if(url_has_scheme) { ++ int i = 0; ++ p = &url[schemelen + 1]; ++ while(p && (*p == '/') && (i < 4)) { ++ p++; ++ i++; ++ } ++ if((i < 1) || (i>3)) ++ /* less than one or more than three slashes */ ++ return CURLUE_MALFORMED_INPUT; ++ ++ schemep = schemebuf; ++ if(junkscan(schemep)) ++ return CURLUE_MALFORMED_INPUT; ++ } ++ else { ++ /* no scheme! */ ++ return CURLUE_MALFORMED_INPUT; ++ } ++ hostp = p; /* host name starts here */ ++ ++ while(*p && !HOSTNAME_END(*p)) /* find end of host name */ ++ p++; ++ ++ len = p - hostp; ++ if(!len) ++ return CURLUE_MALFORMED_INPUT; ++ ++ memcpy(hostname, hostp, len); ++ hostname[len] = 0; ++ ++ len = strlen(p); ++ memcpy(path, p, len); ++ path[len] = 0; ++ ++ u->scheme = strdup(schemep); ++ if(!u->scheme) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ ++ if(junkscan(path)) ++ return CURLUE_MALFORMED_INPUT; ++ ++ query = strchr(path, '?'); ++ if(query) ++ *query++ = 0; ++ ++ fragment = strchr(query?query:path, '#'); ++ if(fragment) ++ *fragment++ = 0; ++ ++ if(!path[0]) ++ /* if there's no path set, unset */ ++ path = NULL; ++ else if(!(flags & CURLU_PATH_AS_IS)) { ++ /* sanitise paths and remove ../ and ./ sequences according to RFC3986 */ ++ char *newp = Curl_dedotdotify(path); ++ if(!newp) ++ return CURLUE_OUT_OF_MEMORY; ++ ++ if(strcmp(newp, path)) { ++ /* if we got a new version */ ++ path = newp; ++ path_alloced = true; ++ } ++ else ++ free(newp); ++ } ++ if(path) { ++ u->path = path_alloced?path:strdup(path); ++ if(!u->path) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ ++ if(hostname) { ++ /* ++ * Parse the login details and strip them out of the host name. ++ */ ++ if(junkscan(hostname)) ++ return CURLUE_MALFORMED_INPUT; ++ ++ result = parse_hostname_login(u, &hostname, flags); ++ if(result) ++ return result; ++ ++ result = parse_port(u, hostname); ++ if(result) ++ return result; ++ ++ result = hostname_check(hostname, flags); ++ if(result) ++ return result; ++ ++ u->host = strdup(hostname); ++ if(!u->host) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ ++ if(query && query[0]) { ++ u->query = strdup(query); ++ if(!u->query) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ if(fragment && fragment[0]) { ++ u->fragment = strdup(fragment); ++ if(!u->fragment) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ ++ free(u->scratch); ++ u->scratch = NULL; ++ ++ return CURLUE_OK; ++} ++ ++/* ++ * Parse the URL and set the relevant members of the Curl_URL struct. ++ */ ++static CURLUcode parseurl(const char *url, struct Curl_URL *u, unsigned int flags) ++{ ++ CURLUcode result = seturl(url, u, flags); ++ if(result) { ++ free_urlhandle(u); ++ memset(u, 0, sizeof(struct Curl_URL)); ++ } ++ return result; ++} ++ ++/* ++ */ ++struct Curl_URL *curl_url(void) ++{ ++ return calloc(sizeof(struct Curl_URL), 1); ++} ++ ++void curl_url_cleanup(struct Curl_URL *u) ++{ ++ if(u) { ++ free_urlhandle(u); ++ free(u); ++ } ++} ++ ++#define DUP(dest, src, name) \ ++ if(src->name) { \ ++ dest->name = strdup(src->name); \ ++ if(!dest->name) \ ++ goto fail; \ ++ } ++ ++struct Curl_URL *curl_url_dup(struct Curl_URL *in) ++{ ++ struct Curl_URL *u = calloc(sizeof(struct Curl_URL), 1); ++ if(u) { ++ DUP(u, in, scheme); ++ DUP(u, in, user); ++ DUP(u, in, password); ++ DUP(u, in, options); ++ DUP(u, in, host); ++ DUP(u, in, port); ++ DUP(u, in, path); ++ DUP(u, in, query); ++ DUP(u, in, fragment); ++ u->portnum = in->portnum; ++ } ++ return u; ++ fail: ++ curl_url_cleanup(u); ++ return NULL; ++} ++ ++CURLUcode curl_url_get(struct Curl_URL *u, CURLUPart what, ++ char **part, unsigned int flags) ++{ ++ char *ptr; ++ CURLUcode ifmissing = CURLUE_UNKNOWN_PART; ++ bool urldecode = (flags & CURLU_URLDECODE)?1:0; ++ bool plusdecode = false; ++ (void)flags; ++ if(!u) ++ return CURLUE_BAD_HANDLE; ++ if(!part) ++ return CURLUE_BAD_PARTPOINTER; ++ *part = NULL; ++ ++ switch(what) { ++ case CURLUPART_SCHEME: ++ ptr = u->scheme; ++ ifmissing = CURLUE_NO_SCHEME; ++ urldecode = false; /* never for schemes */ ++ break; ++ case CURLUPART_USER: ++ ptr = u->user; ++ ifmissing = CURLUE_NO_USER; ++ break; ++ case CURLUPART_PASSWORD: ++ ptr = u->password; ++ ifmissing = CURLUE_NO_PASSWORD; ++ break; ++ case CURLUPART_OPTIONS: ++ ptr = u->options; ++ ifmissing = CURLUE_NO_OPTIONS; ++ break; ++ case CURLUPART_HOST: ++ ptr = u->host; ++ ifmissing = CURLUE_NO_HOST; ++ break; ++ case CURLUPART_PORT: ++ ptr = u->port; ++ ifmissing = CURLUE_NO_PORT; ++ urldecode = false; /* never for port */ ++ break; ++ case CURLUPART_PATH: ++ ptr = u->path; ++ if(!ptr) { ++ ptr = u->path = strdup("/"); ++ if(!u->path) ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ break; ++ case CURLUPART_QUERY: ++ ptr = u->query; ++ ifmissing = CURLUE_NO_QUERY; ++ plusdecode = urldecode; ++ break; ++ case CURLUPART_FRAGMENT: ++ ptr = u->fragment; ++ ifmissing = CURLUE_NO_FRAGMENT; ++ break; ++ case CURLUPART_URL: { ++ char *url; ++ char *scheme; ++ char *options = u->options; ++ char *port = u->port; ++ if(u->scheme && strcasecmp("file", u->scheme) == 0) { ++ url = aprintf("file://%s%s%s", ++ u->path, ++ u->fragment? "#": "", ++ u->fragment? u->fragment : ""); ++ } ++ else if(!u->host) ++ return CURLUE_NO_HOST; ++ else { ++ if(u->scheme) ++ scheme = u->scheme; ++ else ++ return CURLUE_NO_SCHEME; ++ ++ options = NULL; ++ ++ url = aprintf("%s://%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", ++ scheme, ++ u->user ? u->user : "", ++ u->password ? ":": "", ++ u->password ? u->password : "", ++ options ? ";" : "", ++ options ? options : "", ++ (u->user || u->password || options) ? "@": "", ++ u->host, ++ port ? ":": "", ++ port ? port : "", ++ (u->path && (u->path[0] != '/')) ? "/": "", ++ u->path ? u->path : "/", ++ u->query? "?": "", ++ u->query? u->query : "", ++ u->fragment? "#": "", ++ u->fragment? u->fragment : ""); ++ } ++ if(!url) ++ return CURLUE_OUT_OF_MEMORY; ++ *part = url; ++ return CURLUE_OK; ++ break; ++ } ++ default: ++ ptr = NULL; ++ } ++ if(ptr) { ++ *part = strdup(ptr); ++ if(!*part) ++ return CURLUE_OUT_OF_MEMORY; ++ if(plusdecode) { ++ /* convert + to space */ ++ char *plus; ++ for(plus = *part; *plus; ++plus) { ++ if(*plus == '+') ++ *plus = ' '; ++ } ++ } ++ if(urldecode) { ++ char *decoded; ++ size_t dlen; ++ CURLcode res = Curl_urldecode(*part, 0, &decoded, &dlen, true); ++ free(*part); ++ if(res) { ++ *part = NULL; ++ return CURLUE_URLDECODE; ++ } ++ *part = decoded; ++ } ++ return CURLUE_OK; ++ } ++ else ++ return ifmissing; ++} ++ ++CURLUcode curl_url_set(struct Curl_URL *u, CURLUPart what, ++ const char *part, unsigned int flags) ++{ ++ char **storep = NULL; ++ long port = 0; ++ bool urlencode = (flags & CURLU_URLENCODE)? 1 : 0; ++ bool plusencode = false; ++ bool urlskipslash = false; ++ bool appendquery = false; ++ bool equalsencode = false; ++ ++ if(!u) ++ return CURLUE_BAD_HANDLE; ++ if(!part) { ++ /* setting a part to NULL clears it */ ++ switch(what) { ++ case CURLUPART_URL: ++ break; ++ case CURLUPART_SCHEME: ++ storep = &u->scheme; ++ break; ++ case CURLUPART_USER: ++ storep = &u->user; ++ break; ++ case CURLUPART_PASSWORD: ++ storep = &u->password; ++ break; ++ case CURLUPART_OPTIONS: ++ storep = &u->options; ++ break; ++ case CURLUPART_HOST: ++ storep = &u->host; ++ break; ++ case CURLUPART_PORT: ++ storep = &u->port; ++ break; ++ case CURLUPART_PATH: ++ storep = &u->path; ++ break; ++ case CURLUPART_QUERY: ++ storep = &u->query; ++ break; ++ case CURLUPART_FRAGMENT: ++ storep = &u->fragment; ++ break; ++ default: ++ return CURLUE_UNKNOWN_PART; ++ } ++ if(storep && *storep) { ++ free(*storep); ++ *storep = NULL; ++ } ++ return CURLUE_OK; ++ } ++ ++ switch(what) { ++ case CURLUPART_SCHEME: ++ storep = &u->scheme; ++ urlencode = false; /* never */ ++ break; ++ case CURLUPART_USER: ++ storep = &u->user; ++ break; ++ case CURLUPART_PASSWORD: ++ storep = &u->password; ++ break; ++ case CURLUPART_OPTIONS: ++ storep = &u->options; ++ break; ++ case CURLUPART_HOST: ++ storep = &u->host; ++ break; ++ case CURLUPART_PORT: ++ urlencode = false; /* never */ ++ port = strtol(part, NULL, 10); /* Port number must be decimal */ ++ if((port <= 0) || (port > 0xffff)) ++ return CURLUE_BAD_PORT_NUMBER; ++ storep = &u->port; ++ break; ++ case CURLUPART_PATH: ++ urlskipslash = true; ++ storep = &u->path; ++ break; ++ case CURLUPART_QUERY: ++ plusencode = urlencode; ++ appendquery = (flags & CURLU_APPENDQUERY)?1:0; ++ equalsencode = appendquery; ++ storep = &u->query; ++ break; ++ case CURLUPART_FRAGMENT: ++ storep = &u->fragment; ++ break; ++ case CURLUPART_URL: { ++ /* ++ * Allow a new URL to replace the existing (if any) contents. ++ * ++ * If the existing contents is enough for a URL, allow a relative URL to ++ * replace it. ++ */ ++ CURLUcode result; ++ char *oldurl; ++ char *redired_url; ++ struct Curl_URL *handle2; ++ ++ if(Curl_is_absolute_url(part, NULL, MAX_SCHEME_LEN)) { ++ handle2 = curl_url(); ++ if(!handle2) ++ return CURLUE_OUT_OF_MEMORY; ++ result = parseurl(part, handle2, flags); ++ if(!result) ++ mv_urlhandle(handle2, u); ++ else ++ curl_url_cleanup(handle2); ++ return result; ++ } ++ /* extract the full "old" URL to do the redirect on */ ++ result = curl_url_get(u, CURLUPART_URL, &oldurl, flags); ++ if(result) { ++ /* couldn't get the old URL, just use the new! */ ++ handle2 = curl_url(); ++ if(!handle2) ++ return CURLUE_OUT_OF_MEMORY; ++ result = parseurl(part, handle2, flags); ++ if(!result) ++ mv_urlhandle(handle2, u); ++ else ++ curl_url_cleanup(handle2); ++ return result; ++ } ++ ++ /* apply the relative part to create a new URL */ ++ redired_url = Curl_concat_url(oldurl, part); ++ free(oldurl); ++ if(!redired_url) ++ return CURLUE_OUT_OF_MEMORY; ++ ++ /* now parse the new URL */ ++ handle2 = curl_url(); ++ if(!handle2) { ++ free(redired_url); ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ result = parseurl(redired_url, handle2, flags); ++ free(redired_url); ++ if(!result) ++ mv_urlhandle(handle2, u); ++ else ++ curl_url_cleanup(handle2); ++ return result; ++ } ++ default: ++ return CURLUE_UNKNOWN_PART; ++ } ++ if(storep) { ++ const char *newp = part; ++ size_t nalloc = strlen(part); ++ ++ if(urlencode) { ++ const char *i; ++ char *o; ++ bool free_part = false; ++ char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ ++ if(!enc) ++ return CURLUE_OUT_OF_MEMORY; ++ if(plusencode) { ++ /* space to plus */ ++ i = part; ++ for(o = enc; *i; ++o, ++i) ++ *o = (*i == ' ') ? '+' : *i; ++ *o = 0; /* zero terminate */ ++ part = strdup(enc); ++ if(!part) { ++ free(enc); ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ free_part = true; ++ } ++ for(i = part, o = enc; *i; i++) { ++ if(Curl_isunreserved(*i) || ++ ((*i == '/') && urlskipslash) || ++ ((*i == '=') && equalsencode) || ++ ((*i == '+') && plusencode)) { ++ if((*i == '=') && equalsencode) ++ /* only skip the first equals sign */ ++ equalsencode = false; ++ *o = *i; ++ o++; ++ } ++ else { ++ snprintf(o, 4, "%%%02x", *i); ++ o += 3; ++ } ++ } ++ *o = 0; /* zero terminate */ ++ newp = enc; ++ if(free_part) ++ free((char *)part); ++ } ++ else { ++ char *p; ++ newp = strdup(part); ++ if(!newp) ++ return CURLUE_OUT_OF_MEMORY; ++ p = (char *)newp; ++ while(*p) { ++ /* make sure percent encoded are lower case */ ++ if((*p == '%') && isxdigit(p[1]) && isxdigit(p[2]) && ++ (isupper(p[1]) || isupper(p[2]))) { ++ p[1] = (char)tolower(p[1]); ++ p[2] = (char)tolower(p[2]); ++ p += 3; ++ } ++ else ++ p++; ++ } ++ } ++ ++ if(appendquery) { ++ /* Append the string onto the old query. Add a '&' separator if none is ++ present at the end of the exsting query already */ ++ size_t querylen = u->query ? strlen(u->query) : 0; ++ bool addamperand = querylen && (u->query[querylen -1] != '&'); ++ if(querylen) { ++ size_t newplen = strlen(newp); ++ char *p = malloc(querylen + addamperand + newplen + 1); ++ if(!p) { ++ free((char *)newp); ++ return CURLUE_OUT_OF_MEMORY; ++ } ++ strcpy(p, u->query); /* original query */ ++ if(addamperand) ++ p[querylen] = '&'; /* ampersand */ ++ strcpy(&p[querylen + addamperand], newp); /* new suffix */ ++ free((char *)newp); ++ free(*storep); ++ *storep = p; ++ return CURLUE_OK; ++ } ++ } ++ ++ free(*storep); ++ *storep = (char *)newp; ++ } ++ /* set after the string, to make it not assigned if the allocation above ++ fails */ ++ if(port) ++ u->portnum = port; ++ return CURLUE_OK; ++} diff --git a/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch b/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch @@ -0,0 +1,35 @@ +diff --git a/src/util.c b/src/util.c +index 0a479af3ea734ce25d0806aa086e61bef6b8953b..573e8a717efbe843fce003126cb932928b15f716 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -68,15 +68,15 @@ int + download_resp(FILE *out, struct gemini_response resp, const char *path, + char *url) + { +- char buf[PATH_MAX]; ++ char path_buf[PATH_MAX]; + assert(path); + if (path[0] == '\0') { + path = "./"; + } + if (path[strlen(path)-1] == '/') { +- strncat(strncpy(&buf[0], path, sizeof(buf)), basename(url), +- sizeof(buf)); +- path = &buf[0]; ++ int n = snprintf(path_buf, sizeof(path_buf), "%s%s", path, basename(url)); ++ assert((size_t)n < sizeof(path_buf)); ++ path = path_buf; + } + FILE *f = fopen(path, "w"); + if (f == NULL) { +@@ -85,8 +85,9 @@ path, strerror(errno)); + return 1; + } + fprintf(out, "Downloading %s to %s\n", url, path); ++ char buf[BUFSIZ]; + for (int n = 1; n > 0;) { +- n = BIO_read(resp.bio, buf, BUFSIZ); ++ n = BIO_read(resp.bio, buf, sizeof(buf)); + if (n == -1) { + fprintf(stderr, "Error: read\n"); + return 1; diff --git a/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch b/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch @@ -0,0 +1,111 @@ +diff --git a/src/gmnlm.c b/src/gmnlm.c +index 82d7abc15562ad7ee4fe5f7e5349be477f719ae5..cb7e2843798f66f596e071823d9e41153a555fcc 100644 +--- a/src/gmnlm.c ++++ b/src/gmnlm.c +@@ -19,6 +19,14 @@ #include <gmni/tofu.h> + #include <gmni/url.h> + #include "util.h" + ++#define ANSI_COLOR_RED "\x1b[31m" ++#define ANSI_COLOR_GREEN "\x1b[32m" ++#define ANSI_COLOR_YELLOW "\x1b[33m" ++#define ANSI_COLOR_BLUE "\x1b[34m" ++#define ANSI_COLOR_MAGENTA "\x1b[35m" ++#define ANSI_COLOR_CYAN "\x1b[36m" ++#define ANSI_COLOR_RESET "\x1b[0m" ++ + struct link { + char *url; + struct link *next; +@@ -767,7 +775,7 @@ if (searching) { + out = fopen("/dev/null", "w+"); + } + +- fprintf(out, "\n\n"); ++ fprintf(out, "\n"); + char *text = NULL; + int row = 0, col = 0; + struct gemini_token tok; +@@ -776,20 +784,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { + repeat: + switch (tok.token) { + case GEMINI_TEXT: +- col += fprintf(out, " "); ++ col += fprintf(out, " "); + if (text == NULL) { + text = tok.text; + } + break; + case GEMINI_LINK: