summaryrefslogtreecommitdiff
path: root/jabber-socks5.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-socks5.el')
-rw-r--r--jabber-socks5.el678
1 files changed, 678 insertions, 0 deletions
diff --git a/jabber-socks5.el b/jabber-socks5.el
new file mode 100644
index 0000000..97f6d8a
--- /dev/null
+++ b/jabber-socks5.el
@@ -0,0 +1,678 @@
+;; jabber-socks5.el - SOCKS5 bytestreams by JEP-0065
+
+;; Copyright (C) 2003, 2004, 2007 - Magnus Henoch - mange@freemail.hu
+;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
+
+;; 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
+
+(require 'jabber-iq)
+(require 'jabber-disco)
+(require 'jabber-si-server)
+(require 'jabber-si-client)
+
+;; jabber-core will require fsm for us
+(require 'jabber-core)
+(eval-when-compile (require 'cl))
+
+(defvar jabber-socks5-pending-sessions nil
+ "List of pending sessions.
+
+Each entry is a list, containing:
+ * Stream ID
+ * Full JID of initiator
+ * State machine managing the session")
+
+(defvar jabber-socks5-active-sessions nil
+ "List of active sessions.
+
+Each entry is a list, containing:
+ * Network connection
+ * Stream ID
+ * Full JID of initiator
+ * Profile data function")
+
+(defcustom jabber-socks5-proxies nil
+ "JIDs of JEP-0065 proxies to use for file transfer.
+Put preferred ones first."
+ :type '(repeat string)
+ :group 'jabber
+; :set 'jabber-socks5-set-proxies)
+ )
+
+(defvar jabber-socks5-proxies-data nil
+ "Alist containing information about proxies.
+Keys of the alist are strings, the JIDs of the proxies.
+Values are \"streamhost\" XML nodes.")
+
+(jabber-disco-advertise-feature "http://jabber.org/protocol/bytestreams")
+
+(add-to-list 'jabber-si-stream-methods
+ (list "http://jabber.org/protocol/bytestreams"
+ 'jabber-socks5-client-1
+ 'jabber-socks5-accept))
+
+(defun jabber-socks5-set-proxies (symbol value)
+ "Set `jabber-socks5-proxies' and query proxies.
+This is the set function of `jabber-socks5-proxies-data'."
+ (set-default symbol value)
+ (when jabber-connections
+ (jabber-socks5-query-all-proxies)))
+
+(defun jabber-socks5-query-all-proxies (jc &optional callback)
+ "Ask all proxies in `jabber-socks5-proxies' for connection information.
+If CALLBACK is non-nil, call it with no arguments when all
+proxies have answered."
+ (interactive (list (jabber-read-account)))
+ (setq jabber-socks5-proxies-data nil)
+ (dolist (proxy jabber-socks5-proxies)
+ (jabber-socks5-query-proxy jc proxy callback)))
+
+(defun jabber-socks5-query-proxy (jc jid &optional callback)
+ "Query the SOCKS5 proxy specified by JID for IP and port number."
+ (jabber-send-iq jc jid "get"
+ '(query ((xmlns . "http://jabber.org/protocol/bytestreams")))
+ #'jabber-socks5-process-proxy-response (list callback t)
+ #'jabber-socks5-process-proxy-response (list callback nil)))
+
+(defun jabber-socks5-process-proxy-response (jc xml-data closure-data)
+ "Process response from proxy query."
+ (let* ((query (jabber-iq-query xml-data))
+ (from (jabber-xml-get-attribute xml-data 'from))
+ (streamhosts (jabber-xml-get-children query 'streamhost)))
+
+ (let ((existing-entry (assoc from jabber-socks5-proxies-data)))
+ (when existing-entry
+ (setq jabber-socks5-proxies-data
+ (delq existing-entry jabber-socks5-proxies-data))))
+
+ (destructuring-bind (callback successp) closure-data
+ (when successp
+ (setq jabber-socks5-proxies-data
+ (cons (cons from streamhosts)
+ jabber-socks5-proxies-data)))
+ (message "%s from %s. %d of %d proxies have answered."
+ (if successp "Response" "Error") from
+ (length jabber-socks5-proxies-data) (length jabber-socks5-proxies))
+ (when (and callback (= (length jabber-socks5-proxies-data) (length jabber-socks5-proxies)))
+ (funcall callback)))))
+
+(define-state-machine jabber-socks5
+ :start ((jc jid sid profile-function role)
+ "Start JEP-0065 bytestream with JID.
+SID is the session ID used.
+PROFILE-FUNCTION is the function to call upon success. See `jabber-si-stream-methods'.
+ROLE is either :initiator or :target. The initiator sends an IQ
+set; the target waits for one."
+ (let ((new-state-data (list :jc jc
+ :jid jid
+ :sid sid
+ :profile-function profile-function
+ :role role))
+ (new-state
+ ;; We want information about proxies; it might be needed in
+ ;; various situations.
+ (cond
+ ((null jabber-socks5-proxies)
+ ;; We know no proxy addresses. Try to find them by disco.
+ 'seek-proxies)
+ ((null jabber-socks5-proxies-data)
+ ;; We need to query the proxies for addresses.
+ 'query-proxies)
+ ;; So, we have our proxies.
+ (t
+ 'initiate))))
+ (list new-state new-state-data nil))))
+
+(defun jabber-socks5-accept (jc jid sid profile-function)
+ "Remember that we are waiting for connection from JID, with stream id SID"
+ ;; asking the user for permission is done in the profile
+ (add-to-list 'jabber-socks5-pending-sessions
+ (list sid jid (start-jabber-socks5 jc jid sid profile-function :target))))
+
+(define-enter-state jabber-socks5 seek-proxies (fsm state-data)
+ ;; Look for items at the server.
+ (let* ((jc (plist-get state-data :jc))
+ (server (jabber-jid-server (jabber-connection-jid jc))))
+ (jabber-disco-get-items jc
+ server
+ nil
+ (lambda (jc fsm result)
+ (fsm-send-sync fsm (cons :items result)))
+ fsm))
+ ;; Spend no more than five seconds looking for a proxy.
+ (list state-data 5))
+
+(define-state jabber-socks5 seek-proxies (fsm state-data event callback)
+ "Collect disco results, looking for a bytestreams proxy."
+ ;; We put the number of outstanding requests as :remaining-info in
+ ;; the state-data plist.
+ (cond
+ ;; We're not ready to handle the IQ stanza yet
+ ((eq (car-safe event) :iq)
+ :defer)
+
+ ;; Got list of items at the server.
+ ((eq (car-safe event) :items)
+ (dolist (entry (cdr event))
+ ;; Each entry is ["name" "jid" "node"]. We send a disco info
+ ;; request to everything without a node.
+ (when (null (aref entry 2))
+ (lexical-let ((jid (aref entry 1)))
+ (jabber-disco-get-info
+ (plist-get state-data :jc)
+ jid nil
+ (lambda (jc fsm result)
+ (fsm-send-sync fsm (list :info jid result)))
+ fsm))))
+ ;; Remember number of requests sent. But if none, we just go on.
+ (if (cdr event)
+ (list 'seek-proxies (plist-put state-data :remaining-info (length (cdr event))) :keep)
+ (list 'initiate state-data nil)))
+
+ ;; Got disco info from an item at the server.
+ ((eq (car-safe event) :info)
+ (fsm-debug-output "got disco event")
+ ;; Count the response.
+ (plist-put state-data :remaining-info (1- (plist-get state-data :remaining-info)))
+ (unless (eq (first (third event)) 'error)
+ (let ((identities (first (third event))))
+ ;; Is it a bytestream proxy?
+ (when (dolist (identity identities)
+ (when (and (string= (aref identity 1) "proxy")
+ (string= (aref identity 2) "bytestreams"))
+ (return t)))
+ ;; Yes, it is. Add it to the list.
+ (push (second event) jabber-socks5-proxies))))
+
+ ;; Wait for more responses, if any are to be expected.
+ (if (zerop (plist-get state-data :remaining-info))
+ ;; No more... go on to querying the proxies.
+ (list 'query-proxies state-data nil)
+ ;; We expect more responses...
+ (list 'seek-proxies state-data :keep)))
+
+ ((eq event :timeout)
+ ;; We can't wait anymore...
+ (list 'query-proxies state-data nil))))
+
+(define-enter-state jabber-socks5 query-proxies (fsm state-data)
+ (jabber-socks5-query-all-proxies
+ (plist-get state-data :jc)
+ (lexical-let ((fsm fsm))
+ (lambda () (fsm-send-sync fsm :proxies))))
+ (list state-data 5))
+
+(define-state jabber-socks5 query-proxies (fsm state-data event callback)
+ "Query proxies in `jabber-socks5-proxies'."
+ (cond
+ ;; Can't handle the iq stanza yet...
+ ((eq (car-safe event) :iq)
+ :defer)
+
+ ((eq (car-safe event) :info)
+ ;; stray event... do nothing
+ (list 'query-proxies state-data :keep))
+
+ ;; Got response/error from all proxies, or timeout
+ ((memq event '(:proxies :timeout))
+ (list 'initiate state-data nil))))
+
+(define-enter-state jabber-socks5 initiate (fsm state-data)
+ ;; Sort the alist jabber-socks5-proxies-data such that the
+ ;; keys are in the same order as in jabber-socks5-proxies.
+ (setq jabber-socks5-proxies-data
+ (sort jabber-socks5-proxies-data
+ #'(lambda (a b)
+ (> (length (member (car a) jabber-socks5-proxies))
+ (length (member (car b) jabber-socks5-proxies))))))
+
+ ;; If we're the initiator, send initiation stanza.
+ (when (eq (plist-get state-data :role) :initiator)
+ ;; This is where initiation of server sockets would go
+
+ (jabber-send-iq
+ (plist-get state-data :jc)
+ (plist-get state-data :jid) "set"
+ `(query ((xmlns . "http://jabber.org/protocol/bytestreams")
+ (sid . ,(plist-get state-data :sid)))
+ ,@(mapcar
+ #'(lambda (proxy)
+ (mapcar
+ #'(lambda (streamhost)
+ (list 'streamhost
+ (list (cons 'jid (jabber-xml-get-attribute streamhost 'jid))
+ (cons 'host (jabber-xml-get-attribute streamhost 'host))
+ (cons 'port (jabber-xml-get-attribute streamhost 'port)))
+ ;; (proxy ((xmlns . "http://affinix.com/jabber/stream")))
+ ))
+ (cdr proxy)))
+ jabber-socks5-proxies-data)
+ ;; (fast ((xmlns . "http://affinix.com/jabber/stream")))
+ )
+ (lexical-let ((fsm fsm))
+ (lambda (jc xml-data closure-data)
+ (fsm-send-sync fsm (list :iq xml-data))))
+ nil
+ ;; TODO: error handling
+ #'jabber-report-success "SOCKS5 negotiation"))
+
+ ;; If we're the target, we just wait for an incoming stanza.
+ (list state-data nil))
+
+(add-to-list 'jabber-iq-set-xmlns-alist
+ (cons "http://jabber.org/protocol/bytestreams" 'jabber-socks5-process))
+(defun jabber-socks5-process (jc xml-data)
+ "Accept IQ get for SOCKS5 bytestream"
+ (let* ((jid (jabber-xml-get-attribute xml-data 'from))
+ (id (jabber-xml-get-attribute xml-data 'id))
+ (query (jabber-iq-query xml-data))
+ (sid (jabber-xml-get-attribute query 'sid))
+ (session (dolist (pending-session jabber-socks5-pending-sessions)
+ (when (and (equal sid (nth 0 pending-session))
+ (equal jid (nth 1 pending-session)))
+ (return pending-session)))))
+ ;; check that we really are expecting this session
+ (unless session
+ (jabber-signal-error "auth" 'not-acceptable))
+
+ (setq jabber-socks5-pending-sessions (delq session jabber-socks5-pending-sessions))
+ (fsm-send-sync (nth 2 session) (list :iq xml-data))
+
+ ;; find streamhost to connect to
+;; (let* ((streamhosts (jabber-xml-get-children query 'streamhost))
+;; (streamhost (dolist (streamhost streamhosts)
+;; (let ((connection (jabber-socks5-connect streamhost sid jid (concat jabber-username "@" jabber-server "/" jabber-resource))))
+;; (when connection
+;; ;; We select the first streamhost that we are able to connect to.
+;; (push (list connection sid jid profile-data-function)
+;; jabber-socks5-active-sessions)
+;; ;; Now set the filter, for the rest of the output
+;; (set-process-filter connection #'jabber-socks5-filter)
+;; (set-process-sentinel connection #'jabber-socks5-sentinel)
+;; (return streamhost))))))
+;; (unless streamhost
+;; (jabber-signal-error "cancel" 'item-not-found))
+
+;; ;; tell initiator which streamhost we use
+;; (jabber-send-iq jid "result"
+;; `(query ((xmlns . "http://jabber.org/protocol/bytestreams"))
+;; (streamhost-used ((jid . ,(jabber-xml-get-attribute streamhost 'jid)))))
+;; nil nil nil nil id)
+;; ;; now, as data is sent, it will be passed to the profile.
+;; )
+ ))
+
+(define-state jabber-socks5 initiate (fsm state-data event callback)
+ (let* ((jc (plist-get state-data :jc))
+ (jc-data (fsm-get-state-data jc))
+ (our-jid (concat (plist-get jc-data :username) "@"
+ (plist-get jc-data :server) "/"
+ (plist-get jc-data :resource)))
+ (their-jid (plist-get state-data :jid))
+ (initiator-jid (if (eq (plist-get state-data :role) :initiator) our-jid their-jid))
+ (target-jid (if (eq (plist-get state-data :role) :initiator) their-jid our-jid)))
+ (cond
+ ;; Stray event...
+ ((memq (car-safe event) '(:proxy :info))
+ (list 'initiate state-data :keep))
+
+ ;; Incoming IQ
+ ((eq (car-safe event) :iq)
+ (let ((xml-data (second event)))
+ ;; This is either type "set" (with a list of streamhosts to
+ ;; use), or a "result" (indicating the streamhost finally used
+ ;; by the other party).
+ (cond
+ ((string= (jabber-xml-get-attribute xml-data 'type) "set")
+ ;; A "set" makes sense if we're the initiator and offered
+ ;; Psi's "fast mode". We don't yet, though, so this is only
+ ;; for target.
+ (dolist (streamhost (jabber-xml-get-children (jabber-iq-query xml-data) 'streamhost))
+ (jabber-xml-let-attributes
+ (jid host port) streamhost
+ ;; This is where we would attempt to support zeroconf
+ (when (and jid host port)
+ (start-jabber-socks5-connection
+ jc initiator-jid target-jid jid
+ (plist-get state-data :sid) host port fsm))))
+
+ (list 'wait-for-connection (plist-put state-data :iq-id (jabber-xml-get-attribute xml-data 'id)) 30))
+
+ ((string= (jabber-xml-get-attribute xml-data 'type) "result")
+ ;; The other party has decided what streamhost to use.
+ (let* ((proxy-used (jabber-xml-get-attribute (jabber-xml-path xml-data '(query streamhost-used)) 'jid))
+ ;; If JID is our own JID, we have probably already detected
+ ;; what connection to use. But that is a later problem...
+ (streamhosts (cdr (assoc proxy-used jabber-socks5-proxies-data))))
+ ;; Try to connect to all addresses of this proxy...
+ (dolist (streamhost streamhosts)
+ (jabber-xml-let-attributes
+ (jid host port) streamhost
+ (when (and jid host port)
+ (start-jabber-socks5-connection
+ jc initiator-jid target-jid jid
+ (plist-get state-data :sid) host port fsm)))))
+
+ (list 'wait-for-connection state-data 30))))))))
+
+(define-state-machine jabber-socks5-connection
+ :start
+ ((jc initiator-jid target-jid streamhost-jid sid host port socks5-fsm)
+ "Connect to a single JEP-0065 streamhost."
+ (let ((coding-system-for-read 'binary)
+ (coding-system-for-write 'binary))
+ ;; make-network-process, which we really want, for asynchronous
+ ;; connection and such, was introduced in Emacs 22.
+ (if (fboundp 'make-network-process)
+ (let ((connection
+ (make-network-process
+ :name "socks5"
+ :buffer nil
+ :host host
+ :service (string-to-number port)
+ :nowait t
+ :filter (fsm-make-filter fsm)
+ :sentinel (fsm-make-sentinel fsm))))
+ (list 'wait-for-connection
+ (list :jc jc
+ :connection connection
+ :initiator-jid initiator-jid
+ :target-jid target-jid
+ :streamhost-jid streamhost-jid
+ :sid sid
+ :socks5-fsm socks5-fsm)
+ 30))
+ ;; So we open a stream, and wait for the connection to succeed.
+ (condition-case nil
+ (let ((connection
+ (open-network-stream "socks5" nil
+ host (string-to-number port))))
+ (set-process-filter connection (fsm-make-filter fsm))
+ (set-process-sentinel connection (fsm-make-sentinel fsm))
+ (list 'authenticate
+ (list :jc jc
+ :connection connection
+ :initiator-jid initiator-jid
+ :target-jid target-jid
+ :streamhost-jid streamhost-jid
+ :sid sid
+ :socks5-fsm socks5-fsm)
+ nil))
+ (error (list 'fail '() nil)))))))
+
+(define-state jabber-socks5-connection wait-for-connection
+ (fsm state-data event callback)
+ (cond
+ ((eq (car-safe event) :sentinel)
+ (let ((string (third event)))
+ (cond
+ ;; Connection succeeded
+ ((string= (substring string 0 4) "open")
+ (list 'authenticate state-data nil))
+ ;; Connection failed
+ (t
+ (list 'fail state-data nil)))))))
+
+(define-enter-state jabber-socks5-connection authenticate
+ (fsm state-data)
+ "Send authenticate command."
+ ;; version: 5. number of auth methods supported: 1.
+ ;; which one: no authentication.
+ (process-send-string (plist-get state-data :connection) (string 5 1 0))
+ (list state-data 30))
+
+(define-state jabber-socks5-connection authenticate
+ (fsm state-data event callback)
+ "Receive response to authenticate command."
+ (cond
+ ((eq (car-safe event) :filter)
+ (let ((string (third event)))
+ ;; should return:
+ ;; version: 5. auth method to use: none
+ (if (string= string (string 5 0))
+ ;; Authenticated. Send connect command.
+ (list 'connect state-data nil)
+ ;; Authentication failed...
+ (delete-process (second event))
+ (list 'fail state-data nil))))
+
+ ((eq (car-safe event) :sentinel)
+ (list 'fail state-data nil))))
+
+(define-enter-state jabber-socks5-connection connect (fsm state-data)
+ "Send connect command."
+ (let* ((sid (plist-get state-data :sid))
+ (initiator (plist-get state-data :initiator-jid))
+ (target (plist-get state-data :target-jid))
+ (hash (sha1-string (concat sid initiator target))))
+ (process-send-string
+ (plist-get state-data :connection)
+ (concat (string 5 1 0 3 (length hash))
+ hash
+ (string 0 0)))
+ (list state-data 30)))
+
+(define-state jabber-socks5-connection connect
+ (fsm state-data event callback)
+ "Receive response to connect command."
+ (cond
+ ((eq (car-safe event) :filter)
+ (let ((string (third event)))
+ (if (string= (substring string 0 2) (string 5 0))
+ ;; connection established
+ (progn
+ (fsm-send (plist-get state-data :socks5-fsm)
+ (list :connected
+ (plist-get state-data :connection)
+ (plist-get state-data :streamhost-jid)))
+ ;; Our work is done
+ (list 'done nil))
+ (list 'fail state-data nil))))
+ ((eq (car-safe event) :sentinel)
+ (list 'fail state-data nil))))
+
+(define-state jabber-socks5-connection done
+ (fsm state-data event callback)
+ ;; ignore all events
+ (list 'done nil nil))
+
+(define-enter-state jabber-socks5-connection fail (fsm state-data)
+ ;; Notify parent fsm about failure
+ (fsm-send (plist-get state-data :socks5-fsm)
+ :not-connected)
+ (list nil nil))
+
+(define-state jabber-socks5-connection fail
+ (fsm state-data event callback)
+ ;; ignore all events
+ (list 'fail nil nil))
+
+(define-state jabber-socks5 wait-for-connection
+ (fsm state-data event callback)
+ (cond
+ ((eq (car-safe event) :connected)
+ (destructuring-bind (ignored connection streamhost-jid) event
+ (setq state-data (plist-put state-data :connection connection))
+ ;; If we are expected to tell which streamhost we chose, do so.
+ (let ((iq-id (plist-get state-data :iq-id)))
+ (when iq-id
+ (jabber-send-iq
+ (plist-get state-data :jc)
+ (plist-get state-data :jid) "result"
+ `(query ((xmlns . "http://jabber.org/protocol/bytestreams"))
+ (streamhost-used ((jid . ,streamhost-jid))))
+ nil nil nil nil
+ iq-id)))
+
+ ;; If we are the initiator, we should activate the bytestream.
+ (if (eq (plist-get state-data :role) :initiator)
+ (progn
+ (jabber-send-iq
+ (plist-get state-data :jc)
+ streamhost-jid "set"
+ `(query ((xmlns . "http://jabber.org/protocol/bytestreams")
+ (sid . ,(plist-get state-data :sid)))
+ (activate nil ,(plist-get state-data :jid)))
+ (lambda (jc xml-data fsm) (fsm-send-sync fsm :activated)) fsm
+ (lambda (jc xml-data fsm) (fsm-send-sync fsm :activation-failed)) fsm)
+ (list 'wait-for-activation state-data 10))
+ ;; Otherwise, we just let the data flow.
+ (list 'stream-activated state-data nil))))
+
+ ((eq event :not-connected)
+ ;; If we were counting the streamhosts, we would know when there
+ ;; are no more chances left.
+ (list 'wait-for-connection state-data :keep))
+
+ ((eq event :timeout)
+ (list 'fail (plist-put state-data :error "Timeout when connecting to streamhosts") nil))))
+
+(define-state jabber-socks5 wait-for-activation
+ (fsm state-data event callback)
+ (cond
+ ((eq event :activated)
+ (list 'stream-activated state-data nil))
+ ((eq event :activation-failed)
+ (list 'fail (plist-put state-data :error "Proxy activation failed") nil))
+
+ ;; Stray events from earlier state
+ ((eq (car-safe event) :connected)
+ ;; We just close the connection
+ (delete-process (second event))
+ (list 'wait-for-activation state-data :keep))
+ ((eq event :not-connected)
+ (list 'wait-for-activation state-data :keep))))
+
+(define-enter-state jabber-socks5 stream-activated
+ (fsm state-data)
+ (let ((connection (plist-get state-data :connection))
+ (jc (plist-get state-data :jc))
+ (jid (plist-get state-data :jid))
+ (sid (plist-get state-data :sid))
+ (profile-function (plist-get state-data :profile-function)))
+ (set-process-filter connection (fsm-make-filter fsm))
+ (set-process-sentinel connection (fsm-make-sentinel fsm))
+ ;; Call the profile function, passing the data send function, and
+ ;; receiving the data receiving function. Put the data receiving
+ ;; function in the plist.
+ (list (plist-put state-data
+ :profile-data-function
+ (funcall profile-function
+ jc jid sid
+ (lexical-let ((fsm fsm))
+ (lambda (data)
+ (fsm-send fsm (list :send data))))))
+ nil)))
+
+
+(define-state jabber-socks5 stream-activated
+ (fsm state-data event callback)
+ (let ((jc (plist-get state-data :jc))
+ (connection (plist-get state-data :connection))
+ (profile-data-function (plist-get state-data :profile-data-function))
+ (sid (plist-get state-data :sid))
+ (jid (plist-get state-data :jid)))
+ (cond
+ ((eq (car-safe event) :send)
+ (process-send-string connection (second event))
+ (list 'stream-activated state-data nil))
+
+ ((eq (car-safe event) :filter)
+ ;; Pass data from connection to profile data function
+ ;; If the data function requests it, tear down the connection.
+ (unless (funcall profile-data-function jc jid sid (third event))
+ (fsm-send fsm (list :sentinel (second event) "shutdown")))
+
+ (list 'stream-activated state-data nil))
+
+ ((eq (car-safe event) :sentinel)
+ ;; Connection terminated. Shuffle together the remaining data,
+ ;; and kill the buffer.
+ (delete-process (second event))
+ (funcall profile-data-function jc jid sid nil)
+ (list 'closed nil nil))
+
+ ;; Stray events from earlier state
+ ((eq (car-safe event) :connected)
+ ;; We just close the connection
+ (delete-process (second event))
+ (list 'stream-activated state-data nil))
+ ((eq event :not-connected)
+ (list 'stream-activated state-data nil)))))
+
+(define-enter-state jabber-socks5 fail (fsm state-data)
+ "Tell our caller that we failed."
+ (let ((jc (plist-get state-data :jc))
+ (jid (plist-get state-data :jid))
+ (sid (plist-get state-data :sid))
+ (profile-function (plist-get state-data :profile-function))
+ (iq-id (plist-get state-data :iq-id)))
+ (funcall profile-function jc jid sid (plist-get state-data :error))
+
+ (when iq-id
+ (jabber-send-iq-error jc jid iq-id nil "cancel"
+ 'remote-server-not-found)))
+ (list nil nil))
+
+(defun jabber-socks5-client-1 (jc jid sid profile-function)
+ "Negotiate a SOCKS5 connection with JID.
+This function simply starts a state machine."
+ (add-to-list 'jabber-socks5-pending-sessions
+ (list sid jid (start-jabber-socks5 jc jid sid profile-function :initiator))))
+
+;; (defun jabber-socks5-client-2 (xml-data jid sid profile-function)
+;; "Contact has selected a streamhost to use. Connect to the proxy."
+;; (let* ((query (jabber-iq-query xml-data))
+;; (streamhost-used (car (jabber-xml-get-children query 'streamhost-used)))
+;; (proxy-used (jabber-xml-get-attribute streamhost-used 'jid))
+;; connection)
+;; (let ((streamhosts-left (cdr (assoc proxy-used jabber-socks5-proxies-data))))
+;; (while (and streamhosts-left (not connection))
+;; (setq connection
+;; (jabber-socks5-connect (car streamhosts-left)
+;; sid
+;; (concat jabber-username "@" jabber-server "/" jabber-resource)
+;; jid))
+;; (setq streamhosts-left (cdr streamhosts-left))))
+;; (unless connection
+;; (error "Couldn't connect to proxy %s" proxy-used))
+
+;; ;; Activation is only needed for proxies.
+;; (jabber-send-iq proxy-used "set"
+;; `(query ((xmlns . "http://jabber.org/protocol/bytestreams")
+;; (sid . ,sid))
+;; (activate () ,jid))
+;; (lexical-let ((jid jid) (sid sid) (profile-function profile-function)
+;; (connection connection))
+;; (lambda (xml-data closure-data)
+;; (jabber-socks5-client-3 xml-data jid sid profile-function connection))) nil
+;; ;; TODO: report error to contact?
+;; #'jabber-report-success "Proxy activation")))
+
+;; (defun jabber-socks5-client-3 (xml-data jid sid profile-function proxy-connection)
+;; "Proxy is activated. Start the transfer."
+;; ;; The response from the proxy does not contain any interesting
+;; ;; information, beyond success confirmation.
+
+;; (funcall profile-function jid sid
+;; (lexical-let ((proxy-connection proxy-connection))
+;; (lambda (data)
+;; (process-send-string proxy-connection data)))))
+
+(provide 'jabber-socks5)
+
+;;; arch-tag: 9e70dfea-2522-40c6-a79f-302c8fb82ac5