Provide an option to visit clearnet sites over Tor
[elpher.git] / elpher.el
index f2a8188..e1a2192 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -4,7 +4,7 @@
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.9.1
+;; Version: 2.10.2
 ;; Keywords: comm gopher
 ;; Homepage: http://thelambdalab.xyz/elpher
 ;; Package-Requires: ((emacs "26.2"))
 (require 'ansi-color)
 (require 'nsm)
 (require 'gnutls)
+(require 'socks)
 
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.9.1"
+(defconst elpher-version "2.10.2"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
@@ -178,6 +179,11 @@ This can be useful when browsing from a computer that supports IPv6, because
 some servers which do not support IPv6 can take a long time to time-out."
   :type '(boolean))
 
+(defcustom elpher-socks-always nil
+  "If non-nil, elpher will establish network connections over a SOCKS proxy.
+Otherwise, the SOCKS proxy is only used for connections to onion services."
+  :type '(boolean))
+
 ;; Face customizations
 
 (defgroup elpher-faces nil
@@ -209,7 +215,7 @@ some servers which do not support IPv6 can take a long time to time-out."
   "Face used for html type directory records.")
 
 (defface elpher-gemini
-  '((t :inherit font-lock-regexp-grouping-backslash))
+  '((t :inherit font-lock-constant-face))
   "Face used for Gemini type directory records.")
 
 (defface elpher-other-url
@@ -620,32 +626,18 @@ the host operating system and the local network capabilities."
     (condition-case nil
         (let* ((kill-buffer-query-functions nil)
                (port (elpher-address-port address))
+               (service (if (> port 0) port default-port))
                (host (elpher-address-host address))
+               (socks (or elpher-socks-always (string-suffix-p ".onion" host)))
                (response-string-parts nil)
                (bytes-received 0)
                (hkbytes-received 0)
-               (proc (make-network-process :name "elpher-process"
-                                           :host host
-                                           :family (and force-ipv4 'ipv4)
-                                           :service (if (> port 0) port default-port)
-                                           :buffer nil
-                                           :coding 'binary
-                                           :noquery t
-                                           :nowait t
-                                           :tls-parameters
-                                           (and use-tls
-                                                (cons 'gnutls-x509pki
-                                                      (gnutls-boot-parameters
-                                                       :type 'gnutls-x509pki
-                                                       :hostname host
-                                                       :keylist
-                                                       (elpher-get-current-keylist address))))))
                (timer (run-at-time elpher-connection-timeout nil
                                    (lambda ()
                                      (elpher-process-cleanup)
                                      (cond
                                         ; Try again with IPv4
-                                      ((not force-ipv4)
+                                      ((not (or force-ipv4 socks))
                                        (message "Connection timed out.  Retrying with IPv4.")
                                        (elpher-get-host-response address default-port
                                                                  query-string
@@ -662,8 +654,24 @@ the host operating system and the local network capabilities."
                                                                  response-processor
                                                                  nil force-ipv4))
                                       (t
-                                       (elpher-network-error address "Connection time-out.")))))))
+                                       (elpher-network-error address "Connection time-out."))))))
+               (gnutls-params (list :type 'gnutls-x509pki :hostname host
+                                    :keylist (elpher-get-current-keylist address)))
+               (proc (if socks (socks-open-network-stream "elpher-process" nil host service)
+                       (make-network-process :name "elpher-process"
+                                             :host host
+                                             :family (and force-ipv4 'ipv4)
+                                             :service service
+                                             :buffer nil
+                                             :nowait t
+                                             :tls-parameters
+                                             (and use-tls
+                                                  (cons 'gnutls-x509pki
+                                                        (apply #'gnutls-boot-parameters
+                                                               gnutls-params)))))))
           (setq elpher-network-timer timer)
+          (set-process-coding-system proc 'binary 'binary)
+          (set-process-query-on-exit-flag proc nil)
           (elpher-buffer-message (concat "Connecting to " host "..."
                                          " (press 'u' to abort)"))
           (set-process-filter proc
@@ -696,7 +704,7 @@ the host operating system and the local network capabilities."
                                           (process-send-string proc query-string)))
                                        ((string-prefix-p "deleted" event)) ; do nothing
                                        ((and (not response-string-parts)
-                                             (not (or elpher-ipv4-always force-ipv4)))
+                                             (not (or elpher-ipv4-always force-ipv4 socks)))
                                         ; Try again with IPv4
                                         (message "Connection failed. Retrying with IPv4.")
                                         (elpher-get-host-response address default-port
@@ -712,7 +720,10 @@ the host operating system and the local network capabilities."
                                        (t
                                         (error "No response from server")))
                                     (error
-                                     (elpher-network-error address the-error))))))
+                                     (elpher-network-error address the-error)))))
+          (when socks
+            (if use-tls (apply #'gnutls-negotiate :process proc gnutls-params))
+            (funcall (process-sentinel proc) proc "open\n")))
       (error
        (error "Error initiating connection to server")))))
 
@@ -763,8 +774,8 @@ longer needed for this session."
          (cert-file (concat temporary-file-directory file-base ".crt")))
     (elpher-generate-certificate file-base key-file cert-file t)))
 
-(defun elpher-generate-permanent-certificate (file-base common-name)
-  "Generate and return details of a persistant certificate.
+(defun elpher-generate-persistent-certificate (file-base common-name)
+  "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.
@@ -803,7 +814,9 @@ base for the installed key and certificate files."
           (expand-file-name cert-file))))
 
 (defun elpher-list-existing-certificates ()
-  "Return a list of the persistant certificates in `elpher-certificate-directory'."
+  "Return a list of the persistent certificates in `elpher-certificate-directory'."
+  (unless (file-directory-p elpher-certificate-directory)
+    (make-directory elpher-certificate-directory))
   (mapcar
    (lambda (file)
      (file-name-sans-extension file))
@@ -1190,13 +1203,13 @@ that the response was malformed."
     (pcase (read-answer "What do you want to do? "
                         '(("throwaway" ?t
                            "generate and use throw-away certificate")
-                          ("persistant" ?p
-                           "generate new or use existing persistant certificate")
+                          ("persistent" ?p
+                           "generate new or use existing persistent certificate")
                           ("abort" ?a
                            "stop immediately")))
       ("throwaway"
        (setq elpher-client-certificate (elpher-generate-throwaway-certificate)))
-      ("persistant"
+      ("persistent"
        (let* ((existing-certificates (elpher-list-existing-certificates))
               (file-base (completing-read
                           "Nickname for new or existing certificate (autocompletes, empty response aborts): "
@@ -1218,7 +1231,7 @@ that the response was malformed."
                                                 file-base)))
                   (message "New key and self-signed certificate written to %s"
                            elpher-certificate-directory)
-                  (elpher-generate-permanent-certificate file-base common-name)))
+                  (elpher-generate-persistent-certificate file-base common-name)))
                ("install"
                 (let* ((cert-file (read-file-name "Certificate file: " nil nil t))
                        (key-file (read-file-name "Key file: " nil nil t)))
@@ -1363,17 +1376,20 @@ treatment that a separate function is warranted."
 The gemini map file line describing the header is given
 by HEADER-LINE."
   (when (string-match "^\\(#+\\)[ \t]*" header-line)
-    (let ((level (length (match-string 1 header-line)))
-          (header (substring header-line (match-end 0))))
+    (let* ((level (length (match-string 1 header-line)))
+           (header (substring header-line (match-end 0)))
+          (face (pcase level
+                   (1 'elpher-gemini-heading1)
+                   (2 'elpher-gemini-heading2)
+                   (3 'elpher-gemini-heading3)
+                   (_ 'default)))
+          (fill-column (/ (* fill-column
+                             (font-get (font-spec :name (face-font 'default)) :size))
+                          (font-get (font-spec :name (face-font face)) :size))))
       (unless (display-graphic-p)
         (insert (make-string level ?#) " "))
-      (insert (propertize header 'face
-                          (pcase level
-                            (1 'elpher-gemini-heading1)
-                            (2 'elpher-gemini-heading2)
-                            (3 'elpher-gemini-heading3)
-                            (_ 'default)))
-              "\n"))))
+      (insert (propertize header 'face face))
+      (newline))))
 
 (defun elpher-gemini-insert-text (text-line)
   "Insert a plain non-preformatted TEXT-LINE into a text/gemini document.