mt-uv-rays 2.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +22 -0
- data/lib/faraday/adapter/mt-libuv.rb +89 -0
- data/lib/handsoap/http/drivers/mt-libuv_driver.rb +43 -0
- data/lib/httpi/adapter/mt-libuv.rb +69 -0
- data/lib/mt-uv-rays/abstract_tokenizer.rb +121 -0
- data/lib/mt-uv-rays/buffered_tokenizer.rb +176 -0
- data/lib/mt-uv-rays/connection.rb +190 -0
- data/lib/mt-uv-rays/http/encoding.rb +131 -0
- data/lib/mt-uv-rays/http/parser.rb +175 -0
- data/lib/mt-uv-rays/http/request.rb +262 -0
- data/lib/mt-uv-rays/http_endpoint.rb +336 -0
- data/lib/mt-uv-rays/ping.rb +189 -0
- data/lib/mt-uv-rays/scheduler/time.rb +307 -0
- data/lib/mt-uv-rays/scheduler.rb +386 -0
- data/lib/mt-uv-rays/tcp_server.rb +46 -0
- data/lib/mt-uv-rays/version.rb +5 -0
- data/lib/mt-uv-rays.rb +94 -0
- data/mt-uv-rays.gemspec +38 -0
- data/spec/abstract_tokenizer_spec.rb +129 -0
- data/spec/buffered_tokenizer_spec.rb +277 -0
- data/spec/connection_spec.rb +124 -0
- data/spec/http_endpoint_spec.rb +636 -0
- data/spec/ping_spec.rb +73 -0
- data/spec/scheduler_spec.rb +118 -0
- data/spec/scheduler_time_spec.rb +132 -0
- metadata +300 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddress' # IP Address parser
|
4
|
+
|
5
|
+
module MTUV
|
6
|
+
def self.try_connect(tcp, handler, server, port)
|
7
|
+
if IPAddress.valid? server
|
8
|
+
tcp.finally { handler.on_close }
|
9
|
+
tcp.progress { |*data| handler.on_read(*data) }
|
10
|
+
tcp.connect server, port do
|
11
|
+
tcp.enable_nodelay
|
12
|
+
tcp.start_tls(handler.using_tls) if handler.using_tls
|
13
|
+
|
14
|
+
# on_connect could call use_tls so must come after start_tls
|
15
|
+
handler.on_connect(tcp)
|
16
|
+
tcp.start_read
|
17
|
+
end
|
18
|
+
else
|
19
|
+
tcp.reactor.lookup(server, wait: false).then(
|
20
|
+
proc { |result|
|
21
|
+
MTUV.try_connect(tcp, handler, result[0][0], port)
|
22
|
+
},
|
23
|
+
proc { |failure|
|
24
|
+
# TODO:: Log error on reactor
|
25
|
+
handler.on_close
|
26
|
+
}
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# @abstract
|
33
|
+
class Connection
|
34
|
+
attr_reader :using_tls
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@send_queue = []
|
38
|
+
@paused = false
|
39
|
+
@using_tls = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def pause
|
43
|
+
@paused = true
|
44
|
+
@transport.stop_read
|
45
|
+
end
|
46
|
+
|
47
|
+
def paused?
|
48
|
+
@paused
|
49
|
+
end
|
50
|
+
|
51
|
+
def resume
|
52
|
+
@paused = false
|
53
|
+
@transport.start_read
|
54
|
+
end
|
55
|
+
|
56
|
+
# Compatible with TCP
|
57
|
+
def close_connection(*args)
|
58
|
+
@transport.close
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_read(data, *args) # user to define
|
62
|
+
end
|
63
|
+
|
64
|
+
def post_init(*args)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class TcpConnection < Connection
|
69
|
+
def write(data)
|
70
|
+
@transport.write(data, wait: :promise)
|
71
|
+
end
|
72
|
+
|
73
|
+
def close_connection(after_writing = false)
|
74
|
+
if after_writing
|
75
|
+
@transport.shutdown
|
76
|
+
else
|
77
|
+
@transport.close
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def stream_file(filename, type = :raw)
|
82
|
+
file = @reactor.file(filename, File::RDONLY) do # File is open and available for reading
|
83
|
+
file.send_file(@transport, type, wait: :promise).finally do
|
84
|
+
file.close
|
85
|
+
end
|
86
|
+
end
|
87
|
+
return file
|
88
|
+
end
|
89
|
+
|
90
|
+
def keepalive(raw_time)
|
91
|
+
time = raw_time.to_i
|
92
|
+
if time.to_i <= 0
|
93
|
+
@transport.disable_keepalive
|
94
|
+
else
|
95
|
+
@transport.enable_keepalive(time)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_connect(transport) # user to define
|
100
|
+
end
|
101
|
+
|
102
|
+
def on_close # user to define
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class InboundConnection < TcpConnection
|
107
|
+
def initialize(tcp)
|
108
|
+
super()
|
109
|
+
|
110
|
+
@reactor = tcp.reactor
|
111
|
+
@transport = tcp
|
112
|
+
@transport.finally { on_close }
|
113
|
+
@transport.progress { |*data| on_read(*data) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def use_tls(args = {})
|
117
|
+
args[:server] = true
|
118
|
+
|
119
|
+
if @transport.connected
|
120
|
+
@transport.start_tls(args)
|
121
|
+
else
|
122
|
+
@using_tls = args
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class OutboundConnection < TcpConnection
|
128
|
+
|
129
|
+
def initialize(server, port)
|
130
|
+
super()
|
131
|
+
|
132
|
+
@reactor = reactor
|
133
|
+
@server = server
|
134
|
+
@port = port
|
135
|
+
@transport = @reactor.tcp
|
136
|
+
|
137
|
+
::MTUV.try_connect(@transport, self, @server, @port)
|
138
|
+
end
|
139
|
+
|
140
|
+
def use_tls(args = {})
|
141
|
+
args.delete(:server)
|
142
|
+
|
143
|
+
if @transport.connected
|
144
|
+
@transport.start_tls(args)
|
145
|
+
else
|
146
|
+
@using_tls = args
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def reconnect(server = nil, port = nil)
|
151
|
+
@reactor = reactor
|
152
|
+
|
153
|
+
@transport = @reactor.tcp
|
154
|
+
@server = server || @server
|
155
|
+
@port = port || @port
|
156
|
+
|
157
|
+
::MTUV.try_connect(@transport, self, @server, @port)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class DatagramConnection < Connection
|
162
|
+
def initialize(server = nil, port = nil)
|
163
|
+
super()
|
164
|
+
|
165
|
+
@reactor = reactor
|
166
|
+
@transport = @reactor.udp
|
167
|
+
@transport.progress { |*args| on_read(*args) }
|
168
|
+
|
169
|
+
if not server.nil?
|
170
|
+
server = '127.0.0.1' if server == 'localhost'
|
171
|
+
raise ArgumentError, "Invalid server address #{server}" unless IPAddress.valid?(server)
|
172
|
+
@transport.bind(server, port)
|
173
|
+
end
|
174
|
+
|
175
|
+
@transport.start_read
|
176
|
+
end
|
177
|
+
|
178
|
+
def send_datagram(data, recipient_address, recipient_port)
|
179
|
+
if IPAddress.valid? recipient_address
|
180
|
+
@transport.send recipient_address, recipient_port, data
|
181
|
+
else
|
182
|
+
# Async DNS resolution
|
183
|
+
# Note:: send here will chain the promise
|
184
|
+
@reactor.lookup(recipient_address).then do |result|
|
185
|
+
@transport.send result[0][0], recipient_port, data
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module MTUV
|
5
|
+
module Http
|
6
|
+
module Encoding
|
7
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
8
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
9
|
+
|
10
|
+
def escape(s)
|
11
|
+
if defined?(EscapeUtils)
|
12
|
+
EscapeUtils.escape_url(s.to_s)
|
13
|
+
else
|
14
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/) {
|
15
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def unescape(s)
|
21
|
+
if defined?(EscapeUtils)
|
22
|
+
EscapeUtils.unescape_url(s.to_s)
|
23
|
+
else
|
24
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {
|
25
|
+
[$1.delete('%')].pack('H*')
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if ''.respond_to?(:bytesize)
|
31
|
+
def bytesize(string)
|
32
|
+
string.bytesize
|
33
|
+
end
|
34
|
+
else
|
35
|
+
def bytesize(string)
|
36
|
+
string.size
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Map all header keys to a downcased string version
|
41
|
+
def munge_header_keys(head)
|
42
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def encode_request(method, uri, query)
|
47
|
+
query = encode_query(uri, query)
|
48
|
+
String.new(HTTP_REQUEST_HEADER % [method.to_s.upcase, query])
|
49
|
+
end
|
50
|
+
|
51
|
+
def encode_query(uri, query)
|
52
|
+
encoded_query = if query.kind_of?(Hash)
|
53
|
+
query.map { |k, v| encode_param(k, v) }.join('&')
|
54
|
+
else
|
55
|
+
query.to_s
|
56
|
+
end
|
57
|
+
encoded_query.to_s.empty? ? uri : "#{uri}?#{encoded_query}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# URL encodes query parameters:
|
61
|
+
# single k=v, or a URL encoded array, if v is an array of values
|
62
|
+
def encode_param(k, v)
|
63
|
+
if v.is_a?(Array)
|
64
|
+
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
65
|
+
else
|
66
|
+
escape(k) + "=" + escape(v)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def form_encode_body(obj)
|
71
|
+
pairs = []
|
72
|
+
recursive = Proc.new do |h, prefix|
|
73
|
+
h.each do |k,v|
|
74
|
+
key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
|
75
|
+
|
76
|
+
if v.is_a? Array
|
77
|
+
nh = Hash.new
|
78
|
+
v.size.times { |t| nh[t] = v[t] }
|
79
|
+
recursive.call(nh, key)
|
80
|
+
|
81
|
+
elsif v.is_a? Hash
|
82
|
+
recursive.call(v, key)
|
83
|
+
else
|
84
|
+
pairs << "#{key}=#{escape(v)}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
recursive.call(obj, '')
|
90
|
+
return pairs.join('&')
|
91
|
+
end
|
92
|
+
|
93
|
+
# Encode a field in an HTTP header
|
94
|
+
def encode_field(k, v)
|
95
|
+
FIELD_ENCODING % [k, v]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Encode basic auth in an HTTP header
|
99
|
+
# In: Array ([user, pass]) - for basic auth
|
100
|
+
# String - custom auth string (OAuth, etc)
|
101
|
+
def encode_auth(k,v)
|
102
|
+
if v.is_a? Array
|
103
|
+
FIELD_ENCODING % [k, ["Basic", Base64.strict_encode64(v.join(":"))].join(" ")]
|
104
|
+
else
|
105
|
+
encode_field(k, v)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def encode_headers(head)
|
110
|
+
head.inject(String.new) do |result, (key, value)|
|
111
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
112
|
+
key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
|
113
|
+
result << case key
|
114
|
+
when 'Authorization', 'Proxy-Authorization'
|
115
|
+
encode_auth(key, value)
|
116
|
+
else
|
117
|
+
encode_field(key, value)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def encode_cookie(cookie)
|
123
|
+
if cookie.is_a? Hash
|
124
|
+
cookie.inject(String.new) { |result, (k, v)| result << encode_param(k, v) + ';' }
|
125
|
+
else
|
126
|
+
cookie
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
|
5
|
+
module MTUV
|
6
|
+
module Http
|
7
|
+
class Headers < ::ActiveSupport::HashWithIndifferentAccess
|
8
|
+
# The HTTP version returned
|
9
|
+
attr_accessor :http_version
|
10
|
+
|
11
|
+
# The status code (as an integer)
|
12
|
+
attr_accessor :status
|
13
|
+
|
14
|
+
# The text after the status code
|
15
|
+
attr_accessor :reason_phrase
|
16
|
+
|
17
|
+
# Cookies at the time of the request
|
18
|
+
attr_accessor :cookies
|
19
|
+
|
20
|
+
attr_accessor :keep_alive
|
21
|
+
|
22
|
+
attr_accessor :body
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"HTTP#{http_version} #{status} - keep alive: #{keep_alive}\nheaders: #{super}\nbody: #{body}"
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :inspect, :to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
class Parser
|
32
|
+
def initialize
|
33
|
+
@parser = ::HttpParser::Parser.new(self)
|
34
|
+
@state = ::HttpParser::Parser.new_instance
|
35
|
+
@state.type = :response
|
36
|
+
@headers = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
attr_reader :request
|
41
|
+
|
42
|
+
|
43
|
+
def new_request(request)
|
44
|
+
@headers = nil
|
45
|
+
@request = request
|
46
|
+
@headers_complete = false
|
47
|
+
@state.reset!
|
48
|
+
end
|
49
|
+
|
50
|
+
def received(data)
|
51
|
+
if @parser.parse(@state, data)
|
52
|
+
if @request
|
53
|
+
@request.reject(@state.error)
|
54
|
+
@request = nil
|
55
|
+
@response = nil
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Parser Callbacks:
|
65
|
+
def on_message_begin(parser)
|
66
|
+
@headers = Headers.new
|
67
|
+
@body = String.new
|
68
|
+
@chunked = false
|
69
|
+
@close_connection = false
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_status(parser, data)
|
73
|
+
@headers.reason_phrase = data
|
74
|
+
|
75
|
+
# Different HTTP versions have different defaults
|
76
|
+
if @state.http_minor == 0
|
77
|
+
@close_connection = true
|
78
|
+
else
|
79
|
+
@close_connection = false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_header_field(parser, data)
|
84
|
+
@header = data
|
85
|
+
end
|
86
|
+
|
87
|
+
def on_header_value(parser, data)
|
88
|
+
case @header
|
89
|
+
when 'Set-Cookie'
|
90
|
+
@request.set_cookie(data)
|
91
|
+
|
92
|
+
when 'Connection'
|
93
|
+
# Overwrite the default
|
94
|
+
@close_connection = data == 'close'
|
95
|
+
|
96
|
+
when 'Transfer-Encoding'
|
97
|
+
# If chunked we'll buffer streaming data for notification
|
98
|
+
@chunked = data == 'chunked'
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# Duplicate headers we'll place into an array
|
103
|
+
current = @headers[@header]
|
104
|
+
if current
|
105
|
+
arr = @headers[@header] = Array(current)
|
106
|
+
arr << data
|
107
|
+
else
|
108
|
+
@headers[@header] = data
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def on_headers_complete(parser)
|
113
|
+
@headers_complete = true
|
114
|
+
|
115
|
+
# https://github.com/joyent/http-parser indicates we should extract
|
116
|
+
# this information here
|
117
|
+
@headers.http_version = @state.http_version
|
118
|
+
@headers.status = @state.http_status
|
119
|
+
@headers.cookies = @request.cookies_hash
|
120
|
+
@headers.keep_alive = !@close_connection
|
121
|
+
|
122
|
+
# User code may throw an error
|
123
|
+
# Errors will halt the processing and return a PAUSED error
|
124
|
+
@request.set_headers(@headers)
|
125
|
+
end
|
126
|
+
|
127
|
+
def on_body(parser, data)
|
128
|
+
if @request.streaming?
|
129
|
+
@request.notify(data)
|
130
|
+
else
|
131
|
+
@body << data
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def on_message_complete(parser)
|
136
|
+
@headers.body = @body
|
137
|
+
|
138
|
+
if @request.resolve(@headers)
|
139
|
+
cleanup
|
140
|
+
else
|
141
|
+
req = @request
|
142
|
+
cleanup
|
143
|
+
new_request(req)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# We need to flush the response on disconnect if content-length is undefined
|
148
|
+
# As per the HTTP spec
|
149
|
+
attr_accessor :reason
|
150
|
+
def eof
|
151
|
+
return if @request.nil?
|
152
|
+
|
153
|
+
if @headers_complete && (@headers['Content-Length'].nil? || @request.method == :head)
|
154
|
+
on_message_complete(nil)
|
155
|
+
else
|
156
|
+
# Reject if this is a partial response
|
157
|
+
@request.reject(@reason || :partial_response)
|
158
|
+
cleanup
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
|
166
|
+
def cleanup
|
167
|
+
@request = nil
|
168
|
+
@body = nil
|
169
|
+
@headers = nil
|
170
|
+
@reason = nil
|
171
|
+
@headers_complete = false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|