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