Permalink
Browse files

Separated the differnet parts of the app into their own files. Accord…

…ing to unit tests it's able to perform proxy requests and queries memcached for the presence of a proxy endpoint for a given url. If present it will perform the request and inform the proxy location by way of HTTP headers
  • Loading branch information...
tobi authored and Tobias Lütke committed May 10, 2009
1 parent e9998ed commit 3d38548b4ce4a4d702137830385b2d973e037c8e
Showing with 377 additions and 37 deletions.
  1. +15 −0 Rakefile
  2. +28 −37 api_proxy.rb
  3. +9 −0 lib/crc32.rb
  4. +81 −0 lib/proxy.rb
  5. +74 −0 lib/proxy_endpoint.rb
  6. +33 −0 lib/request.rb
  7. +8 −0 logs/dump.log
  8. +78 −0 test/proxy_endpoint_test.rb
  9. +51 −0 test/request_test.rb
View
@@ -0,0 +1,15 @@
+# Add your own tasks in files placed in config/tasks ending in .rake,
+# for example config/tasks/switchtower.rake, and they will automatically be available to Rake.
+
+require 'rake'
+require 'rake/testtask'
+
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList['test/*_test.rb']
+ t.verbose = true
+end
+
+task :default => ['test']
View
@@ -1,67 +1,58 @@
-require 'lib/em-proxy'
+require 'lib/em-proxy'
+require 'lib/request'
+require 'lib/proxy_endpoint'
require 'memcached'
require 'httparty'
require 'zlib'
-$cache = Memcached.new('localhost:11211')
+$cache = Memcached.new('localhost:11211')
-class Request
- Command = /(GET|POST|PUT|HEAD|DELETE) (\/\S*)\r\n/
- Host = /^(Host\: .*\r\n)/
-
- attr_accessor :data, :method, :path
- def initialize(data)
- @data = data
- _, @method, @path = *Command.scan(data)
- end
+class Util
+
+ def self.generate_key
+ "proxy-content/#{crc32([Time.now, rand, @uri].join)}"
+ end
- def add_header(name, value)
- @data.sub(Host, "#{$1}#{name}: #{value}\r\n")
- end
-end
-
-def Proxy
- def initialize(url)
- @body = url
+ def self.crc32(content)
+ Zlib.crc32(content, 0)
end
- def forward_request
- Net::HTTP.
- end
-
- def crc32
- Zlib.crc32(@body, 0)
- end
end
-
- # /proxy/*
Proxy.start(:host => "0.0.0.0", :port => 3005) do |conn|
- conn.server :shopify, :host => "127.0.0.1", :port => 80
-
- # put <pri> <delay> <ttr> <bytes>\r\n
+ conn.server :shopify, :host => "127.0.0.1", :port => 80
conn.on_data do |data|
- request = Request.new(data)
- if request.path =~ /^\/proxy/
- proxy = Proxy.new(request.request_uri)
+ request = Request.new(data)
+
+ if request.path =~ /^\/proxy/
+
+
+ forwarder = Forwarder.new
+ forwarder.forward(request)
+
+ proxy = ProxyEndpoint.new(request.request_uri)
if proxy.available?
- proxy.forward_request
+ proxy.forward(request)
+
+ cache_key = "proxy-content/#{proxy.content.crc32}"
- $cache.set proxy.cache_key, proxy.content
+ $cache.set cache_key, proxy.content
+ request.add_header('X-Proxy-Content', cache_key)
request.add_header('X-Proxy-Status', proxy.status)
- request.add_header('X-Proxy-Content', proxy.cache_key)
request.data
end
request.data
+ else
+ # ...
end
end
View
@@ -0,0 +1,9 @@
+def crc32
+ Zlib.crc32(@body, 0)
+end
+
+def String
+ def crc32
+ Kernel.crc32(self)
+ end
+end
View
@@ -0,0 +1,81 @@
+require 'zlib'
+require 'net/http'
+require 'net/https'
+
+def Proxy
+ attr_accessor :content, :status
+
+ class Error < StandardError
+ end
+
+ class ConnectionError < Error; end
+ class TimeoutError < Error; end
+ class MethodNotAllowed < Error; end
+
+ def initialize(uri)
+ @uri = uri
+ end
+
+ def proxy_request_url
+ @proxy_request_url ||= begin
+
+ path = @uri.path.split('/')[0..1].join('/')
+
+ "#{@uri.protocol}://#{@uri.hostname}/#{path}"
+ end
+ end
+
+ def endpoint
+ @endpoint ||= $cache.get("proxy/#{proxy_root_url}")
+ end
+
+ def available?
+ endpoint
+ end
+
+ def forward_request
+ @endpoint = proxy_root_url
+
+ http = Net::HTTP.new(@uri.host, @uri.port)
+ http.open_timeout = 10
+ http.read_timeout = 10
+
+ if http.use_ssl = (@uri.scheme == 'https')
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+
+ headers = { 'X-Shopify-Proxied-For' => shop.local_domain.host }
+
+ response = case request.method
+ when :get
+ http.get(@uri.request_uri, headers)
+ when :post
+ data = request.raw_post
+ headers['Content-Type'] = request.content_type.to_s
+ headers['Content-Length'] = data.length.to_s
+ http.post(@uri.request_uri, data, headers)
+ else
+ raise MethodNotAllowed.new(api_client)
+ end
+
+ if (200..299).include?(response.code.to_i)
+ response.body
+ else
+ raise RequestError.new(api_client, response)
+ end
+
+ self.content = response.body
+ self.status = response.status
+
+ rescue EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED => e
+ raise ConnectionError.new(api_client)
+ rescue Timeout::Error, Errno::ETIMEDOUT => e
+ raise TimeoutError.new(api_client)
+ end
+
+ def crc32
+ Zlib.crc32(@body, 0)
+ end
+end
+
+
View
@@ -0,0 +1,74 @@
+require 'zlib'
+require 'net/http'
+require 'net/https'
+
+class ProxyEndpoint
+ attr_accessor :uri, :content, :status, :proxy_root
+
+ class Error < StandardError; end
+ class ConnectionError < Error; end
+ class TimeoutError < Error; end
+ class MethodNotAllowed < Error; end
+
+ ProxyRoot = /https?\:\/\/.*?\/[\w_-]+\/[\w_-]+/
+
+ def initialize(uri)
+ @uri = uri
+ @proxy_root = uri.scan(ProxyRoot).flatten.first
+
+ if @proxy_root.nil?
+ raise Error, "could not find proxy root for url"
+ end
+ end
+
+ def endpoint_location
+ @endpoint_location ||= $cache.get("proxy/#{proxy_root}")
+ end
+
+ def available?
+ !endpoint_location.nil?
+ end
+
+ def forward(request)
+ uri = URI.parse(endpoint_location)
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.open_timeout = 10
+ http.read_timeout = 10
+
+ if http.use_ssl = (uri.scheme == 'https')
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+
+ #headers = { 'X-Shopify-Proxied-For' => shop.local_domain.host }
+ headers = {}
+
+ response = case request.method
+ when 'GET'
+ http.get(uri.request_uri, headers)
+ when 'POST'
+ data = request.raw_content
+ headers['Content-Type'] = request.content_type || 'application/html'
+ headers['Content-Length'] = data.length.to_s
+ http.post(uri.request_uri, data, headers)
+ else
+ raise MethodNotAllowed.new(api_client)
+ end
+
+ if (200..299).include?(response.code.to_i)
+ response.body
+ else
+ raise RequestError.new(api_client, response)
+ end
+
+ self.content = response.body
+ self.status = response.code
+
+ rescue EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED => e
+ raise ConnectionError.new(api_client)
+ rescue Timeout::Error, Errno::ETIMEDOUT => e
+ raise TimeoutError.new(api_client)
+ end
+end
+
+
View
@@ -0,0 +1,33 @@
+
+class Request
+ Command = /(GET|POST) (\/\S*)/
+ Host = /^(Host\: .*)$/
+ Headers = /^([\w\-]+)\:(.*)$/
+ Content = /\r\n\r\n(.*)/m
+
+ attr_reader :headers, :data, :method, :path
+
+ def initialize(data)
+ @data = data
+ @method, @path = data.scan(Command).first
+ end
+
+ def content_type
+ headers['Content-Type']
+ end
+
+ def headers
+ @headers ||= @data.scan(Headers).inject(Hash.new) do |hash, (k, v)|
+ hash[k] = v.strip; hash
+ end
+ end
+
+ def add_header(name, value)
+ @headers = nil
+ @data.sub!(Host, "\\1\n#{name}: #{value}\r")
+ end
+
+ def raw_content
+ @data.scan(Content).flatten.first
+ end
+end
View
@@ -0,0 +1,8 @@
+GET / HTTP/1.1
+User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1
+Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
+Accept-Language: en-us
+Accept-Encoding: gzip, deflate
+Connection: keep-alive
+Host: localhost:3005
+
Oops, something went wrong.

0 comments on commit 3d38548

Please sign in to comment.