strobe 0.1.6 → 0.2.0.beta.1

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