proto-stream.el: New library to provide protocol-specific TLS/STARTTLS connections...
authorLars Magne Ingebrigtsen <larsi@quimbies.gnus.org>
Sat, 27 Nov 2010 14:17:13 +0000 (15:17 +0100)
committerLars Magne Ingebrigtsen <larsi@quimbies.gnus.org>
Sat, 27 Nov 2010 14:39:04 +0000 (15:39 +0100)
lisp/nnimap.el
lisp/proto-stream.el [new file with mode: 0644]

index cb4c9f0..c7fe512 100644 (file)
@@ -45,6 +45,7 @@
 (require 'tls)
 (require 'parse-time)
 (require 'nnmail)
+(require 'meta-stream)
 
 (eval-when-compile
   (require 'gnus-sum))
@@ -271,16 +272,6 @@ textual parts.")
     (push (current-buffer) nnimap-process-buffers)
     (current-buffer)))
 
-(defun nnimap-open-shell-stream (name buffer host port)
-  (let ((process-connection-type nil))
-    (start-process name buffer shell-file-name
-                  shell-command-switch
-                  (format-spec
-                   nnimap-shell-program
-                   (format-spec-make
-                    ?s host
-                    ?p port)))))
-
 (defun nnimap-credentials (address ports &optional inhibit-create)
   (let (port credentials)
     ;; Request the credentials from all ports, but only query on the
@@ -310,110 +301,58 @@ textual parts.")
                        (* 5 60)))
            (nnimap-send-command "NOOP")))))))
 
-(declare-function gnutls-negotiate "gnutls"
-                 (proc type &optional priority-string trustfiles keyfiles))
-
 (defun nnimap-open-connection (buffer)
   (unless nnimap-keepalive-timer
     (setq nnimap-keepalive-timer (run-at-time (* 60 15) (* 60 15)
                                              'nnimap-keepalive)))
-  (block nil
-    (with-current-buffer (nnimap-make-process-buffer buffer)
-      (let* ((coding-system-for-read 'binary)
-            (coding-system-for-write 'binary)
-            (port nil)
-            (ports
-             (cond
-              ((or (eq nnimap-stream 'network)
-                   (and (eq nnimap-stream 'starttls)
-                        (fboundp 'open-gnutls-stream)))
-               (nnheader-message 7 "Opening connection to %s..."
-                                 nnimap-address)
-               (open-network-stream
-                "*nnimap*" (current-buffer) nnimap-address
-                (setq port
-                      (or nnimap-server-port
-                          (if (netrc-find-service-number "imap")
-                              "imap"
-                            "143"))))
-               '("143" "imap"))
-              ((eq nnimap-stream 'shell)
-               (nnheader-message 7 "Opening connection to %s via shell..."
-                                 nnimap-address)
-               (nnimap-open-shell-stream
-                "*nnimap*" (current-buffer) nnimap-address
-                (setq port (or nnimap-server-port "imap")))
-               '("imap"))
-              ((eq nnimap-stream 'starttls)
-               (nnheader-message 7 "Opening connection to %s via starttls..."
-                        nnimap-address)
-               (let ((tls-program
-                      '("openssl s_client -connect %h:%p -no_ssl2 -ign_eof -starttls imap")))
-                 (open-tls-stream
-                  "*nnimap*" (current-buffer) nnimap-address
-                  (setq port (or nnimap-server-port "imap"))))
-               '("imap"))
-              ((memq nnimap-stream '(ssl tls))
-               (nnheader-message 7 "Opening connection to %s via tls..."
-                                 nnimap-address)
-               (funcall (if (fboundp 'open-gnutls-stream)
-                            'open-gnutls-stream
-                          'open-tls-stream)
-                        "*nnimap*" (current-buffer) nnimap-address
-                        (setq port
-                              (or nnimap-server-port
-                                  (if (netrc-find-service-number "imaps")
-                                      "imaps"
-                                    "993"))))
-               '("143" "993" "imap" "imaps"))
-              (t
-               (error "Unknown stream type: %s" nnimap-stream))))
-            connection-result login-result credentials)
-       (setf (nnimap-process nnimap-object)
-             (get-buffer-process (current-buffer)))
-       (if (not (and (nnimap-process nnimap-object)
-                     (memq (process-status (nnimap-process nnimap-object))
-                           '(open run))))
+  (with-current-buffer (nnimap-make-process-buffer buffer)
+    (let* ((coding-system-for-read 'binary)
+          (coding-system-for-write 'binary)
+          (port nil)
+          (ports
+           (cond
+            ((or (eq nnimap-stream 'network)
+                 (eq nnimap-stream 'starttls))
+             (nnheader-message 7 "Opening connection to %s..."
+                               nnimap-address)
+             '("143" "imap"))
+            ((eq nnimap-stream 'shell)
+             (nnheader-message 7 "Opening connection to %s via shell..."
+                               nnimap-address)
+             '("imap"))
+            ((memq nnimap-stream '(ssl tls))
+             (nnheader-message 7 "Opening connection to %s via tls..."
+                               nnimap-address)
+             '("143" "993" "imap" "imaps"))
+            (t
+             (error "Unknown stream type: %s" nnimap-stream))))
+          connection-result login-result credentials)
+      (destructuring-bind (stream greeting capabilities)
+         (open-proto-stream
+          "*nnimap*" (current-buffer) nnimap-address (car (last ports))
+          :type nnimap-stream
+          :shell-command nnimap-shell-program
+          :capability-command "1 CAPABILITY\r\n"
+          :starttls-function
+          (lambda (stream capabilities)
+            (if (not (string-match "STARTTLS" capabilities))
+                ;; Not a STARTTLS-capable server.
+                nil
+              "1 STARTTLS")))
+       (setf (nnimap-process nnimap-object) stream)
+       (if (not stream)
            (nnheader-report 'nnimap "Unable to contact %s:%s via %s"
                             nnimap-address port nnimap-stream)
-         (gnus-set-process-query-on-exit-flag
-          (nnimap-process nnimap-object) nil)
-         (if (not (setq connection-result (nnimap-wait-for-connection)))
-             (nnheader-report 'nnimap
-                              "%s" (buffer-substring
-                                    (point) (line-end-position)))
+         (gnus-set-process-query-on-exit-flag stream nil)
+         (if (not (string-match "[*.] \\([A-Z0-9]+\\)" greeting))
+             (nnheader-report 'nnimap "%s" greeting)
            ;; Store the greeting (for debugging purposes).
-           (setf (nnimap-greeting nnimap-object)
-                 (buffer-substring (line-beginning-position)
-                                   (line-end-position)))
-           (nnimap-get-capabilities)
+           (setf (nnimap-greeting nnimap-object) greeting)
+           (setf (nnimap-capabilities nnimap-object)
+                 (mapcar #'upcase
+                         (split-string capabilities)))
            (when nnimap-server-port
              (push (format "%s" nnimap-server-port) ports))
-           ;; If this is a STARTTLS-capable server, then sever the
-           ;; connection and start a STARTTLS connection instead.
-           (cond
-            ((and (or (and (eq nnimap-stream 'network)
-                           (nnimap-capability "STARTTLS"))
-                      (eq nnimap-stream 'starttls))
-                  (fboundp 'open-gnutls-stream))
-             (nnimap-command "STARTTLS")
-             (gnutls-negotiate (nnimap-process nnimap-object) nil)
-             ;; Get the capabilities again -- they may have changed
-             ;; after doing STARTTLS.
-             (nnimap-get-capabilities))
-            ((and (eq nnimap-stream 'network)
-                  (nnimap-capability "STARTTLS"))
-             (let ((nnimap-stream 'starttls))
-               (let ((tls-process
-                      (nnimap-open-connection buffer)))
-                 ;; If the STARTTLS connection was successful, we
-                 ;; kill our first non-encrypted connection.  If it
-                 ;; wasn't successful, we just use our unencrypted
-                 ;; connection.
-                 (when (memq (process-status tls-process) '(open run))
-                   (delete-process (nnimap-process nnimap-object))
-                   (kill-buffer (current-buffer))
-                   (return tls-process))))))
            (unless (equal connection-result "PREAUTH")
              (if (not (setq credentials
                             (if (eq nnimap-authenticator 'anonymous)
@@ -456,13 +395,6 @@ textual parts.")
                (nnimap-command "ENABLE QRESYNC"))
              (nnimap-process nnimap-object))))))))
 
-(defun nnimap-get-capabilities ()
-  (setf (nnimap-capabilities nnimap-object)
-       (mapcar
-        #'upcase
-        (nnimap-find-parameter
-         "CAPABILITY" (cdr (nnimap-command "CAPABILITY"))))))
-
 (defun nnimap-quote-specials (string)
   (with-temp-buffer
     (insert string)
diff --git a/lisp/proto-stream.el b/lisp/proto-stream.el
new file mode 100644 (file)
index 0000000..8421aea
--- /dev/null
@@ -0,0 +1,165 @@
+;;; proto-stream.el --- negotiating TLS, STARTTLS and other connections
+;; Copyright (C) 2010 Free Software Foundation, Inc.
+
+;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
+;; Keywords: network
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.         See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs; see the file COPYING.  If not, write to the
+;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+;; Boston, MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This library is meant to provide the glue between modules that want
+;; to establish a network connection to a server for protocols such as
+;; IMAP, NNTP, SMTP and POP3.
+
+;; The main problem is that there's more than a couple of interfaces
+;; towards doing this.  You have normal, plain connections, which are
+;; no trouble at all, but you also have TLS/SSL connections, and you
+;; have STARTTLS.  Negotiating this for each protocol can be rather
+;; tedious, so this library provides a single entry point, and hides
+;; much of the ugliness.
+
+;;; Code:
+
+(eval-when-compile
+  (require 'cl))
+(require 'tls)
+(require 'starttls)
+(require 'format-spec)
+
+;;;###autoload
+(defun open-proto-stream (name buffer host service &rest parameters)
+  "Open a network stream to HOST.
+The first four parameters have the same meaning as in
+`open-network-stream'.  The PARAMETERS is a keyword list that can
+have the following values:
+
+:type -- either `network', `tls', `shell' or `starttls'.  If
+omitted, the default is `network'.
+
+:capability-command -- a function that takes a stream parameter"
+  (let ((type (or (cadr (memq :type parameters)) 'stream)))
+    (when (and (eq type 'starttls)
+              (fboundp 'open-gnutls-stream))
+      (setq type 'network))
+    (when (eq type 'ssl)
+      (setq type 'tls))
+    (destructuring-bind (stream greeting capabilities)
+       (funcall (intern (format "proto-stream-open-%s" type) obarray)
+                name buffer host service parameters)
+      (list (and stream
+                (memq (process-status stream)
+                      '(open run))
+                stream)
+           greeting capabilities))))
+
+(defun proto-stream-open-network (name buffer host service parameters)
+  (let ((start (with-current-buffer buffer (point)))
+       (stream (open-network-stream name buffer host service))
+       (capability-command (cadr (memq :capability-command parameters)))
+       (greeting (proto-stream-get-response stream start)))
+    (if (not capability-command)
+       (list stream greeting nil)
+      (let* ((capabilities
+             (proto-stream-capabilities stream capability-command))
+            (starttls-command
+             (funcall (cadr (memq :starttls-function parameters))
+                      stream capabilities)))
+       (cond
+        ((not starttls-command)
+         ;; If this server doesn't support STARTTLS, but we have
+         ;; requested it explicitly, then close the connection and
+         ;; return nil.
+         (if (eq (cadr (memq :type parameters)) 'starttls)
+             (progn
+               (delete-process stream)
+               nil)
+           ;; Otherwise, just return this plain network connection.
+           (list stream greeting capabilities)))
+        ((fboundp 'open-gnutls-stream)
+         (setq start (with-current-buffer buffer (point)))
+         (process-send-string stream starttls-command)
+         (proto-stream-get-response stream start)
+         (gnutls-negotiate stream nil)
+         ;; Re-get the capabilities, since they may have changed
+         ;; after switching to TLS.
+         (setq start (with-current-buffer buffer (point)))
+         (process-send-string stream capability-command)
+         (list stream greeting (proto-stream-get-response stream start)))
+        (t
+         (delete-process stream)
+         (proto-stream-open-starttls name buffer host service parameters)))))))
+
+(defun proto-stream-capabilities (stream command)
+  (let ((start (with-current-buffer buffer (point))))
+    (process-send-string stream command)
+    (proto-stream-get-response stream start)))
+
+(defun proto-stream-open-starttls (name buffer host service parameters)
+  (proto-stream-capability-open
+   (with-current-buffer buffer (point))
+   (starttls-open-stream name buffer host service)
+   parameters))
+
+(defun proto-stream-get-response (stream start)
+  (with-current-buffer (process-buffer stream)
+    (save-excursion
+      (goto-char start)
+      (while (and (memq (process-status stream)
+                       '(open run))
+                 (not (search-forward "\n" nil t)))
+       (accept-process-output stream 0 50)
+       (goto-char start))
+      (if (= start (point))
+         ;; The process died; return nil.
+         nil
+       ;; Return the data we got back.
+       (buffer-substring start (point))))))
+
+(defun proto-stream-open-tls (name buffer host service parameters)
+  (proto-stream-capability-open
+   (with-current-buffer buffer (point))
+   (funcall (if (fboundp 'open-gnutls-stream)
+               'open-gnutls-stream
+             'open-tls-stream)
+           name buffer host service)
+   parameters))
+
+(defun proto-stream-open-shell (name buffer host service parameters)
+  (proto-stream-capability-open
+   (with-current-buffer buffer (point))
+   (let ((process-connection-type nil))
+     (start-process name buffer shell-file-name
+                   shell-command-switch
+                   (format-spec
+                    (cadr (memq :shell-command parameters))
+                    (format-spec-make
+                     ?s host
+                     ?p port))))
+   parameters))
+
+(defun proto-stream-capability-open (start stream parameters)
+  (let ((capability-command (cadr (memq :capability-command parameters)))
+       (greeting (proto-stream-get-response stream start)))
+    (list stream greeting
+         (and capability-command
+              (proto-stream-capabilities stream capability-command)))))
+
+(provide 'proto-stream)
+
+;;; proto-stream.el ends here