;;; elpher.el --- A friendly gopher and gemini client -*- lexical-binding: t -*-
-;; Copyright (C) 2019-2022 Tim Vaughan <plugd@thelambdalab.xyz>
+;; Copyright (C) 2019-2023 Tim Vaughan <plugd@thelambdalab.xyz>
;; Copyright (C) 2020-2022 Elpher contributors (See info manual for full list)
;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
;; Created: 11 April 2019
-;; Version: 3.4.2
-;; Keywords: comm gopher
+;; Version: 3.4.3
+;; Keywords: comm gopher gemini
;; Homepage: https://thelambdalab.xyz/elpher
;; Package-Requires: ((emacs "27.1"))
;;; Global constants
;;
-(defconst elpher-version "3.4.2"
+(defconst elpher-version "3.4.3"
"Current version of elpher.")
(defconst elpher-margin-width 6
"Label of button used to toggle formatted text."
:type '(string))
+(defcustom elpher-client-certificate-map nil
+ "An alist representing a mapping between gemini URLs and the names
+of client certificates which will be automatically activated for those
+URLs. Beware that the certificates will also be active for all
+subdirectories of the given URLs."
+ :type '(alist :key-type string :value-type string))
+
;; Face customizations
(defgroup elpher-faces nil
;;; Client-side TLS Certificate Management
;;
-(defun elpher-generate-certificate (common-name key-file cert-file &optional temporary)
+(defun elpher-generate-certificate (common-name key-file cert-file url-prefix
+ &optional temporary)
"Generate a key and a self-signed client TLS certificate using openssl.
The Common Name field of the certificate is set to COMMON-NAME. The
"-subj" (concat "/CN=" common-name)
"-keyout" exp-key-file
"-out" exp-cert-file)
- (list (elpher-address-host (elpher-page-address elpher-current-page))
- temporary exp-key-file exp-cert-file))
+ (list url-prefix temporary exp-key-file exp-cert-file))
(error
(message "Check that openssl is installed, or customize `elpher-openssl-command`.")
(error "Program 'openssl', required for certificate generation, not found")))))
-(defun elpher-generate-throwaway-certificate ()
+(defun elpher-generate-throwaway-certificate (url-prefix)
"Generate and return details of a throwaway certificate.
The key and certificate files will be deleted when they are no
longer needed for this session."
(let* ((file-base (make-temp-name "elpher"))
(key-file (concat temporary-file-directory file-base ".key"))
(cert-file (concat temporary-file-directory file-base ".crt")))
- (elpher-generate-certificate file-base key-file cert-file t)))
+ (elpher-generate-certificate file-base key-file cert-file url-prefix t)))
-(defun elpher-generate-persistent-certificate (file-base common-name)
+(defun elpher-generate-persistent-certificate (file-base common-name url-prefix)
"Generate and return details of a persistent certificate.
The argument FILE-BASE is used as the base for the key and certificate
files, while COMMON-NAME specifies the common name field of the
The key and certificate files are written to in `elpher-certificate-directory'."
(let* ((key-file (concat elpher-certificate-directory file-base ".key"))
(cert-file (concat elpher-certificate-directory file-base ".crt")))
- (elpher-generate-certificate common-name key-file cert-file)))
+ (elpher-generate-certificate common-name key-file cert-file url-prefix)))
-(defun elpher-get-existing-certificate (file-base)
+(defun elpher-get-existing-certificate (file-base url-prefix)
"Return a certificate object corresponding to an existing certificate.
It is assumed that the key files FILE-BASE.key and FILE-BASE.crt exist in
the directory `elpher-certificate-directory'."
(let* ((key-file (concat elpher-certificate-directory file-base ".key"))
(cert-file (concat elpher-certificate-directory file-base ".crt")))
- (list (elpher-address-host (elpher-page-address elpher-current-page))
+ (list (elpher-address-to-url (elpher-page-address elpher-current-page))
nil
(expand-file-name key-file)
(expand-file-name cert-file))))
-(defun elpher-install-and-use-existing-certificate (key-file-src cert-file-src file-base)
+(defun elpher-install-certificate (key-file-src cert-file-src file-base)
"Install a key+certificate file pair in `elpher-certificate-directory'.
The strings KEY-FILE-SRC and CERT-FILE-SRC are the existing key and
certificate files to install. The argument FILE-BASE is used as the
(if (or (file-exists-p key-file)
(file-exists-p cert-file))
(error "A certificate with base name %s is already installed" file-base))
+ (unless (and (file-exists-p key-file-src)
+ (file-exists-p cert-file-src))
+ (error "Either of the key or certificate files do not exist"))
(copy-file key-file-src key-file)
(copy-file cert-file-src cert-file)
- (list (elpher-address-host (elpher-page-address elpher-current-page))
+ (list (elpher-address-to-url (elpher-page-address elpher-current-page))
nil
(expand-file-name key-file)
(expand-file-name cert-file))))
If `elpher-current-certificate' is non-nil, and its host name doesn't
match that of ADDRESS, the certificate is forgotten."
(if elpher-client-certificate
- (if (string= (car elpher-client-certificate)
- (elpher-address-host address))
+ (if (string-prefix-p (car elpher-client-certificate)
+ (elpher-address-to-url address))
(list (cddr elpher-client-certificate))
(elpher-forget-current-certificate)
(message "Disabling client certificate for new host")
(elpher-with-clean-buffer
(insert "Gemini server is requesting input."))
(let* ((query-string
- (if (eq (elt response-code 1) ?1)
- (read-passwd (concat response-meta ": "))
- (read-string (concat response-meta ": "))))
+ (with-local-quit
+ (if (eq (elt response-code 1) ?1)
+ (read-passwd (concat response-meta ": "))
+ (read-string (concat response-meta ": ")))))
(query-address (seq-copy (elpher-page-address elpher-current-page)))
(old-fname (url-filename query-address)))
- (setf (url-filename query-address)
- (concat old-fname "?" (url-build-query-string `((,query-string)))))
- (elpher-get-gemini-response query-address renderer)))
+ (if (not query-string)
+ (elpher-visit-previous-page)
+ (setf (url-filename query-address)
+ (concat old-fname "?" (url-build-query-string `((,query-string)))))
+ (elpher-get-gemini-response query-address renderer))))
(?2 ; Normal response
(funcall renderer response-body response-meta))
(?3 ; Redirect
(insert "Gemini server is requesting a valid TLS certificate:\n\n"))
(auto-fill-mode 1)
(elpher-gemini-insert-text response-meta))
- (let ((chosen-certificate (elpher-choose-client-certificate)))
+ (let ((chosen-certificate
+ (with-local-quit
+ (elpher-acquire-client-certificate
+ (elpher-address-to-url (elpher-page-address elpher-current-page))))))
(unless chosen-certificate
(error "Gemini server requires a client certificate and none was provided"))
(setq elpher-client-certificate chosen-certificate))
(error "Gemini server response unknown: %s %s"
response-code response-meta))))))
+(defun elpher-acquire-client-certificate (url-prefix)
+ "Select a pre-defined client certificate or prompt for one.
+In this case, \"pre-defined\" means a certificate provided by
+the `elpher-client-certificate-map' variable."
+ (let ((entry (assoc url-prefix
+ elpher-client-certificate-map
+ #'string-prefix-p)))
+ (if entry
+ (let ((cert-url-prefix (car entry))
+ (cert-name (cadr entry)))
+ (message "Using certificate \"%s\" specified in elpher-client-certificate-map"
+ cert-name)
+ (elpher-get-existing-certificate cert-name cert-url-prefix))
+ (elpher-prompt-for-client-certificate url-prefix))))
+
(defun elpher--read-answer-polyfill (question answers)
"Polyfill for `read-answer' in Emacs 26.1.
QUESTION is a string containing a question, and ANSWERS
-is a list of possible answers."
- (completing-read question (mapcar 'identity answers)))
+is a list of possible answers, or an alist whose keys
+are the possible answers."
+ (completing-read question answers))
(if (fboundp 'read-answer)
(defalias 'elpher-read-answer 'read-answer)
(defalias 'elpher-read-answer 'elpher--read-answer-polyfill))
-(defun elpher-choose-client-certificate ()
+
+
+(defun elpher-prompt-for-client-certificate (url-prefix)
"Prompt for a client certificate to use to establish a TLS connection."
(let* ((read-answer-short t))
(pcase (read-answer "What do you want to do? "
nil
(if (member file-base existing-certificates)
(setq elpher-client-certificate
- (elpher-get-existing-certificate file-base))
+ (elpher-get-existing-certificate file-base url-prefix))
(pcase (read-answer "Generate new certificate or install externally-generated one? "
'(("new" ?n
"generate new certificate")
file-base)))
(message "New key and self-signed certificate written to %s"
elpher-certificate-directory)
- (elpher-generate-persistent-certificate file-base common-name)))
+ (elpher-generate-persistent-certificate file-base
+ common-name
+ url-prefix)))
("install"
(let* ((cert-file (read-file-name "Certificate file: " nil nil t))
(key-file (read-file-name "Key file: " nil nil t)))
(message "Key and certificate installed in %s for future use"
elpher-certificate-directory)
- (elpher-install-and-use-existing-certificate key-file
- cert-file
- file-base)))
+ (elpher-install-certificate key-file cert-file file-base
+ url-prefix)))
("abort" nil))))))
("abort" nil))))
:export (lambda (link description format _plist)
(elpher-org-export-link link description format "gopher"))
:follow (lambda (link _arg) (elpher-org-follow-link link "gopher")))
+ (org-link-set-parameters
+ "gophers"
+ :export (lambda (link description format _plist)
+ (elpher-org-export-link link description format "gophers"))
+ :follow (lambda (link _arg) (elpher-org-follow-link link "gophers")))
(org-link-set-parameters
"finger"
:export (lambda (link description format _plist)
(if (boundp 'browse-url-default-handlers)
(add-to-list
'browse-url-default-handlers
- '("^\\(gopher\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
+ '("^\\(gopher\\|gophers\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
;; Patch `browse-url-browser-function' for older ones. The value of
;; that variable is `browse-url-default-browser' by default, so
;; that's the function that gets advised. If the value is an alist,
(lambda (url &rest _args)
"Handle gemini, gopher, and finger schemes using Elpher."
(let ((scheme (downcase (car (split-string url ":" t)))))
- (if (member scheme '("gemini" "gopher" "finger"))
+ (if (member scheme '("gemini" "gopher" "gophers" "finger"))
;; `elpher-go' always returns nil, which will stop the
;; advice chain here in a before-while
(elpher-go url)
;; Make mu4e aware of the gemini world
(setq mu4e~view-beginning-of-url-regexp
- "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:")
+ "\\(?:https?\\|gopher\\|gophers\\|finger\\|gemini\\)://\\|mailto:")
;; eww:
;; Let elpher handle gemini, gopher links in eww buffer.
(setq eww-use-browse-url
- "\\`mailto:\\|\\(\\`gemini\\|\\`gopher\\|\\`finger\\)://")
+ "\\`mailto:\\|\\(\\`gemini\\|\\`gopher\\|\\`gophers\\|\\`finger\\)://")
;;; Interactive procedures
"Go to a particular gopher site HOST-OR-URL.
When run interactively HOST-OR-URL is read from the minibuffer."
(interactive (list
- (read-string (format "Visit URL (default scheme %s): " (elpher-get-default-url-scheme)))))
+ (read-string (format "Visit URL (default scheme %s): "
+ (elpher-get-default-url-scheme)))))
(let ((trimmed-host-or-url (string-trim host-or-url)))
(unless (string-empty-p trimmed-host-or-url)
(let ((page (elpher-page-from-url trimmed-host-or-url
current page."
(interactive)
(let* ((address (elpher-page-address elpher-current-page))
- (url (read-string (format "Visit URL (default scheme %s): " (elpher-get-default-url-scheme))
+ (url (read-string (format "Visit URL (default scheme %s): "
+ (elpher-get-default-url-scheme))
(elpher-address-to-url address))))
- (unless (string-empty-p (string-trim url))
- (elpher-visit-page (elpher-page-from-url url)))))
+ (let ((trimmed-url (string-trim url)))
+ (unless (string-empty-p trimmed-url)
+ (elpher-with-clean-buffer
+ (elpher-visit-page
+ (elpher-page-from-url trimmed-url (elpher-get-default-url-scheme))))))))
(defun elpher-redraw ()
"Redraw current page."
(if (elpher-address-about-p (elpher-page-address elpher-current-page))
(error "Cannot download %s"
(elpher-page-display-string elpher-current-page))
- (elpher-visit-page (elpher-make-page
- (elpher-page-display-string elpher-current-page)
- (elpher-page-address elpher-current-page))
+ (elpher-visit-page elpher-current-page
#'elpher-render-download
t)))