xmpp_gateway 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+