strobe 0.1.6 → 0.2.0.beta.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.
@@ -1,155 +1,365 @@
1
- require 'thread'
2
- require 'net/http'
1
+ require 'em-http'
3
2
 
4
3
  module Strobe
5
- class Middleware::Proxy
6
-
7
- # A wrapper around the Net/HTTP response body
8
- # that allows rack to stream the result down
9
- class Body
10
- def initialize(queue, options = {})
11
- @queue = queue
12
- @options = options
4
+ module Middleware
5
+ class Proxy
6
+ PROXY_PATH = %r[/_strobe/proxy/([^/]+)(/.*)?$]
7
+
8
+ def self.proxy_response?(status, hdrs)
9
+ status == 302 && hdrs.any? { |k,v| k =~ /x-strobe-location/i }
13
10
  end
14
11
 
15
- def each
16
- while chunk = @queue.pop
17
- if Exception === chunk
18
- raise chunk
19
- else
20
- yield prepare_chunk(chunk)
21
- end
12
+ def self.strobe_redirect(is_head, _hdrs, body, callback)
13
+ hdrs = {}
14
+ _hdrs.each { |k,v| hdrs[k.to_s.downcase] = v }
15
+
16
+ location = hdrs['x-strobe-location']
17
+ env = redirect_env(is_head, hdrs, body, callback)
18
+ options = redirect_opts(hdrs)
19
+
20
+ request = ProxyRequest.new(env, location, options)
21
+
22
+ request.handle!
23
+ end
24
+
25
+ def self.redirect_env(is_head, hdrs, body, callback)
26
+ env = {}
27
+
28
+ parse_header_list(hdrs['x-strobe-forward-headers']).each do |from, to|
29
+ env[rackify(to)] = hdrs[from]
22
30
  end
23
31
 
24
- yield "0\r\n\r\n" if chunked?
32
+ env['REQUEST_METHOD'] = hdrs['x-strobe-redirect-method']
33
+ env['REQUEST_METHOD'] ||= is_head ? 'HEAD' : 'GET'
34
+ env['async.callback'] = callback
35
+ env['rack.input'] = StringIO.new(body.join)
36
+ env
25
37
  end
26
38
 
27
- def prepare_chunk(chunk)
28
- if chunked?
29
- size = chunk.respond_to?(:bytesize) ? chunk.bytesize : chunk.length
30
- "#{size.to_s(16)}\r\n#{chunk}\r\n"
31
- else
32
- chunk
39
+ def self.redirect_opts(hdrs)
40
+ extra_headers = {}
41
+ block_headers = []
42
+
43
+ parse_header_list(hdrs['x-strobe-add-headers']).each do |from, to|
44
+ extra_headers[to] = hdrs[from]
45
+ end
46
+
47
+ parse_header_list(hdrs['x-strobe-block-headers']).each do |key, _|
48
+ block_headers << key
49
+ end
50
+
51
+ { :extra_headers => extra_headers,
52
+ :block_headers => block_headers,
53
+ :block_cookies => true }
54
+ end
55
+
56
+ def self.parse_header_list(str)
57
+ return {} unless str
58
+ parsed = {}
59
+
60
+ str.downcase.split(',').each do |item|
61
+ from, to, _ = item.split('=')
62
+ to ||= from
63
+ parsed[from.strip] = to.strip
33
64
  end
65
+
66
+ parsed
34
67
  end
35
68
 
36
- def chunked?
37
- @options[:chunked]
69
+ def self.rackify(key)
70
+ key = key.to_s.gsub('-', '_').upcase
71
+ key = "HTTP_#{key}" unless %w(REQUEST_METHOD
72
+ CONTENT_LENGTH
73
+ QUERY_STRING
74
+ SERVER_NAME
75
+ SERVER_PORT
76
+ PATH_INFO
77
+ SCRIPT_NAME).include?(key)
78
+ key
38
79
  end
39
- end
40
80
 
41
- def initialize(app)
42
- @app = app
43
- end
81
+ def initialize(app, opts = {})
82
+ @app, @opts = app, opts
83
+ end
44
84
 
45
- def redirect?(status)
46
- status >= 300 && status < 400
47
- end
85
+ def call(env)
86
+ if env['PATH_INFO'] =~ PROXY_PATH
87
+ scheme = scheme_from_env(env)
88
+ host, port = $1.split(':')
89
+ path = $2 || '/'
90
+ port = (port || 80).to_i
48
91
 
49
- def call(env)
50
- if env['PATH_INFO'] =~ proxy_path
51
- host, port = $1.split(':')
52
- path = $2 || '/'
92
+ url = "#{scheme}://#{host}"
53
93
 
54
- running = true
55
- while running
56
- queue = run_request(env, host, ( port || 80 ).to_i, path)
94
+ if port
95
+ if (scheme == 'http' && port != 80) || (scheme == 'https' && port != 443)
96
+ url << ":#{port}"
97
+ end
98
+ end
57
99
 
58
- msg = queue.pop
59
- status = msg[0].to_i
100
+ url << path
60
101
 
61
- if redirect?(status) && location = msg[1]['location']
62
- uri = URI.parse(location)
63
- host, port, path = uri.host, uri.port, uri.path
64
- else
65
- running = false
102
+ if !env['QUERY_STRING'].empty?
103
+ url << "?#{env['QUERY_STRING']}"
66
104
  end
67
- end
68
105
 
69
- if Exception === msg
70
- raise msg
106
+ request = ProxyRequest.new(env, url, @opts)
107
+ request.handle!
108
+
109
+ throw :async
71
110
  else
72
- [ msg[0], msg[1], Body.new(queue, :chunked => chunked?(msg[1])) ]
111
+ @app.call(env)
73
112
  end
74
- else
75
- @app.call(env)
76
113
  end
77
- end
78
114
 
79
- private
115
+ def scheme_from_env(env)
116
+ env['HTTP_X_STROBE_PROXY_PROTOCOL'] || env['rack.url_scheme']
117
+ end
80
118
 
81
- def chunked?(headers)
82
- headers["transfer-encoding"] == "chunked"
83
- end
119
+ class ProxyRequest
120
+ KEEP = [ 'CONTENT_LENGTH', 'CONTENT_TYPE' ]
84
121
 
85
- def proxy_path
86
- %r[/_strobe/proxy/([^/]+)(/.*)?$]
87
- end
122
+ def self.x_strobe_proxy(url, env, opts)
123
+ request = ProxyRequest.new(env, url, opts)
88
124
 
89
- KEEP = [ 'CONTENT_LENGTH', 'CONTENT_TYPE', 'Connection' ]
125
+ request.handle!
90
126
 
91
- def run_request(env, host, port, path)
92
- queue = Queue.new
127
+ throw :async
128
+ end
93
129
 
94
- if env['CONTENT_LENGTH'] || env['HTTP_TRANSFER_ENCODING']
95
- body = env['rack.input']
96
- end
130
+ class DeferrableBody
131
+ include EM::Deferrable
97
132
 
98
- env["Connection"] = "keep-alive"
133
+ def initialize(request)
134
+ @request = request
135
+ end
99
136
 
100
- unless env['QUERY_STRING'].blank?
101
- path += "?#{env['QUERY_STRING']}"
102
- end
137
+ def call(body)
138
+ Array(body).each do |chunk|
139
+ chunk = prepare_chunk(chunk)
140
+ @callback.call(chunk)
141
+ end
142
+ end
103
143
 
104
- http = Net::HTTP.new(host, port)
105
- http.read_timeout = 60
106
- http.open_timeout = 60
144
+ def prepare_chunk(chunk)
145
+ if @request.chunked_response_body?
146
+ size = chunk.respond_to?(:bytesize) ? chunk.bytesize : chunk.length
147
+ "#{size.to_s(16)}\r\n#{chunk}\r\n"
148
+ else
149
+ # Thin doesn't like null bodies
150
+ chunk || ''
151
+ end
152
+ end
107
153
 
108
- request = Net::HTTPGenericRequest.new(
109
- env['REQUEST_METHOD'], !!body, true, path,
110
- env_to_http_headers(env))
154
+ def each(&blk)
155
+ @callback = blk
156
+ end
157
+ end
158
+
159
+ attr_reader :url, :response_headers
160
+
161
+ def initialize(env, url, opts = {})
162
+ @env = env
163
+ @url = url
164
+ @opts = opts
111
165
 
112
- request.body_stream = body if body
166
+ # STATE options
167
+ @http = nil
168
+ @body = nil
169
+ @redirect = false
170
+ @responded = false
171
+ end
113
172
 
114
- Thread.new do
115
- begin
116
- http.request(request) do |response|
117
- hdrs = {}
118
- response.each_header do |name, val|
119
- hdrs[name] = val
173
+ def handle!
174
+ EM.next_tick do
175
+ begin
176
+ conn = EM::HttpRequest.new(url)
177
+
178
+ @http = case request_method
179
+ when 'GET' then conn.get request_options
180
+ when 'POST' then conn.post request_options
181
+ when 'PUT' then conn.put request_options
182
+ when 'DELETE' then conn.delete request_options
183
+ when 'HEAD' then conn.head request_options
184
+ else raise "Unknown HTTP method '#{request_method}'"
185
+ end
186
+
187
+ @http.headers { |hdrs| handle_headers(hdrs) }
188
+ @http.stream { |data| handle_chunk(data) }
189
+ @http.callback { handle_done }
190
+ @http.errback { handle_error }
120
191
  end
192
+ end
193
+ end
121
194
 
122
- queue << [ response.code.to_i, hdrs ]
195
+ def handle_headers(hdrs)
196
+ headers = {}
123
197
 
124
- response.read_body do |chunk|
125
- queue << chunk
198
+ hdrs.each do |key, value|
199
+ key = headerize(key)
200
+ if key == "Set-Cookie"
201
+ headers["Set-Cookie"] = handle_cookies(value)
202
+ else
203
+ headers[key] = value
126
204
  end
205
+ end
206
+
207
+ # Let's simplify things by always chunking the response
208
+ # Also, responses will never be compressed
209
+ headers.delete("Content-Length")
210
+ headers.delete("Content-Encoding")
211
+ headers['Transfer-Encoding'] = 'chunked'
212
+
213
+ @redirect = headers['Location'] && @http.req.follow_redirect?
127
214
 
128
- queue << nil
215
+ unless @redirect
216
+ headers = headers.select { |k,v| !block_headers.include?(k.downcase) }
217
+ headers = headers.merge(extra_headers)
218
+ @response_headers = ::Rack::Utils::HeaderHash.new(headers)
219
+ @body = DeferrableBody.new(self) if has_response_body?
220
+ respond(@http.response_header.status, @response_headers)
129
221
  end
130
- rescue Exception => e
131
- queue << e
132
222
  end
133
- end
134
223
 
135
- queue
136
- end
224
+ def handle_cookies(value)
225
+ cookies = []
226
+ uri = URI.parse(url)
227
+
228
+ case value
229
+ when Array then value.each { |c| cookies << c }
230
+ when Hash then value.each { |_, c| cookies << c }
231
+ else cookies << value
232
+ end
233
+
234
+ result = cookies.flatten.join(",").split(/,(?=[^;,]*=)|,$/).map do |cookie|
235
+ parts = cookie.split(/;+/).map!(&:strip)
137
236
 
138
- def env_to_http_headers(env)
139
- {}.tap do |hdrs|
140
- env.each do |name, val|
141
- next unless name.is_a?(String)
142
- next if name == 'HTTP_HOST'
143
- next unless name =~ /^HTTP_/ || KEEP.include?(name)
237
+ # delete domain and extract path from the cookie
238
+ parts.delete_if { |p| p.start_with?("domain=") }
239
+ path_part = parts.find { |p| p.start_with?("path=") }.tap { |p| parts.delete(p) }
240
+
241
+ if (uri.scheme == "http" && uri.port != 80) || (uri.scheme == "https" && uri.port != 443)
242
+ domain = "#{uri.host}:#{uri.port}"
243
+ else
244
+ domain = uri.host
245
+ end
144
246
 
145
- hdrs[ headerize(name) ] = val
247
+ path = path_part.to_s.split("=", 2)[1] || "/"
248
+
249
+ # add a new path
250
+ parts << "path=/_strobe/proxy/#{domain.sub(/^[.]/, "")}#{path.sub(/\/$/, "")}"
251
+
252
+ parts.join("; ")
253
+ end
254
+
255
+ result.join(", ")
256
+ end
257
+
258
+ def handle_chunk(chunk)
259
+ return if redirect?
260
+ @body.call(chunk) if @body
261
+ end
262
+
263
+ def handle_done
264
+ if has_response_body? && @body
265
+ @body.call ['']
266
+ @body.succeed
267
+ end
268
+ end
269
+
270
+ def handle_error
271
+ unless @responded
272
+ status = @http.response_header.status
273
+ headers = { "Connection" => "close", "Content-Length" => "0" }
274
+ respond(status, headers, "")
275
+ end
276
+ end
277
+
278
+ def respond(status, hdrs, body = @body)
279
+ @responded = true
280
+ @env['async.callback'].call([status, hdrs, body || ['']])
281
+ end
282
+
283
+ def redirect?
284
+ @redirect
285
+ end
286
+
287
+ def has_response_body?
288
+ status = @http.response_header.status
289
+ status >= 200 && status != 204 && status != 304
290
+ end
291
+
292
+ def chunked_response_body?
293
+ has_response_body? && response_headers['Transfer-Encoding'] &&
294
+ response_headers['Transfer-Encoding'].downcase == 'chunked'
146
295
  end
147
- end
148
- end
149
296
 
150
- def headerize(str)
151
- parts = str.gsub(/^HTTP_/, '').split('_')
152
- parts.map! { |p| p.capitalize }.join('-')
297
+ def request_method
298
+ @env['REQUEST_METHOD'].upcase
299
+ end
300
+
301
+ def request_headers
302
+ return @request_headers if @request_headers
303
+ @request_headers = {}
304
+ @env.each do |key, value|
305
+ next unless key.is_a?(String)
306
+ next if key =~ /^HTTP_(HOST|VERSION|X_STROBE_PROXY_PROTOCOL)$/i
307
+ next unless key =~ /^HTTP_/ || KEEP.include?(key)
308
+
309
+ key = headerize(key)
310
+ @request_headers[key] = value
311
+ end
312
+
313
+ if requires_request_body? && !has_request_body?
314
+ @request_headers['Content-Length'] = '0'
315
+ end
316
+
317
+ @request_headers
318
+ end
319
+
320
+ def request_body
321
+ return unless has_request_body?
322
+ return @request_body if @request_body
323
+ @request_body = @env['rack.input'].read
324
+ @env['rack.input'].rewind
325
+ @request_body
326
+ end
327
+
328
+ def requires_request_body?
329
+ ['POST', 'PUT'].include?(@env['REQUEST_METHOD'])
330
+ end
331
+
332
+ def has_request_body?
333
+ @env['CONTENT_LENGTH'] || @env['HTTP_TRANSFER_ENCODING']
334
+ end
335
+
336
+ def request_options
337
+ return @request_options if @request_options
338
+ @request_options = {}
339
+ @request_options[:head] = request_headers
340
+ @request_options[:body] = request_body if has_request_body?
341
+ @request_options[:timeout] = 60
342
+ @request_options[:redirects] = 10
343
+ @request_options
344
+ end
345
+
346
+ def headerize(str)
347
+ parts = str.gsub(/^HTTP_/, '').split('_')
348
+ parts.map! { |p| p.downcase.capitalize }.join('-')
349
+ end
350
+
351
+ def block_cookies?
352
+ @opts[:block_cookies]
353
+ end
354
+
355
+ def block_headers
356
+ @opts[:block_headers] || []
357
+ end
358
+
359
+ def extra_headers
360
+ @opts[:extra_headers] || {}
361
+ end
362
+ end
153
363
  end
154
364
  end
155
365
  end