require "social_avatar_proxy/config" require "social_avatar_proxy/facebook_avatar" require "social_avatar_proxy/twitter_avatar" require "social_avatar_proxy/routes" require "social_avatar_proxy/timeout_error" require "social_avatar_proxy/too_many_redirects_error" require "rack" module SocialAvatarProxy class App def self.call(env, options = {}) new(options).call(env) end def self.routes(options = {}) new(options).routes end attr_reader :options, :request def initialize(options = {}) @options = { expires: 86400, # 1 day from now cache_control: { max_age: 86400, # store for 1 day, after that re-request max_stale: 86400, # allow cache to be a day stale public: true # allow proxy caching } }.merge(options) end def call(env) @request = Rack::Request.new(env) begin response.finish rescue TimeoutError => e timeout rescue TooManyRedirectsError => e bad_gateway end end def path_prefix (options[:path_prefix] || (request && request.env["SCRIPT_NAME"]) || "").gsub(/\/$/, "") end def response # ensure this is a valid avatar request unless request.path =~ /^#{path_prefix}\/(facebook|twitter)\/([\w\-\.]+)(\.(jpe?g|png|gif|bmp))?$/i return not_found end # load the avatar avatar = load_avatar($1, $2) # if it exists if avatar.exist? # render the avatar to the response Rack::Response.new do |response| # set the response data response.write(avatar.body) # set the response headers response = set_avatar_headers(response, avatar) response = set_caching_headers(response) # return the response response end # if the avatar doesn't exist else not_found end end def not_found Rack::Response.new([], 404) do |response| # if we have a custom default image if Config.default_image render_default_image(response) # without a default image else response.write "Not Found" end end end def timeout Rack::Response.new([], 504) do |response| # if we have a custom default image if Config.default_image render_default_image(response) # without a default image else response.write "Remote server timeout" end end end def bad_gateway Rack::Response.new([], 502) do |response| # if we have a custom default image if Config.default_image render_default_image(response) # without a default image else response.write "Bad response from remote server" end end end def routes Routes.new(self) end private def render_default_image(response) # render the image response.write(Config.default_image_data) # set the content type response["Content-Type"] = Config.default_image_content_type # set expiry response = set_caching_headers(response) # return the response response end def set_avatar_headers(response, avatar) # set the last modified header response["Last-Modified"] = avatar.last_modified # set the content type header response["Content-Type"] = avatar.content_type # return the response response end def set_caching_headers(response) # if we want to expire in a set time, calculate the header if options[:expires] response["Expires"] = (Time.now + options[:expires]).httpdate end # if we want to set cache control settings if cc = options[:cache_control] directives = [] directives << "no-cache" if cc[:no_cache] directives << "max-stale=#{cc[:max_stale]}" if cc[:max_stale] directives << "max-age=#{cc[:max_age]}" if cc[:max_age] directives << (cc[:public] ? "public" : "private") response["Cache-Control"] = directives.join(", ") end # return the response response end def load_avatar(service, id) # titleize the service name service = service.gsub(/[\_\-]/, " ").gsub(/\b([a-z])/) do |match| match.upcase end # pass it onto the SocialAvatarProxy.const_get("#{service}Avatar").new(id) end end end