nchan_tools 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/CODE_OF_CONDUCT.md +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/nchan-benchmark +214 -0
- data/exe/nchan-pub +96 -0
- data/exe/nchan-sub +139 -0
- data/lib/nchan_tools/pubsub.rb +1897 -0
- data/lib/nchan_tools/version.rb +3 -0
- data/lib/nchan_tools.rb +5 -0
- data/nchan_tools.gemspec +43 -0
- metadata +258 -0
@@ -0,0 +1,1897 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
require 'typhoeus'
|
3
|
+
require 'json'
|
4
|
+
require 'oga'
|
5
|
+
require 'yaml'
|
6
|
+
require 'pry'
|
7
|
+
require 'celluloid/current'
|
8
|
+
require 'date'
|
9
|
+
Typhoeus::Config.memoize = false
|
10
|
+
require 'celluloid/io'
|
11
|
+
|
12
|
+
require 'websocket/driver'
|
13
|
+
require 'permessage_deflate'
|
14
|
+
|
15
|
+
require 'uri'
|
16
|
+
require "http/parser"
|
17
|
+
|
18
|
+
|
19
|
+
begin
|
20
|
+
require "http/2"
|
21
|
+
rescue Exception => e
|
22
|
+
HTTP2_MISSING=true
|
23
|
+
else
|
24
|
+
HTTP2_MISSING=false
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
PUBLISH_TIMEOUT=3 #seconds
|
29
|
+
|
30
|
+
module URI
|
31
|
+
class Generic
|
32
|
+
def set_host_unchecked(str)
|
33
|
+
set_host(str)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
def self.parse_possibly_unix_socket(str)
|
37
|
+
u = URI.parse(str)
|
38
|
+
if u && u.scheme == "unix"
|
39
|
+
m = u.path.match "([^:]*):(.*)"
|
40
|
+
if m
|
41
|
+
u.set_host_unchecked("#{u.host}#{m[1]}")
|
42
|
+
u.path=m[2]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
u
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
$seq = 0
|
51
|
+
class Message
|
52
|
+
attr_accessor :content_type, :message, :times_seen, :etag, :last_modified, :eventsource_event
|
53
|
+
def initialize(msg, last_modified=nil, etag=nil)
|
54
|
+
@times_seen=1
|
55
|
+
@message, @last_modified, @etag = msg, last_modified, etag
|
56
|
+
$seq+=1
|
57
|
+
@seq = $seq
|
58
|
+
@idhist = []
|
59
|
+
end
|
60
|
+
def serverside_id
|
61
|
+
timestamp=nil
|
62
|
+
if last_modified
|
63
|
+
timestamp = DateTime.httpdate(last_modified).to_time.utc.to_i
|
64
|
+
end
|
65
|
+
if last_modified || etag
|
66
|
+
"#{timestamp}:#{etag}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
def id=(val)
|
70
|
+
@id=val.dup
|
71
|
+
end
|
72
|
+
def id
|
73
|
+
@id||=serverside_id
|
74
|
+
end
|
75
|
+
def unique_id
|
76
|
+
if id && id.include?(",")
|
77
|
+
time, etag = id.split ":"
|
78
|
+
etag = etag.split(",").map{|x| x[0] == "[" ? x : "?"}.join "," #]
|
79
|
+
[time, etag].join ":"
|
80
|
+
else
|
81
|
+
id
|
82
|
+
end
|
83
|
+
end
|
84
|
+
def to_s
|
85
|
+
@message
|
86
|
+
end
|
87
|
+
def length
|
88
|
+
self.to_s.length
|
89
|
+
end
|
90
|
+
def ==(msg)
|
91
|
+
@message == (msg.respond_to?(:message) ? msg.message : msg)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.each_multipart_message(content_type, body)
|
95
|
+
content_type = content_type.last if Array === content_type
|
96
|
+
matches=/^multipart\/mixed; boundary=(?<boundary>.*)/.match content_type
|
97
|
+
|
98
|
+
if matches
|
99
|
+
splat = body.split(/^--#{Regexp.escape matches[:boundary]}-?-?\r?\n?/)
|
100
|
+
splat.shift
|
101
|
+
|
102
|
+
splat.each do |v|
|
103
|
+
mm=(/(Content-Type:\s(?<content_type>.*?)\r\n)?\r\n(?<body>.*)\r\n/m).match v
|
104
|
+
yield mm[:content_type], mm[:body], true
|
105
|
+
end
|
106
|
+
|
107
|
+
else
|
108
|
+
yield content_type, body
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class MessageStore
|
114
|
+
include Enumerable
|
115
|
+
attr_accessor :msgs, :name
|
116
|
+
|
117
|
+
def matches? (other_msg_store, opt={})
|
118
|
+
my_messages = messages(raw: true)
|
119
|
+
if MessageStore === other_msg_store
|
120
|
+
other_messages = other_msg_store.messages(raw: true)
|
121
|
+
other_name = other_msg_store.name
|
122
|
+
else
|
123
|
+
other_messages = other_msg_store
|
124
|
+
other_name = "?"
|
125
|
+
end
|
126
|
+
unless my_messages.count == other_messages.count
|
127
|
+
err = "Message count doesn't match:\r\n"
|
128
|
+
err << "#{self.name}: #{my_messages.count}\r\n"
|
129
|
+
err << "#{self.to_s}\r\n"
|
130
|
+
|
131
|
+
err << "#{other_name}: #{other_messages.count}\r\n"
|
132
|
+
err << "#{other_msg_store.to_s}"
|
133
|
+
return false, err
|
134
|
+
end
|
135
|
+
other_messages.each_with_index do |msg, i|
|
136
|
+
mymsg = my_messages[i]
|
137
|
+
# puts "#{msg}, #{msg.class}"
|
138
|
+
return false, "Message #{i} doesn't match. (#{self.name} |#{mymsg.length}|, #{other_name} |#{msg.length}|) " unless mymsg == msg
|
139
|
+
[:content_type, :id, :eventsource_event].each do |field|
|
140
|
+
if opt[field] or opt[:all]
|
141
|
+
return false, "Message #{i} #{field} doesn't match. ('#{mymsg.send field}', '#{msg.send field}')" unless mymsg.send(field) == msg.send(field)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize(opt={})
|
149
|
+
@array||=opt[:noid]
|
150
|
+
clear
|
151
|
+
end
|
152
|
+
|
153
|
+
def messages(opt={})
|
154
|
+
if opt[:raw]
|
155
|
+
self.to_a
|
156
|
+
else
|
157
|
+
self.to_a.map{|m|m.to_s}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
#remove n oldest messages
|
162
|
+
def remove_old(n=1)
|
163
|
+
n.times {@msgs.shift}
|
164
|
+
@msgs.count
|
165
|
+
end
|
166
|
+
|
167
|
+
def clear
|
168
|
+
@msgs= @array ? [] : {}
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_a
|
172
|
+
@array ? @msgs : @msgs.values
|
173
|
+
end
|
174
|
+
def to_s
|
175
|
+
buf=""
|
176
|
+
each do |msg|
|
177
|
+
m = msg.to_s
|
178
|
+
m = m.length > 20 ? "#{m[0...20]}..." : m
|
179
|
+
buf<< "<#{msg.id}> \"#{m}\" (count: #{msg.times_seen})\r\n"
|
180
|
+
end
|
181
|
+
buf
|
182
|
+
end
|
183
|
+
|
184
|
+
def [](i)
|
185
|
+
@msgs[i]
|
186
|
+
end
|
187
|
+
|
188
|
+
def each
|
189
|
+
if @array
|
190
|
+
@msgs.each {|msg| yield msg }
|
191
|
+
else
|
192
|
+
@msgs.each {|key, msg| yield msg }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def select
|
197
|
+
cpy = self.class.new(noid: @array ? true : nil)
|
198
|
+
cpy.name = self.name
|
199
|
+
self.each do |msg|
|
200
|
+
cpy << msg if yield msg
|
201
|
+
end
|
202
|
+
cpy
|
203
|
+
end
|
204
|
+
|
205
|
+
def <<(msg)
|
206
|
+
if @array
|
207
|
+
@msgs << msg
|
208
|
+
else
|
209
|
+
if (cur_msg=@msgs[msg.unique_id])
|
210
|
+
#puts "Different messages with same id: #{msg.id}, \"#{msg.to_s}\" then \"#{cur_msg.to_s}\"" unless cur_msg.message == msg.message
|
211
|
+
cur_msg.times_seen+=1
|
212
|
+
cur_msg.times_seen
|
213
|
+
else
|
214
|
+
@msgs[msg.unique_id]=msg
|
215
|
+
1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class Subscriber
|
222
|
+
|
223
|
+
class Logger
|
224
|
+
def initialize
|
225
|
+
@log = []
|
226
|
+
end
|
227
|
+
|
228
|
+
def log(id, type, msg=nil)
|
229
|
+
@log << {time: Time.now.to_f.round(4), id: id.to_sym, type: type, data: msg}
|
230
|
+
end
|
231
|
+
|
232
|
+
def filter(opt)
|
233
|
+
opt[:id] = opt[:id].to_sym if opt[:id]
|
234
|
+
opt[:type] = opt[:type].to_sym if opt[:type]
|
235
|
+
@log.select do |l|
|
236
|
+
true unless ((opt[:id] && opt[:id] != l[:id]) ||
|
237
|
+
(opt[:type] && opt[:type] != l[:type]) ||
|
238
|
+
(opt[:data] && !l.match(opt[:data])))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def show
|
243
|
+
@log
|
244
|
+
end
|
245
|
+
|
246
|
+
def to_s
|
247
|
+
@log.map {|l| "#{l.id} (#{l.type}) #{msg.to_s}"}.join "\n"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class SubscriberError < Exception
|
252
|
+
end
|
253
|
+
class Client
|
254
|
+
attr_accessor :concurrency
|
255
|
+
class ErrorResponse
|
256
|
+
attr_accessor :code, :msg, :connected, :caller, :bundle
|
257
|
+
def initialize(code, msg, bundle=nil, what=nil, failword=nil)
|
258
|
+
self.code = code
|
259
|
+
self.msg = msg
|
260
|
+
self.bundle = bundle
|
261
|
+
self.connected = bundle.connected? if bundle
|
262
|
+
|
263
|
+
@what = what || ["handshake", "connection"]
|
264
|
+
@failword = failword || " failed"
|
265
|
+
end
|
266
|
+
|
267
|
+
def to_s
|
268
|
+
"#{(caller.class.name.split('::').last || self.class.name.split('::')[-2])} #{connected ? @what.last : @what.first}#{@failword}: #{msg} (code #{code})"
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.inherited(subclass)
|
274
|
+
@@inherited||=[]
|
275
|
+
@@inherited << subclass
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.lookup(name)
|
279
|
+
@@inherited.each do |klass|
|
280
|
+
return klass if klass.aliases.include? name
|
281
|
+
end
|
282
|
+
nil
|
283
|
+
end
|
284
|
+
def self.aliases
|
285
|
+
[]
|
286
|
+
end
|
287
|
+
|
288
|
+
def self.unique_aliases
|
289
|
+
uniqs=[]
|
290
|
+
@@inherited.each do |klass|
|
291
|
+
uniqs << klass.aliases.first if klass.aliases.length > 0
|
292
|
+
end
|
293
|
+
uniqs
|
294
|
+
end
|
295
|
+
|
296
|
+
def provides_msgid?
|
297
|
+
true
|
298
|
+
end
|
299
|
+
|
300
|
+
def error(code, msg, bundle=nil)
|
301
|
+
err=ErrorResponse.new code, msg, bundle, @error_what, @error_failword
|
302
|
+
err.caller=self
|
303
|
+
err
|
304
|
+
end
|
305
|
+
|
306
|
+
class ParserBundle
|
307
|
+
attr_accessor :id, :uri, :sock, :body_buf, :connected, :verbose, :parser, :subparser, :headers, :code, :last_modified, :etag
|
308
|
+
def initialize(uri, opt={})
|
309
|
+
@uri=uri
|
310
|
+
@id=(opt[:id] or :"~").to_s.to_sym
|
311
|
+
@logger = opt[:logger]
|
312
|
+
open_socket
|
313
|
+
end
|
314
|
+
def open_socket
|
315
|
+
case uri.scheme
|
316
|
+
when /^unix$/
|
317
|
+
@sock = Celluloid::IO::UNIXSocket.new(uri.host)
|
318
|
+
when /^(ws|http|h2c)$/
|
319
|
+
@sock = Celluloid::IO::TCPSocket.new(uri.host, uri.port)
|
320
|
+
when /^(wss|https|h2)$/
|
321
|
+
@sock = Celluloid::IO::SSLSocket.new(Celluloid::IO::TCPSocket.new(uri.host, uri.port))
|
322
|
+
else
|
323
|
+
raise ArgumentError, "unexpected uri scheme #{uri.scheme}"
|
324
|
+
end
|
325
|
+
self
|
326
|
+
end
|
327
|
+
|
328
|
+
def buffer_body!
|
329
|
+
@body_buf||=""
|
330
|
+
end
|
331
|
+
def connected?
|
332
|
+
@connected
|
333
|
+
end
|
334
|
+
def on_headers(code=nil, h=nil, &block)
|
335
|
+
@body_buf.clear if @body_buf
|
336
|
+
if block_given?
|
337
|
+
@on_headers = block
|
338
|
+
else
|
339
|
+
@logger.log @id, :headers, "#{code or "no code"}; headers: #{h or "none"}" if @logger
|
340
|
+
@on_headers.call(code, h) if @on_headers
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def on_chunk(ch=nil, &block)
|
345
|
+
if block_given?
|
346
|
+
@on_chunk = block
|
347
|
+
else
|
348
|
+
@body_buf << ch if @body_buf
|
349
|
+
@logger.log @id, :chunk, ch if @logger
|
350
|
+
@on_chunk.call(ch) if @on_chunk
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def on_response(code=nil, headers=nil, &block)
|
355
|
+
if block_given?
|
356
|
+
@on_response = block
|
357
|
+
else
|
358
|
+
@logger.log @id, :response, "code #{code or "-"}, headers: #{headers or "-"}, body: #{@body_buf}" if @logger
|
359
|
+
@on_response.call(code, headers, @body_buf) if @on_response
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
def on_error(msg=nil, e=nil, &block)
|
365
|
+
if block_given?
|
366
|
+
@on_error = block
|
367
|
+
else
|
368
|
+
@logger.log @id, :error, "#{e.to_s}, #{msg}" if @logger
|
369
|
+
@on_error.call(msg, e) if @on_error
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def handle_bundle_error(bundle, msg, err)
|
375
|
+
if err && !(EOFError === err)
|
376
|
+
msg="<#{msg}>\n#{err.backtrace.join "\n"}"
|
377
|
+
end
|
378
|
+
@subscriber.on_failure error(0, msg, bundle)
|
379
|
+
@subscriber.finished+=1
|
380
|
+
close bundle
|
381
|
+
end
|
382
|
+
|
383
|
+
def poke(what=nil, timeout = nil)
|
384
|
+
begin
|
385
|
+
if what == :ready
|
386
|
+
(@notready.nil? || @notready > 0) && @cooked_ready.wait(timeout)
|
387
|
+
else
|
388
|
+
@connected > 0 && @cooked.wait(timeout)
|
389
|
+
end
|
390
|
+
rescue Celluloid::ConditionError => e
|
391
|
+
#just ignore it
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def initialize(subscriber, arg={})
|
396
|
+
@notready = 9000
|
397
|
+
@cooked_ready=Celluloid::Condition.new
|
398
|
+
@logger = arg[:logger]
|
399
|
+
end
|
400
|
+
|
401
|
+
def run
|
402
|
+
raise SubscriberError, "Not Implemented"
|
403
|
+
end
|
404
|
+
|
405
|
+
def stop(msg = "Stopped", src_bundle = nil)
|
406
|
+
@subscriber.on_failure error(0, msg, src_bundle)
|
407
|
+
@logger.log :subscriber, :stop if @logger
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
|
412
|
+
class WebSocketClient < Client
|
413
|
+
include Celluloid::IO
|
414
|
+
|
415
|
+
def self.aliases
|
416
|
+
[:websocket, :ws]
|
417
|
+
end
|
418
|
+
|
419
|
+
#patch that monkey
|
420
|
+
module WebSocketDriverExtensions
|
421
|
+
def last_message
|
422
|
+
@last_message
|
423
|
+
end
|
424
|
+
def emit_message
|
425
|
+
@last_message = @message
|
426
|
+
super
|
427
|
+
end
|
428
|
+
end
|
429
|
+
class WebSocket::Driver::Hybi
|
430
|
+
prepend WebSocketDriverExtensions
|
431
|
+
end
|
432
|
+
|
433
|
+
class WebSocket::Driver::Client
|
434
|
+
def response_body
|
435
|
+
@http.body
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
class WebSocketBundle
|
440
|
+
attr_accessor :ws, :sock, :url, :last_message_time, :last_message_frame_type
|
441
|
+
attr_accessor :connected
|
442
|
+
def initialize(url, sock, opt={})
|
443
|
+
@buf=""
|
444
|
+
@url = url
|
445
|
+
driver_opt = {max_length: 2**28-1} #256M
|
446
|
+
if opt[:subprotocol]
|
447
|
+
driver_opt[:protocols]=opt[:subprotocol]
|
448
|
+
end
|
449
|
+
@ws = WebSocket::Driver.client self, driver_opt
|
450
|
+
if opt[:permessage_deflate]
|
451
|
+
if opt[:permessage_deflate_max_window_bits] or opt[:permessage_deflate_server_max_window_bits]
|
452
|
+
deflate = PermessageDeflate.configure(
|
453
|
+
:max_window_bits => opt[:permessage_deflate_max_window_bits],
|
454
|
+
:request_max_window_bits => opt[:permessage_deflate_server_max_window_bits]
|
455
|
+
)
|
456
|
+
@ws.add_extension deflate
|
457
|
+
else
|
458
|
+
@ws.add_extension PermessageDeflate
|
459
|
+
end
|
460
|
+
end
|
461
|
+
if opt[:extra_headers]
|
462
|
+
opt[:extra_headers].each {|k, v| @ws.set_header(k, v)}
|
463
|
+
end
|
464
|
+
|
465
|
+
@sock = sock
|
466
|
+
@id = opt[:id] || :"~"
|
467
|
+
@logger = opt[:logger]
|
468
|
+
end
|
469
|
+
|
470
|
+
|
471
|
+
def connected?
|
472
|
+
@connected
|
473
|
+
end
|
474
|
+
def headers
|
475
|
+
@ws.headers
|
476
|
+
end
|
477
|
+
def body_buf
|
478
|
+
@ws.response_body
|
479
|
+
end
|
480
|
+
|
481
|
+
def send_handshake
|
482
|
+
ret = @ws.start
|
483
|
+
end
|
484
|
+
|
485
|
+
def send_data data
|
486
|
+
@ws.text data
|
487
|
+
end
|
488
|
+
|
489
|
+
def send_binary data
|
490
|
+
@ws.binary data
|
491
|
+
end
|
492
|
+
|
493
|
+
def write(data)
|
494
|
+
@sock.write data
|
495
|
+
end
|
496
|
+
|
497
|
+
def read
|
498
|
+
@buf.clear
|
499
|
+
sock.readpartial(4096, @buf)
|
500
|
+
@ws.parse @buf
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def provides_msgid?
|
505
|
+
@subprotocol == "ws+meta.nchan"
|
506
|
+
end
|
507
|
+
|
508
|
+
attr_accessor :last_modified, :etag, :timeout, :ws
|
509
|
+
def initialize(subscr, opt={})
|
510
|
+
super
|
511
|
+
@last_modified, @etag, @timeout = opt[:last_modified], opt[:etag], opt[:timeout].to_i || 10
|
512
|
+
@connect_timeout = opt[:connect_timeout]
|
513
|
+
@subscriber=subscr
|
514
|
+
@subprotocol = opt[:subprotocol]
|
515
|
+
@url=subscr.url
|
516
|
+
@url = @url.gsub(/^h(ttp|2)(s)?:/, "ws\\2:")
|
517
|
+
|
518
|
+
if opt[:permessage_deflate]
|
519
|
+
@permessage_deflate = true
|
520
|
+
end
|
521
|
+
@permessage_deflate_max_window_bits = opt[:permessage_deflate_max_window_bits]
|
522
|
+
@permessage_deflate_server_max_window_bits = opt[:permessage_deflate_server_max_window_bits]
|
523
|
+
|
524
|
+
@concurrency=(opt[:concurrency] || opt[:clients] || 1).to_i
|
525
|
+
@retry_delay=opt[:retry_delay]
|
526
|
+
@ws = {}
|
527
|
+
@connected=0
|
528
|
+
@nomsg = opt[:nomsg]
|
529
|
+
@http2 = opt[:http2]
|
530
|
+
@extra_headers = opt[:extra_headers]
|
531
|
+
end
|
532
|
+
|
533
|
+
def stop(msg = "Stopped", src_bundle = nil)
|
534
|
+
super msg, (@ws.first && @ws.first.first)
|
535
|
+
@ws.each do |b, v|
|
536
|
+
close b
|
537
|
+
end
|
538
|
+
@timer.cancel if @timer
|
539
|
+
end
|
540
|
+
|
541
|
+
def run(was_success = nil)
|
542
|
+
uri = URI.parse_possibly_unix_socket(@url)
|
543
|
+
uri.port ||= (uri.scheme == "ws" || uri.scheme == "unix" ? 80 : 443)
|
544
|
+
@cooked=Celluloid::Condition.new
|
545
|
+
@connected = @concurrency
|
546
|
+
if @http2
|
547
|
+
@subscriber.on_failure error(0, "Refusing to try websocket over HTTP/2")
|
548
|
+
@connected = 0
|
549
|
+
@notready = 0
|
550
|
+
@cooked_ready.signal false
|
551
|
+
@cooked.signal true
|
552
|
+
return
|
553
|
+
end
|
554
|
+
raise ArgumentError, "invalid websocket scheme #{uri.scheme} in #{@url}" unless uri.scheme == "unix" || uri.scheme.match(/^wss?$/)
|
555
|
+
@notready=@concurrency
|
556
|
+
if @timeout
|
557
|
+
@timer = after(@timeout) do
|
558
|
+
stop "Timeout"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
@concurrency.times do |i|
|
562
|
+
begin
|
563
|
+
sock = ParserBundle.new(uri).open_socket.sock
|
564
|
+
rescue SystemCallError => e
|
565
|
+
@subscriber.on_failure error(0, e.to_s)
|
566
|
+
close nil
|
567
|
+
return
|
568
|
+
end
|
569
|
+
|
570
|
+
if uri.scheme == "unix"
|
571
|
+
hs_url="http://#{uri.host.match "[^/]+$"}#{uri.path}#{uri.query && "?#{uri.query}"}"
|
572
|
+
else
|
573
|
+
hs_url=@url
|
574
|
+
end
|
575
|
+
|
576
|
+
bundle = WebSocketBundle.new hs_url, sock, id: i, permessage_deflate: @permessage_deflate, subprotocol: @subprotocol, logger: @logger, permessage_deflate_max_window_bits: @permessage_deflate_max_window_bits, permessage_deflate_server_max_window_bits: @permessage_deflate_server_max_window_bits, extra_headers: @extra_headers
|
577
|
+
|
578
|
+
bundle.ws.on :open do |ev|
|
579
|
+
bundle.connected = true
|
580
|
+
@notready-=1
|
581
|
+
@cooked_ready.signal true if @notready == 0
|
582
|
+
end
|
583
|
+
|
584
|
+
bundle.ws.on :ping do |ev|
|
585
|
+
@on_ping.call if @on_ping
|
586
|
+
end
|
587
|
+
|
588
|
+
bundle.ws.on :pong do |ev|
|
589
|
+
@on_pong.call if @on_pong
|
590
|
+
end
|
591
|
+
|
592
|
+
bundle.ws.on :error do |ev|
|
593
|
+
http_error_match = ev.message.match(/Unexpected response code: (\d+)/)
|
594
|
+
@subscriber.on_failure error(http_error_match ? http_error_match[1] : 0, ev.message, bundle)
|
595
|
+
close bundle
|
596
|
+
end
|
597
|
+
|
598
|
+
bundle.ws.on :close do |ev|
|
599
|
+
@subscriber.on_failure error(ev.code, ev.reason, bundle)
|
600
|
+
bundle.connected = false
|
601
|
+
close bundle
|
602
|
+
end
|
603
|
+
|
604
|
+
bundle.ws.on :message do |ev|
|
605
|
+
@timer.reset if @timer
|
606
|
+
|
607
|
+
data = ev.data
|
608
|
+
if Array === data #binary String
|
609
|
+
data = data.map(&:chr).join
|
610
|
+
data.force_encoding "ASCII-8BIT"
|
611
|
+
bundle.last_message_frame_type=:binary
|
612
|
+
else
|
613
|
+
bundle.last_message_frame_type=:text
|
614
|
+
end
|
615
|
+
|
616
|
+
if bundle.ws.protocol == "ws+meta.nchan"
|
617
|
+
@meta_regex ||= /^id: (?<id>\d+:[^n]+)\n(content-type: (?<content_type>[^\n]+)\n)?\n(?<data>.*)/m
|
618
|
+
match = @meta_regex.match data
|
619
|
+
if not match
|
620
|
+
@subscriber.on_failure error(0, "Invalid ws+meta.nchan message received")
|
621
|
+
close bundle
|
622
|
+
else
|
623
|
+
if @nomsg
|
624
|
+
msg = match[:data]
|
625
|
+
else
|
626
|
+
msg= Message.new match[:data]
|
627
|
+
msg.content_type = match[:content_type]
|
628
|
+
msg.id = match[:id]
|
629
|
+
end
|
630
|
+
end
|
631
|
+
else
|
632
|
+
msg= @nomsg ? data : Message.new(data)
|
633
|
+
end
|
634
|
+
|
635
|
+
bundle.last_message_time=Time.now.to_f
|
636
|
+
if @subscriber.on_message(msg, bundle) == false
|
637
|
+
close bundle
|
638
|
+
end
|
639
|
+
|
640
|
+
end
|
641
|
+
|
642
|
+
@ws[bundle]=true
|
643
|
+
|
644
|
+
#handhsake
|
645
|
+
bundle.send_handshake
|
646
|
+
|
647
|
+
async.listen bundle
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def on_ping
|
652
|
+
@on_ping = Proc.new if block_given?
|
653
|
+
end
|
654
|
+
def on_pong
|
655
|
+
@on_pong = Proc.new if block_given?
|
656
|
+
end
|
657
|
+
|
658
|
+
def listen(bundle)
|
659
|
+
while @ws[bundle]
|
660
|
+
begin
|
661
|
+
bundle.read
|
662
|
+
rescue IOError => e
|
663
|
+
@subscriber.on_failure error(0, "Connection closed: #{e}"), bundle
|
664
|
+
close bundle
|
665
|
+
return false
|
666
|
+
rescue EOFError
|
667
|
+
bundle.sock.close
|
668
|
+
close bundle
|
669
|
+
return
|
670
|
+
rescue Errno::ECONNRESET
|
671
|
+
close bundle
|
672
|
+
return
|
673
|
+
end
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
def ws_client
|
678
|
+
if @ws.first
|
679
|
+
@ws.first.first
|
680
|
+
else
|
681
|
+
raise SubscriberError, "Websocket client connection gone"
|
682
|
+
end
|
683
|
+
end
|
684
|
+
private :ws_client
|
685
|
+
|
686
|
+
def send_ping(data=nil)
|
687
|
+
ws_client.ping data
|
688
|
+
end
|
689
|
+
def send_close(code=1000, reason=nil)
|
690
|
+
ws_client.send_close code, reason
|
691
|
+
end
|
692
|
+
def send_data(data)
|
693
|
+
ws_client.send_data data
|
694
|
+
end
|
695
|
+
def send_binary(data)
|
696
|
+
ws_client.send_binary data
|
697
|
+
end
|
698
|
+
|
699
|
+
def close(bundle)
|
700
|
+
if bundle
|
701
|
+
@ws.delete bundle
|
702
|
+
bundle.sock.close unless bundle.sock.closed?
|
703
|
+
end
|
704
|
+
@connected -= 1
|
705
|
+
if @connected <= 0
|
706
|
+
binding.pry unless @ws.count == 0
|
707
|
+
@cooked.signal true
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
end
|
712
|
+
|
713
|
+
class LongPollClient < Client
|
714
|
+
include Celluloid::IO
|
715
|
+
|
716
|
+
def self.aliases
|
717
|
+
[:longpoll]
|
718
|
+
end
|
719
|
+
|
720
|
+
def error(*args)
|
721
|
+
@error_what||= ["#{@http2 ? "HTTP/2" : "HTTP"} Request"]
|
722
|
+
super
|
723
|
+
end
|
724
|
+
|
725
|
+
class HTTPBundle < ParserBundle
|
726
|
+
attr_accessor :parser, :sock, :last_message_time, :done, :time_requested, :request_time, :stop_after_headers
|
727
|
+
|
728
|
+
def initialize(uri, opt={})
|
729
|
+
super
|
730
|
+
@accept = opt[:accept] or "*/*"
|
731
|
+
@rcvbuf=""
|
732
|
+
@sndbuf=""
|
733
|
+
@parser = Http::Parser.new
|
734
|
+
@done = false
|
735
|
+
extra_headers = (opt[:headers] or opt[:extra_headers] or {}).map{|k,v| "#{k}: #{v}\n"}.join ""
|
736
|
+
host = uri.host.match "[^/]+$"
|
737
|
+
request_uri = "#{uri.path}#{uri.query && "?#{uri.query}"}"
|
738
|
+
@send_noid_str= <<-END.gsub(/^ {10}/, '')
|
739
|
+
GET #{request_uri} HTTP/1.1
|
740
|
+
Host: #{host}#{uri.default_port == uri.port ? "" : ":#{uri.port}"}
|
741
|
+
#{extra_headers}Accept: #{@accept}
|
742
|
+
User-Agent: #{opt[:useragent] || "HTTPBundle"}
|
743
|
+
|
744
|
+
END
|
745
|
+
|
746
|
+
@send_withid_fmt= <<-END.gsub(/^ {10}/, '')
|
747
|
+
GET #{request_uri.gsub("%", "%%")} HTTP/1.1
|
748
|
+
Host: #{host}#{uri.default_port == uri.port ? "" : ":#{uri.port}"}
|
749
|
+
#{extra_headers}Accept: #{@accept}
|
750
|
+
User-Agent: #{opt[:useragent] || "HTTPBundle"}
|
751
|
+
If-Modified-Since: %s
|
752
|
+
If-None-Match: %s
|
753
|
+
|
754
|
+
END
|
755
|
+
|
756
|
+
@send_withid_no_etag_fmt= <<-END.gsub(/^ {10}/, '')
|
757
|
+
GET #{request_uri.gsub("%", "%%")} HTTP/1.1
|
758
|
+
Host: #{host}#{uri.default_port == uri.port ? "" : ":#{uri.port}"}
|
759
|
+
#{extra_headers}Accept: #{@accept}
|
760
|
+
User-Agent: #{opt[:useragent] || "HTTPBundle"}
|
761
|
+
If-Modified-Since: %s
|
762
|
+
|
763
|
+
END
|
764
|
+
|
765
|
+
@parser.on_headers_complete = proc do |h|
|
766
|
+
if verbose
|
767
|
+
puts "< HTTP/1.1 #{@parser.status_code} [...]\r\n#{h.map {|k,v| "< #{k}: #{v}"}.join "\r\n"}"
|
768
|
+
end
|
769
|
+
@headers=h
|
770
|
+
@last_modified = h['Last-Modified']
|
771
|
+
@etag = h['Etag']
|
772
|
+
@chunky = h['Transfer-Encoding']=='chunked'
|
773
|
+
@gzipped = h['Content-Encoding']=='gzip'
|
774
|
+
@code=@parser.status_code
|
775
|
+
on_headers @parser.status_code, h
|
776
|
+
if @stop_after_headers
|
777
|
+
@bypass_parser = true
|
778
|
+
:stop
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
@parser.on_body = proc do |chunk|
|
783
|
+
handle_chunk chunk
|
784
|
+
end
|
785
|
+
|
786
|
+
@parser.on_message_complete = proc do
|
787
|
+
@chunky = nil
|
788
|
+
@gzipped = nil
|
789
|
+
on_response @parser.status_code, @parser.headers
|
790
|
+
end
|
791
|
+
|
792
|
+
end
|
793
|
+
|
794
|
+
|
795
|
+
def handle_chunk(chunk)
|
796
|
+
chunk = Zlib::GzipReader.new(StringIO.new(chunk)).read if @gzipped
|
797
|
+
on_chunk chunk
|
798
|
+
end
|
799
|
+
private :handle_chunk
|
800
|
+
|
801
|
+
def reconnect?
|
802
|
+
true
|
803
|
+
end
|
804
|
+
|
805
|
+
def send_GET(msg_time=nil, msg_tag=nil)
|
806
|
+
@last_modified = msg_time.to_s if msg_time
|
807
|
+
@etag = msg_tag.to_s if msg_tag
|
808
|
+
@sndbuf.clear
|
809
|
+
begin
|
810
|
+
data = if @last_modified
|
811
|
+
@etag ? sprintf(@send_withid_fmt, @last_modified, @etag) : sprintf(@send_withid_no_etag_fmt, @last_modified)
|
812
|
+
else
|
813
|
+
@send_noid_str
|
814
|
+
end
|
815
|
+
rescue Exception => e
|
816
|
+
binding.pry
|
817
|
+
end
|
818
|
+
|
819
|
+
@sndbuf << data
|
820
|
+
|
821
|
+
if @headers && @headers["Connection"]=="close" && [200, 201, 202, 304, 408].member?(@parser.status_code) && reconnect?
|
822
|
+
sock.close
|
823
|
+
open_socket
|
824
|
+
@parser.reset!
|
825
|
+
end
|
826
|
+
|
827
|
+
@time_requested=Time.now.to_f
|
828
|
+
if verbose
|
829
|
+
puts "", data.gsub(/^.*$/, "> \\0")
|
830
|
+
end
|
831
|
+
sock << @sndbuf
|
832
|
+
end
|
833
|
+
|
834
|
+
def read
|
835
|
+
@rcvbuf.clear
|
836
|
+
begin
|
837
|
+
sock.readpartial(1024*10000, @rcvbuf)
|
838
|
+
while @rcvbuf.size > 0
|
839
|
+
unless @bypass_parser
|
840
|
+
offset = @parser << @rcvbuf
|
841
|
+
if offset < @rcvbuf.size
|
842
|
+
@rcvbuf = @rcvbuf[offset..-1]
|
843
|
+
else
|
844
|
+
@rcvbuf.clear
|
845
|
+
end
|
846
|
+
else
|
847
|
+
handle_chunk @rcvbuf
|
848
|
+
@rcvbuf.clear
|
849
|
+
end
|
850
|
+
end
|
851
|
+
rescue HTTP::Parser::Error => e
|
852
|
+
on_error "Invalid HTTP Respose - #{e}", e
|
853
|
+
rescue EOFError => e
|
854
|
+
on_error "Server closed connection...", e
|
855
|
+
rescue => e
|
856
|
+
on_error "#{e.class}: #{e}", e
|
857
|
+
end
|
858
|
+
return false if @done || sock.closed?
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
class HTTP2Bundle < ParserBundle
|
863
|
+
attr_accessor :stream, :sock, :last_message_time, :done, :time_requested, :request_time
|
864
|
+
GET_METHOD="GET"
|
865
|
+
def initialize(uri, opt = {})
|
866
|
+
if HTTP2_MISSING
|
867
|
+
raise SubscriberError, "HTTP/2 gem missing"
|
868
|
+
end
|
869
|
+
super
|
870
|
+
@done = false
|
871
|
+
@rcvbuf=""
|
872
|
+
@head = {
|
873
|
+
':scheme' => uri.scheme,
|
874
|
+
':method' => GET_METHOD,
|
875
|
+
':path' => "#{uri.path}#{uri.query && "?#{uri.query}"}",
|
876
|
+
':authority' => [uri.host, uri.port].join(':'),
|
877
|
+
'user-agent' => "#{opt[:useragent] || "HTTP2Bundle"}",
|
878
|
+
'accept' => opt[:accept] || "*/*"
|
879
|
+
}
|
880
|
+
if opt[:headers]
|
881
|
+
opt[:headers].each{ |h, v| @head[h.to_s.downcase]=v }
|
882
|
+
end
|
883
|
+
@client = HTTP2::Client.new
|
884
|
+
@client.on(:frame) do |bytes|
|
885
|
+
#puts "Sending bytes: #{bytes.unpack("H*").first}"
|
886
|
+
@sock.print bytes
|
887
|
+
@sock.flush
|
888
|
+
end
|
889
|
+
|
890
|
+
@client.on(:frame_sent) do |frame|
|
891
|
+
#puts "Sent frame: #{frame.inspect}" if verbose
|
892
|
+
end
|
893
|
+
@client.on(:frame_received) do |frame|
|
894
|
+
#puts "Received frame: #{frame.inspect}" if verbose
|
895
|
+
end
|
896
|
+
@resp_headers={}
|
897
|
+
@resp_code=nil
|
898
|
+
end
|
899
|
+
|
900
|
+
def reconnect?
|
901
|
+
false
|
902
|
+
end
|
903
|
+
|
904
|
+
def send_GET(msg_time=nil, msg_tag=nil)
|
905
|
+
@last_modified = msg_time.to_s if msg_time
|
906
|
+
@etag = msg_tag.to_s if msg_tag
|
907
|
+
@time_requested=Time.now.to_f
|
908
|
+
if msg_time
|
909
|
+
@head['if-modified-since'] = msg_time.to_s
|
910
|
+
else
|
911
|
+
@head.delete @head['if-modified-since']
|
912
|
+
end
|
913
|
+
|
914
|
+
if msg_tag
|
915
|
+
@head['if-none-match'] = msg_tag.to_s
|
916
|
+
else
|
917
|
+
@head.delete @head['if-none-match']
|
918
|
+
end
|
919
|
+
|
920
|
+
@stream = @client.new_stream
|
921
|
+
@resp_headers.clear
|
922
|
+
@resp_code=0
|
923
|
+
@stream.on(:close) do |k,v|
|
924
|
+
on_response @resp_code, @resp_headers
|
925
|
+
end
|
926
|
+
@stream.on(:headers) do |h|
|
927
|
+
h.each do |v|
|
928
|
+
puts "< #{v.join ': '}" if verbose
|
929
|
+
case v.first
|
930
|
+
when ":status"
|
931
|
+
@resp_code = v.last.to_i
|
932
|
+
when /^:/
|
933
|
+
@resp_headers[v.first] = v.last
|
934
|
+
else
|
935
|
+
@resp_headers[v.first.gsub(/(?<=^|\W)\w/) { |v| v.upcase }]=v.last
|
936
|
+
end
|
937
|
+
end
|
938
|
+
@headers = @resp_headers
|
939
|
+
@code = @resp_code
|
940
|
+
on_headers @resp_code, @resp_headers
|
941
|
+
end
|
942
|
+
@stream.on(:data) do |d|
|
943
|
+
#puts "got data chunk #{d}"
|
944
|
+
on_chunk d
|
945
|
+
end
|
946
|
+
|
947
|
+
@stream.on(:altsvc) do |f|
|
948
|
+
puts "received ALTSVC #{f}" if verbose
|
949
|
+
end
|
950
|
+
|
951
|
+
@stream.on(:half_close) do
|
952
|
+
puts "", @head.map {|k,v| "> #{k}: #{v}"}.join("\r\n") if verbose
|
953
|
+
end
|
954
|
+
|
955
|
+
@stream.headers(@head, end_stream: true)
|
956
|
+
end
|
957
|
+
|
958
|
+
def read
|
959
|
+
return false if @done || @sock.closed?
|
960
|
+
begin
|
961
|
+
@rcv = @sock.readpartial 1024
|
962
|
+
@client << @rcv
|
963
|
+
rescue EOFError => e
|
964
|
+
if @rcv && @rcv[0..5]=="HTTP/1"
|
965
|
+
on_error @rcv.match(/^HTTP\/1.*/)[0].chomp, e
|
966
|
+
else
|
967
|
+
on_error "Server closed connection...", e
|
968
|
+
end
|
969
|
+
@sock.close
|
970
|
+
rescue => e
|
971
|
+
on_error "#{e.class}: #{e.to_s}", e
|
972
|
+
@sock.close
|
973
|
+
end
|
974
|
+
return false if @done || @sock.closed?
|
975
|
+
end
|
976
|
+
|
977
|
+
end
|
978
|
+
|
979
|
+
attr_accessor :timeout
|
980
|
+
def initialize(subscr, opt={})
|
981
|
+
super
|
982
|
+
@last_modified, @etag, @timeout = opt[:last_modified], opt[:etag], opt[:timeout].to_i || 10
|
983
|
+
@connect_timeout = opt[:connect_timeout]
|
984
|
+
@subscriber=subscr
|
985
|
+
@url=subscr.url
|
986
|
+
@concurrency=opt[:concurrency] || opt[:clients] || 1
|
987
|
+
@gzip=opt[:gzip]
|
988
|
+
@retry_delay=opt[:retry_delay]
|
989
|
+
@nomsg=opt[:nomsg]
|
990
|
+
@bundles={}
|
991
|
+
@body_buf=""
|
992
|
+
@extra_headers = opt[:extra_headers]
|
993
|
+
@verbose=opt[:verbose]
|
994
|
+
@http2=opt[:http2] || opt[:h2]
|
995
|
+
end
|
996
|
+
|
997
|
+
def stop(msg="Stopped", src_bundle=nil)
|
998
|
+
super msg, (@bundles.first && @bundles.first.first)
|
999
|
+
@bundles.each do |b, v|
|
1000
|
+
close b
|
1001
|
+
end
|
1002
|
+
@timer.cancel if @timer
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def run(was_success = nil)
|
1006
|
+
uri = URI.parse_possibly_unix_socket(@url)
|
1007
|
+
uri.port||= uri.scheme.match(/^(ws|http)$/) ? 80 : 443
|
1008
|
+
@cooked=Celluloid::Condition.new
|
1009
|
+
@connected = @concurrency
|
1010
|
+
@notready = @concurrency
|
1011
|
+
@timer.cancel if @timer
|
1012
|
+
if @timeout
|
1013
|
+
@timer = after(@timeout) do
|
1014
|
+
stop "Timeout"
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
@concurrency.times do |i|
|
1018
|
+
begin
|
1019
|
+
bundle = new_bundle(uri, id: i, useragent: "pubsub.rb #{self.class.name} #{@use_http2 ? "(http/2)" : ""} ##{i}", logger: @logger)
|
1020
|
+
rescue SystemCallError => e
|
1021
|
+
@subscriber.on_failure error(0, e.to_s)
|
1022
|
+
close nil
|
1023
|
+
return
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
@bundles[bundle]=true
|
1027
|
+
bundle.send_GET @last_modified, @etag
|
1028
|
+
async.listen bundle
|
1029
|
+
end
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
def request_code_ok(code, bundle)
|
1033
|
+
if code != 200
|
1034
|
+
if code == 304 || code == 408
|
1035
|
+
@subscriber.on_failure error(code, "", bundle)
|
1036
|
+
@subscriber.finished+=1
|
1037
|
+
close bundle
|
1038
|
+
elsif @subscriber.on_failure(error(code, "", bundle)) == false
|
1039
|
+
@subscriber.finished+=1
|
1040
|
+
close bundle
|
1041
|
+
else
|
1042
|
+
Celluloid.sleep @retry_delay if @retry_delay
|
1043
|
+
bundle.send_GET
|
1044
|
+
end
|
1045
|
+
false
|
1046
|
+
else
|
1047
|
+
@timer.reset if @timer
|
1048
|
+
true
|
1049
|
+
end
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
def new_bundle(uri, opt={})
|
1053
|
+
opt[:headers]||={}
|
1054
|
+
if @extra_headers
|
1055
|
+
opt[:headers].merge! @extra_headers
|
1056
|
+
end
|
1057
|
+
if @gzip
|
1058
|
+
opt[:headers]["Accept-Encoding"]="gzip, deflate"
|
1059
|
+
end
|
1060
|
+
b=(@http2 ? HTTP2Bundle : HTTPBundle).new(uri, opt)
|
1061
|
+
b.on_error do |msg, err|
|
1062
|
+
handle_bundle_error b, msg, err
|
1063
|
+
end
|
1064
|
+
b.verbose=@verbose
|
1065
|
+
setup_bundle b
|
1066
|
+
b
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
def setup_bundle(b)
|
1070
|
+
b.buffer_body!
|
1071
|
+
b.on_response do |code, headers, body|
|
1072
|
+
@subscriber.waiting-=1
|
1073
|
+
# Headers and body is all parsed
|
1074
|
+
b.last_modified = headers["Last-Modified"]
|
1075
|
+
b.etag = headers["Etag"]
|
1076
|
+
b.request_time = Time.now.to_f - b.time_requested
|
1077
|
+
if request_code_ok(code, b)
|
1078
|
+
on_message_ret=nil
|
1079
|
+
Message.each_multipart_message(headers["Content-Type"], body) do |content_type, msg_body, multi|
|
1080
|
+
unless @nomsg
|
1081
|
+
msg=Message.new msg_body.dup
|
1082
|
+
msg.content_type=content_type
|
1083
|
+
unless multi
|
1084
|
+
msg.last_modified= headers["Last-Modified"]
|
1085
|
+
msg.etag= headers["Etag"]
|
1086
|
+
end
|
1087
|
+
else
|
1088
|
+
msg=msg_body.dup
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
on_message_ret= @subscriber.on_message(msg, b)
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
unless on_message_ret == false
|
1095
|
+
@subscriber.waiting+=1
|
1096
|
+
b.send_GET
|
1097
|
+
else
|
1098
|
+
@subscriber.finished+=1
|
1099
|
+
close b
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
b.on_error do |msg, err|
|
1105
|
+
handle_bundle_error b, msg, err
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
def listen(bundle)
|
1110
|
+
loop do
|
1111
|
+
begin
|
1112
|
+
return false if bundle.read == false
|
1113
|
+
rescue EOFError
|
1114
|
+
@subscriber.on_failure error(0, "Server Closed Connection"), bundle
|
1115
|
+
close bundle
|
1116
|
+
return false
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
end
|
1120
|
+
|
1121
|
+
def close(bundle)
|
1122
|
+
if bundle
|
1123
|
+
bundle.done=true
|
1124
|
+
bundle.sock.close unless bundle.sock.closed?
|
1125
|
+
@bundles.delete bundle
|
1126
|
+
end
|
1127
|
+
@connected -= 1
|
1128
|
+
if @connected <= 0
|
1129
|
+
@cooked.signal true
|
1130
|
+
end
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
class IntervalPollClient < LongPollClient
|
1136
|
+
def self.aliases
|
1137
|
+
[:intervalpoll, :http, :interval, :poll]
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
def request_code_ok(code, bundle)
|
1141
|
+
if code == 304
|
1142
|
+
if @subscriber.on_failure(error(code, "", bundle), true) == false
|
1143
|
+
@subscriber.finished+=1
|
1144
|
+
close bundle
|
1145
|
+
else
|
1146
|
+
Celluloid.sleep(@retry_delay || 1)
|
1147
|
+
bundle.send_GET
|
1148
|
+
false
|
1149
|
+
end
|
1150
|
+
else
|
1151
|
+
super
|
1152
|
+
end
|
1153
|
+
end
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
class EventSourceClient < LongPollClient
|
1157
|
+
include Celluloid::IO
|
1158
|
+
|
1159
|
+
def self.aliases
|
1160
|
+
[:eventsource, :sse]
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
def error(c,m,cn=nil)
|
1164
|
+
@error_what ||= [ "#{@http2 ? 'HTTP/2' : 'HTTP'} Request failed", "connection closed" ]
|
1165
|
+
@error_failword ||= ""
|
1166
|
+
super
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
class EventSourceParser
|
1170
|
+
attr_accessor :buf, :on_headers, :connected
|
1171
|
+
def initialize
|
1172
|
+
@buf={data: "", id: "", comments: ""}
|
1173
|
+
buf_reset
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def buf_reset
|
1177
|
+
@buf[:data].clear
|
1178
|
+
@buf[:id].clear
|
1179
|
+
@buf[:comments].clear
|
1180
|
+
@buf[:retry_timeout] = nil
|
1181
|
+
@buf[:event] = nil
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
def buf_empty?
|
1185
|
+
@buf[:comments].length == 0 && @buf[:data].length == 0
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
def parse_line(line)
|
1189
|
+
ret = nil
|
1190
|
+
case line
|
1191
|
+
when /^: ?(.*)/
|
1192
|
+
@buf[:comments] << "#{$1}\n"
|
1193
|
+
when /^data(: (.*))?/
|
1194
|
+
@buf[:data] << "#{$2}\n" or "\n"
|
1195
|
+
when /^id(: (.*))?/
|
1196
|
+
@buf[:id] = $2 or ""
|
1197
|
+
when /^event(: (.*))?/
|
1198
|
+
@buf[:event] = $2 or ""
|
1199
|
+
when /^retry: (.*)/
|
1200
|
+
@buf[:retry_timeout] = $1
|
1201
|
+
when /^$/
|
1202
|
+
ret = parse_event
|
1203
|
+
else
|
1204
|
+
raise SubscriberError, "Invalid eventsource data: #{line}"
|
1205
|
+
end
|
1206
|
+
ret
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
def parse_event
|
1210
|
+
|
1211
|
+
if @buf[:comments].length > 0
|
1212
|
+
@on_event.call :comment, @buf[:comments].chomp!
|
1213
|
+
elsif @buf[:data].length > 0 || @buf[:id].length > 0 || !@buf[:event].nil?
|
1214
|
+
@on_event.call @buf[:event], @buf[:data].chomp!, @buf[:id]
|
1215
|
+
end
|
1216
|
+
buf_reset
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
def on_event(&block)
|
1220
|
+
@on_event=block
|
1221
|
+
end
|
1222
|
+
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
def new_bundle(uri, opt={})
|
1226
|
+
opt[:accept]="text/event-stream"
|
1227
|
+
super
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
def setup_bundle(b)
|
1231
|
+
b.on_headers do |code, headers|
|
1232
|
+
if code == 200
|
1233
|
+
@notready-=1
|
1234
|
+
@cooked_ready.signal true if @notready == 0
|
1235
|
+
b.connected = true
|
1236
|
+
end
|
1237
|
+
end
|
1238
|
+
b.buffer_body!
|
1239
|
+
b.subparser=EventSourceParser.new
|
1240
|
+
b.on_chunk do |chunk|
|
1241
|
+
while b.body_buf.slice! /^.*\n/ do
|
1242
|
+
b.subparser.parse_line $~[0]
|
1243
|
+
end
|
1244
|
+
end
|
1245
|
+
b.on_error do |msg, err|
|
1246
|
+
if EOFError === err && !b.subparser.buf_empty?
|
1247
|
+
b.subparser.parse_line "\n"
|
1248
|
+
end
|
1249
|
+
handle_bundle_error b, msg, err
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
b.on_response do |code, headers, body|
|
1253
|
+
if code != 200
|
1254
|
+
@subscriber.on_failure error(code, "", b)
|
1255
|
+
@subscriber.finished+=1
|
1256
|
+
else
|
1257
|
+
if !b.subparser.buf_empty?
|
1258
|
+
b.subparser.parse_line "\n"
|
1259
|
+
else
|
1260
|
+
@subscriber.on_failure error(0, "Response completed unexpectedly", b)
|
1261
|
+
end
|
1262
|
+
@subscriber.finished+=1
|
1263
|
+
end
|
1264
|
+
close b
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
b.subparser.on_event do |evt, data, evt_id|
|
1268
|
+
case evt
|
1269
|
+
when :comment
|
1270
|
+
if data.match(/^(?<code>\d+): (?<message>.*)/)
|
1271
|
+
@subscriber.on_failure error($~[:code].to_i, $~[:message], b)
|
1272
|
+
@subscriber.finished+=1
|
1273
|
+
close b
|
1274
|
+
end
|
1275
|
+
else
|
1276
|
+
@timer.reset if @timer
|
1277
|
+
unless @nomsg
|
1278
|
+
msg=Message.new data.dup
|
1279
|
+
msg.id=evt_id
|
1280
|
+
msg.eventsource_event=evt
|
1281
|
+
else
|
1282
|
+
msg=data
|
1283
|
+
end
|
1284
|
+
if @subscriber.on_message(msg, b) == false
|
1285
|
+
@subscriber.finished+=1
|
1286
|
+
close b
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
b
|
1291
|
+
end
|
1292
|
+
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
class MultiparMixedClient < LongPollClient
|
1296
|
+
include Celluloid::IO
|
1297
|
+
|
1298
|
+
def self.aliases
|
1299
|
+
[:multipart, :multipartmixed, :mixed]
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
class MultipartMixedParser
|
1303
|
+
attr_accessor :bound, :finished, :buf
|
1304
|
+
def initialize(multipart_header)
|
1305
|
+
matches=/^multipart\/mixed; boundary=(?<boundary>.*)/.match multipart_header
|
1306
|
+
raise SubscriberError, "malformed Content-Type multipart/mixed header" unless matches[:boundary]
|
1307
|
+
@bound = matches[:boundary]
|
1308
|
+
@buf = ""
|
1309
|
+
@preambled = false
|
1310
|
+
@headered = nil
|
1311
|
+
@headers = {}
|
1312
|
+
@ninished = nil
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
def on_part(&block)
|
1316
|
+
@on_part = block
|
1317
|
+
end
|
1318
|
+
def on_finish(&block)
|
1319
|
+
@on_finish = block
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
def <<(chunk)
|
1323
|
+
@buf << chunk
|
1324
|
+
#puts @buf
|
1325
|
+
repeat = true
|
1326
|
+
while repeat do
|
1327
|
+
if !@preambled && @buf.slice!(/^--#{Regexp.escape @bound}/)
|
1328
|
+
@finished = nil
|
1329
|
+
@preambled = true
|
1330
|
+
@headered = nil
|
1331
|
+
end
|
1332
|
+
if @preambled && @buf.slice!(/^(\r\n(.*?))?\r\n\r\n/m)
|
1333
|
+
@headered = true
|
1334
|
+
($~[2]).each_line do |l|
|
1335
|
+
if l.match(/(?<name>[^:]+):\s(?<val>[^\r\n]*)/)
|
1336
|
+
@headers[$~[:name]]=$~[:val]
|
1337
|
+
end
|
1338
|
+
end
|
1339
|
+
else
|
1340
|
+
repeat = false
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
if @headered && @buf.slice!(/^(.*?)\r\n--#{Regexp.escape @bound}/m)
|
1344
|
+
@on_part.call @headers, $~[1]
|
1345
|
+
@headered = nil
|
1346
|
+
@headers.clear
|
1347
|
+
repeat = true
|
1348
|
+
else
|
1349
|
+
repeat = false
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
if (@preambled && !@headered && @buf.slice!(/^--\r\n/)) ||
|
1353
|
+
(!@preambled && @buf.slice!(/^--#{Regexp.escape @bound}--\r\n/))
|
1354
|
+
@on_finish.call
|
1355
|
+
repeat = false
|
1356
|
+
end
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
end
|
1361
|
+
|
1362
|
+
def new_bundle(uri, opt)
|
1363
|
+
opt[:accept]="multipart/mixed"
|
1364
|
+
super
|
1365
|
+
end
|
1366
|
+
|
1367
|
+
def setup_bundle b
|
1368
|
+
super
|
1369
|
+
b.on_headers do |code, headers|
|
1370
|
+
if code == 200
|
1371
|
+
b.connected = true
|
1372
|
+
@notready -= 1
|
1373
|
+
@cooked_ready.signal true if @notready == 0
|
1374
|
+
b.subparser = MultipartMixedParser.new headers["Content-Type"]
|
1375
|
+
b.subparser.on_part do |headers, message|
|
1376
|
+
@timer.reset if @timer
|
1377
|
+
unless @nomsg
|
1378
|
+
@timer.reset if @timer
|
1379
|
+
msg=Message.new message.dup, headers["Last-Modified"], headers["Etag"]
|
1380
|
+
msg.content_type=headers["Content-Type"]
|
1381
|
+
else
|
1382
|
+
msg=message
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
if @subscriber.on_message(msg, b) == false
|
1386
|
+
@subscriber.finished+=1
|
1387
|
+
close b
|
1388
|
+
end
|
1389
|
+
end
|
1390
|
+
|
1391
|
+
b.subparser.on_finish do
|
1392
|
+
b.subparser.finished = true
|
1393
|
+
end
|
1394
|
+
else
|
1395
|
+
#puts "BUFFER THE BODY"
|
1396
|
+
#b.buffer_body!
|
1397
|
+
end
|
1398
|
+
end
|
1399
|
+
|
1400
|
+
b.on_chunk do |chunk|
|
1401
|
+
if b.subparser
|
1402
|
+
b.subparser << chunk
|
1403
|
+
if HTTPBundle === b && b.subparser.finished
|
1404
|
+
@subscriber.on_failure error(410, "Server Closed Connection", b)
|
1405
|
+
@subscriber.finished+=1
|
1406
|
+
close b
|
1407
|
+
end
|
1408
|
+
end
|
1409
|
+
end
|
1410
|
+
|
1411
|
+
b.on_response do |code, headers, body|
|
1412
|
+
if !b.subparser
|
1413
|
+
@subscriber.on_failure error(code, "", b)
|
1414
|
+
elsif b.subparser.finished
|
1415
|
+
@subscriber.on_failure error(410, "Server Closed Connection", b)
|
1416
|
+
else
|
1417
|
+
@subscriber.on_failure error(0, "Response completed unexpectedly", b)
|
1418
|
+
end
|
1419
|
+
@subscriber.finished+=1
|
1420
|
+
close b
|
1421
|
+
end
|
1422
|
+
end
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
class HTTPChunkedClient < LongPollClient
|
1426
|
+
include Celluloid::IO
|
1427
|
+
|
1428
|
+
def provides_msgid?
|
1429
|
+
false
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
def run(*args)
|
1433
|
+
if @http2
|
1434
|
+
@subscriber.on_failure error(0, "Chunked transfer is not allowed in HTTP/2")
|
1435
|
+
@connected = 0
|
1436
|
+
return
|
1437
|
+
end
|
1438
|
+
super
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
def self.aliases
|
1442
|
+
[:chunked]
|
1443
|
+
end
|
1444
|
+
|
1445
|
+
def new_bundle(uri, opt)
|
1446
|
+
opt[:accept]="*/*"
|
1447
|
+
opt[:headers]=(opt[:headers] or {}).merge({"TE" => "Chunked"})
|
1448
|
+
super
|
1449
|
+
end
|
1450
|
+
|
1451
|
+
def setup_bundle(b)
|
1452
|
+
super
|
1453
|
+
b.body_buf = nil
|
1454
|
+
b.on_headers do |code, headers|
|
1455
|
+
if code == 200
|
1456
|
+
if headers["Transfer-Encoding"] != "chunked"
|
1457
|
+
@subscriber.on_failure error(0, "Transfer-Encoding should be 'chunked', was '#{headers["Transfer-Encoding"]}'.", b)
|
1458
|
+
close b
|
1459
|
+
else
|
1460
|
+
@notready -= 1
|
1461
|
+
@cooked_ready.signal true if @notready == 0
|
1462
|
+
b.connected= true
|
1463
|
+
end
|
1464
|
+
else
|
1465
|
+
b.buffer_body!
|
1466
|
+
b.stop_after_headers = false
|
1467
|
+
end
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
b.stop_after_headers = true
|
1471
|
+
@inchunk = false
|
1472
|
+
@chunksize = 0
|
1473
|
+
@repeat = true
|
1474
|
+
@chunkbuf = ""
|
1475
|
+
b.on_chunk do |chunk|
|
1476
|
+
#puts "yeah"
|
1477
|
+
@chunkbuf << chunk
|
1478
|
+
@repeat = true
|
1479
|
+
while @repeat
|
1480
|
+
@repeat = false
|
1481
|
+
if !@inchunk && @chunkbuf.slice!(/^([a-fA-F0-9]+)\r\n/m)
|
1482
|
+
@chunksize = $~[1].to_i(16)
|
1483
|
+
@inchunk = true
|
1484
|
+
end
|
1485
|
+
|
1486
|
+
if @inchunk
|
1487
|
+
if @chunkbuf.length >= @chunksize + 2
|
1488
|
+
msgbody = @chunkbuf.slice!(0...@chunksize)
|
1489
|
+
@chunkbuf.slice!(/^\r\n/m)
|
1490
|
+
@timer.reset if @timer
|
1491
|
+
unless @nomsg
|
1492
|
+
msg=Message.new msgbody, nil, nil
|
1493
|
+
else
|
1494
|
+
msg=msgbody
|
1495
|
+
end
|
1496
|
+
if @subscriber.on_message(msg, b) == false
|
1497
|
+
@subscriber.finished+=1
|
1498
|
+
close b
|
1499
|
+
end
|
1500
|
+
@repeat = true if @chunkbuf.length > 0
|
1501
|
+
@inchunk = false
|
1502
|
+
@chunksize = 0
|
1503
|
+
end
|
1504
|
+
end
|
1505
|
+
end
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
b.on_response do |code, headers, body|
|
1509
|
+
if code != 200
|
1510
|
+
@subscriber.on_failure(error(code, "", b))
|
1511
|
+
else
|
1512
|
+
@subscriber.on_failure error(410, "Server Closed Connection", b)
|
1513
|
+
end
|
1514
|
+
close b
|
1515
|
+
end
|
1516
|
+
|
1517
|
+
b
|
1518
|
+
end
|
1519
|
+
|
1520
|
+
end
|
1521
|
+
|
1522
|
+
attr_accessor :url, :client, :messages, :max_round_trips, :quit_message, :errors, :concurrency, :waiting, :finished, :client_class, :log
|
1523
|
+
def initialize(url, concurrency=1, opt={})
|
1524
|
+
@care_about_message_ids=opt[:use_message_id].nil? ? true : opt[:use_message_id]
|
1525
|
+
@url=url
|
1526
|
+
@quit_message = opt[:quit_message]
|
1527
|
+
opt[:timeout] ||= 30
|
1528
|
+
opt[:connect_timeout] ||= 5
|
1529
|
+
#puts "Starting subscriber on #{url}"
|
1530
|
+
@Client_Class = Client.lookup(opt[:client] || :longpoll)
|
1531
|
+
if @Client_Class.nil?
|
1532
|
+
raise SubscriberError, "unknown client type #{opt[:client]}"
|
1533
|
+
end
|
1534
|
+
|
1535
|
+
if !opt[:nostore] && opt[:nomsg]
|
1536
|
+
opt[:nomsg] = nil
|
1537
|
+
puts "nomsg reverted to false because nostore is false"
|
1538
|
+
end
|
1539
|
+
opt[:concurrency]=concurrency
|
1540
|
+
@concurrency = opt[:concurrency]
|
1541
|
+
@opt=opt
|
1542
|
+
if opt[:log]
|
1543
|
+
@log = Subscriber::Logger.new
|
1544
|
+
opt[:logger]=@log
|
1545
|
+
end
|
1546
|
+
new_client
|
1547
|
+
reset
|
1548
|
+
end
|
1549
|
+
def new_client
|
1550
|
+
@client=@Client_Class.new self, @opt
|
1551
|
+
end
|
1552
|
+
def reset
|
1553
|
+
@errors=[]
|
1554
|
+
unless @nostore
|
1555
|
+
@messages=MessageStore.new :noid => !(client.provides_msgid? && @care_about_message_ids)
|
1556
|
+
@messages.name="sub"
|
1557
|
+
end
|
1558
|
+
@waiting=0
|
1559
|
+
@finished=0
|
1560
|
+
new_client if terminated?
|
1561
|
+
self
|
1562
|
+
end
|
1563
|
+
def abort
|
1564
|
+
@client.terminate
|
1565
|
+
end
|
1566
|
+
def errors?
|
1567
|
+
not no_errors?
|
1568
|
+
end
|
1569
|
+
def no_errors?
|
1570
|
+
@errors.empty?
|
1571
|
+
end
|
1572
|
+
def match_errors(regex)
|
1573
|
+
return false if no_errors?
|
1574
|
+
@errors.each do |err|
|
1575
|
+
return false unless err =~ regex
|
1576
|
+
end
|
1577
|
+
true
|
1578
|
+
end
|
1579
|
+
|
1580
|
+
|
1581
|
+
def run
|
1582
|
+
begin
|
1583
|
+
client.current_actor
|
1584
|
+
rescue Celluloid::DeadActorError
|
1585
|
+
return false
|
1586
|
+
end
|
1587
|
+
@client.async.run
|
1588
|
+
self
|
1589
|
+
end
|
1590
|
+
def stop
|
1591
|
+
begin
|
1592
|
+
@client.stop
|
1593
|
+
rescue Celluloid::DeadActorError
|
1594
|
+
return false
|
1595
|
+
end
|
1596
|
+
true
|
1597
|
+
end
|
1598
|
+
def terminate
|
1599
|
+
begin
|
1600
|
+
@client.terminate
|
1601
|
+
rescue Celluloid::DeadActorError
|
1602
|
+
return false
|
1603
|
+
end
|
1604
|
+
true
|
1605
|
+
end
|
1606
|
+
def terminated?
|
1607
|
+
begin
|
1608
|
+
client.current_actor unless client == nil
|
1609
|
+
rescue Celluloid::DeadActorError
|
1610
|
+
return true
|
1611
|
+
end
|
1612
|
+
false
|
1613
|
+
end
|
1614
|
+
def wait(until_what=nil, timeout = nil)
|
1615
|
+
@client.poke until_what, timeout
|
1616
|
+
end
|
1617
|
+
|
1618
|
+
def on_message(msg=nil, bundle=nil, &block)
|
1619
|
+
#puts "received message #{msg && msg.to_s[0..15]}"
|
1620
|
+
if block_given?
|
1621
|
+
@on_message=block
|
1622
|
+
else
|
1623
|
+
@messages << msg if @messages
|
1624
|
+
if @quit_message == msg.to_s
|
1625
|
+
@on_message.call(msg, bundle) if @on_message
|
1626
|
+
return false
|
1627
|
+
end
|
1628
|
+
@on_message.call(msg, bundle) if @on_message
|
1629
|
+
end
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
def make_error(client, what, code, msg, failword=" failed")
|
1633
|
+
"#{client.class.name.split('::').last} #{what}#{failword}: #{msg} (code #{code})"
|
1634
|
+
end
|
1635
|
+
|
1636
|
+
def on_failure(err=nil, nostore=false, &block)
|
1637
|
+
if block_given?
|
1638
|
+
@on_failure=block
|
1639
|
+
else
|
1640
|
+
@errors << err.to_s unless nostore
|
1641
|
+
@on_failure.call(err.to_s, err.bundle) if @on_failure.respond_to? :call
|
1642
|
+
end
|
1643
|
+
end
|
1644
|
+
end
|
1645
|
+
|
1646
|
+
class Publisher
|
1647
|
+
#include Celluloid
|
1648
|
+
|
1649
|
+
class PublisherError < Exception
|
1650
|
+
end
|
1651
|
+
|
1652
|
+
attr_accessor :messages, :response, :response_code, :response_body, :nofail, :accept, :url, :extra_headers, :verbose, :ws, :channel_info, :channel_info_type
|
1653
|
+
def initialize(url, opt={})
|
1654
|
+
@url= url
|
1655
|
+
unless opt[:nostore]
|
1656
|
+
@messages = MessageStore.new :noid => true
|
1657
|
+
@messages.name = "pub"
|
1658
|
+
end
|
1659
|
+
@timeout = opt[:timeout]
|
1660
|
+
@accept = opt[:accept]
|
1661
|
+
@verbose = opt[:verbose]
|
1662
|
+
@on_response = opt[:on_response]
|
1663
|
+
|
1664
|
+
@ws_wait_until_response = true
|
1665
|
+
|
1666
|
+
if opt[:ws] || opt[:websocket]
|
1667
|
+
@ws = Subscriber.new url, 1, timeout: 100000, client: :websocket, permessage_deflate: opt[:permessage_deflate]
|
1668
|
+
@ws_sent_msg = []
|
1669
|
+
@ws.on_message do |msg|
|
1670
|
+
sent = @ws_sent_msg.shift
|
1671
|
+
if @messages && sent
|
1672
|
+
@messages << sent[:msg]
|
1673
|
+
end
|
1674
|
+
|
1675
|
+
self.response=Typhoeus::Response.new
|
1676
|
+
self.response_code=200 #fake it
|
1677
|
+
self.response_body=msg
|
1678
|
+
|
1679
|
+
sent[:response] = self.response
|
1680
|
+
sent[:condition].signal true if sent[:condition]
|
1681
|
+
|
1682
|
+
@on_response.call(self.response_code, self.response_body) if @on_response
|
1683
|
+
end
|
1684
|
+
@ws.on_failure do |err|
|
1685
|
+
raise PublisherError, err
|
1686
|
+
end
|
1687
|
+
|
1688
|
+
@ws.run
|
1689
|
+
@ws.wait :ready
|
1690
|
+
end
|
1691
|
+
end
|
1692
|
+
|
1693
|
+
def with_url(alt_url)
|
1694
|
+
prev_url=@url
|
1695
|
+
@url=alt_url
|
1696
|
+
if block_given?
|
1697
|
+
yield
|
1698
|
+
@url=prev_url
|
1699
|
+
else
|
1700
|
+
self
|
1701
|
+
end
|
1702
|
+
end
|
1703
|
+
|
1704
|
+
def parse_channel_info(data, content_type=nil)
|
1705
|
+
info = {}
|
1706
|
+
case content_type
|
1707
|
+
when "text/plain"
|
1708
|
+
mm = data.match(/^queued messages: (.*)\r$/)
|
1709
|
+
info[:messages] = mm[1].to_i if mm
|
1710
|
+
mm = data.match(/^last requested: (.*) sec\. ago\r$/)
|
1711
|
+
info[:last_requested] = mm[1].to_i if mm
|
1712
|
+
mm = data.match(/^active subscribers: (.*)\r$/)
|
1713
|
+
info[:subscribers] = mm[1].to_i if mm
|
1714
|
+
mm = data.match(/^last message id: (.*)$/)
|
1715
|
+
info[:last_message_id] = mm[1] if mm
|
1716
|
+
return info, :plain
|
1717
|
+
when "text/json", "application/json"
|
1718
|
+
begin
|
1719
|
+
info_json=JSON.parse data
|
1720
|
+
rescue JSON::ParserError => e
|
1721
|
+
return nil
|
1722
|
+
end
|
1723
|
+
info[:messages] = info_json["messages"].to_i
|
1724
|
+
info[:last_requested] = info_json["requested"].to_i
|
1725
|
+
info[:subscribers] = info_json["subscribers"].to_i
|
1726
|
+
info[:last_message_id] = info_json["last_message_id"]
|
1727
|
+
return info, :json
|
1728
|
+
when "application/xml", "text/xml"
|
1729
|
+
ix = Oga.parse_xml(data, :strict => true)
|
1730
|
+
info[:messages] = ix.at_xpath('//messages').text.to_i
|
1731
|
+
info[:last_requested] = ix.at_xpath('//requested').text.to_i
|
1732
|
+
info[:subscribers] = ix.at_xpath('//subscribers').text.to_i
|
1733
|
+
info[:last_message_id] = ix.at_xpath('//last_message_id').text
|
1734
|
+
return info, :xml
|
1735
|
+
when "application/yaml", "text/yaml"
|
1736
|
+
begin
|
1737
|
+
yam=YAML.load data
|
1738
|
+
rescue
|
1739
|
+
return nil
|
1740
|
+
end
|
1741
|
+
info[:messages] = yam["messages"].to_i
|
1742
|
+
info[:last_requested] = yam["requested"].to_i
|
1743
|
+
info[:subscribers] = yam["subscribers"].to_i
|
1744
|
+
info[:last_message_id] = yam["last_message_id"]
|
1745
|
+
return info, :yaml
|
1746
|
+
when nil
|
1747
|
+
["text/plain", "text/json", "text/xml", "text/yaml"].each do |try_content_type|
|
1748
|
+
ret, type = parse_channel_info data, try_content_type
|
1749
|
+
return ret, type if ret
|
1750
|
+
end
|
1751
|
+
else
|
1752
|
+
raise PublisherError, "Unexpected content-type #{content_type}"
|
1753
|
+
end
|
1754
|
+
end
|
1755
|
+
|
1756
|
+
def on_response(&block)
|
1757
|
+
@on_response = block if block_given?
|
1758
|
+
@on_response
|
1759
|
+
end
|
1760
|
+
|
1761
|
+
def on_complete(&block)
|
1762
|
+
raise ArgumentError, "block must be given" unless block
|
1763
|
+
@on_complete = block
|
1764
|
+
end
|
1765
|
+
|
1766
|
+
def submit_ws(body, content_type, &block)
|
1767
|
+
sent = {condition: Celluloid::Condition.new}
|
1768
|
+
sent[:msg] = body && @messages ? Message.new(body) : body
|
1769
|
+
@ws_sent_msg << sent
|
1770
|
+
if content_type == "application/octet-stream"
|
1771
|
+
@ws.client.send_binary(body)
|
1772
|
+
else
|
1773
|
+
@ws.client.send_data(body)
|
1774
|
+
end
|
1775
|
+
if @ws_wait_until_response
|
1776
|
+
while not sent[:response] do
|
1777
|
+
Celluloid.sleep 0.1
|
1778
|
+
end
|
1779
|
+
end
|
1780
|
+
sent[:msg]
|
1781
|
+
end
|
1782
|
+
private :submit_ws
|
1783
|
+
def terminate
|
1784
|
+
@ws.terminate if @ws
|
1785
|
+
end
|
1786
|
+
|
1787
|
+
def submit(body, method=:POST, content_type= :'text/plain', eventsource_event=nil, &block)
|
1788
|
+
self.response=nil
|
1789
|
+
self.response_code=nil
|
1790
|
+
self.response_body=nil
|
1791
|
+
|
1792
|
+
if Enumerable===body
|
1793
|
+
i=0
|
1794
|
+
body.each{|b| i+=1; submit(b, method, content_type, &block)}
|
1795
|
+
return i
|
1796
|
+
end
|
1797
|
+
|
1798
|
+
return submit_ws body, content_type, &block if @ws
|
1799
|
+
|
1800
|
+
headers = {:'Content-Type' => content_type, :'Accept' => accept}
|
1801
|
+
headers[:'X-Eventsource-Event'] = eventsource_event if eventsource_event
|
1802
|
+
headers.merge! @extra_headers if @extra_headers
|
1803
|
+
post = Typhoeus::Request.new(
|
1804
|
+
@url,
|
1805
|
+
headers: headers,
|
1806
|
+
method: method,
|
1807
|
+
body: body,
|
1808
|
+
timeout: @timeout || PUBLISH_TIMEOUT,
|
1809
|
+
connecttimeout: @timeout || PUBLISH_TIMEOUT,
|
1810
|
+
verbose: @verbose
|
1811
|
+
)
|
1812
|
+
if body && @messages
|
1813
|
+
msg=Message.new body
|
1814
|
+
msg.content_type=content_type
|
1815
|
+
msg.eventsource_event=eventsource_event
|
1816
|
+
end
|
1817
|
+
if @on_complete
|
1818
|
+
post.on_complete @on_complete
|
1819
|
+
else
|
1820
|
+
post.on_complete do |response|
|
1821
|
+
self.response=response
|
1822
|
+
self.response_code=response.code
|
1823
|
+
self.response_body=response.body
|
1824
|
+
if response.success?
|
1825
|
+
#puts "published message #{msg.to_s[0..15]}"
|
1826
|
+
@channel_info, @channel_info_type = parse_channel_info response.body, response.headers["Content-Type"]
|
1827
|
+
if @messages && msg
|
1828
|
+
msg.id = @channel_info[:last_message_id] if @channel_info
|
1829
|
+
@messages << msg
|
1830
|
+
end
|
1831
|
+
|
1832
|
+
elsif response.timed_out?
|
1833
|
+
# aw hell no
|
1834
|
+
#puts "publisher err: timeout"
|
1835
|
+
|
1836
|
+
pub_url=URI.parse_possibly_unix_socket(response.request.url)
|
1837
|
+
pub_url = "#{pub_url.path}#{pub_url.query ? "?#{pub_url.query}" : nil}"
|
1838
|
+
raise PublisherError, "Publisher #{response.request.options[:method]} to #{pub_url} timed out."
|
1839
|
+
elsif response.code == 0
|
1840
|
+
# Could not get an http response, something's wrong.
|
1841
|
+
#puts "publisher err: #{response.return_message}"
|
1842
|
+
errmsg="No HTTP response: #{response.return_message}"
|
1843
|
+
unless self.nofail then
|
1844
|
+
raise PublisherError, errmsg
|
1845
|
+
end
|
1846
|
+
else
|
1847
|
+
# Received a non-successful http response.
|
1848
|
+
#puts "publisher err: #{response.code.to_s}"
|
1849
|
+
errmsg="HTTP request failed: #{response.code.to_s}"
|
1850
|
+
unless self.nofail then
|
1851
|
+
raise PublisherError, errmsg
|
1852
|
+
end
|
1853
|
+
end
|
1854
|
+
block.call(self.response_code, self.response_body) if block
|
1855
|
+
on_response.call(self.response_code, self.response_body) if on_response
|
1856
|
+
end
|
1857
|
+
end
|
1858
|
+
#puts "publishing to #{@url}"
|
1859
|
+
begin
|
1860
|
+
post.run
|
1861
|
+
rescue Exception => e
|
1862
|
+
last=nil, i=0
|
1863
|
+
e.backtrace.select! do |bt|
|
1864
|
+
if bt.match(/(gems\/(typhoeus|ethon)|pubsub\.rb)/)
|
1865
|
+
last=i
|
1866
|
+
false
|
1867
|
+
else
|
1868
|
+
i+=1
|
1869
|
+
true
|
1870
|
+
end
|
1871
|
+
end
|
1872
|
+
e.backtrace.insert last, "..."
|
1873
|
+
raise PublisherError, e
|
1874
|
+
end
|
1875
|
+
end
|
1876
|
+
|
1877
|
+
def get(accept_header=nil)
|
1878
|
+
self.accept=accept_header
|
1879
|
+
submit nil, :GET
|
1880
|
+
self.accept=nil
|
1881
|
+
end
|
1882
|
+
def delete
|
1883
|
+
submit nil, :DELETE
|
1884
|
+
end
|
1885
|
+
def post(body, content_type=nil, es_event=nil, &block)
|
1886
|
+
submit body, :POST, content_type, es_event, &block
|
1887
|
+
end
|
1888
|
+
def put(body, content_type=nil, es_event=nil, &block)
|
1889
|
+
submit body, :PUT, content_type, es_event, &block
|
1890
|
+
end
|
1891
|
+
|
1892
|
+
def reset
|
1893
|
+
@messages.clear
|
1894
|
+
end
|
1895
|
+
|
1896
|
+
|
1897
|
+
end
|