PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/rfc/xmpp.scm

http://github.com/gemmat/Gauche-XMPP
Scheme | 528 lines | 415 code | 53 blank | 60 comment | 0 complexity | 6dde5c029d87e0c3e676308c526d04b0 MD5 | raw file
  1. ;;;
  2. ;;; xmpp.scm - XMPP(RFC3920, RFC3921) client library for the Gauche.
  3. ;;;
  4. ;;; rfc.xmpp
  5. ;;;
  6. ;;; Copyright (c) 2010 Teruaki Gemma(teruakigemma@gmail.com)
  7. ;;;
  8. ;;;
  9. ;;; Permission is hereby granted, free of charge, to any person obtaining
  10. ;;; a copy of this software and associated documentation files (the
  11. ;;; "Software"), to deal in the Software without restriction, including
  12. ;;; without limitation the rights to use, copy, modify, merge, publish,
  13. ;;; distribute, sublicense, and/or sell copies of the Software, and to
  14. ;;; permit persons to whom the Software is furnished to do so, subject to
  15. ;;; the following conditions:
  16. ;;;
  17. ;;; The above copyright notice and this permission notice shall be
  18. ;;; included in all copies or substantial portions of the Software.
  19. ;;;
  20. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  21. ;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  22. ;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  23. ;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  24. ;;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  25. ;;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  26. ;;; WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  27. ;;;
  28. ;;; $Id$
  29. ;;;
  30. (define-module rfc.xmpp
  31. (use srfi-1)
  32. (use srfi-13)
  33. (use srfi-14)
  34. (use util.list)
  35. (use gauche.net)
  36. (use gauche.uvector)
  37. (use sxml.ssax)
  38. (use sxml.sxpath)
  39. (use sxml.tools)
  40. (use sxml.serializer)
  41. (use rfc.md5)
  42. (use rfc.base64)
  43. (use math.mt-random)
  44. (export <xmpp-error>
  45. <xmpp-connection>
  46. xmpp-connect
  47. xmpp-disconnect
  48. call-with-xmpp-connection
  49. xmpp-receive-stanza
  50. xmpp-bind
  51. xmpp-session
  52. xmpp-message
  53. xmpp-presence
  54. xmpp-subscribe
  55. xmpp-subscribed
  56. xmpp-unsubscribe
  57. xmpp-unsubscribed
  58. xmpp-iq
  59. xmpp-get-roster
  60. xmpp-push-roster
  61. xmpp-get-privacy-lists-names
  62. xmpp-get-privacy-lists
  63. xmpp-set-privacy-lists
  64. xmpp-auth
  65. xmpp-auth-select-mechanism
  66. xmpp-sasl-anonymous
  67. xmpp-sasl-plain
  68. xmpp-sasl-digest-md5
  69. )
  70. )
  71. (select-module rfc.xmpp)
  72. (define *default-port* 5222)
  73. (define *mt* (make <mersenne-twister> :seed (sys-time)))
  74. (define-constant *alnum* (list->vector (char-set->list (char-set-union char-set:lower-case char-set:digit))))
  75. (define (random-id)
  76. (let1 len (vector-length *alnum*)
  77. (list->string
  78. (map (lambda (x)
  79. (vector-ref *alnum* (mt-random-integer *mt* len)))
  80. (iota 8)))))
  81. (define-condition-type <xmpp-error> <error> #f)
  82. (define-class <xmpp-connection> ()
  83. ((socket :init-keyword :socket)
  84. (socket-iport :init-keyword :socket-iport)
  85. (socket-oport :init-keyword :socket-oport)
  86. (id)
  87. (from)
  88. (to)
  89. (version)
  90. (xml:lang :init-keyword :xml:lang)
  91. (features)
  92. (stream-default-namespace)
  93. (jid-domain-part :init-keyword :jid-domain-part)
  94. (hostname :init-keyword :hostname)
  95. (port :init-keyword :port)
  96. (channel :init-form (channel-essential))))
  97. (define-method write-object ((conn <xmpp-connection>) out)
  98. (format out "<connection to ~A:~A> ~A"
  99. (ref conn 'hostname)
  100. (ref conn 'port)
  101. (socket-status (ref conn 'socket))))
  102. ;; Channel is a hash-table.
  103. ;; Its key is a predicate function for a stanza and
  104. ;; the value is a handler for the stanza the predicate is true.
  105. (define (channel-essential)
  106. (define (handle-stream conn sxml)
  107. (set! (ref conn 'id)
  108. ((if-car-sxpath '(http://etherx.jabber.org/streams:stream @ id *text*)) sxml))
  109. (set! (ref conn 'from)
  110. ((if-car-sxpath '(http://etherx.jabber.org/streams:stream @ from *text*)) sxml))
  111. (set! (ref conn 'to)
  112. ((if-car-sxpath '(http://etherx.jabber.org/streams:stream @ to *text*)) sxml))
  113. (set! (ref conn 'version)
  114. ((if-car-sxpath '(http://etherx.jabber.org/streams:stream @ version *text*)) sxml))
  115. (set! (ref conn 'xml:lang)
  116. ((if-car-sxpath '(http://etherx.jabber.org/streams:stream @ xml:lang *text*)) sxml))
  117. (and-let* ((version (ref conn 'version)))
  118. (when (< (string->number version) 1.0)
  119. (error <xmpp-error> "The server using a too old version of XMPP stream. version:" version))))
  120. (define (handle-features conn sxml)
  121. (set! (ref conn 'features) ((sxpath '(http://etherx.jabber.org/streams:features *)) sxml)))
  122. `((,(if-sxpath '(http://etherx.jabber.org/streams:stream)) . ,handle-stream)
  123. (,(if-sxpath '(http://etherx.jabber.org/streams:features)) . ,handle-features)))
  124. (define (xmpp-connect hostname . args)
  125. (let-keywords args ((port *default-port*)
  126. (jid-domain-part #f)
  127. (xml:lang #f))
  128. (let* ((socket (make-client-socket 'inet hostname port))
  129. (conn (make <xmpp-connection>
  130. :socket socket
  131. :socket-iport (socket-input-port socket :buffering :none)
  132. :socket-oport (socket-output-port socket)
  133. :hostname hostname
  134. :port port
  135. :jid-domain-part jid-domain-part
  136. :xml:lang xml:lang)))
  137. (begin-xml-stream conn)
  138. (xmpp-receive-stanza conn) ; stream
  139. (xmpp-receive-stanza conn) ; features
  140. conn)))
  141. (define-method xmpp-disconnect ((conn <xmpp-connection>))
  142. (end-xml-stream conn)
  143. (let1 s (ref conn 'socket)
  144. (socket-close s)
  145. (socket-shutdown s SHUT_RDWR)))
  146. (define (call-with-xmpp-connection hostname proc . args)
  147. (let1 conn (apply xmpp-connect (cons hostname args))
  148. (unwind-protect (proc conn)
  149. (xmpp-disconnect conn))))
  150. (define-method xmpp-receive-stanza ((conn <xmpp-connection>))
  151. (define (FINISH-ELEMENT elem-gi attributes namespaces parent-seed seed)
  152. (define (RES-NAME->SXML res-name)
  153. (string->symbol (string-append (symbol->string (car res-name)) ":" (symbol->string (cdr res-name)))))
  154. (let ((seed (ssax:reverse-collect-str-drop-ws seed))
  155. (attrs (attlist-fold (lambda (attr accum)
  156. (cons (list (if (symbol? (car attr))
  157. (car attr)
  158. (RES-NAME->SXML (car attr)))
  159. (cdr attr))
  160. accum))
  161. '()
  162. attributes)))
  163. (cons (cons (if (symbol? elem-gi)
  164. elem-gi
  165. (RES-NAME->SXML elem-gi))
  166. (if (null? attrs)
  167. seed
  168. (cons (cons '@ attrs) seed)))
  169. parent-seed)))
  170. (define elem-parser (ssax:make-elem-parser (lambda (elem-gi attributes namespaces expected-content seed)
  171. '())
  172. FINISH-ELEMENT
  173. (lambda (string1 string2 seed)
  174. (if (string=? "" string2)
  175. (cons string1 seed)
  176. (cons* string2 string1 seed)))
  177. ()))
  178. (define (read-stanza inp)
  179. (call/cc
  180. (lambda (return)
  181. (while #t
  182. (when (char-ready? inp)
  183. (receive (_ token) (ssax:read-char-data inp #t (lambda _ #t) #t)
  184. (cond
  185. ((eof-object? token)
  186. (error <xmpp-error> "The connection closed by peer."))
  187. ((equal? token '(START . (stream . stream)))
  188. ;; <stream:stream> XMPP stream start.
  189. (receive
  190. (elem-gi attributes namespaces elem-content-model)
  191. (ssax:complete-start-tag '(stream . stream) inp #f '() '())
  192. (set! (ref conn 'stream-default-namespace) namespaces)
  193. (return (cons '*TOP* (FINISH-ELEMENT elem-gi attributes namespaces '() '())))))
  194. ((equal? token '(END . (stream . stream)))
  195. ;;</stream:stream> XMPP stream end.
  196. (return '(end-tag-of-stream)))
  197. ((eq? 'PI (xml-token-kind token))
  198. ;; if you need to process the xml PI, try a following line.
  199. ;;`(*PI* ,(xml-token-head token) ,(ssax:read-pi-body-as-string inp)))
  200. (ssax:skip-pi inp))
  201. ((eq? 'START (xml-token-kind token))
  202. (return (cons '*TOP*
  203. (elem-parser (xml-token-head token) inp #f '() (ref conn 'stream-default-namespace) #f '()))))
  204. (else
  205. ;;something wrong
  206. (error <xmpp-error> "Oops. Something wrong at XMPP parsing:" (read-char inp))))))))))
  207. (flush (ref conn 'socket-oport))
  208. (let1 stanza (read-stanza (ref conn 'socket-iport))
  209. (for-each (lambda (x)
  210. (receive (pred handler) (car+cdr x)
  211. (when (pred stanza)
  212. (handler conn stanza))))
  213. (ref conn 'channel))
  214. stanza))
  215. (define-syntax with-output-to-connection
  216. (syntax-rules ()
  217. ((_ conn body ...)
  218. (with-output-to-port (ref conn 'socket-oport)
  219. (lambda ()
  220. body
  221. ...
  222. (flush))))))
  223. (define (print-sxml sxml)
  224. (srl:sxml->xml sxml (current-output-port)))
  225. (define (filter-map-extra . args)
  226. (filter-map (lambda (x)
  227. (and (not (null? x)) x))
  228. args))
  229. ;;
  230. ;; Operators for communicating over the XML stream
  231. ;;
  232. ;;"Begin XML stream. This should be the first thing to happen on a
  233. ;; newly connected connection."
  234. (define-method begin-xml-stream ((conn <xmpp-connection>) . args)
  235. (let-keywords args ((xml-identifier #t))
  236. (with-output-to-connection conn
  237. (when xml-identifier
  238. (print "<?xml version='1.0' ?>"))
  239. (format #t "<stream:stream to='~a' ~a xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>"
  240. (or (ref conn 'jid-domain-part) (ref conn 'hostname))
  241. (if (ref conn 'xml:lang)
  242. (format #f "xml:lang='~a'" (ref conn 'xml:lang))
  243. "")))))
  244. ;;"Closes the XML stream. At this point you'd have to
  245. ;; call BEGIN-XML-STREAM if you wished to communicate with
  246. ;; the server again."
  247. (define-method end-xml-stream ((conn <xmpp-connection>))
  248. (with-output-to-connection conn
  249. (print "</stream:stream>")))
  250. (define-syntax xmpp-message
  251. (syntax-rules ()
  252. ((_ (conn args ...))
  253. (xmpp-message (conn args ...) #f))
  254. ((_ (conn args ...) extra ...)
  255. (let-keywords (list args ...) ((from #f)
  256. (id #f)
  257. (to #f)
  258. (type "normal")
  259. (xml:lang #f))
  260. (with-output-to-connection conn
  261. (print-sxml `(message (|@| ,@(cond-list
  262. (from `(from ,from))
  263. (id `(id ,id))
  264. (to `(to ,to))
  265. (type `(type ,type))
  266. (xml:lang `(xml:lang ,xml:lang))))
  267. ,@(filter-map-extra extra ...))))))))
  268. (define-syntax xmpp-presence
  269. (syntax-rules ()
  270. ((_ (conn args ...))
  271. (xmpp-presence (conn args ...) #f))
  272. ((_ (conn args ...) extra ...)
  273. (let-keywords (list args ...) ((from #f)
  274. (id #f)
  275. (to #f)
  276. (type #f)
  277. (xml:lang #f))
  278. (with-output-to-connection conn
  279. (print-sxml `(presence (|@| ,@(cond-list
  280. (from `(from ,from))
  281. (id `(id ,id))
  282. (to `(to ,to))
  283. (type `(type ,type))
  284. (xml:lang `(xml:lang ,xml:lang))))
  285. ,@(filter-map-extra extra ...))))))))
  286. ;;
  287. ;; Managing Subscriptions
  288. ;;
  289. (define-method xmpp-subscribe ((conn <xmpp-connection>) to)
  290. (xmpp-presence (conn :type "subscribe" :to to)))
  291. (define-method xmpp-subscribed ((conn <xmpp-connection>) to)
  292. (xmpp-presence (conn :type "subscribed" :to to)))
  293. (define-method xmpp-unsubscribe ((conn <xmpp-connection>) to)
  294. (xmpp-presence (conn :type "unsubscribe" :to to)))
  295. (define-method xmpp-unsubscribed ((conn <xmpp-connection>) to)
  296. (xmpp-presence (conn :type "unsubscribed" :to to)))
  297. (define-syntax xmpp-iq
  298. (syntax-rules ()
  299. ((_ (conn args ...))
  300. (xmpp-iq (conn args ...) #f))
  301. ((_ (conn args ...) extra ...)
  302. (let-keywords (list args ...) ((from #f)
  303. (id (random-id))
  304. (to #f)
  305. (type "get")
  306. (xml:lang #f))
  307. (with-output-to-connection conn
  308. (print-sxml `(iq (|@| ,@(cond-list
  309. (from `(from ,from))
  310. (id `(id ,id))
  311. (to `(to ,to))
  312. (#t `(type ,type))
  313. (xml:lang `(xml:lang ,xml:lang))))
  314. ,@(filter-map-extra extra ...))))))))
  315. ;;
  316. ;; Basic operations
  317. ;;
  318. (define-method xmpp-bind ((conn <xmpp-connection>) resource)
  319. (xmpp-iq (conn :type "set")
  320. `(bind (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-bind"))
  321. (resource ,resource))))
  322. (define-method xmpp-session ((conn <xmpp-connection>))
  323. (xmpp-iq (conn :type "set")
  324. `(session (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-session")))))
  325. ;;
  326. ;; Roster Management
  327. ;;
  328. (define-method xmpp-get-roster ((conn <xmpp-connection>))
  329. (xmpp-iq (conn :type "get")
  330. '(query (|@| (xmlns "jabber:iq:roster")))))
  331. (define-method xmpp-push-roster ((conn <xmpp-connection>) items)
  332. (xmpp-iq (conn :type "set")
  333. `(query (|@| (xmlns "jabber:iq:roster"))
  334. ,@items)))
  335. ;;
  336. ;; Blocking Communication
  337. ;;
  338. (define-method xmpp-get-privacy-lists-names ((conn <xmpp-connection>))
  339. (xmpp-iq (conn :type "get")
  340. `(query (|@| (xmlns "jabber:iq:privacy")))))
  341. (define-method xmpp-get-privacy-lists ((conn <xmpp-connection>) names)
  342. (xmpp-iq (conn :type "get")
  343. `(query (|@| (xmlns "jabber:iq:privacy"))
  344. ,@(map (lambda (name)
  345. `(list (|@| (name ,name))))
  346. names))))
  347. (define-method xmpp-set-privacy-lists ((conn <xmpp-connection>) body)
  348. (xmpp-iq (conn :type "set")
  349. `(query (|@| (xmlns "jabber:iq:privacy"))
  350. ,@body)))
  351. ;; --- SASL Authentication---
  352. (define-method xmpp-auth ((conn <xmpp-connection>) username password)
  353. (define mechanism-has-digest-md5?
  354. (if-car-sxpath '(// (urn:ietf:params:xml:ns:xmpp-sasl:mechanism ((equal? "DIGEST-MD5"))))))
  355. (define mechanism-has-plain?
  356. (if-car-sxpath '(// (urn:ietf:params:xml:ns:xmpp-sasl:mechanism ((equal? "PLAIN"))))))
  357. (define mechanism-has-anonymous?
  358. (if-car-sxpath '(// (urn:ietf:params:xml:ns:xmpp-sasl:mechanism ((equal? "ANONYMOUS"))))))
  359. (let1 features (ref conn 'features)
  360. (cond ((mechanism-has-digest-md5? features)
  361. (xmpp-sasl-digest-md5 conn username password))
  362. ((mechanism-has-plain? features)
  363. (xmpp-sasl-plain conn username password))
  364. ((mechanism-has-anonymous? features)
  365. (xmpp-sasl-anonymous conn)))))
  366. (define-method xmpp-auth-select-mechanism ((conn <xmpp-connection>) mechanism)
  367. (with-output-to-connection conn
  368. (print-sxml `(auth (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl")
  369. (mechanism ,mechanism))))))
  370. (define (if-successful-restart-stream conn reply)
  371. (if (eq? (caadr reply) 'urn:ietf:params:xml:ns:xmpp-sasl:success)
  372. (begin
  373. (begin-xml-stream conn :xml-identifier #f)
  374. (xmpp-receive-stanza conn) ; stream
  375. (xmpp-receive-stanza conn) ; features
  376. :success)
  377. :failure))
  378. (define-method xmpp-sasl-anonymous ((conn <xmpp-connection>))
  379. (with-output-to-connection conn
  380. (print-sxml `(auth (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl")
  381. (mechanism "ANONYMOUS")))))
  382. (if-successful-restart-stream conn (xmpp-receive-stanza conn)))
  383. (define-method xmpp-sasl-plain ((conn <xmpp-connection>) username password)
  384. (with-output-to-connection conn
  385. (print-sxml `(auth (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl")
  386. (mechanism "PLAIN"))
  387. ,(base64-encode-string (string-join `("" ,username ,password) "\u0000")))))
  388. (if-successful-restart-stream conn (xmpp-receive-stanza conn)))
  389. (define-method xmpp-sasl-digest-md5 ((conn <xmpp-connection>) username password)
  390. ;; We immediately return when any auth steps have failed.
  391. (define (step1 return)
  392. (let1 initial-challenge (xmpp-receive-stanza conn)
  393. (if (eq? (caadr initial-challenge) 'urn:ietf:params:xml:ns:xmpp-sasl:challenge)
  394. (let1 challenge-string (base64-decode-string (sxml:string-value initial-challenge))
  395. (make-digest-md5-response username password (ref conn 'hostname) challenge-string))
  396. (return initial-challenge))))
  397. (define (step2 return response rspauth-expected)
  398. (let* ((second-challenge (xmpp-receive-stanza conn))
  399. (rspauth (assoc-ref (parse-challenge (base64-decode-string (sxml:string-value second-challenge))) "rspauth")))
  400. (or (and (eq? (caadr second-challenge) 'urn:ietf:params:xml:ns:xmpp-sasl:challenge)
  401. (string=? rspauth-expected rspauth))
  402. (return second-challenge))))
  403. (if-successful-restart-stream
  404. conn
  405. (call/cc
  406. (lambda (return)
  407. (with-output-to-connection conn
  408. (print-sxml `(auth (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl")
  409. (mechanism "DIGEST-MD5")))))
  410. (receive (response rspauth) (step1 return)
  411. (with-output-to-connection conn
  412. (print-sxml `(response (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl"))
  413. ,(base64-encode-string response))))
  414. (step2 return response rspauth))
  415. (with-output-to-connection conn
  416. (print-sxml `(response (|@| (xmlns "urn:ietf:params:xml:ns:xmpp-sasl")))))
  417. (xmpp-receive-stanza conn)))))
  418. (define (parse-challenge str)
  419. (filter-map (lambda (x)
  420. (rxmatch-if (#/^([^=]+)=(.*)/ x)
  421. (#f key value)
  422. ;; strip double-quotes.
  423. (rxmatch-if (#/^\"(.*)\"$/ value)
  424. (#f s)
  425. (cons key s)
  426. (cons key value))
  427. #f))
  428. (string-split str ",")))
  429. (define (make-digest-md5-response username password hostname challenge)
  430. (define (dblq str)
  431. (string-append "\"" str "\""))
  432. (let1 l (parse-challenge challenge)
  433. (let ((nonce (assoc-ref l "nonce"))
  434. (qop (assoc-ref l "qop"))
  435. (charset (assoc-ref l "charset"))
  436. (digest-uri (string-append "xmpp/" hostname))
  437. (cnonce (make-cnonce))
  438. (realm "")
  439. (nc "00000001"))
  440. (let ((rsp (digest-md5 username #f realm password digest-uri nonce cnonce nc qop #t))
  441. (rspauth-expected (digest-md5 username #f realm password digest-uri nonce cnonce nc qop #f)))
  442. (values (string-join (map (cut string-join <> "=")
  443. `(("username" ,(dblq username))
  444. ("realm" ,(dblq realm))
  445. ("nonce" ,(dblq nonce))
  446. ("cnonce" ,(dblq cnonce))
  447. ("digest-uri" ,(dblq digest-uri))
  448. ("response" ,rsp)
  449. ("nc" ,nc)
  450. ("qop" ,qop)
  451. ("charset" ,charset)))
  452. ",")
  453. rspauth-expected)))))
  454. (define (make-cnonce)
  455. (let1 uv (make-u32vector 4)
  456. (mt-random-fill-u32vector! *mt* uv)
  457. (base64-encode-string (u32vector->string uv))))
  458. (define (digest-md5 authc-id authz-id realm password digest-uri nonce cnonce nc qop request)
  459. (and-let* ((X (string-join `(,authc-id ,realm ,password) ":"))
  460. (Y (md5-digest-string X))
  461. (A1 (string-join (if authz-id
  462. `(,Y ,nonce ,cnonce ,authz-id)
  463. `(,Y ,nonce ,cnonce))
  464. ":"))
  465. (A2 (string-join `(,(if request
  466. "AUTHENTICATE"
  467. "")
  468. ,digest-uri)
  469. ":"))
  470. (HA1 (digest-hexify (md5-digest-string A1)))
  471. (HA2 (digest-hexify (md5-digest-string A2)))
  472. (KD (string-join `(,HA1 ,nonce ,nc ,cnonce ,qop ,HA2) ":"))
  473. (Z (digest-hexify (md5-digest-string KD))))
  474. Z))
  475. (provide "rfc/xmpp")