More tweaks of skeleton documentation wrt \n behavior at bol/eol.
[bpt/emacs.git] / lisp / net / network-stream.el
index 61e4630..28e9d0c 100644 (file)
@@ -1,6 +1,6 @@
 ;;; network-stream.el --- open network processes, possibly with encryption
 
-;; Copyright (C) 2010-2011 Free Software Foundation, Inc.
+;; Copyright (C) 2010-2014 Free Software Foundation, Inc.
 
 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
 ;; Keywords: network
 
 (require 'tls)
 (require 'starttls)
+(require 'auth-source)
 
-(declare-function gnutls-negotiate "gnutls" t t) ; defun*
+(autoload 'gnutls-negotiate "gnutls")
+(autoload 'open-gnutls-stream "gnutls")
 
 ;;;###autoload
 (defun open-network-stream (name buffer host service &rest parameters)
@@ -96,6 +98,10 @@ values:
 
 :end-of-command specifies a regexp matching the end of a command.
 
+:end-of-capability specifies a regexp matching the end of the
+  response to the command specified for :capability-command.
+  It defaults to the regexp specified for :end-of-command.
+
 :success specifies a regexp matching a message indicating a
   successful STARTTLS negotiation.  For instance, the default
   should be \"^3\" for an NNTP connection.
@@ -109,8 +115,24 @@ values:
   capability command, and should return the command to switch on
   STARTTLS if the server supports STARTTLS, and nil otherwise.
 
+:always-query-capabilities says whether to query the server for
+  capabilities, even if we're doing a `plain' network connection.
+
+:client-certificate should either be a list where the first
+  element is the certificate key file name, and the second
+  element is the certificate file name itself, or `t', which
+  means that `auth-source' will be queried for the key and the
+  certificate.  This parameter will only be used when doing TLS
+  or STARTTLS connections.
+
+:use-starttls-if-possible is a boolean that says to do opportunistic
+STARTTLS upgrades even if Emacs doesn't have built-in TLS functionality.
+
+:nogreeting is a boolean that can be used to inhibit waiting for
+a greeting from the server.
+
 :nowait is a boolean that says the connection should be made
-asynchronously, if possible."
+  asynchronously, if possible."
   (unless (featurep 'make-network-process)
     (error "Emacs was compiled without networking support"))
   (let ((type (plist-get parameters :type))
@@ -126,8 +148,11 @@ asynchronously, if possible."
                              :nowait (plist-get parameters :nowait))
       (let ((work-buffer (or buffer
                             (generate-new-buffer " *stream buffer*")))
-           (fun (cond ((eq type 'plain) 'network-stream-open-plain)
-                      ((memq type '(nil network starttls))
+           (fun (cond ((and (eq type 'plain)
+                            (not (plist-get parameters
+                                            :always-query-capabilities)))
+                       'network-stream-open-plain)
+                      ((memq type '(nil network starttls plain))
                        'network-stream-open-starttls)
                       ((memq type '(tls ssl)) 'network-stream-open-tls)
                       ((eq type 'shell) 'network-stream-open-shell)
@@ -143,9 +168,26 @@ asynchronously, if possible."
            (list (car result)
                  :greeting     (nth 1 result)
                  :capabilities (nth 2 result)
-                 :type         (nth 3 result))
+                 :type         (nth 3 result)
+                 :error        (nth 4 result))
          (car result))))))
 
+(defun network-stream-certificate (host service parameters)
+  (let ((spec (plist-get :client-certificate parameters)))
+    (cond
+     ((listp spec)
+      ;; Either nil or a list with a key/certificate pair.
+      spec)
+     ((eq spec t)
+      (let* ((auth-info
+             (car (auth-source-search :max 1
+                                      :host host
+                                      :port service)))
+            (key (plist-get auth-info :key))
+            (cert (plist-get auth-info :cert)))
+       (and key cert
+            (list key cert)))))))
+
 ;;;###autoload
 (defalias 'open-protocol-stream 'open-network-stream)
 
@@ -167,41 +209,80 @@ asynchronously, if possible."
         (success-string     (plist-get parameters :success))
         (capability-command (plist-get parameters :capability-command))
         (eoc                (plist-get parameters :end-of-command))
+        (eo-capa            (or (plist-get parameters :end-of-capability)
+                                eoc))
         ;; Return (STREAM GREETING CAPABILITIES RESULTING-TYPE)
         (stream (make-network-process :name name :buffer buffer
                                       :host host :service service))
-        (greeting (network-stream-get-response stream start eoc))
-        (capabilities (network-stream-command stream capability-command eoc))
+        (greeting (and (not (plist-get parameters :nogreeting))
+                       (network-stream-get-response stream start eoc)))
+        (capabilities (network-stream-command stream capability-command
+                                              eo-capa))
         (resulting-type 'plain)
-        starttls-command)
-
+        (builtin-starttls (and (fboundp 'gnutls-available-p)
+                               (gnutls-available-p)))
+        starttls-available starttls-command error)
+
+    ;; First check whether the server supports STARTTLS at all.
+    (when (and capabilities success-string starttls-function)
+      (setq starttls-command
+           (funcall starttls-function capabilities)))
     ;; If we have built-in STARTTLS support, try to upgrade the
     ;; connection.
-    (when (and (or (fboundp 'open-gnutls-stream)
-                  (and require-tls
-                       (executable-find "gnutls-cli")))
-              capabilities success-string starttls-function
-              (setq starttls-command
-                    (funcall starttls-function capabilities)))
+    (when (and starttls-command
+              (setq starttls-available
+                    (or builtin-starttls
+                        (and (or require-tls
+                                 (plist-get parameters :use-starttls-if-possible))
+                             (starttls-available-p))))
+              (not (eq (plist-get parameters :type) 'plain)))
       ;; If using external STARTTLS, drop this connection and start
       ;; anew with `starttls-open-stream'.
-      (unless (fboundp 'open-gnutls-stream)
+      (unless builtin-starttls
        (delete-process stream)
        (setq start (with-current-buffer buffer (point-max)))
-       (let* ((starttls-use-gnutls t)
-              (starttls-extra-arguments
-               (if require-tls
+       (let* ((starttls-extra-arguments
+               (if (or require-tls
+                       (member "--insecure" starttls-extra-arguments))
                    starttls-extra-arguments
                  ;; For opportunistic TLS upgrades, we don't really
                  ;; care about the identity of the peer.
-                 (cons "--insecure" starttls-extra-arguments))))
+                 (cons "--insecure" starttls-extra-arguments)))
+              (starttls-extra-args starttls-extra-args)
+              (cert (network-stream-certificate host service parameters)))
+         ;; There are client certificates requested, so add them to
+         ;; the command line.
+         (when cert
+           (setq starttls-extra-arguments
+                 (nconc (list "--x509keyfile" (expand-file-name (nth 0 cert))
+                              "--x509certfile" (expand-file-name (nth 1 cert)))
+                        starttls-extra-arguments)
+                 starttls-extra-args
+                 (nconc (list "--key-file" (expand-file-name (nth 0 cert))
+                              "--cert-file" (expand-file-name (nth 1 cert)))
+                        starttls-extra-args)))
          (setq stream (starttls-open-stream name buffer host service)))
-       (network-stream-get-response stream start eoc))
-      (when (string-match success-string
-                         (network-stream-command stream starttls-command eoc))
+       (network-stream-get-response stream start eoc)
+       ;; Requery capabilities for protocols that require it; i.e.,
+       ;; EHLO for SMTP.
+       (when (plist-get parameters :always-query-capabilities)
+         (network-stream-command stream capability-command eo-capa)))
+      (when (let ((response
+                  (network-stream-command stream starttls-command eoc)))
+             (and response (string-match success-string response)))
        ;; The server said it was OK to begin STARTTLS negotiations.
-       (if (fboundp 'open-gnutls-stream)
-           (gnutls-negotiate :process stream :hostname host)
+       (if builtin-starttls
+           (let ((cert (network-stream-certificate host service parameters)))
+             (condition-case nil
+                 (gnutls-negotiate :process stream :hostname host
+                                   :keylist (and cert (list cert)))
+               ;; If we get a gnutls-specific error (for instance if
+               ;; the certificate the server gives us is completely
+               ;; syntactically invalid), then close the connection
+               ;; and possibly (further down) try to create a
+               ;; non-encrypted connection.
+               (gnutls-error
+                (delete-process stream))))
          (unless (starttls-negotiate stream)
            (delete-process stream)))
        (if (memq (process-status stream) '(open run))
@@ -215,14 +296,31 @@ asynchronously, if possible."
            (network-stream-get-response stream start eoc)))
        ;; Re-get the capabilities, which may have now changed.
        (setq capabilities
-             (network-stream-command stream capability-command eoc))))
+             (network-stream-command stream capability-command eo-capa))))
 
     ;; If TLS is mandatory, close the connection if it's unencrypted.
-    (and require-tls
-        (eq resulting-type 'plain)
-        (delete-process stream))
+    (when (and require-tls
+              ;; ... but Emacs wasn't able to -- either no built-in
+              ;; support, or no gnutls-cli installed.
+              (eq resulting-type 'plain))
+      (setq error
+           (if (or (null starttls-command)
+                   starttls-available)
+               "Server does not support TLS"
+             ;; See `starttls-available-p'.  If this predicate
+             ;; changes to allow running under Windows, the error
+             ;; message below should be amended.
+             (if (memq system-type '(windows-nt ms-dos))
+                 (concat "Emacs does not support TLS")
+               (concat "Emacs does not support TLS, and no external `"
+                       (if starttls-use-gnutls
+                           starttls-gnutls-program
+                         starttls-program)
+                       "' program was found"))))
+      (delete-process stream)
+      (setq stream nil))
     ;; Return value:
-    (list stream greeting capabilities resulting-type)))
+    (list stream greeting capabilities resulting-type error)))
 
 (defun network-stream-command (stream command eoc)
   (when command
@@ -246,7 +344,8 @@ asynchronously, if possible."
 (defun network-stream-open-tls (name buffer host service parameters)
   (with-current-buffer buffer
     (let* ((start (point-max))
-          (use-builtin-gnutls (fboundp 'open-gnutls-stream))
+          (use-builtin-gnutls (and (fboundp 'gnutls-available-p)
+                                   (gnutls-available-p)))
           (stream
            (funcall (if use-builtin-gnutls
                         'open-gnutls-stream
@@ -257,7 +356,8 @@ asynchronously, if possible."
          (list nil nil nil 'plain)
        ;; If we're using tls.el, we have to delete the output from
        ;; openssl/gnutls-cli.
-       (when (and (null use-builtin-gnutls) eoc)
+       (when (and (null use-builtin-gnutls)
+                  eoc)
          (network-stream-get-response stream start eoc)
          (goto-char (point-min))
          (when (re-search-forward eoc nil t)
@@ -284,7 +384,9 @@ asynchronously, if possible."
                                    ?p service))))))
     (list stream
          (network-stream-get-response stream start eoc)
-         (network-stream-command stream capability-command eoc)
+         (network-stream-command stream capability-command
+                                 (or (plist-get parameters :end-of-capability)
+                                     eoc))
          'plain)))
 
 (provide 'network-stream)