nchan_tools 0.1.1

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