summaryrefslogtreecommitdiff
path: root/jabber-conn.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-conn.el')
-rw-r--r--jabber-conn.el396
1 files changed, 396 insertions, 0 deletions
diff --git a/jabber-conn.el b/jabber-conn.el
new file mode 100644
index 0000000..6a4c2d5
--- /dev/null
+++ b/jabber-conn.el
@@ -0,0 +1,396 @@
+;; jabber-conn.el - Network transport functions
+
+;; Copyright (C) 2005 - Georg Lehner - jorge@magma.com.ni
+;; mostly inspired by Gnus.
+
+;; Copyright (C) 2005 - Carl Henrik Lunde - chlunde+jabber+@ping.uio.no
+;; (starttls)
+
+;; This file is a part of jabber.el.
+
+;; This program 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 2 of the License, or
+;; (at your option) any later version.
+
+;; This program 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 this program; if not, write to the Free Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+;; A collection of functions, that hide the details of transmitting to
+;; and fro a Jabber Server
+
+(eval-when-compile (require 'cl))
+
+;; Emacs 24 can be linked with GnuTLS
+(ignore-errors (require 'gnutls))
+
+;; Try two different TLS/SSL libraries, but don't fail if none available.
+(or (ignore-errors (require 'tls))
+ (ignore-errors (require 'ssl)))
+
+(ignore-errors (require 'starttls))
+
+(require 'srv)
+
+(defgroup jabber-conn nil "Jabber Connection Settings"
+ :group 'jabber)
+
+(defun jabber-have-starttls ()
+ "Return true if we can use STARTTLS."
+ (or (and (fboundp 'gnutls-available-p)
+ (gnutls-available-p))
+ (and (featurep 'starttls)
+ (or (and (bound-and-true-p starttls-gnutls-program)
+ (executable-find starttls-gnutls-program))
+ (and (bound-and-true-p starttls-program)
+ (executable-find starttls-program))))))
+
+(defconst jabber-default-connection-type
+ (cond
+ ;; Use STARTTLS if we can...
+ ((jabber-have-starttls)
+ 'starttls)
+ ;; ...else default to unencrypted connection.
+ (t
+ 'network))
+ "Default connection type.
+See `jabber-connect-methods'.")
+
+(defcustom jabber-connection-ssl-program nil
+ "Program used for SSL/TLS connections.
+nil means prefer gnutls but fall back to openssl.
+'gnutls' means use gnutls (through `open-tls-stream').
+'openssl means use openssl (through `open-ssl-stream')."
+ :type '(choice (const :tag "Prefer gnutls, fall back to openssl" nil)
+ (const :tag "Use gnutls" gnutls)
+ (const :tag "Use openssl" openssl))
+ :group 'jabber-conn)
+
+(defcustom jabber-invalid-certificate-servers ()
+ "Jabber servers for which we accept invalid TLS certificates.
+This is a list of server names, each matching the hostname part
+of your JID.
+
+This option has effect only when using native GnuTLS in Emacs 24
+or later."
+ :type '(repeat string)
+ :group 'jabber-conn)
+
+(defvar jabber-connect-methods
+ `((network jabber-network-connect jabber-network-send)
+ (starttls
+ ,(if (and (fboundp 'gnutls-available-p)
+ (gnutls-available-p))
+ ;; With "native" TLS, we can use a normal connection.
+ 'jabber-network-connect
+ 'jabber-starttls-connect)
+ jabber-network-send)
+ (ssl jabber-ssl-connect jabber-ssl-send)
+ (virtual jabber-virtual-connect jabber-virtual-send))
+ "Alist of connection methods and functions.
+First item is the symbol naming the method.
+Second item is the connect function.
+Third item is the send function.")
+
+(defun jabber-get-connect-function (type)
+ "Get the connect function associated with TYPE.
+TYPE is a symbol; see `jabber-connection-type'."
+ (let ((entry (assq type jabber-connect-methods)))
+ (nth 1 entry)))
+
+(defun jabber-get-send-function (type)
+ "Get the send function associated with TYPE.
+TYPE is a symbol; see `jabber-connection-type'."
+ (let ((entry (assq type jabber-connect-methods)))
+ (nth 2 entry)))
+
+(defun jabber-srv-targets (server network-server port)
+ "Find host and port to connect to.
+If NETWORK-SERVER and/or PORT are specified, use them.
+If we can't find SRV records, use standard defaults."
+ ;; If the user has specified a host or a port, obey that.
+ (if (or network-server port)
+ (list (cons (or network-server server)
+ (or port 5222)))
+ (or (condition-case nil
+ (srv-lookup (concat "_xmpp-client._tcp." server))
+ (error nil))
+ (list (cons server 5222)))))
+
+;; Plain TCP/IP connection
+(defun jabber-network-connect (fsm server network-server port)
+ "Connect to a Jabber server with a plain network connection.
+Send a message of the form (:connected CONNECTION) to FSM if
+connection succeeds. Send a message (:connection-failed ERRORS) if
+connection fails."
+ (cond
+ ((featurep 'make-network-process '(:nowait t))
+ ;; We can connect asynchronously!
+ (jabber-network-connect-async fsm server network-server port))
+ (t
+ ;; Connecting to the server will block Emacs.
+ (jabber-network-connect-sync fsm server network-server port))))
+
+(defun jabber-network-connect-async (fsm server network-server port)
+ ;; Get all potential targets...
+ (lexical-let ((targets (jabber-srv-targets server network-server port))
+ errors
+ (fsm fsm))
+ ;; ...and connect to them one after another, asynchronously, until
+ ;; connection succeeds.
+ (labels
+ ((connect
+ (target remaining-targets)
+ (lexical-let ((target target) (remaining-targets remaining-targets))
+ (labels ((connection-successful
+ (c)
+ ;; This mustn't be `fsm-send-sync', because the FSM
+ ;; needs to change the sentinel, which cannot be done
+ ;; from inside the sentinel.
+ (fsm-send fsm (list :connected c)))
+ (connection-failed
+ (c status)
+ (when (and (> (length status) 0)
+ (eq (aref status (1- (length status))) ?\n))
+ (setq status (substring status 0 -1)))
+ (let ((err
+ (format "Couldn't connect to %s:%s: %s"
+ (car target) (cdr target) status)))
+ (message "%s" err)
+ (push err errors))
+ (when c (delete-process c))
+ (if remaining-targets
+ (progn
+ (message
+ "Connecting to %s:%s..."
+ (caar remaining-targets) (cdar remaining-targets))
+ (connect (car remaining-targets) (cdr remaining-targets)))
+ (fsm-send fsm (list :connection-failed (nreverse errors))))))
+ (condition-case e
+ (make-network-process
+ :name "jabber"
+ :buffer (generate-new-buffer jabber-process-buffer)
+ :host (car target) :service (cdr target)
+ :coding 'utf-8
+ :nowait t
+ :sentinel
+ (lexical-let ((target target) (remaining-targets remaining-targets))
+ (lambda (connection status)
+ (cond
+ ((string-match "^open" status)
+ (connection-successful connection))
+ ((string-match "^failed" status)
+ (connection-failed connection status))
+ ((string-match "^deleted" status)
+ ;; This happens when we delete a process in the
+ ;; "failed" case above.
+ nil)
+ (t
+ (message "Unknown sentinel status `%s'" status))))))
+ (file-error
+ ;; A file-error has the error message in the third list
+ ;; element.
+ (connection-failed nil (car (cddr e))))
+ (error
+ ;; Not sure if we ever get anything but file-errors,
+ ;; but let's make sure we report them:
+ (connection-failed nil (error-message-string e))))))))
+ (message "Connecting to %s:%s..." (caar targets) (cdar targets))
+ (connect (car targets) (cdr targets)))))
+
+(defun jabber-network-connect-sync (fsm server network-server port)
+ ;; This code will AFAIK only be used on Windows. Apologies in
+ ;; advance for any bit rot...
+ (let ((coding-system-for-read 'utf-8)
+ (coding-system-for-write 'utf-8)
+ (targets (jabber-srv-targets server network-server port))
+ errors)
+ (catch 'connected
+ (dolist (target targets)
+ (condition-case e
+ (let ((process-buffer (generate-new-buffer jabber-process-buffer))
+ connection)
+ (unwind-protect
+ (setq connection (open-network-stream
+ "jabber"
+ process-buffer
+ (car target)
+ (cdr target)))
+
+ (unless (or connection jabber-debug-keep-process-buffers)
+ (kill-buffer process-buffer)))
+
+ (when connection
+ (fsm-send fsm (list :connected connection))
+ (throw 'connected connection)))
+ (file-error
+ ;; A file-error has the error message in the third list
+ ;; element.
+ (let ((err (format "Couldn't connect to %s:%s: %s"
+ (car target) (cdr target)
+ (car (cddr e)))))
+ (message "%s" err)
+ (push err errors)))
+ (error
+ ;; Not sure if we ever get anything but file-errors,
+ ;; but let's make sure we report them:
+ (let ((err (format "Couldn't connect to %s:%s: %s"
+ (car target) (cdr target)
+ (error-message-string e))))
+ (message "%s" err)
+ (push err errors)))))
+ (fsm-send fsm (list :connection-failed (nreverse errors))))))
+
+(defun jabber-network-send (connection string)
+ "Send a string via a plain TCP/IP connection to the Jabber Server."
+ (process-send-string connection string))
+
+;; SSL connection, we use openssl's s_client function for encryption
+;; of the link
+;; TODO: make this configurable
+(defun jabber-ssl-connect (fsm server network-server port)
+ "connect via OpenSSL or GnuTLS to a Jabber Server
+Send a message of the form (:connected CONNECTION) to FSM if
+connection succeeds. Send a message (:connection-failed ERRORS) if
+connection fails."
+ (let ((coding-system-for-read 'utf-8)
+ (coding-system-for-write 'utf-8)
+ (connect-function
+ (cond
+ ((and (memq jabber-connection-ssl-program '(nil gnutls))
+ (fboundp 'open-tls-stream))
+ 'open-tls-stream)
+ ((and (memq jabber-connection-ssl-program '(nil openssl))
+ (fboundp 'open-ssl-stream))
+ 'open-ssl-stream)
+ (t
+ (error "Neither TLS nor SSL connect functions available"))))
+ error-msg)
+ (let ((process-buffer (generate-new-buffer jabber-process-buffer))
+ connection)
+ (setq network-server (or network-server server))
+ (setq port (or port 5223))
+ (condition-case e
+ (setq connection (funcall connect-function
+ "jabber"
+ process-buffer
+ network-server
+ port))
+ (error
+ (setq error-msg
+ (format "Couldn't connect to %s:%d: %s" network-server port
+ (error-message-string e)))
+ (message "%s" error-msg)))
+ (unless (or connection jabber-debug-keep-process-buffers)
+ (kill-buffer process-buffer))
+ (if connection
+ (fsm-send fsm (list :connected connection))
+ (fsm-send fsm (list :connection-failed
+ (when error-msg (list error-msg))))))))
+
+(defun jabber-ssl-send (connection string)
+ "Send a string via an SSL-encrypted connection to the Jabber Server."
+ ;; It seems we need to send a linefeed afterwards.
+ (process-send-string connection string)
+ (process-send-string connection "\n"))
+
+(defun jabber-starttls-connect (fsm server network-server port)
+ "Connect via an external GnuTLS process to a Jabber Server.
+Send a message of the form (:connected CONNECTION) to FSM if
+connection succeeds. Send a message (:connection-failed ERRORS) if
+connection fails."
+ (let ((coding-system-for-read 'utf-8)
+ (coding-system-for-write 'utf-8)
+ (targets (jabber-srv-targets server network-server port))
+ errors)
+ (unless (fboundp 'starttls-open-stream)
+ (error "starttls.el not available"))
+ (catch 'connected
+ (dolist (target targets)
+ (condition-case e
+ (let ((process-buffer (generate-new-buffer jabber-process-buffer))
+ connection)
+ (unwind-protect
+ (setq connection
+ (starttls-open-stream
+ "jabber"
+ process-buffer
+ (car target)
+ (cdr target)))
+ (unless (or connection jabber-debug-keep-process-buffers)
+ (kill-buffer process-buffer)))
+ (if (null connection)
+ ;; It seems we don't actually get an error if we
+ ;; can't connect. Let's try to convey some useful
+ ;; information to the user at least.
+ (let ((err (format "Couldn't connect to %s:%s"
+ (car target) (cdr target))))
+ (message "%s" err)
+ (push err errors))
+ (fsm-send fsm (list :connected connection))
+ (throw 'connected connection)))
+ (error
+ (let ((err (format "Couldn't connect to %s: %s" target
+ (error-message-string e))))
+ (message "%s" err)
+ (push err errors)))))
+ (fsm-send fsm (list :connection-failed (nreverse errors))))))
+
+(defun jabber-starttls-initiate (fsm)
+ "Initiate a starttls connection"
+ (jabber-send-sexp fsm
+ '(starttls ((xmlns . "urn:ietf:params:xml:ns:xmpp-tls")))))
+
+(defun jabber-starttls-process-input (fsm xml-data)
+ "Process result of starttls request.
+On failure, signal error."
+ (cond
+ ((eq (car xml-data) 'proceed)
+ (let* ((state-data (fsm-get-state-data fsm))
+ (connection (plist-get state-data :connection)))
+ ;; Did we use open-network-stream or starttls-open-stream? We
+ ;; can tell by process-type.
+ (case (process-type connection)
+ (network
+ (let* ((hostname (plist-get state-data :server))
+ (verifyp (not (member hostname jabber-invalid-certificate-servers))))
+ ;; gnutls-negotiate might signal an error, which is caught
+ ;; by our caller
+ (gnutls-negotiate
+ :process connection
+ ;; This is the hostname that the certificate should be valid for:
+ :hostname hostname
+ :verify-hostname-error verifyp
+ :verify-error verifyp)))
+ (real
+ (or
+ (starttls-negotiate connection)
+ (error "Negotiation failure"))))))
+ ((eq (car xml-data) 'failure)
+ (error "Command rejected by server"))))
+
+(defvar *jabber-virtual-server-function* nil
+ "Function to use for sending stanzas on a virtual connection.
+The function should accept two arguments, the connection object
+and a string that the connection wants to send.")
+
+(defun jabber-virtual-connect (fsm server network-server port)
+ "Connect to a virtual \"server\".
+Use `*jabber-virtual-server-function*' as send function."
+ (unless (functionp *jabber-virtual-server-function*)
+ (error "No virtual server function specified"))
+ ;; We pass the fsm itself as "connection object", as that is what a
+ ;; virtual server needs to send stanzas.
+ (fsm-send fsm (list :connected fsm)))
+
+(defun jabber-virtual-send (connection string)
+ (funcall *jabber-virtual-server-function* connection string))
+
+(provide 'jabber-conn)
+;; arch-tag: f95ec240-8cd3-11d9-9dbf-000a95c2fcd0