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.
@@ -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