-- Leo's gemini proxy

-- Connecting to gmi.noulin.net:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini

Early gemini support in w3m


Feed


date: 2021-11-14 16:39:19


categories: gemini


firstPublishDate: 2021-11-14 16:39:19


I added support from the gemini protocol and gemtext in w3m.


The patch below is a prototype, the bare minimum is implemented:


all certificates are accepted, tofu is not implemented

the response and meta from the servers are not handle

it handles only the 'text/gemini' mime type

socket and tls errors are not handled, it is assumed it always works

gemini input is not supported


With this patch, w3m can only load gemtext pages and follow links.


To use this patch, w3m has to be build with the patch applied.


First, save the patch below as `gemini.patch`, then run:


git clone https://github.com/tats/w3m
cd w3m
git checkout b201f426e4d184113a7b2ed95ae8cb2f1ff68f5a
git apply gemini.patch

# build w3m
sudo apt-get install -y libgc-dev libssl-dev libglib2.0-dev libgdk-pixbuf-2.0-dev libgdk-pixbuf-xlib-2.0-dev gettext
./configure
make

The patch:


From 3ee741499188817824e8abb3b049512389ec84f7 Mon Sep 17 00:00:00 2001
From: Remy Noulin <loader2x@gmail.com>
Date: Sun, 14 Nov 2021 16:21:43 +0200
Subject: [PATCH] prototype support for gemini

The bare minimum is implemented:
- all certificates are accepted, tofu is not implemented
- the response and meta from the servers are not handle
- it handles only the 'text/gemini' mime type
- socket and tls errors are not handled, it is assumed it always works
- gemini input is not supported

With this patch, w3m can only load gemtext pages and follow links.

Bonus/oldconfigure.sh |   1 +
acinclude.m4          |  40 +++--
config.h.dist         |   7 +-
config.h.in           |   1 +
configure             |  18 +++
file.c                | 425 ++++++++++++++++++++++++++++++++++++++++++++++----
html.h                |   1 +
main.c                |   6 +
proto.h               |   3 +
url.c                 |  65 +++++++-
10 files changed, 508 insertions(+), 59 deletions(-)
---
 Bonus/oldconfigure.sh |   1 +
 acinclude.m4          |  40 ++--
 config.h.dist         |   7 +-
 config.h.in           |   1 +
 configure             |  18 ++
 file.c                | 425 ++++++++++++++++++++++++++++++++++++++----
 html.h                |   1 +
 main.c                |   6 +
 proto.h               |   3 +
 url.c                 |  67 ++++++-
 10 files changed, 509 insertions(+), 60 deletions(-)

diff --git a/Bonus/oldconfigure.sh b/Bonus/oldconfigure.sh
index 541facb..cc30bbe 100755
--- a/Bonus/oldconfigure.sh
+++ b/Bonus/oldconfigure.sh
@@ -116,6 +116,7 @@ opt_enable_set "$use_history" history
 opt_enable_set "$use_digest_auth" digest-auth
 opt_enable_set "$use_nntp" nntp
 opt_enable_set "$use_gopher" gopher
+opt_enable_set "$use_gemini" gemini
 if test x"$use_lynx_key" = xy; then
   opt_push "--enable-keymap=lynx"
 else
diff --git a/acinclude.m4 b/acinclude.m4
index 398eb8c..24dbee8 100644
--- a/acinclude.m4
+++ b/acinclude.m4
@@ -117,7 +117,7 @@ AC_DEFUN([AC_W3M_NNTP],
   [enable_nntp="yes"])
  test x"$enable_nntp" = xyes && AC_DEFINE(USE_NNTP)
  AC_MSG_RESULT($enable_nntp)])
-#
+#
 # ----------------------------------------------------------------
 # AC_W3M_GOPHER
 # ----------------------------------------------------------------
@@ -131,6 +131,18 @@ AC_DEFUN([AC_W3M_GOPHER],
  AC_MSG_RESULT($enable_gopher)])
 #
 # ----------------------------------------------------------------
+# AC_W3M_GEMINI
+# ----------------------------------------------------------------
+AC_DEFUN([AC_W3M_GEMINI],
+[AC_SUBST(USE_GEMINI)
+ AC_MSG_CHECKING(if Gemini is enabled)
+ AC_ARG_ENABLE(gemini,
+  [  --disable-gemini        disable Gemini],,
+  [enable_gemini="yes"])
+ test x"$enable_gemini" = xyes &&  AC_DEFINE(USE_GEMINI)
+ AC_MSG_RESULT($enable_gemini)])
+#
+# ----------------------------------------------------------------
 # AC_W3M_M17N
 # ----------------------------------------------------------------
 # m17n enable?
@@ -210,7 +222,7 @@ else
    S*) charset=Shift_JIS;;
    J*) charset=ISO-2022-JP;;
    U*) charset=UTF-8;;
-   esac
+   esac
  fi
  display_charset=$charset
  AC_MSG_CHECKING(which charset is used for display)
@@ -371,7 +383,7 @@ AC_DEFUN([AC_W3M_HELP_CGI],
   [enable_help_cgi="yes"])
  test x"$enable_help_cgi" = xyes && AC_DEFINE(USE_HELP_CGI)
  AC_MSG_RESULT($enable_help_cgi)])
-#
+#
 # ----------------------------------------------------------------
 # AC_W3M_EXTERNAL_URI_LOADER
 # ----------------------------------------------------------------
@@ -383,7 +395,7 @@ AC_DEFUN([AC_W3M_EXTERNAL_URI_LOADER],
  [enable_external_uri_loader="yes"])
  test x"$enable_external_uri_loader" = xyes && AC_DEFINE(USE_EXTERNAL_URI_LOADER)
  AC_MSG_RESULT($enable_external_uri_loader)])
-#
+#
 # ----------------------------------------------------------------
 # AC_W3M_W3MMAILER
 # ----------------------------------------------------------------
@@ -406,7 +418,7 @@ AC_DEFUN([AC_W3M_EXTLIBS],
  extlib="not found"
  for dir in /lib /usr/lib /usr/local/lib /usr/ucblib /usr/ccslib /usr/ccs/lib /lib64 /usr/lib64
  do
-   if test -f $dir/lib$lib.a -o -f $dir/lib$lib.so ; then
+   if test -f $dir/lib$lib.a -o -f $dir/lib$lib.so ; then
     LIBS="$LIBS -l$lib"
     extlib="found at $dir"
     break
@@ -587,7 +599,7 @@ AC_DEFUN([AC_W3M_ALARM],
  fi])
 #
 # ----------------------------------------------------------------
-# AC_W3M_CHECK_VER(name, version, major, minor, micro,
+# AC_W3M_CHECK_VER(name, version, major, minor, micro,
 #		action-if-ok, message-if-badver, action-if-nover)
 # ----------------------------------------------------------------
 AC_DEFUN([AC_W3M_CHECK_VER],
@@ -643,14 +655,14 @@ AC_DEFUN([AC_W3M_IMAGE],
   if test x"$enable_image" = xyes; then
     enable_image=x11
     case "`uname -s`" in
-    Linux|linux|LINUX|FreeBSD|freebsd|FREEBSD)
+    Linux|linux|LINUX|FreeBSD|freebsd|FREEBSD)
    if test -c /dev/fb0; then
      enable_image=x11,fb
         fi;;
     CYGWIN*)
    enable_image=x11,win;;
     esac
-  fi
+  fi
   save_ifs="$IFS"; IFS=",";
   set x $enable_image; shift
   IFS="$save_ifs"
@@ -745,14 +757,14 @@ AC_DEFUN([AC_W3M_IMAGE],
    if test x"$have_imlib2" = xyes; then
      AC_DEFINE(USE_W3MIMG_X11)
      IMGOBJS="$IMGOBJS x11/x11_w3mimg.o"
-     IMGTARGETS="x11"
+     IMGTARGETS="x11"
      AC_DEFINE(USE_IMLIB2)
      IMGX11CFLAGS="`${IMLIB2_CONFIG} --cflags`"
      IMGX11LDFLAGS="-lX11 `${PKG_CONFIG} --libs imlib2`"
    elif test x"$have_gtk2" = xyes; then
      AC_DEFINE(USE_W3MIMG_X11)
      IMGOBJS="$IMGOBJS x11/x11_w3mimg.o"
-     IMGTARGETS="x11"
+     IMGTARGETS="x11"
      AC_DEFINE(USE_GDKPIXBUF)
      AC_DEFINE(USE_GTK2)
      IMGX11CFLAGS="`${PKG_CONFIG} --cflags gdk-pixbuf-2.0 gdk-pixbuf-xlib-2.0`"
@@ -760,18 +772,18 @@ AC_DEFUN([AC_W3M_IMAGE],
    elif test x"$have_gdkpixbuf" = xyes; then
      AC_DEFINE(USE_W3MIMG_X11)
      IMGOBJS="$IMGOBJS x11/x11_w3mimg.o"
-     IMGTARGETS="x11"
+     IMGTARGETS="x11"
      AC_DEFINE(USE_GDKPIXBUF)
      IMGX11CFLAGS="`${GDKPIXBUF_CONFIG} --cflags`"
      IMGX11LDFLAGS="`${GDKPIXBUF_CONFIG} --libs` -lgdk_pixbuf_xlib"
    elif test x"$have_imlib" = xyes; then
      AC_DEFINE(USE_W3MIMG_X11)
      IMGOBJS="$IMGOBJS x11/x11_w3mimg.o"
-     IMGTARGETS="x11"
+     IMGTARGETS="x11"
      AC_DEFINE(USE_IMLIB)
      IMGX11CFLAGS="`${IMLIB_CONFIG} --cflags`"
      IMGX11LDFLAGS="`${IMLIB_CONFIG} --libs`"
-     IMGTARGETS="x11"
+     IMGTARGETS="x11"
    else
      AC_MSG_WARN([unable to build w3mimgdisplay with X11 support])
    fi
@@ -850,7 +862,7 @@ AC_MSG_RESULT($enable_ipv6)
 if test x"$enable_ipv6" = xyes; then
  AC_MSG_CHECKING(if IPv6 API available)
  AC_SUBST(INET6)
- AC_CHECK_FUNC(getaddrinfo,
+ AC_CHECK_FUNC(getaddrinfo,
    [enable_ipv6="yes"],
    [enable_ipv6="no"])
  if test x"$enable_ipv6" = xno; then
diff --git a/config.h.dist b/config.h.dist
index fdb591a..876c6fc 100644
--- a/config.h.dist
+++ b/config.h.dist
@@ -8,7 +8,7 @@

 /* User Configuration */

-/*
+/*
    If you define USE_DICT, you can use dictionary look-up function
    in w3m. See README.dict for detail.
 */
@@ -80,8 +80,8 @@ ETC_DIR = /usr/local/etc/w3m
 RC_DIR = ~/.w3m
 HELP_FILE = w3mhelp-w3m_ja.html
 RC_DIR = ~/.w3m/
-SYS_LIBRARIES = -lgpm  -lbsd -lnsl -lncurses  -L/usr/lib -L/usr/lib -L/usr/local/ssl/lib -L/usr/local/ssl/lib -lssl -lcrypto
-LOCAL_LIBRARIES =
+SYS_LIBRARIES = -lgpm  -lbsd -lnsl -lncurses  -L/usr/lib -L/usr/lib -L/usr/local/ssl/lib -L/usr/local/ssl/lib -lssl -lcrypto
+LOCAL_LIBRARIES =
 CC = gcc
 MYCFLAGS = -O -I./gc/include  -I/usr/local/ssl/include/openssl -I/usr/local/ssl/include
 GCCFLAGS = -O -I./gc/include -I./$(srcdir)/include -DATOMIC_UNCOLLECTABLE -DNO_SIGNALS -DNO_EXECUTE_PERMISSION -DSILENT -DALL_INTERIOR_POINTERS
@@ -132,6 +132,7 @@ INSTALL_W3MIMGDISPLAY=$(INSTALL_PROGRAM)
 #define DEF_CAFILE	""
 #undef USE_NNTP
 #undef USE_GOPHER
+#undef USE_GEMINI
 #define USE_EXTERNAL_URI_LOADER
 #undef USE_ALARM
 #undef USE_IMAGE
diff --git a/config.h.in b/config.h.in
index 8a3829d..237aad4 100644
--- a/config.h.in
+++ b/config.h.in
@@ -67,6 +67,7 @@
 #undef USE_W3MMAILER
 #undef USE_NNTP
 #undef USE_GOPHER
+#undef USE_GEMINI
 #undef USE_ALARM
 #undef USE_IMAGE
 #undef USE_W3MIMG_X11
diff --git a/configure b/configure
index 5126dec..be6ea45 100755
--- a/configure
+++ b/configure
@@ -654,6 +654,7 @@ USE_EXTERNAL_URI_LOADER
 USE_HELP_CGI
 USE_DICT
 USE_GOPHER
+USE_GEMINI
 USE_NNTP
 USE_COOKIE
 USE_ALARM
@@ -826,6 +827,7 @@ enable_alarm
 enable_cookie
 enable_nntp
 enable_gopher
+enable_gemini
 enable_dict
 enable_help_cgi
 enable_external_uri_loader
@@ -1508,6 +1510,7 @@ Optional Features:
   --disable-cookie        disable cookie
   --disable-nntp          disable NNTP
   --disable-gopher        disable Gopher
+  --disable-gemini        disable Gemini
   --disable-dict          disable dictionary lookup
   --disable-help-cgi      disable help cgi
   --disable-external-uri-loader  disable external URI loader
@@ -7966,6 +7969,21 @@ fi
 $as_echo "$enable_gopher" >&6; }

+ { $as_echo "$as_me:${as_lineno-$LINENO}: checking if Gemini is enabled" >&5
+$as_echo_n "checking if Gemini is enabled... " >&6; }
+ # Check whether --enable-gemini was given.
+if test "${enable_gemini+set}" = set; then :
+  enableval=$enable_gemini;
+else
+  enable_gemini="yes"
+fi
+
+ test x"$enable_gemini" = xyes &&  $as_echo "#define USE_GEMINI 1" >>confdefs.h
+
+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: $enable_gemini" >&5
+$as_echo "$enable_gemini" >&6; }
+
+
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking if dictionary lookup is enabled" >&5
 $as_echo_n "checking if dictionary lookup is enabled... " >&6; }
 # Check whether --enable-dict was given.
diff --git a/file.c b/file.c
index f2e0563..4138ecd 100644
--- a/file.c
+++ b/file.c
@@ -13,6 +13,13 @@
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <utime.h>
+#include <ctype.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <sys/socket.h>
+#include <assert.h>
+#include <netdb.h>
+#include <openssl/x509.h>
 /* foo */

 #include "html.h"
@@ -164,20 +171,20 @@ static struct compression_decoder {
     int use_d_arg;
 } compression_decoders[] = {
     { CMP_COMPRESS, ".gz", "application/x-gzip",
-      0, GUNZIP_CMDNAME, GUNZIP_NAME, "gzip",
-      {"gzip", "x-gzip", NULL}, 0 },
+      0, GUNZIP_CMDNAME, GUNZIP_NAME, "gzip",
+      {"gzip", "x-gzip", NULL}, 0 },
     { CMP_COMPRESS, ".Z", "application/x-compress",
       0, GUNZIP_CMDNAME, GUNZIP_NAME, "compress",
-      {"compress", "x-compress", NULL}, 0 },
+      {"compress", "x-compress", NULL}, 0 },
     { CMP_BZIP2, ".bz2", "application/x-bzip",
       0, BUNZIP2_CMDNAME, BUNZIP2_NAME, "bzip, bzip2",
-      {"x-bzip", "bzip", "bzip2", NULL}, 0 },
+      {"x-bzip", "bzip", "bzip2", NULL}, 0 },
     { CMP_DEFLATE, ".deflate", "application/x-deflate",
       1, INFLATE_CMDNAME, INFLATE_NAME, "deflate",
-      {"deflate", "x-deflate", NULL}, 0 },
+      {"deflate", "x-deflate", NULL}, 0 },
     { CMP_BROTLI, ".br", "application/x-br",
       0, BROTLI_CMDNAME, BROTLI_NAME, "br",
-      {"br", "x-br", NULL}, 1 },
+      {"br", "x-br", NULL}, 1 },
     { CMP_NOCOMPRESS, NULL, NULL, 0, NULL, NULL, NULL, {NULL}, 0},
 };
 /* *INDENT-ON* */
@@ -472,7 +479,7 @@ acceptableEncoding()
     return encodings->ptr;
 }

-/*
+/*
  * convert line
  */
 #ifdef USE_M17N
@@ -1186,7 +1193,7 @@ AuthBasicCred(struct http_auth *ha, Str uname, Str pw, ParsedURL *pu,
 #include <openssl/md5.h>

 /* RFC2617: 3.2.2 The Authorization Request Header
- *
+ *
  * credentials      = "Digest" digest-response
  * digest-response  = 1#( username | realm | nonce | digest-uri
  *                    | response | [ algorithm ] | [cnonce] |
@@ -1425,7 +1432,7 @@ struct auth_param basic_auth_param[] = {
 #ifdef USE_DIGEST_AUTH
 /* RFC2617: 3.2.1 The WWW-Authenticate Response Header
  * challenge        =  "Digest" digest-challenge
- *
+ *
  * digest-challenge  = 1#( realm | [ domain ] | nonce |
  *                       [ opaque ] |[ stale ] | [ algorithm ] |
  *                        [ qop-options ] | [auth-param] )
@@ -1539,7 +1546,7 @@ getAuthCookie(struct http_auth *hauth, char *auth_header,
    		 auth_header_len);
     if (a_found) {
    /* This means that *-Authenticate: header is received after
-	 * Authorization: header is sent to the server.
+	 * Authorization: header is sent to the server.
     */
    if (fmInitialized) {
        message("Wrong username or password", 0, 0);
@@ -1555,7 +1562,7 @@ getAuthCookie(struct http_auth *hauth, char *auth_header,
     *uname = NULL;
     *pwd = NULL;

-    if (!a_found && find_auth_user_passwd(pu, realm, (Str*)uname, (Str*)pwd,
+    if (!a_found && find_auth_user_passwd(pu, realm, (Str*)uname, (Str*)pwd,
    				  proxy)) {
    /* found username & password in passwd file */ ;
     }
@@ -1594,7 +1601,7 @@ getAuthCookie(struct http_auth *hauth, char *auth_header,
    		realm);
    	exit(1);
        }
-
+
        /* FIXME: gettextize? */
        printf(proxy ? "Proxy Username for %s: " : "Username for %s: ",
    	   realm);
@@ -1689,7 +1696,69 @@ getLinkNumberStr(int correction)
     return Sprintf("[%d]", cur_hseq + correction);
 }

-/*
+
+static int
+verify_callback(X509_STORE_CTX *ctx, void *data)
+{
+    	// accept all certificates
+	(void)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[512 / 8];
+	const EVP_MD *sha512 = EVP_sha512();
+	unsigned int len = sizeof(md);
+	rc = X509_digest(cert, sha512, md, &len);
+	assert(rc == 1);
+
+	char fingerprint[512 / 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;
+	}
+
+	rc = X509_check_host(cert, servername, strlen(servername), 0, NULL);
+	if (rc != 1) {
+		rc = X509_V_ERR_HOSTNAME_MISMATCH;
+		goto invalid_cert;
+	}
+
+	return 0;
+
+invalid_cert:
+	return 0;
+}
+
+
+/*
  * loadGeneralFile: load file to buffer
  */
 #define DO_EXTERNAL ((Buffer *(*)(URLFile *, Buffer *))doExternal)
@@ -1747,6 +1816,145 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
    }
     }
     TRAP_OFF;
+    if (pu.scheme == SCM_GEMINI) {
+	// get uri
+	// request URL is tpath
+	// open and close connection here
+	// assume text/gemini
+
+	SSL_CTX *ssl_ctx;
+	SSL *ssl;
+	int sock = -1;
+	SSL_load_error_strings();
+	ERR_load_crypto_strings();
+	ssl_ctx = SSL_CTX_new(TLS_method());
+	SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, NULL);
+	BIO *sbio = BIO_new(BIO_f_ssl());
+	// open url
+	struct addrinfo hints = {0};
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	char pbuf[7];
+	snprintf(pbuf, sizeof(pbuf), "%d", pu.port);
+	struct addrinfo *addr;
+	int r = getaddrinfo(pu.host, pbuf, &hints, &addr);
+	if (r != 0) {
+	    // error resolve
+	    puts("gemini error resolve");
+	}
+	for (struct addrinfo *rp = addr; rp != NULL; rp = rp->ai_next) {
+	    sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+	    if (sock == -1) continue;
+	    if (connect(sock, rp->ai_addr, rp->ai_addrlen) < 0) {
+		close(sock);
+		sock = -1;
+		continue;
+	    }
+	    break;
+	}
+	freeaddrinfo(addr);
+	if (sock == -1) {
+	    // Error
+	    puts("gemini open socket");
+	}
+	// set sock
+	ssl = SSL_new(ssl_ctx);
+	SSL_set_connect_state(ssl);
+	r = SSL_set1_host(ssl, pu.host);
+	if (r != 1) {
+	    // Error
+	    puts("gemini error");
+	}
+	r = SSL_set_tlsext_host_name(ssl, pu.host);
+	if (r != 1) {
+	    // Error
+	    puts("gemini error");
+	}
+	r = SSL_set_fd(ssl, sock);
+	if (r != 1) {
+	    // Error
+	}
+	r = SSL_connect(ssl);
+	if (r != 1) {
+	    // Error
+	}
+	X509 *cert = SSL_get_peer_certificate(ssl);
+	if (!cert) {
+	    // Error
+	    puts("gemini cert error");
+	}
+	X509_free(cert);
+	long vr = SSL_get_verify_result(ssl);
+	if (vr != X509_V_OK) {
+	    // Error
+	    puts("gemini cert not ok");
+	}
+	BIO_set_ssl(sbio, ssl, 0);
+	BIO *bio;
+	bio = BIO_new(BIO_f_buffer());
+	BIO_push(bio, sbio);
+	char req[1024 + 3];
+	if (!strncmp(tpath, "gemini://", strlen("gemini://"))) {
+		r = snprintf(req, sizeof(req), "%s\r\n", tpath);
+	}
+	else {
+		// relative path
+		// remove filename in referer
+		char *q = referer + strlen(referer);
+		while(q > referer) {
+		    q--;
+		    if (*q == '/') break;
+		}
+		memcpy(req, referer, q - referer +1);
+		req[q-referer+1] = 0;
+		strncat(req, tpath, sizeof(req)-1);
+		strncat(req, "\r\n", sizeof(req)-1);
+	}
+	r = BIO_puts(sbio, req);
+	if (r == -1) {
+	    // Error
+	    puts(ERR_error_string(SSL_get_error(ssl, -1),NULL));
+	}
+	/* assert(r == (int)strlen(req)); */
+	#define GEMINI_META_MAXLEN 1024
+	#define GEMINI_STATUS_MAXLEN 2
+	char buf[GEMINI_META_MAXLEN
+	+ GEMINI_STATUS_MAXLEN
+	+ 2 /* CRLF */ + 1 /* NUL */] = {0};
+	r = BIO_gets(bio, buf, sizeof(buf));
+	if (r == -1) {
+	    // Error
+	    puts("BIO_gets error");
+	}
+	// check status, meta, \r\n
+	Str gemtext = Strnew();
+	char buf2[BUFSIZ];
+	int n = 1;
+	while (n > 0) {
+	    n = BIO_read(bio, buf2, sizeof(buf2));
+	    if (n == -1) {
+		puts("gemini error read");
+	    }
+	    else if (n > 0) {
+		// save string in buf2
+		Strcat_charp_n(gemtext, buf2, n);
+	    }
+	}
+	init_stream(&f, SCM_MISSING, NULL);
+	f.scheme = pu.scheme;
+	f.url = parsedURL2Str(&pu)->ptr;
+	f.ext = filename_extension(pu.file, 1);
+	f.stream = newStrStream(gemtext);
+	page = loadGemini(&f, &pu, &charset);
+	t = "text/gemini";
+	close(sock);
+	BIO_free(BIO_pop(bio)); // ssl bio
+	BIO_free(bio); // buffered bio
+	SSL_free(ssl);
+	SSL_CTX_free(ssl_ctx);
+	TRAP_OFF;
+	goto page_loaded;
+    }
     url_option.referer = referer;
     url_option.flag = flag;
     f = openURL(tpath, &pu, current, &url_option, request, extra_header, of,
@@ -1847,6 +2055,9 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
 #ifdef USE_GOPHER
         (pu.scheme == SCM_GOPHER && non_null(GOPHER_proxy)) ||
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+	     pu.scheme == SCM_GEMINI ||
+#endif				/* USE_GEMINI */
         (pu.scheme == SCM_FTP && non_null(FTP_proxy))
     ) && !Do_not_use_proxy && !check_no_proxy(pu.host))) {

@@ -1898,7 +2109,7 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
        t = "text/plain";
    if (add_auth_cookie_flag && realm && uname && pwd) {
        /* If authorization is required and passed */
-	    add_auth_user_passwd(&pu, qstr_unquote(realm)->ptr, uname, pwd,
+	    add_auth_user_passwd(&pu, qstr_unquote(realm)->ptr, uname, pwd,
    			  0);
        add_auth_cookie_flag = 0;
    }
@@ -1931,7 +2142,7 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
    	&& (realm = get_auth_param(hauth.param, "realm")) != NULL) {
    	auth_pu = schemeToProxy(pu.scheme);
    	getAuthCookie(&hauth, "Proxy-Authorization:",
-			      extra_header, auth_pu, &hr, request,
+			      extra_header, auth_pu, &hr, request,
    		      &uname, &pwd);
    	if (uname == NULL) {
    	    /* abort */
@@ -2011,6 +2222,18 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
    }
     }
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+    else if (pu.scheme == SCM_GEMINI) {
+	// TODO remove this? copied before f = openURL
+	// parse the URL only to get the scheme
+	// open and close connection here
+	// assume text/gemini
+	page = loadGemini(&f, &pu, &charset);
+	t = "text/gemini";
+	TRAP_OFF;
+	goto page_loaded;
+    }
+#endif				/* USE_GEMINI */
     else if (pu.scheme == SCM_FTP) {
    check_compression(path, &f);
    if (f.compression != CMP_NOCOMPRESS) {
@@ -2112,7 +2335,7 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
     /* XXX: can we use guess_type to give the type to loadHTMLstream
      *      to support default utf8 encoding for XHTML here? */
     f.guess_type = t;
-
+
   page_loaded:
     if (page) {
    FILE *src;
@@ -2239,7 +2462,7 @@ loadGeneralFile(char *path, ParsedURL *volatile current, char *referer,
 #endif
     else if (w3m_backend) ;
     else if (!(w3m_dump & ~DUMP_FRAME) || is_dump_text_type(t)) {
-	if (!do_download &&
+	if (!do_download &&
 #ifdef USE_GOPHER
    	!gopher_download &&
 #endif
@@ -3657,10 +3880,10 @@ process_input(struct parsed_tag *tag)
    case FORM_INPUT_RESET:
        q = "RESET";
        break;
-	    /* if no VALUE attribute is specified in
-	     * <INPUT TYPE=CHECKBOX> tag, then the value "on" is used
-	     * as a default value. It is not a part of HTML4.0
-	     * specification, but an imitation of Netscape behaviour.
+	    /* if no VALUE attribute is specified in
+	     * <INPUT TYPE=CHECKBOX> tag, then the value "on" is used
+	     * as a default value. It is not a part of HTML4.0
+	     * specification, but an imitation of Netscape behaviour.
         */
    case FORM_INPUT_CHECKBOX:
        q = "on";
@@ -4344,7 +4567,7 @@ process_idattr(struct readbuffer *obuf, int cmd, struct parsed_tag *tag)
     char *id = NULL, *framename = NULL;
     Str idtag = NULL;

-    /*
+    /*
      * HTML_TABLE is handled by the other process.
      */
     if (cmd == HTML_TABLE)
@@ -5725,7 +5948,7 @@ HTMLlineproc2body(Buffer *buf, Str (*feed) (), int llimit)
 #endif
        }
        else if (*str == '&') {
-		/*
+		/*
    	 * & escape processing
    	 */
    	p = getescapecmd(&str);
@@ -6474,7 +6697,7 @@ HTMLlineproc0(char *line, struct html_feed_environ *h_env, int internal)
        tbl_mode->end_tag : obuf->end_tag;

    if (*line == '<' || obuf->status != R_ST_NORMAL) {
-	    /*
+	    /*
         * Tag processing
         */
        if (obuf->status == R_ST_EOL)
@@ -6554,7 +6777,7 @@ HTMLlineproc0(char *line, struct html_feed_environ *h_env, int internal)

       proc_normal:
    if (obuf->table_level >= 0 && tbl && tbl_mode) {
-	    /*
+	    /*
         * within table: in <table>..</table>, all input tokens
         * are fed to the table renderer, and then the renderer
         * makes HTML output.
@@ -6900,7 +7123,7 @@ addnewline(Buffer *buf, char *line, Lineprop *prop, Linecolor *color, int pos,
     }
 }

-/*
+/*
  * loadHTMLBuffer: read file and make new buffer
  */
 Buffer *
@@ -7431,7 +7654,7 @@ loadHTMLstream(URLFile *f, Buffer *newBuf, FILE * src, int internal)
     HTMLlineproc2(newBuf, htmlenv1.buf);
 }

-/*
+/*
  * loadHTMLString: read string and make new buffer
  */
 Buffer *
@@ -7474,7 +7697,7 @@ loadHTMLString(Str page)

 #ifdef USE_GOPHER

-/*
+/*
  * loadGopherDir: get gopher directory
  */
 #ifdef USE_M17N
@@ -7634,7 +7857,141 @@ loadGopherSearch0(URLFile *uf, ParsedURL *pu)
 }
 #endif				/* USE_GOPHER */

-/*
+#ifdef USE_GEMINI
+Str
+loadGemini(URLFile *uf, ParsedURL *pu, wc_ces * charset)
+{
+    Str volatile tmp;
+    Str lbuf, url, name;
+    char *volatile p, *volatile q;
+    int pre, quote, haslinkname;
+    MySignalHandler(*volatile prevtrap) (SIGNAL_ARG) = NULL;
+#ifdef USE_M17N
+    wc_ces doc_charset = DocumentCharset;
+#endif
+
+    tmp = parsedURL2Str(pu);
+    p = html_quote(tmp->ptr);
+    tmp =
+	convertLine(NULL, Strnew_charp(file_unquote(tmp->ptr)), RAW_MODE,
+		    charset, doc_charset);
+    q = html_quote(tmp->ptr);
+    // p is url
+    // q is title
+    tmp = Strnew_m_charp("<html>\n<head>\n<base href=\"", p, "\">\n<title>", q,
+			 "</title>\n</head>\n<body>\n", NULL);
+
+    if (SETJMP(AbortLoading) != 0)
+	goto gemini_end;
+    TRAP_ON;
+    pre = quote = 0;
+    // gemini://gemini.circumlunar.space/docs/specification.gmi
+    // TODO escape html like escape-goat https://github.com/RangerMauve/gemini-to-html
+    while (1) {
+	if (lbuf = StrUFgets(uf), lbuf->length == 0)
+	    break;
+	lbuf = convertLine(uf, lbuf, HTML_MODE, charset, doc_charset);
+	p = lbuf->ptr;
+	if (quote && p[0] != '>') {
+	    quote = 0;
+	    Strcat_m_charp(tmp, "</blockquote>\n", NULL);
+	}
+	if (pre) {
+	    if (!strcmp(p, "```\n")) {
+	    	Strcat_m_charp(tmp, "</pre>\n", NULL);
+		pre = 0;
+	    }
+	    else
+ 	    	Strcat_m_charp(tmp, p, "\n", NULL);
+	}
+	else if (strncmp(p, "=>", 2) == 0) {
+	    // link
+	    for (q = p+2; *q && isspace(*q); q++);
+	    // url
+	    p = q;
+	    haslinkname = 0;
+	    while(*q) {
+		if (isspace(*q)) {
+		    haslinkname = 1;
+		    break;
+		}
+		q++;
+	    }
+	    url = Strnew_charp_n(p, q - p);
+	    // optional link name
+	    if (haslinkname) {
+	    	for (; *q && isspace(*q); q++);
+		// link name
+		p = q;
+	    	//for (; *q && !isspace(*q); q++);
+		//name = Strnew_charp_n(p, q - p);
+		// TODO handle links without a protocol > add gemini://
+		Strcat_m_charp(tmp, "<a href=\"",
+			       html_quote(url_encode(url->ptr, NULL, *charset)),
+			       "\">", html_quote(p/*name->ptr*/), "</a><br>\n", NULL);
+	    }
+	    else {
+		Strcat_m_charp(tmp, "<a href=\"",
+			       html_quote(url_encode(url->ptr, NULL, *charset)),
+			       "\">", html_quote(url->ptr), "</a><br>\n", NULL);
+	    }
+	}
+	else if (!strcmp(p, "```\n")) {
+	    // preformated
+	    pre = 1;
+	    Strcat_m_charp(tmp, "<pre>\n", NULL);
+	}
+	else if (p[0] == '#') {
+	    // header line
+	    if (p[1] == '#') {
+		if (p[2] == '#') {
+		    Strcat_m_charp(tmp, "<h3>", p, "</h3>\n", NULL);
+		}
+		else {
+		    Strcat_m_charp(tmp, "<h2>", p, "</h2>\n", NULL);
+		}
+	    }
+	    else {
+		Strcat_m_charp(tmp, "<h1>", p, "</h1>\n", NULL);
+	    }
+	}
+	else if (p[0] == '*') {
+	    // list line
+	    // TODO ul and li
+	    Strcat_m_charp(tmp, p, "<br/>\n", NULL);
+	}
+	else if (p[0] == '>') {
+	    // quote line
+	    if (!quote) {
+		Strcat_m_charp(tmp, "<blockquote>", p, "\n", NULL);
+		quote = 1;
+	    }
+	    else
+		Strcat_m_charp(tmp, p, "\n", NULL);
+	}
+	else {
+	    // text line inside <p></p>
+	    // blank line = <br\>
+	    if (non_null(p)) {
+	    	Strcat_m_charp(tmp, "<p>", p, "</p>\n", NULL);
+	    }
+	    else {
+	    	Strcat_m_charp(tmp, "<br/>\n", NULL);
+	    }
+	}
+    }
+   gemini_end:
+    TRAP_OFF;
+    if (pre)
+	Strcat_m_charp(tmp, "</pre>\n", NULL);
+    if (quote)
+	Strcat_m_charp(tmp, "</blockquote>\n", NULL);
+    Strcat_charp(tmp, "</body>\n</html>\n");
+    return tmp;
+}
+#endif				/* USE_GEMINI */
+
+/*
  * loadBuffer: read file and make new buffer
  */
 Buffer *
@@ -7842,7 +8199,7 @@ conv_symbol(Line *l)
    return Strnew_charp_n(l->lineBuf, l->len);
 }

-/*
+/*
  * saveBuffer: write buffer to file
  */
 static void
@@ -7913,7 +8270,7 @@ loadcmdout(char *cmd,
     return buf;
 }

-/*
+/*
  * getshell: execute shell command and get the result into a buffer
  */
 Buffer *
@@ -7930,7 +8287,7 @@ getshell(char *cmd)
     return buf;
 }

-/*
+/*
  * getpipe: execute shell command and connect pipe to the buffer
  */
 Buffer *
@@ -7956,7 +8313,7 @@ getpipe(char *cmd)
     return buf;
 }

-/*
+/*
  * Open pager buffer
  */
 Buffer *
@@ -8514,7 +8871,7 @@ doFileSave(URLFile uf, char *defstr)
     char *p, *q;
     pid_t pid;
     char *lock;
-    char *tmpf = NULL;
+    char *tmpf = NULL;
 #if !(defined(HAVE_SYMLINK) && defined(HAVE_LSTAT))
     FILE *f;
 #endif
diff --git a/html.h b/html.h
index 749e7ef..d101d87 100644
--- a/html.h
+++ b/html.h
@@ -417,5 +417,6 @@ struct environment {
 #ifdef USE_SSL
 #define SCM_HTTPS       13
 #endif				/* USE_SSL */
+#define SCM_GEMINI	14

 #endif				/* _HTML_H */
diff --git a/main.c b/main.c
index ced6f40..267bd6f 100644
--- a/main.c
+++ b/main.c
@@ -179,6 +179,9 @@ fversion(FILE * f)
 #ifdef USE_GOPHER
        ",gopher"
 #endif
+#ifdef USE_GEMINI
+	    ",gemini"
+#endif
 #ifdef INET6
        ",ipv6"
 #endif
@@ -5067,6 +5070,9 @@ chkURLBuffer(Buffer *buf)
 #ifdef USE_GOPHER
    "gopher://[a-zA-Z0-9][a-zA-Z0-9:%\\-\\./_]*",
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+	"gemini://[a-zA-Z0-9][a-zA-Z0-9:%\\-\\./?=~_\\&+@#,\\$;]*[a-zA-Z0-9_/=\\-]",
+#endif				/* USE_GEMINI */
    "ftp://[a-zA-Z0-9][a-zA-Z0-9:%\\-\\./=_+@#,\\$]*[a-zA-Z0-9_/]",
 #ifdef USE_NNTP
    "news:[^<> 	][^<> 	]*",
diff --git a/proto.h b/proto.h
index 66d497e..2c4fdfa 100644
--- a/proto.h
+++ b/proto.h
@@ -271,6 +271,9 @@ extern Str loadGopherSearch0(URLFile *uf, ParsedURL *pu);
 #define loadGopherSearch(uf,pu,charset) loadGopherSearch0(uf,pu)
 #endif
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+extern Str loadGemini(URLFile *uf, ParsedURL *pu, wc_ces * charset);
+#endif				/* USE_GEMINI */
 extern Buffer *loadBuffer(URLFile *uf, Buffer *newBuf);
 #ifdef USE_IMAGE
 extern Buffer *loadImageBuffer(URLFile *uf, Buffer *newBuf);
diff --git a/url.c b/url.c
index 1fbda17..a356749 100644
--- a/url.c
+++ b/url.c
@@ -75,6 +75,7 @@ static int
 #ifdef USE_SSL
     443,			/* https */
 #endif				/* USE_SSL */
+    1965,			/* gemini */
 };

 struct cmdtable schemetable[] = {
@@ -95,6 +96,7 @@ struct cmdtable schemetable[] = {
 #ifdef USE_SSL
     {"https", SCM_HTTPS},
 #endif				/* USE_SSL */
+    {"gemini", SCM_GEMINI},
     {NULL, SCM_UNKNOWN},
 };

@@ -130,6 +132,8 @@ static char * schemeNumToName(int scheme);
 #define HTTP_DEFAULT_FILE "/"
 #endif				/* not HTTP_DEFAULT_FILE */

+#define GEMINI_DEFAULT_FILE "/index.gmi"
+
 #ifdef SOCK_DEBUG
 #include <stdarg.h>

@@ -231,6 +235,10 @@ DefaultFile(int scheme)
     case SCM_GOPHER:
    return allocStr("1", -1);
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+    case SCM_GEMINI:
+	return allocStr(GEMINI_DEFAULT_FILE, -1);
+#endif				/* USE_GEMINI */
     case SCM_LOCAL:
     case SCM_LOCAL_CGI:
     case SCM_FTP:
@@ -928,7 +936,7 @@ parseURL(char *url, ParsedURL *p_url, ParsedURL *current)
    p_url->host != NULL && *p_url->host != '\0' &&
    !is_localhost(p_url->host)) {
    /*
-	 * In the environments other than CYGWIN, a URL like
+	 * In the environments other than CYGWIN, a URL like
     * file://host/file is regarded as ftp://host/file.
     * On the other hand, file://host/file on CYGWIN is
     * regarded as local access to the file //host/file.
@@ -996,14 +1004,14 @@ parseURL(char *url, ParsedURL *p_url, ParsedURL *current)
    while (*p && *p != '#' && p != cgi)
        p++;
    if (*p == '#' && p_url->scheme == SCM_LOCAL) {
-	    /*
+	    /*
         * According to RFC2396, # means the beginning of
         * URI-reference, and # should be escaped.  But,
         * if the scheme is SCM_LOCAL, the special
         * treatment will apply to # for convinience.
         */
        if (p > q && *(p - 1) == '/' && (cgi == NULL || p < cgi)) {
-		/*
+		/*
    	 * # comes as the first character of the file name
    	 * that means, # is not a label but a part of the file
    	 * name.
@@ -1012,7 +1020,7 @@ parseURL(char *url, ParsedURL *p_url, ParsedURL *current)
    	goto again;
        }
        else if (*(p + 1) == '\0') {
-		/*
+		/*
    	 * # comes as the last character of the file name that
    	 * means, # is not a label but a part of the file
    	 * name.
@@ -1136,7 +1144,7 @@ parseURL2(char *url, ParsedURL *pu, ParsedURL *current)
 #ifdef USE_EXTERNAL_URI_LOADER
        if (pu->scheme == SCM_UNKNOWN
    	&& strchr(pu->file, ':') == NULL
-		&& current && (p = strchr(current->file, ':')) != NULL) {
+		&& current && current->file && (p = strchr(current->file, ':')) != NULL) {
    	pu->file = Sprintf("%s:%s",
    			   allocStr(current->file,
    				    p - current->file), pu->file)->ptr;
@@ -1213,10 +1221,10 @@ parseURL2(char *url, ParsedURL *pu, ParsedURL *current)
        ) {
        if (relative_uri) {
    	/* In this case, pu->file is created by [process 1] above.
-		 * pu->file may contain relative path (for example,
+		 * pu->file may contain relative path (for example,
    	 * "/foo/../bar/./baz.html"), cleanupName() must be applied.
    	 * When the entire abs_path is given, it still may contain
-		 * elements like `//', `..' or `.' in the pu->file. It is
+		 * elements like `//', `..' or `.' in the pu->file. It is
    	 * server's responsibility to canonicalize such path.
    	 */
    	pu->file = cleanupName(pu->file);
@@ -1259,6 +1267,7 @@ _parsedURL2Str(ParsedURL *pu, int pass, int user, int label)
    "news", "news", "data", "mailto",
 #ifdef USE_SSL
    "https",
+	"gemini",
 #endif				/* USE_SSL */
     };

@@ -1448,7 +1457,7 @@ otherinfo(ParsedURL *target, ParsedURL *current, char *referer)
    int cross_origin = FALSE;
    if (CrossOriginReferer && current && current->host &&
        (!target || !target->host ||
-	     strcasecmp(current->host, target->host) != 0 ||
+	     strcasecmp(current->host, target->host) != 0 ||
         current->port != target->port ||
         current->scheme != target->scheme))
        cross_origin = TRUE;
@@ -1601,6 +1610,19 @@ HTTPrequest(ParsedURL *pu, ParsedURL *current, HRequest *hr, TextList *extra)
     return tmp;
 }

+#ifdef USE_GEMINI
+static Str
+GEMINIrequest(ParsedURL *pu, ParsedURL *current, HRequest *hr, TextList *extra)
+{
+    Str tmp;
+    // TODO remove not needed, because with gemini we just write the url in the request, pu is parsed URL - tmp = Strnew_m_charp(pu->ptr,"\r\n", NULL);
+#ifdef DEBUG
+    fprintf(stderr, "GEMINIrequest: [ %s ]\n\n", tmp->ptr);
+#endif				/* DEBUG */
+    return tmp;
+}
+#endif  /* USE_GEMINI */
+
 void
 init_stream(URLFile *uf, int scheme, InputStream stream)
 {
@@ -1959,6 +1981,33 @@ openURL(char *url, ParsedURL *pu, ParsedURL *current,
    }
    break;
 #endif				/* USE_GOPHER */
+#ifdef USE_GEMINI
+    case SCM_GEMINI:
+	sock = openSocket(pu->host, schemeNumToName(pu->scheme), pu->port);
+	if (sock < 0) {
+	    *status = HTST_MISSING;
+	    return uf;
+	}
+	if (!(sslh = openSSLHandle(sock, pu->host,
+				   &uf.ssl_certificate))) {
+	    *status = HTST_MISSING;
+	    return uf;
+	}
+	hr->flag |= HR_FLAG_LOCAL;
+	tmp = GEMINIrequest(pu, current, hr, extra_header);
+	*status = HTST_NORMAL;
+	uf.stream = newSSLStream(sslh, sock);
+	SSL_write(sslh, tmp->ptr, tmp->length);
+	if(w3m_reqlog){
+	    FILE *ff = fopen(w3m_reqlog, "a");
+	    if (ff == NULL)
+		return uf;
+ 	    fputs("GEMINI: request via SSL\n", ff);
+	    fwrite(tmp->ptr, sizeof(char), tmp->length, ff);
+	    fclose(ff);
+	}
+	return uf;
+#endif				/* USE_GEMINI */
 #ifdef USE_NNTP
     case SCM_NNTP:
     case SCM_NNTP_GROUP:
@@ -2132,7 +2181,7 @@ check_no_proxy(char *domain)
     if (!NOproxy_netaddr) {
    return 0;
     }
-    /*
+    /*
      * to check noproxy by network addr
      */
     if (SETJMP(AbortLoading) != 0) {
--
2.30.2

hashtags: #gemini


Feed

-- Response ended

-- Page fetched on Tue May 21 12:54:52 2024