class SC::Rack::Proxy
Rack
application proxies requests as needed for the given project.
Public Class Methods
new(project)
click to toggle source
# File lib/sproutcore/rack/proxy.rb, line 85 def initialize(project) @project = project @proxies = project.buildfile.proxies end
Public Instance Methods
call(env)
click to toggle source
# File lib/sproutcore/rack/proxy.rb, line 90 def call(env) path = env['PATH_INFO'] @proxies.each do |proxy, value| # If the url matches a proxied url, handle it if path.match(/^#{Regexp.escape(proxy.to_s)}/) handle_proxy(value, proxy.to_s, env) # Don't block waiting for a response throw :async end end return [404, {}, "not found"] end
chunked?(headers)
click to toggle source
# File lib/sproutcore/rack/proxy.rb, line 106 def chunked?(headers) headers['Transfer-Encoding'] == "chunked" end
handle_proxy(proxy, proxy_url, env)
click to toggle source
# File lib/sproutcore/rack/proxy.rb, line 110 def handle_proxy(proxy, proxy_url, env) if proxy[:secure] && !SC::HTTPS_ENABLED SC.logger << "~ WARNING: HTTPS is not supported on your system, using HTTP instead.\n" SC.logger << " If you are using Ubuntu, you can run `apt-get install libopenssl-ruby`\n" proxy[:secure] = false end headers = request_headers(env, proxy) # ex. {"Host"=>"localhost:4020", "Connection"=>"... path = env['PATH_INFO'] # ex. /contacts params = env['QUERY_STRING'] # ex. since=yesterday&unread=true # Switch to https if proxy[:secure] configured protocol = proxy[:secure] ? 'https' : 'http' # Adjust the path if proxy[:url] configured if proxy[:url] path = path.sub(/^#{Regexp.escape proxy_url}/, proxy[:url]) end # The endpoint URL url = protocol + '://' + proxy[:to] url += path unless path.empty? url += '?' + params unless params.empty? if env['CONTENT_LENGTH'] || env['HTTP_TRANSFER_ENCODING'] req_body = env['rack.input'] req_body.rewind # May not be necessary but can't hurt req_body = req_body.read end # Options for the connection connect_options = {} if proxy[:inactivity_timeout] # allow the more verbose setting to take precedence connect_options[:inactivity_timeout] = proxy[:inactivity_timeout] elsif proxy[:timeout] # check the legacy and simpler setting connect_options[:inactivity_timeout] = proxy[:timeout] end connect_options[:connect_timeout] = proxy[:connect_timeout] if proxy[:connect_timeout] # Options for the request request_options = {} request_options[:head] = headers request_options[:body] = req_body if !!req_body request_options[:redirects] = 5 if proxy[:redirect] != false request_options[:decoding] = false # don't decode gzipped content EventMachine.run { body = nil conn = EM::HttpRequest.new(url, connect_options) chunked = false headers = {} method = env['REQUEST_METHOD'].upcase status = 0 conn.use RedirectHostHeaderKiller case method when 'GET' http = conn.get request_options when 'POST' http = conn.post request_options when 'PUT' http = conn.put request_options when 'DELETE' http = conn.delete request_options else http = conn.head request_options end # Received error http.errback { status = http.response_header.status path = env['PATH_INFO'] url = http.last_effective_url SC.logger << "~ PROXY FAILED: #{method} #{path} -> #{status} #{url}\n" # If a body has been sent use it, otherwise respond with generic message if !body body = "Unable to proxy to #{url}. Received status: #{status}" size = body.respond_to?(:bytesize) ? body.bytesize : body.length headers = { 'Content-Length' => size.to_s } body = [body] end env['async.callback'].call [502, headers, body] } # Received response http.callback { # Too many redirects if redirect? status body = "Unable to proxy to #{url}. Too many redirects." size = body.respond_to?(:bytesize) ? body.bytesize : body.length headers = { 'Content-Length' => size.to_s } env['async.callback'].call [502, headers, [body]] else # Terminate the deferred body (which may have been chunked) if body body.call [''] body.succeed end # Log the initial path and the final url path = env['PATH_INFO'] url = http.last_effective_url SC.logger << "~ PROXY: #{method} #{path} -> #{status} #{url}\n" end } # Received headers http.headers { |hash| status = http.response_header.status headers = response_headers(hash) # Don't respond on redirection, but fail out on bad redirects if redirect? status if status == 304 env["async.callback"].call [status, headers, ['']] SC.logger << "~ PROXY: #{method} #{path} -> #{status} #{url}\n" elsif !headers['Location'] body = "Unable to proxy to #{url}. Received redirect with no Location." size = body.respond_to?(:bytesize) ? body.bytesize : body.length headers = { 'Content-Length' => size.to_s } http.close end else # Stream the body right across in the format it was sent chunked = chunked?(headers) body = DeferrableBody.new({ :chunked => chunked }) # Start responding to the client immediately env["async.callback"].call [status, headers, body] end } # Received chunk of data http.stream { |chunk| # Ignore body of redirects if !redirect? status body.call [chunk] end } # If the client disconnects early, make sure we close our other connection too # TODO: this is waiting for changes not yet available in em-http # Test with: curl http://0.0.0.0:4020/stream.twitter.com/1/statuses/sample.json -uTWITTER_USERNAME:TWITTER_PASSWORD # env["async.close"].callback { # conn.close # } } end
headerize(str)
click to toggle source
remove HTTP_, dasherize and titleize
# File lib/sproutcore/rack/proxy.rb, line 337 def headerize(str) parts = str.gsub(/^HTTP_/, '').split('_') parts.map! { |p| p.capitalize }.join('-') end
redirect?(status)
click to toggle source
# File lib/sproutcore/rack/proxy.rb, line 270 def redirect?(status) status >= 300 && status < 400 end
request_headers(env, proxy)
click to toggle source
collect headers…
# File lib/sproutcore/rack/proxy.rb, line 275 def request_headers(env, proxy) result = {} env.each do |key, value| next unless key =~ /^HTTP_/ key = headerize(key) if !key.eql? "Version" result[key] = value end end # Rack documentation says CONTENT_TYPE and CONTENT_LENGTH aren't prefixed by HTTP_ result['Content-Type'] = env['CONTENT_TYPE'] if env['CONTENT_TYPE'] length = env['CONTENT_LENGTH'] result['Content-Length'] = length if length # added 4/23/09 per Charles Jolley, corrects problem # when making requests to virtual hosts result['Host'] = proxy[:to] result end
response_headers(hash)
click to toggle source
construct and display specific response headers
# File lib/sproutcore/rack/proxy.rb, line 300 def response_headers(hash) result = {} hash.each do |key, value| key = headerize(key) # Because Set-Cookie header can appear more the once in the response body, # but Rack only accepts a hash of headers, we store it in a line break separated string # for Ruby 1.9 and as an Array for Ruby 1.8 # See http://groups.google.com/group/rack-devel/browse_thread/thread/e8759b91a82c5a10/a8dbd4574fe97d69?#a8dbd4574fe97d69 if key.downcase == 'set-cookie' cookies = [] case value when Array then value.each { |c| cookies << strip_domain(c) } when Hash then value.each { |_, c| cookies << strip_domain(c) } else cookies << strip_domain(value) end # Remove nil values result['Set-Cookie'] = [result['Set-Cookie'], cookies].compact if Thin.ruby_18? result['Set-Cookie'].flatten! else result['Set-Cookie'] = result['Set-Cookie'].join("\n") end end SC.logger << " #{key}: #{value}\n" result[key] = value end ::Rack::Utils::HeaderHash.new(result) end
strip_domain(cookie)
click to toggle source
Strip out the domain of passed in cookie. This technically may break certain scenarios where services try to set cross-domain cookies, but those services should not be doing that anyway…
# File lib/sproutcore/rack/proxy.rb, line 345 def strip_domain(cookie) cookie.to_s.gsub!(/domain=[^\;]+\;? ?/,'') end