vinesmod 0.4.5
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/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +43 -0
- data/Rakefile +57 -0
- data/bin/vines +93 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3366 -0
- data/conf/config.rb +149 -0
- data/lib/vines.rb +197 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/register.rb +27 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/command/unregister.rb +27 -0
- data/lib/vines/config.rb +213 -0
- data/lib/vines/config/host.rb +119 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +35 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/register.rb +42 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/message.rb +43 -0
- data/lib/vines/stanza/presence.rb +156 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/storage.rb +188 -0
- data/lib/vines/storage/local.rb +165 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +260 -0
- data/lib/vines/store.rb +94 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/sasl.rb +92 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
- data/lib/vines/stream/server/ready.rb +24 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/state.rb +60 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +43 -0
- data/vines.gemspec +36 -0
- data/web/404.html +51 -0
- data/web/apple-touch-icon.png +0 -0
- data/web/chat/coffeescripts/chat.coffee +362 -0
- data/web/chat/coffeescripts/init.coffee +15 -0
- data/web/chat/index.html +16 -0
- data/web/chat/javascripts/app.js +1 -0
- data/web/chat/stylesheets/chat.css +144 -0
- data/web/favicon.png +0 -0
- data/web/lib/coffeescripts/button.coffee +25 -0
- data/web/lib/coffeescripts/contact.coffee +32 -0
- data/web/lib/coffeescripts/filter.coffee +49 -0
- data/web/lib/coffeescripts/layout.coffee +30 -0
- data/web/lib/coffeescripts/login.coffee +68 -0
- data/web/lib/coffeescripts/logout.coffee +5 -0
- data/web/lib/coffeescripts/navbar.coffee +84 -0
- data/web/lib/coffeescripts/notification.coffee +14 -0
- data/web/lib/coffeescripts/router.coffee +40 -0
- data/web/lib/coffeescripts/session.coffee +229 -0
- data/web/lib/coffeescripts/transfer.coffee +106 -0
- data/web/lib/images/dark-gray.png +0 -0
- data/web/lib/images/default-user.png +0 -0
- data/web/lib/images/light-gray.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/images/white.png +0 -0
- data/web/lib/javascripts/base.js +12 -0
- data/web/lib/javascripts/icons.js +110 -0
- data/web/lib/javascripts/jquery.cookie.js +91 -0
- data/web/lib/javascripts/jquery.js +4 -0
- data/web/lib/javascripts/raphael.js +6 -0
- data/web/lib/javascripts/strophe.js +1 -0
- data/web/lib/stylesheets/base.css +385 -0
- data/web/lib/stylesheets/login.css +68 -0
- metadata +423 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Component
|
6
|
+
class Handshake < State
|
7
|
+
def initialize(stream, success=Ready)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
raise StreamErrors::NotAuthorized unless handshake?(node)
|
13
|
+
stream.write('<handshake/>')
|
14
|
+
stream.router << stream
|
15
|
+
advance
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def handshake?(node)
|
21
|
+
node.name == 'handshake' && node.text == stream.secret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Component
|
6
|
+
class Ready < State
|
7
|
+
def node(node)
|
8
|
+
stanza = to_stanza(node)
|
9
|
+
raise StreamErrors::UnsupportedStanzaType unless stanza
|
10
|
+
to, from = stanza.validate_to, stanza.validate_from
|
11
|
+
raise StreamErrors::ImproperAddressing unless to && from
|
12
|
+
raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain
|
13
|
+
stream.user = User.new(jid: from)
|
14
|
+
if stanza.local? || stanza.to_pubsub_domain?
|
15
|
+
stanza.process
|
16
|
+
else
|
17
|
+
stanza.route
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Component
|
6
|
+
class Start < State
|
7
|
+
def initialize(stream, success=Handshake)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
raise StreamErrors::NotAuthorized unless stream?(node)
|
13
|
+
stream.start(node)
|
14
|
+
advance
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http < Client
|
6
|
+
attr_accessor :session
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
super
|
10
|
+
@session = Http::Session.new(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Override +Stream#create_parser+ to provide an HTTP parser rather than
|
14
|
+
# a Nokogiri XML parser.
|
15
|
+
def create_parser
|
16
|
+
@parser = ::Http::Parser.new.tap do |p|
|
17
|
+
body = ''
|
18
|
+
p.on_body = proc {|data| body << data }
|
19
|
+
p.on_message_complete = proc {
|
20
|
+
process_request(Request.new(self, @parser, body))
|
21
|
+
body = ''
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# If the session ID is valid, switch this stream's session to the new
|
27
|
+
# ID and return true. Some clients, like Google Chrome, reuse one stream
|
28
|
+
# for multiple sessions.
|
29
|
+
def valid_session?(sid)
|
30
|
+
if session = Sessions[sid]
|
31
|
+
@session = session
|
32
|
+
end
|
33
|
+
!!session
|
34
|
+
end
|
35
|
+
|
36
|
+
%w[max_stanza_size max_resources_per_account bind root].each do |name|
|
37
|
+
define_method name do |*args|
|
38
|
+
config[:http].send(name, *args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_request(request)
|
43
|
+
if request.path == self.bind && request.options?
|
44
|
+
request.reply_to_options
|
45
|
+
elsif request.path == self.bind
|
46
|
+
body = Nokogiri::XML(request.body).root
|
47
|
+
if session = Sessions[body['sid']]
|
48
|
+
@session = session
|
49
|
+
else
|
50
|
+
@session = Http::Session.new(self)
|
51
|
+
end
|
52
|
+
@session.request(request)
|
53
|
+
@nodes.push(body)
|
54
|
+
else
|
55
|
+
request.reply_with_file(self.root)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Alias the Stream#write method before overriding it so we can call
|
60
|
+
# it later from a Session instance.
|
61
|
+
alias :stream_write :write
|
62
|
+
|
63
|
+
# Override Stream#write to queue stanzas rather than immediately writing
|
64
|
+
# to the stream. Stanza responses must be paired with a queued request.
|
65
|
+
def write(data)
|
66
|
+
@session.write(data)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return an array of Node objects inside the body element.
|
70
|
+
# TODO This parses the XML again just to strip namespaces. Figure out
|
71
|
+
# Nokogiri namespace handling instead.
|
72
|
+
def parse_body(body)
|
73
|
+
body.namespace = nil
|
74
|
+
body.elements.map do |node|
|
75
|
+
Nokogiri::XML(node.to_s.sub(' xmlns="jabber:client"', '')).root
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def start(node)
|
80
|
+
domain, type, hold, wait, rid = %w[to content hold wait rid].map {|a| (node[a] || '').strip }
|
81
|
+
version = node.attribute_with_ns('version', NAMESPACES[:bosh]).value rescue nil
|
82
|
+
|
83
|
+
@session.inactivity = 20
|
84
|
+
@session.domain = domain
|
85
|
+
@session.content_type = type unless type.empty?
|
86
|
+
@session.hold = hold.to_i unless hold.empty?
|
87
|
+
@session.wait = wait.to_i unless wait.empty?
|
88
|
+
|
89
|
+
raise StreamErrors::UndefinedCondition.new('rid required') if rid.empty?
|
90
|
+
raise StreamErrors::UnsupportedVersion unless version == '1.0'
|
91
|
+
raise StreamErrors::ImproperAddressing unless valid_address?(domain)
|
92
|
+
raise StreamErrors::HostUnknown unless config.vhost?(domain)
|
93
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:http_bind]
|
94
|
+
|
95
|
+
Sessions[@session.id] = @session
|
96
|
+
send_stream_header
|
97
|
+
end
|
98
|
+
|
99
|
+
def terminate
|
100
|
+
doc = Nokogiri::XML::Document.new
|
101
|
+
node = doc.create_element('body',
|
102
|
+
'type' => 'terminate',
|
103
|
+
'xmlns' => NAMESPACES[:http_bind])
|
104
|
+
@session.reply(node)
|
105
|
+
close_stream
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def send_stream_header
|
111
|
+
doc = Nokogiri::XML::Document.new
|
112
|
+
node = doc.create_element('body',
|
113
|
+
'charsets' => 'UTF-8',
|
114
|
+
'from' => @session.domain,
|
115
|
+
'hold' => @session.hold,
|
116
|
+
'inactivity' => @session.inactivity,
|
117
|
+
'polling' => '5',
|
118
|
+
'requests' => '2',
|
119
|
+
'sid' => @session.id,
|
120
|
+
'ver' => '1.6',
|
121
|
+
'wait' => @session.wait,
|
122
|
+
'xmpp:version' => '1.0',
|
123
|
+
'xmlns' => NAMESPACES[:http_bind],
|
124
|
+
'xmlns:xmpp' => NAMESPACES[:bosh],
|
125
|
+
'xmlns:stream' => NAMESPACES[:stream])
|
126
|
+
|
127
|
+
node << doc.create_element('stream:features') do |el|
|
128
|
+
el << doc.create_element('mechanisms') do |mechanisms|
|
129
|
+
mechanisms.default_namespace = NAMESPACES[:sasl]
|
130
|
+
mechanisms << doc.create_element('mechanism', 'PLAIN')
|
131
|
+
end
|
132
|
+
end
|
133
|
+
@session.reply(node)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Override +Stream#send_stream_error+ to wrap the error XML in a BOSH
|
137
|
+
# terminate body tag.
|
138
|
+
def send_stream_error(e)
|
139
|
+
doc = Nokogiri::XML::Document.new
|
140
|
+
node = doc.create_element('body',
|
141
|
+
'condition' => 'remote-stream-error',
|
142
|
+
'type' => 'terminate',
|
143
|
+
'xmlns' => NAMESPACES[:http_bind],
|
144
|
+
'xmlns:stream' => NAMESPACES[:stream])
|
145
|
+
node.inner_html = e.to_xml
|
146
|
+
@session.reply(node)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Override +Stream#close_stream+ to simply close the connection without
|
150
|
+
# writing a closing stream tag.
|
151
|
+
def close_stream
|
152
|
+
close_connection_after_writing
|
153
|
+
@session.close
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Auth < Client::Auth
|
7
|
+
def initialize(stream, success=BindRestart)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
|
13
|
+
raise StreamErrors::NotAuthorized
|
14
|
+
end
|
15
|
+
nodes = stream.parse_body(node)
|
16
|
+
raise StreamErrors::NotAuthorized unless nodes.size == 1
|
17
|
+
super(nodes.first)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Bind < Client::Bind
|
7
|
+
FEATURES = %Q{<stream:features xmlns:stream="#{NAMESPACES[:stream]}"/>}.freeze
|
8
|
+
|
9
|
+
def initialize(stream, success=Ready)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def node(node)
|
14
|
+
unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
|
15
|
+
raise StreamErrors::NotAuthorized
|
16
|
+
end
|
17
|
+
nodes = stream.parse_body(node)
|
18
|
+
raise StreamErrors::NotAuthorized unless nodes.size == 1
|
19
|
+
super(nodes.first)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Override Client::Bind#send_empty_features to properly namespace the
|
25
|
+
# empty features element.
|
26
|
+
def send_empty_features
|
27
|
+
stream.write(FEATURES)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class BindRestart < State
|
7
|
+
def initialize(stream, success=Bind)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
raise StreamErrors::NotAuthorized unless restart?(node)
|
13
|
+
|
14
|
+
doc = Document.new
|
15
|
+
body = doc.create_element('body') do |el|
|
16
|
+
el.add_namespace(nil, NAMESPACES[:http_bind])
|
17
|
+
el.add_namespace('stream', NAMESPACES[:stream])
|
18
|
+
el << doc.create_element('stream:features') do |features|
|
19
|
+
features << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
stream.reply(body)
|
23
|
+
advance
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def restart?(node)
|
29
|
+
session = stream.valid_session?(node['sid'])
|
30
|
+
restart = node.attribute_with_ns('restart', NAMESPACES[:bosh]).value rescue nil
|
31
|
+
domain = node['to'] == stream.domain
|
32
|
+
session && body?(node) && domain && restart == 'true' && node['rid']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Ready < Client::Ready
|
7
|
+
RID, SID, TYPE, TERMINATE = %w[rid sid type terminate].map {|s| s.freeze }
|
8
|
+
|
9
|
+
def node(node)
|
10
|
+
unless stream.valid_session?(node[SID]) && body?(node) && node[RID]
|
11
|
+
raise StreamErrors::NotAuthorized
|
12
|
+
end
|
13
|
+
stream.parse_body(node).each do |child|
|
14
|
+
begin
|
15
|
+
super(child)
|
16
|
+
rescue StanzaError => e
|
17
|
+
stream.error(e)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
stream.terminate if terminate?(node)
|
21
|
+
end
|
22
|
+
|
23
|
+
def terminate?(node)
|
24
|
+
node[TYPE] == TERMINATE
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Request
|
7
|
+
BUF_SIZE = 1024
|
8
|
+
MODIFIED = '%a, %d %b %Y %H:%M:%S GMT'.freeze
|
9
|
+
MOVED = 'Moved Permanently'.freeze
|
10
|
+
NOT_FOUND = 'Not Found'.freeze
|
11
|
+
NOT_MODIFIED = 'Not Modified'.freeze
|
12
|
+
IF_MODIFIED = 'If-Modified-Since'.freeze
|
13
|
+
TEXT_PLAIN = 'text/plain'.freeze
|
14
|
+
OPTIONS = 'OPTIONS'.freeze
|
15
|
+
CONTENT_TYPES = {
|
16
|
+
'html' => 'text/html; charset="utf-8"',
|
17
|
+
'js' => 'application/javascript; charset="utf-8"',
|
18
|
+
'css' => 'text/css',
|
19
|
+
'png' => 'image/png',
|
20
|
+
'jpg' => 'image/jpeg',
|
21
|
+
'jpeg' => 'image/jpeg',
|
22
|
+
'gif' => 'image/gif',
|
23
|
+
'manifest' => 'text/cache-manifest'
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :stream, :body, :headers, :method, :path, :url, :query
|
27
|
+
|
28
|
+
def initialize(stream, parser, body)
|
29
|
+
@stream, @body = stream, body
|
30
|
+
@headers = parser.headers
|
31
|
+
@method = parser.http_method
|
32
|
+
@path = parser.request_path
|
33
|
+
@url = parser.request_url
|
34
|
+
@query = parser.query_string
|
35
|
+
@received = Time.now
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the number of seconds since this request was received.
|
39
|
+
def age
|
40
|
+
Time.now - @received
|
41
|
+
end
|
42
|
+
|
43
|
+
# Write the requested file to the client out of the given document root
|
44
|
+
# directory. Take care to prevent directory traversal attacks with paths
|
45
|
+
# like ../../../etc/passwd. Use the If-Modified-Since request header
|
46
|
+
# to implement caching.
|
47
|
+
def reply_with_file(dir)
|
48
|
+
path = File.expand_path(File.join(dir, @path))
|
49
|
+
|
50
|
+
# redirect requests missing a slash so relative links work
|
51
|
+
if File.directory?(path) && !@path.end_with?('/')
|
52
|
+
send_status(301, MOVED, "Location: #{redirect_uri}")
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
path = File.join(path, 'index.html') if File.directory?(path)
|
57
|
+
|
58
|
+
if path.start_with?(dir) && File.exist?(path)
|
59
|
+
modified?(path) ? send_file(path) : send_status(304, NOT_MODIFIED)
|
60
|
+
else
|
61
|
+
missing = File.join(dir, '404.html')
|
62
|
+
if File.exist?(missing)
|
63
|
+
send_file(missing, 404, NOT_FOUND)
|
64
|
+
else
|
65
|
+
send_status(404, NOT_FOUND)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Send an HTTP 200 OK response wrapping the XMPP node content back
|
71
|
+
# to the client.
|
72
|
+
def reply(node, content_type)
|
73
|
+
body = node.to_s
|
74
|
+
header = [
|
75
|
+
"HTTP/1.1 200 OK",
|
76
|
+
"Access-Control-Allow-Origin: *",
|
77
|
+
"Content-Type: #{content_type}",
|
78
|
+
"Content-Length: #{body.bytesize}",
|
79
|
+
vroute_cookie
|
80
|
+
].compact.join("\r\n")
|
81
|
+
@stream.stream_write([header, body].join("\r\n\r\n"))
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return true if the request method is OPTIONS, signaling a
|
85
|
+
# CORS preflight check.
|
86
|
+
def options?
|
87
|
+
@method == OPTIONS
|
88
|
+
end
|
89
|
+
|
90
|
+
# Send a 200 OK response, allowing any origin domain to connect to the
|
91
|
+
# server, in response to CORS preflight OPTIONS requests. This allows
|
92
|
+
# any web application using strophe.js to connect to our BOSH port.
|
93
|
+
def reply_to_options
|
94
|
+
allow = @headers['Access-Control-Request-Headers']
|
95
|
+
headers = [
|
96
|
+
"Access-Control-Allow-Origin: *",
|
97
|
+
"Access-Control-Allow-Methods: POST, GET, OPTIONS",
|
98
|
+
"Access-Control-Allow-Headers: #{allow}",
|
99
|
+
"Access-Control-Max-Age: #{60 * 60 * 24 * 30}"
|
100
|
+
]
|
101
|
+
send_status(200, 'OK', headers)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Attempt to rebuild the full request URI from the Host header. If it
|
107
|
+
# wasn't sent by the client, just return the relative path that
|
108
|
+
# was requested. The Location response header must contain the fully
|
109
|
+
# qualified URI, but most browsers will accept relative paths as well.
|
110
|
+
def redirect_uri
|
111
|
+
host = headers['Host']
|
112
|
+
uri = "#{path}/"
|
113
|
+
uri = "#{uri}?#{query}" unless (query || '').empty?
|
114
|
+
uri = "http://#{host}#{uri}" if host
|
115
|
+
uri
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return true if the file has been modified since the client last
|
119
|
+
# requested it with the If-Modified-Since header.
|
120
|
+
def modified?(path)
|
121
|
+
@headers[IF_MODIFIED] != mtime(path)
|
122
|
+
end
|
123
|
+
|
124
|
+
def mtime(path)
|
125
|
+
File.mtime(path).utc.strftime(MODIFIED)
|
126
|
+
end
|
127
|
+
|
128
|
+
def send_status(status, message, *headers)
|
129
|
+
header = [
|
130
|
+
"HTTP/1.1 #{status} #{message}",
|
131
|
+
"Content-Length: 0",
|
132
|
+
*headers
|
133
|
+
].join("\r\n")
|
134
|
+
@stream.stream_write("#{header}\r\n\r\n")
|
135
|
+
end
|
136
|
+
|
137
|
+
# Stream the contents of the file to the client in a 200 OK response.
|
138
|
+
# Send a Last-Modified response header so clients can send us an
|
139
|
+
# If-Modified-Since request header for caching.
|
140
|
+
def send_file(path, status=200, message='OK')
|
141
|
+
header = [
|
142
|
+
"HTTP/1.1 #{status} #{message}",
|
143
|
+
"Content-Type: #{content_type(path)}",
|
144
|
+
"Content-Length: #{File.size(path)}",
|
145
|
+
"Last-Modified: #{mtime(path)}"
|
146
|
+
].join("\r\n")
|
147
|
+
@stream.stream_write("#{header}\r\n\r\n")
|
148
|
+
|
149
|
+
File.open(path) do |file|
|
150
|
+
while (buf = file.read(BUF_SIZE)) != nil
|
151
|
+
@stream.stream_write(buf)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def content_type(path)
|
157
|
+
ext = File.extname(path).sub('.', '')
|
158
|
+
CONTENT_TYPES[ext] || TEXT_PLAIN
|
159
|
+
end
|
160
|
+
|
161
|
+
# Provide a vroute cookie in each response that uniquely identifies this
|
162
|
+
# HTTP server. Reverse proxy servers (nginx/apache) can use this cookie
|
163
|
+
# to implement sticky sessions. Return nil if vroute was not set in
|
164
|
+
# config.rb and no cookie should be sent.
|
165
|
+
def vroute_cookie
|
166
|
+
route = @stream.config[:http].vroute
|
167
|
+
route ? "Set-Cookie: vroute=#{route}; path=/; HttpOnly" : nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|