X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=elpher.el;h=608f09d28ec61965b65d40f89a4dc499dc465e96;hb=81f2883614c303184116449ec3583ef5c136ec2f;hp=b18a897a393e790eb3dad159bae8d7c1b4b7c3c1;hpb=eb329aec7b8f444fd24ceb4f25168fefb3f570da;p=elpher.git diff --git a/elpher.el b/elpher.el index b18a897..608f09d 100644 --- a/elpher.el +++ b/elpher.el @@ -1,12 +1,12 @@ ;;; elpher.el --- A friendly gopher and gemini client -*- lexical-binding: t -*- -;; Copyright (C) 2019-2022 Tim Vaughan +;; Copyright (C) 2019-2023 Tim Vaughan ;; Copyright (C) 2020-2022 Elpher contributors (See info manual for full list) ;; Author: Tim Vaughan ;; Created: 11 April 2019 -;; Version: 3.4.2 -;; Keywords: comm gopher +;; Version: 3.5.0 +;; Keywords: comm gopher gemini ;; Homepage: https://thelambdalab.xyz/elpher ;; Package-Requires: ((emacs "27.1")) @@ -71,7 +71,7 @@ ;;; Global constants ;; -(defconst elpher-version "3.4.2" +(defconst elpher-version "3.5.0" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -241,6 +241,14 @@ meaningfully." "Label of button used to toggle formatted text." :type '(string)) +(defcustom elpher-certificate-map nil + "Register client certificates to be used for gemini URLs. +This variable contains 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 @@ -951,7 +959,8 @@ the host operating system and the local network capabilities.)" ;;; 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 @@ -965,7 +974,8 @@ when the certificate is no longer needed for the current session. Otherwise, the certificate will be given a 100 year expiration period and the files will not be deleted. -The function returns a list containing the current host name, the +The function returns a list containing the URL-PREFIX of addresses +for which the certificate should be used in this session, the temporary flag, and the key and cert file names in the form required by `gnutls-boot-parameters`." (let ((exp-key-file (expand-file-name key-file)) @@ -979,56 +989,70 @@ by `gnutls-boot-parameters`." "-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." +longer needed for this session. + +The certificate will be marked as applying to all addresses with URLs +starting with URL-PREFIX." (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 certificate. -The key and certificate files are written to in `elpher-certificate-directory'." +The key and certificate files are written to in `elpher-certificate-directory'. + +In this session, the certificate will remain active for all addresses +having URLs starting with URL-PREFIX." (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'." +the directory `elpher-certificate-directory'. + +In this session, the certificate will remain active for all addresses +having URLs starting with URL-PREFIX." (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 url-prefix 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 url-prefix) "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 -base for the installed key and certificate files." +base for the installed key and certificate files. + +In this session, the certificate will remain active for all addresses +having URLs starting with URL-PREFIX." (let* ((key-file (concat elpher-certificate-directory file-base ".key")) (cert-file (concat elpher-certificate-directory file-base ".crt"))) (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 url-prefix nil (expand-file-name key-file) (expand-file-name cert-file)))) @@ -1054,7 +1078,7 @@ are also deleted." (when (cadr elpher-client-certificate) (delete-file (elt elpher-client-certificate 2)) (delete-file (elt elpher-client-certificate 3))) - (setq elpher-client-certificate nil) + (setq-local elpher-client-certificate nil) (if (called-interactively-p 'any) (message "Client certificate forgotten."))))) @@ -1062,14 +1086,14 @@ are also deleted." "Retrieve the `gnutls-boot-parameters'-compatable keylist. This is obtained from the client certificate described by -`elpher-current-certificate', if one is available and the host for -that certificate matches the host in ADDRESS. +`elpher-current-certificate', if one is available and the +URL prefix for that certificate matches ADDRESS. -If `elpher-current-certificate' is non-nil, and its host name doesn't +If `elpher-current-certificate' is non-nil, and its URL prefix 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") @@ -1352,14 +1376,17 @@ that the response was malformed." (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 @@ -1388,28 +1415,55 @@ that the response was malformed." (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)) + (setq-local elpher-client-certificate chosen-certificate)) (elpher-with-clean-buffer) (elpher-get-gemini-response (elpher-page-address elpher-current-page) renderer)) (_other (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-certificate-map' variable. + +For this session, the certificate will remain active for all addresses +having URLs begining with URL-PREFIX." + (let ((entry (assoc url-prefix + elpher-certificate-map + #'string-prefix-p))) + (if entry + (let ((cert-url-prefix (car entry)) + (cert-name (cadr entry))) + (message "Using certificate \"%s\" specified in elpher-certificate-map with prefix \"%s\"" + cert-name cert-url-prefix) + (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 () - "Prompt for a client certificate to use to establish a TLS connection." + + +(defun elpher-prompt-for-client-certificate (url-prefix) + "Prompt for a client certificate to use to establish a TLS connection. + +In this session, the chosen certificate will remain active for all +addresses with URLs matching URL-PREFIX." (let* ((read-answer-short t)) (pcase (read-answer "What do you want to do? " '(("throwaway" ?t @@ -1419,7 +1473,7 @@ is a list of possible answers." ("abort" ?a "stop immediately"))) ("throwaway" - (setq elpher-client-certificate (elpher-generate-throwaway-certificate))) + (setq-local elpher-client-certificate (elpher-generate-throwaway-certificate url-prefix))) ("persistent" (let* ((existing-certificates (elpher-list-existing-certificates)) (file-base (completing-read @@ -1428,8 +1482,8 @@ is a list of possible answers." (if (string-empty-p (string-trim file-base)) nil (if (member file-base existing-certificates) - (setq elpher-client-certificate - (elpher-get-existing-certificate file-base)) + (setq-local elpher-client-certificate + (elpher-get-existing-certificate file-base url-prefix)) (pcase (read-answer "Generate new certificate or install externally-generated one? " '(("new" ?n "generate new certificate") @@ -1442,15 +1496,16 @@ is a list of possible answers." 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)))) @@ -2188,6 +2243,11 @@ supports the old protocol elpher, where the link is self-contained." :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) @@ -2209,7 +2269,7 @@ supports the old protocol elpher, where the link is self-contained." (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, @@ -2220,7 +2280,7 @@ supports the old protocol elpher, where the link is self-contained." (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) @@ -2235,13 +2295,13 @@ supports the old protocol elpher, where the link is self-contained." ;; 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 @@ -2267,7 +2327,8 @@ supports the old protocol elpher, where the link is self-contained." "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 @@ -2284,10 +2345,14 @@ Unlike `elpher-go', the reader is initialized with the URL of the 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."