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.
- data/lib/strobe.rb +32 -16
- data/lib/strobe/addons/social.rb +130 -0
- data/lib/strobe/cli.rb +20 -12
- data/lib/strobe/cli/main.rb +53 -11
- data/lib/strobe/cli/preview.rb +4 -2
- data/lib/strobe/cli/settings.rb +95 -42
- data/lib/strobe/cli/users.rb +4 -2
- data/lib/strobe/collection.rb +1 -1
- data/lib/strobe/config.rb +181 -0
- data/lib/strobe/connection.rb +58 -19
- data/lib/strobe/exception_notifier.rb +59 -0
- data/lib/strobe/identity_map.rb +4 -2
- data/lib/strobe/middleware/addons.rb +73 -0
- data/lib/strobe/middleware/proxy.rb +315 -105
- data/lib/strobe/middleware/rewrite.rb +14 -1
- data/lib/strobe/resource/base.rb +2 -2
- data/lib/strobe/resources.rb +1 -0
- data/lib/strobe/resources/application.rb +10 -1
- data/lib/strobe/resources/deploy.rb +14 -0
- data/lib/strobe/sproutcore.rb +2 -1
- metadata +46 -8
@@ -1,155 +1,365 @@
|
|
1
|
-
require '
|
2
|
-
require 'net/http'
|
1
|
+
require 'em-http'
|
3
2
|
|
4
3
|
module Strobe
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
37
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
81
|
+
def initialize(app, opts = {})
|
82
|
+
@app, @opts = app, opts
|
83
|
+
end
|
44
84
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
if env['PATH_INFO'] =~ proxy_path
|
51
|
-
host, port = $1.split(':')
|
52
|
-
path = $2 || '/'
|
92
|
+
url = "#{scheme}://#{host}"
|
53
93
|
|
54
|
-
|
55
|
-
|
56
|
-
|
94
|
+
if port
|
95
|
+
if (scheme == 'http' && port != 80) || (scheme == 'https' && port != 443)
|
96
|
+
url << ":#{port}"
|
97
|
+
end
|
98
|
+
end
|
57
99
|
|
58
|
-
|
59
|
-
status = msg[0].to_i
|
100
|
+
url << path
|
60
101
|
|
61
|
-
if
|
62
|
-
|
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
|
-
|
70
|
-
|
106
|
+
request = ProxyRequest.new(env, url, @opts)
|
107
|
+
request.handle!
|
108
|
+
|
109
|
+
throw :async
|
71
110
|
else
|
72
|
-
|
111
|
+
@app.call(env)
|
73
112
|
end
|
74
|
-
else
|
75
|
-
@app.call(env)
|
76
113
|
end
|
77
|
-
end
|
78
114
|
|
79
|
-
|
115
|
+
def scheme_from_env(env)
|
116
|
+
env['HTTP_X_STROBE_PROXY_PROTOCOL'] || env['rack.url_scheme']
|
117
|
+
end
|
80
118
|
|
81
|
-
|
82
|
-
|
83
|
-
end
|
119
|
+
class ProxyRequest
|
120
|
+
KEEP = [ 'CONTENT_LENGTH', 'CONTENT_TYPE' ]
|
84
121
|
|
85
|
-
|
86
|
-
|
87
|
-
end
|
122
|
+
def self.x_strobe_proxy(url, env, opts)
|
123
|
+
request = ProxyRequest.new(env, url, opts)
|
88
124
|
|
89
|
-
|
125
|
+
request.handle!
|
90
126
|
|
91
|
-
|
92
|
-
|
127
|
+
throw :async
|
128
|
+
end
|
93
129
|
|
94
|
-
|
95
|
-
|
96
|
-
end
|
130
|
+
class DeferrableBody
|
131
|
+
include EM::Deferrable
|
97
132
|
|
98
|
-
|
133
|
+
def initialize(request)
|
134
|
+
@request = request
|
135
|
+
end
|
99
136
|
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
166
|
+
# STATE options
|
167
|
+
@http = nil
|
168
|
+
@body = nil
|
169
|
+
@redirect = false
|
170
|
+
@responded = false
|
171
|
+
end
|
113
172
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
195
|
+
def handle_headers(hdrs)
|
196
|
+
headers = {}
|
123
197
|
|
124
|
-
|
125
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
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
|