xmpp_gateway 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2010 by Theo Cushion
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.mdown ADDED
@@ -0,0 +1,67 @@
1
+ Description
2
+ ===========
3
+
4
+ XMPP Gateway provides a HTTP interface to talking to an XMPP server. It is designed to speed up and simplify communications between a web app and a generic XMPP server. It achieves this by taking care of the XMPP connection management, and allowing the sending and receiving of replies via a very simple synchronous HTTP interface.
5
+
6
+ HTTP Basic Authentication is used to provide the credentials for authenticating an XMPP session. An XMPP stanza can then be sent by making a HTTP Post (see 'Operation' below).
7
+
8
+ There are 3 stanza types in XMPP: IQ, message and presence. IQ always expects a reply, and this will be returned in the body of the HTTP response. Message and presence are purely fire and forget.
9
+
10
+ Currently it is not production ready and merely a proof of concept.
11
+
12
+ Requirements
13
+ ============
14
+
15
+ - ruby 1.9.2
16
+
17
+ Operation
18
+ =========
19
+
20
+ To install the gems
21
+
22
+ $ gem install xmpp_gateway
23
+
24
+ To start the server
25
+
26
+ $ xmpp_gateway --debug
27
+
28
+ Server should now be available on [http://127.0.0.1:8000](http://127.0.0.1:8000)
29
+
30
+ To send a message:
31
+
32
+ $ curl --user YOURNAME@jabber.org:PASSWORD --data "stanza=<message to='theozaurus@jabber.org'><body>Howdy</body></message>" http://127.0.0.1:8000
33
+
34
+ To send an IQ:
35
+
36
+ $ curl --user YOURNAME@jabber.org:PASSWORD -d "stanza=<iq to='jabber.org'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>" http://127.0.0.1:8000
37
+ <iq type="result" id="blather0007" from="jabber.org" to="theozaurus@jabber.org/457fd1b2e49ebb39">
38
+ <query xmlns="http://jabber.org/protocol/disco#info">
39
+ <identity category="server" type="im" name="Isode M-Link 14.6a6"/>
40
+ <identity category="pubsub" type="pep"/>
41
+ <feature var="http://jabber.org/protocol/disco#info"/>
42
+ <feature var="http://jabber.org/protocol/disco#items"/>
43
+ <feature var="urn:xmpp:ping"/>
44
+ <feature var="vcard-temp"/>
45
+ <feature var="http://jabber.org/protocol/commands"/>
46
+ <feature var="http://jabber.org/protocol/compress"/>
47
+ <feature var="jabber:iq:auth"/>
48
+ <feature var="jabber:iq:private"/>
49
+ <feature var="jabber:iq:version"/>
50
+ <feature var="http://jabber.org/protocol/pubsub#access-presence"/>
51
+ <feature var="http://jabber.org/protocol/pubsub#auto-create"/>
52
+ <feature var="http://jabber.org/protocol/pubsub#auto-subscribe"/>
53
+ <feature var="http://jabber.org/protocol/pubsub#create-nodes"/>
54
+ <feature var="http://jabber.org/protocol/pubsub#filtered-notifications"/>
55
+ <feature var="http://jabber.org/protocol/pubsub#publish"/>
56
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-items"/>
57
+ <feature var="http://jabber.org/protocol/pubsub#subscribe"/>
58
+ </query>
59
+ </iq>
60
+
61
+
62
+ TODO
63
+ ====
64
+
65
+ - Test suite
66
+ - Add HTTP callbacks so that asynchronous events can be fed back to the HTTP server
67
+ - HTTPS support
data/bin/xmpp_gateway ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require "xmpp_gateway"
5
+ rescue LoadError
6
+ xmpp_gateway_path = File.expand_path('../../lib', __FILE__)
7
+ $:.unshift(xmpp_gateway_path)
8
+ require "xmpp_gateway"
9
+ end
@@ -0,0 +1,93 @@
1
+ require 'optparse'
2
+
3
+ options = {
4
+ bind: '127.0.0.1',
5
+ port: '8000'
6
+ }
7
+
8
+ optparse = OptionParser.new do |opts|
9
+ opts.banner = "Run with #{$0}"
10
+
11
+ opts.on('-b', '--bind', "What address to bind to, by default #{options[:bind]}") do |bind|
12
+ options[:bind] = bind
13
+ end
14
+
15
+ opts.on('-p', '--port', "What port to bind to, by default #{options[:port]}") do |port|
16
+ options[:port] = port
17
+ end
18
+
19
+ opts.on('-D', '--debug', 'Run in debug mode') do
20
+ options[:debug] = true
21
+ end
22
+
23
+ opts.on('-d', '--daemonize', 'Daemonize the process') do |daemonize|
24
+ options[:daemonize] = daemonize
25
+ end
26
+
27
+ opts.on('-p', '--pid', 'Write the PID to this file') do |pid|
28
+ if !File.writable?(File.dirname(pid))
29
+ $stderr.puts "Unable to write log file to #{pid}"
30
+ exit 1
31
+ end
32
+ options[:pid] = pid
33
+ end
34
+
35
+ opts.on('-l', '--log', 'Write the Log to this file instead of stdout/stderr') do |log|
36
+ if !File.writable?(File.dirname(log))
37
+ $stderr.puts "Unable to write log file to #{log}"
38
+ exit 1
39
+ end
40
+ options[:log] = log
41
+ end
42
+
43
+ opts.on_tail('-h', '--help', 'Show this message') do
44
+ puts opts
45
+ exit
46
+ end
47
+
48
+ end
49
+ optparse.parse!
50
+
51
+ at_exit do
52
+
53
+ def run(options)
54
+ require_relative 'xmpp_gateway/http_interface'
55
+ require_relative 'xmpp_gateway/logger'
56
+
57
+ $stdin.reopen "/dev/null"
58
+
59
+ if options[:log]
60
+ log = File.new(options[:log], 'a')
61
+ log.sync = options[:debug]
62
+ $stdout.reopen log
63
+ $stderr.reopen $stdout
64
+ end
65
+
66
+ XmppGateway.logger.level = Logger::DEBUG if options[:debug]
67
+
68
+ trap(:INT) { EventMachine.stop }
69
+ trap(:TERM) { EventMachine.stop }
70
+
71
+ EventMachine.run do
72
+ include XmppGateway
73
+
74
+ HttpInterface.start(options[:bind],options[:port])
75
+ end
76
+ XmppGateway.logger.info("Stopped server")
77
+ end
78
+
79
+ if options[:daemonize]
80
+ pid = fork do
81
+ Process.setsid
82
+ exit if fork
83
+ File.open(options[:pid], 'w') { |f| f << Process.pid } if options[:pid]
84
+ run options
85
+ FileUtils.rm(options[:pid]) if options[:pid]
86
+ end
87
+ ::Process.detach pid
88
+ exit
89
+ else
90
+ run options
91
+ end
92
+
93
+ end
@@ -0,0 +1,148 @@
1
+ require 'evma_httpserver'
2
+ require 'base64'
3
+ require 'cgi'
4
+
5
+ require_relative 'xmpp_pool'
6
+
7
+ module XmppGateway
8
+ class HttpInterface < EM::Connection
9
+
10
+ include EM::HttpServer
11
+
12
+ def self.start(host = '127.0.0.1', port = 8000)
13
+ XmppGateway.logger.info "Starting server at http://#{host}:#{port}"
14
+ EM.start_server(host, port, self)
15
+ end
16
+
17
+ def acceptable_method?
18
+ %w(GET POST).include? @http_request_method
19
+ end
20
+
21
+ def process_http_request
22
+ # the http request details are available via the following instance variables:
23
+ # @http_protocol
24
+ # @http_request_method
25
+ # @http_cookie
26
+ # @http_if_none_match
27
+ # @http_content_type
28
+ # @http_path_info
29
+ # @http_request_uri
30
+ # @http_query_string
31
+ # @http_post_content
32
+ # @http_headers
33
+
34
+ XmppGateway.logger.info "Received request #{@http_request_method} #{post_params.inspect}"
35
+
36
+ Fiber.new{
37
+ if acceptable_method?
38
+ user, password = credentials
39
+ @connection = XmppPool.new( user, password )
40
+ if @connection.connected
41
+ case @http_request_method
42
+ when "GET"
43
+ get.send_response
44
+ when "POST"
45
+ stanza = XmppPool.stanza(post_params['stanza'])
46
+ if stanza
47
+ result = @connection.write stanza
48
+ response(result).send_response
49
+ else
50
+ bad_request.send_response
51
+ end
52
+ else
53
+ # acceptable_method should have stopped us arriving here
54
+ # battle on bravely
55
+ method_not_allowed.send_response
56
+ end
57
+ else
58
+ unauthorized.send_response
59
+ end
60
+ else
61
+ method_not_allowed.send_response
62
+ end
63
+ }.resume
64
+ end
65
+
66
+ def headers
67
+ @headers ||= Hash[@http_headers.split("\000").map{|kv| kv.split(':',2).map{|v| v.strip} }]
68
+ end
69
+
70
+ def basic_auth
71
+ @_basic_auth ||= ( headers['Authorization'] || '' ).match(/\ABasic +(.+)\Z/).to_a[1]
72
+ end
73
+
74
+ def post_params
75
+ @_post_params ||= Hash[@http_post_content.split('&').map{|kv| kv.split('=',2).map{|v| CGI.unescape v } }]
76
+ end
77
+
78
+ def credentials
79
+ if basic_auth
80
+ Base64.decode64(basic_auth).split(':')
81
+ end
82
+ end
83
+
84
+ def response(stanza)
85
+ XmppGateway.logger.debug "Success"
86
+
87
+ response = EM::DelegatedHttpResponse.new(self)
88
+ response.status = 200
89
+ response.content_type 'application/xml'
90
+ response.content = stanza.to_s + "\n"
91
+ return response
92
+ end
93
+
94
+ def unauthorized
95
+ XmppGateway.logger.debug "Unauthorized, username & password wrong"
96
+
97
+ response = EM::DelegatedHttpResponse.new(self)
98
+ response.status = 401
99
+ response.content_type 'text/html'
100
+ response.headers = {"WWW-Authenticate" => 'Basic realm="Secure Area"'}
101
+ response.content = "Unauthorised\n"
102
+ return response
103
+ end
104
+
105
+ def method_not_allowed
106
+ XmppGateway.logger.debug "Method #{@http_request_method} not allowed"
107
+
108
+ response = EM::DelegatedHttpResponse.new(self)
109
+ response.status = 405
110
+ response.content_type 'text/html'
111
+ response.content = "Method not allowed\n"
112
+ return response
113
+ end
114
+
115
+ def bad_request
116
+ XmppGateway.logger.debug "Bad request '#{post_params['stanza']}' is not a valid stanza"
117
+
118
+ response = EM::DelegatedHttpResponse.new(self)
119
+ response.status = 400
120
+ response.content_type 'text/html'
121
+ response.content = "Please send a valid stanza\n"
122
+ return response
123
+ end
124
+
125
+ def get
126
+ XmppGateway.logger.debug "Success, send stanza form"
127
+
128
+ response = EM::DelegatedHttpResponse.new(self)
129
+ response.status = 200
130
+ response.content_type 'text/html'
131
+ response.content =
132
+ "<h1>Enter Stanza</h1>" +
133
+ "<form method='post'>" +
134
+ "<label for='stanza'>Stanza</label>" +
135
+ "<textarea name='stanza' cols=50 rows=20>" +
136
+ "<iq type='get'\n" +
137
+ " to='#{@connection.client.jid.domain}'\n" +
138
+ " id='info1'>\n" +
139
+ " <query xmlns='http://jabber.org/protocol/disco#info'/>\n" +
140
+ "</iq>" +
141
+ "</textarea>" +
142
+ "<input type='submit' value='Submit'/>" +
143
+ "</form>\n"
144
+ return response
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,15 @@
1
+ module XmppGateway
2
+
3
+ def self.logger
4
+ @@logger ||= begin
5
+ l = Logger.new($stdout)
6
+ l.level = Logger::INFO
7
+ l
8
+ end
9
+ end
10
+
11
+ def self.logger=(logger)
12
+ @@logger = logger
13
+ end
14
+
15
+ end
@@ -0,0 +1,93 @@
1
+ require "fiber"
2
+ require "blather/client/client"
3
+
4
+ module XmppGateway
5
+ class XmppInterface
6
+
7
+ attr_reader :client
8
+ attr_reader :connected
9
+
10
+ def self.stanza(stanza)
11
+ noko = Nokogiri::XML::Document.parse( stanza ).root
12
+ return nil unless noko
13
+ blather = Blather::XMPPNode.import( noko )
14
+ return nil if blather.class == Blather::XMPPNode # This means it isn't a valid presence, iq or message stanza
15
+ return blather
16
+ end
17
+
18
+ def initialize(user,password)
19
+ @jid = Blather::JID.new(user)
20
+
21
+ f = Fiber.current
22
+
23
+ @client = Blather::Client.setup @jid, password
24
+ @client.register_handler(:ready) do
25
+ @connected = true
26
+ f.resume( @connected ) if f.alive?
27
+ end
28
+ @client.register_handler(:disconnected) do
29
+ XmppGateway.logger.debug "XMPP connection #{@jid} disconnected"
30
+ @connected = false
31
+ f.resume( @connected ) if f.alive?
32
+ # Prevent EventMachine from stopping by returning true on disconnected
33
+ true
34
+ end
35
+ @client.clear_handlers(:error)
36
+ @client.register_handler(:error){ f.resume( false ) if f.alive? }
37
+
38
+ begin
39
+ @client.run
40
+ rescue
41
+ @connected = false
42
+ end
43
+
44
+ # Waits until it has connected unless an error was thrown
45
+ @connected = Fiber.yield if @connected.nil?
46
+
47
+ schedule_activity_monitor
48
+
49
+ XmppGateway.logger.debug "XMPP connection #{@jid} #{@connected ? '' : 'not '}connected"
50
+ end
51
+
52
+ def write(stanza)
53
+ XmppGateway.logger.debug "Sending stanza #{stanza.to_s}"
54
+
55
+ f = Fiber.current
56
+
57
+ @client.write_with_handler( stanza ){|result|
58
+ f.resume(result) if f.alive?
59
+ }
60
+
61
+ if reply_expected stanza
62
+ # Remove timer while we wait for response
63
+ cancel_timer
64
+ reply = Fiber.yield
65
+ XmppGateway.logger.debug "Received #{reply}"
66
+ # Schedule new timer
67
+ schedule_activity_monitor
68
+ return reply
69
+ end
70
+
71
+ schedule_activity_monitor
72
+ end
73
+
74
+ private
75
+
76
+ def schedule_activity_monitor
77
+ @timer = EventMachine::Timer.new(60) do
78
+ XmppGateway.logger.debug "XMPP connection #{@jid} inactive for over 60 seconds"
79
+ @client.close if @connected
80
+ end if @connected
81
+ end
82
+
83
+ def cancel_timer
84
+ @timer.cancel if @timer
85
+ end
86
+
87
+ def reply_expected(stanza)
88
+ stanza.is_a? Blather::Stanza::Iq
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,63 @@
1
+ require_relative 'xmpp_interface'
2
+
3
+ module XmppGateway
4
+ class XmppPool
5
+
6
+ attr_reader :connected
7
+
8
+ def self.stanza(s)
9
+ XmppInterface.stanza s
10
+ end
11
+
12
+ def self.connections
13
+ @@connections ||= {}
14
+ end
15
+
16
+ def initialize(user, password)
17
+ @jid = Blather::JID.new(user)
18
+
19
+ if stale?
20
+ XmppGateway.logger.debug "Stale XMPP connection #{@jid}"
21
+ connections.delete @jid.to_s
22
+ end
23
+
24
+ if record = connections[@jid.to_s]
25
+ # Connection exists
26
+ XmppGateway.logger.debug "Reusing XMPP connection #{@jid}"
27
+
28
+ @connection = record[:connection]
29
+ @connected = record[:password] == password
30
+ else
31
+ # Create connection
32
+ XmppGateway.logger.debug "Creating XMPP connection #{@jid}"
33
+
34
+ @connection = XmppInterface.new(user, password)
35
+ @connected = @connection.connected
36
+
37
+ if @connected
38
+ # Cache the connection
39
+ connections[@jid.to_s] = {:password => password, :connection => @connection}
40
+ end
41
+ end
42
+ end
43
+
44
+ def client
45
+ @connection.client
46
+ end
47
+
48
+ def write(s)
49
+ @connection.write(s)
50
+ end
51
+
52
+ private
53
+
54
+ def stale?
55
+ connections[@jid.to_s] && !connections[@jid.to_s][:connection].connected
56
+ end
57
+
58
+ def connections
59
+ self.class.connections
60
+ end
61
+
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xmpp_gateway
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 2
9
+ version: 0.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Theo Cushion
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-24 00:00:00 +00:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: blather
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: eventmachine_httpserver
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :runtime
45
+ version_requirements: *id002
46
+ description: XmppGateway is a server that allows XMPP stanzas to be posted via HTTP POST requests, it provides a synchronous API for IQ messages so that a reply is included in the body of the response
47
+ email: theo.c@zepler.net
48
+ executables:
49
+ - xmpp_gateway
50
+ extensions: []
51
+
52
+ extra_rdoc_files: []
53
+
54
+ files:
55
+ - bin/xmpp_gateway
56
+ - lib/xmpp_gateway/http_interface.rb
57
+ - lib/xmpp_gateway/logger.rb
58
+ - lib/xmpp_gateway/xmpp_interface.rb
59
+ - lib/xmpp_gateway/xmpp_pool.rb
60
+ - lib/xmpp_gateway.rb
61
+ - LICENSE
62
+ - README.mdown
63
+ has_rdoc: true
64
+ homepage: http://github.com/theozaurus/xmpp_gateway
65
+ licenses:
66
+ - MIT
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 1
79
+ - 9
80
+ - 2
81
+ version: 1.9.2
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ segments:
88
+ - 1
89
+ - 3
90
+ - 6
91
+ version: 1.3.6
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.3.7
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: A server for sending and receiving XMPP stanzas via a HTTP interface
99
+ test_files: []
100
+