lib/clavem/authorizer.rb in clavem-1.4.0 vs lib/clavem/authorizer.rb in clavem-2.0.0

- old
+ new

@@ -1,165 +1,124 @@ # encoding: utf-8 # -# This file is part of the clavem gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>. +# This file is part of the clavem gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>. # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php. # Lazier.load!(:object) # A local callback server for oAuth web-flow. module Clavem # Exceptions used by {Authorizer Authorizer}. module Exceptions # This exception is raised whether an error occurs. - class Failure < ::Exception + class Failure < ::StandardError end - - # This exception is raised if the timeout expired. - class Timeout < ::Exception - end - - # This exception is raised if the authorization was denied. - class AuthorizationDenied < ::RuntimeError - end end # A class to handle oAuth authorizations. # # @attribute url # @return [String] The URL where to send the user to start authorization.. # @attribute host # @return [String] The host address on which listening for replies. Default is `localhost`. # @attribute port - # @return [Fixnum] The port on which listening for replies. Default is `2501`. + # @return [Fixnum] The port on which listening for replies. Default is `7772`. # @attribute command # @return [String] The command to open the URL. `{{URL}}` is replaced with the specified URL. Default is `open "{{URL}}"`. - # @attribute title - # @return [String] The title for response template. Default is `Clavem Authorization`. - # @attribute template - # @return [String] Alternative template to show progress in user's browser. # @attribute timeout - # @return [Fixnum] The amount of milliseconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. + # @return [Fixnum] The amount of seconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. # @attribute response_handler - # @return [Proc] A Ruby block to handle response and check for success. @see {#default_response_handler}. + # @return [Proc] A Ruby block to handle response and check for success. The block must accept a querystring hash (which all values are arrays) and return a token or `nil` if the authentication was denied. # @attribute token # @return [String] The token obtained by the remote endpoint. - # @attribute token - # @return [Symbol] The status of the request. Can be `:success`, `:denied`, `:failure` and `:waiting`. - # @attribute localizer + # @attribute status + # @return [Symbol] The status of the request. Can be `:succeeded`, `:denied`, `:failed` and `:waiting`. + # @attribute :i18n # @return [R18N::Translation] A localizer object. class Authorizer include R18n::Helpers attr_accessor :url attr_accessor :host attr_accessor :port attr_accessor :command - attr_accessor :title - attr_accessor :template attr_accessor :timeout attr_accessor :response_handler attr_accessor :token attr_accessor :status attr_accessor :i18n # Returns a unique (singleton) instance of the authorizer. # # @param host [String] The host address on which listening for replies. Default is `localhost`. - # @param port [Fixnum] The port on which listening for replies. Default is `2501`. + # @param port [Fixnum] The port on which listening for replies. Default is `7772`. # @param command [String|nil] The command to open the URL. `{{URL}}` is replaced with the specified URL. Default is `open "{{URL}}"`. - # @param title [String|nil] The title for response template. Default is `Clavem Authorization` - # @param template [String|nil] Alternative template to show progress in user's browser. - # @param timeout [Fixnum] The amount of milliseconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. - # @param response_handler [Proc] A Ruby block to handle response and check for success. See {#default_response_handler}. + # @param timeout [Fixnum] The amount of seconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. + # @param response_handler [Proc] A Ruby block to handle response and check for success. See {#response_handler}. # @param force [Boolean] If to force recreation of the instance. # @return [Authorizer] The unique (singleton) instance of the authorizer. - def self.instance(host = "localhost", port = 2501, command = nil, title = nil, template = nil, timeout = 0, force = false, &response_handler) + def self.instance(host = "localhost", port = 7772, command = nil, timeout = 0, force = false, &response_handler) @instance = nil if force - @instance ||= Clavem::Authorizer.new(host, port, command, title, template, timeout, &response_handler) + @instance ||= Clavem::Authorizer.new(host, port, command, timeout, &response_handler) @instance end # Creates a new authorizer. # # @param host [String] The host address on which listening for replies. Default is `localhost`. - # @param port [Fixnum] The port on which listening for replies. Default is `2501`. + # @param port [Fixnum] The port on which listening for replies. Default is `7772`. # @param command [String|nil] The command to open the URL. `{{URL}}` is replaced with the specified URL. Default is `open "{{URL}}"`. - # @param title [String|nil] The title for response template. Default is `Clavem Authorization` - # @param template [String|nil] Alternative template to show progress in user's browser. - # @param timeout [Fixnum] The amount of milliseconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. - # @param response_handler [Proc] A Ruby block to handle response and check for success. See {#default_response_handler}. + # @param timeout [Fixnum] The amount of seconds to wait for response from the remote endpoint before returning a failure. Default is `0`, which means *disabled*. + # @param response_handler [Proc] A Ruby block to handle response and check for success. See {#response_handler}. # @return [Authorizer] The new authorizer. - def initialize(host = "localhost", port = 2501, command = nil, title = nil, template = nil, timeout = 0, &response_handler) + def initialize(host = "localhost", port = 7772, command = nil, timeout = 0, &response_handler) @i18n = self.localize - @host = host.ensure_string @port = port.to_integer @command = command.ensure_string - @title = title.ensure_string - @template = template.ensure_string @timeout = timeout.to_integer @response_handler = response_handler - - sanitize_arguments - @token = nil @status = :waiting - @compiled_template ||= ::ERB.new(@template) - @timeout_expired = false - @timeout_thread = nil + sanitize_arguments self end # Starts the authorization flow. # # @param url [String] The URL where to send the user to start authorization. - # @return [Authorizer] The authorizer. - def authorize(url) + # @param append_callback [Boolean] If to append the callback to the url using `oauth_callback` parameter. + # @return [Boolean] `true` if authorization succeeded, `false` otherwise. + def authorize(url, append_callback = true) + url += "#{url.index("?") ? "&" : "?"}oauth_callback=#{callback_url}" if append_callback @url = url @status = :waiting + @token = nil begin - # Setup stuff - setup_webserver - setup_interruptions_handling - setup_timeout_handling - - # Open the endpoint then start the server - open_endpoint - @server.start - - raise Clavem::Exceptions::Timeout.new if @timeout_expired - rescue Clavem::Exceptions::Timeout => t - @status = :failure - raise t + perform_request + process_response rescue => e - @status = :failure + @status = :failed raise Clavem::Exceptions::Failure.new(@i18n.errors.response_failure(e.to_s)) - ensure - cleanup end - raise Clavem::Exceptions::AuthorizationDenied.new if @status == :denied - self + succeeded? end # Returns the callback_url for this authorizer. # # @return [String] The callback_url for this authorizer. def callback_url "http://#{host}:#{port}/" end - # Handles a response from the remote endpoint. + # Returns the response handler for the authorizer. # - # @param [Authorizer] authorizer The current authorizer. - # @param [WEBrick::HTTPRequest] request The request that the remote endpoint made to notify authorization status. - # @param [WEBrick::HTTPResponse] response The request to send to the browser. - # @return [String|nil] The oAuth access token. Returning `nil` means *authorization denied*. - def default_response_handler(authorizer, request, response) - request.query['oauth_token'].ensure_string + def response_handler + @response_handler || Proc.new {|querystring| (querystring || {}).fetch("oauth_token", []).ensure_array.first } end # Set the current locale for messages. # # @param locale [String] The new locale. Default is the current system locale. @@ -167,77 +126,72 @@ def localize(locale = nil) @i18n_locales_path ||= ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/") R18n::I18n.new([locale || :en, ENV["LANG"], R18n::I18n.system_locale].compact, @i18n_locales_path).t.clavem end + # Checks if authentication succeeded. + # + # @return [Boolean] `true` if authorization succeeded, `false otherwise`. + def succeeded? + @status == :succeeded + end + + # Checks if authentication was denied. + # + # @return [Boolean] `true` if authorization was denied, `false otherwise`. + def denied? + @status == :denied + end + + # Checks if authentication failed (which means that some error occurred). + # + # @return [Boolean] `true` if authorization failed, `false otherwise`. + def failed? + @status == :failed + end + + # Checks if authentication is still pending. + # + # @return [Boolean] `true` if authorization is still pending, `false otherwise`. + def waiting? + @status == :waiting + end + private # sanitize_arguments def sanitize_arguments @host = "localhost" if @host.blank? - @port = 2501 if @port.to_integer < 1 + @port = 7772 if @port.to_integer < 1 @command = "open \"{{URL}}\"" if @command.blank? - @title = @i18n.default_title if @title.blank? - @template = File.read(File.dirname(__FILE__) + "/template.html.erb") if @template.blank? @timeout = 0 if @timeout < 0 end - # Open the remote endpoint - def open_endpoint + # Performs the authentication request. + def perform_request # Open the oAuth endpoint into the browser begin Kernel.system(@command.gsub("{{URL}}", @url.ensure_string)) rescue => e raise Clavem::Exceptions::Failure.new(@i18n.errors.open_failure(@url.ensure_string, e.to_s)) end end - # Handle interruptions for the process. - def setup_interruptions_handling - ["USR2", "INT", "TERM", "KILL"].each {|signal| Kernel.trap(signal){ @server.shutdown if @server } } - end + # Processes the authentication response. + def process_response + # Signal handling + ["INT", "TERM", "KILL"].each {|signal| + Kernel.trap(signal){ EM.stop } + } - # Handle timeout for the response. - def setup_timeout_handling - if @timeout > 0 then - @timeout_thread = Thread.new do - Kernel.sleep(@timeout.to_f / 1000) - @timeout_expired = true - Process.kill("USR2", 0) - end + # Start eventmachine + EM.run do + EM.add_timer(@timeout){ handle_timeout } if @timeout > 0 + EM.start_server(@host, @port, Clavem::Server, self) end end - # Prepare the webserver for handling the response. - def setup_webserver - @server = ::WEBrick::HTTPServer.new(BindAddress: @host, Port: @port, Logger: WEBrick::Log.new("/dev/null"), AccessLog: [nil, nil]) - @server.mount_proc("/"){ |request, response| dispatch_request(request, response) } - end - - # Handles a response from the remote endpoint. - # - # @param [WEBrick::HTTPRequest] request The request that the remote endpoint made to notify authorization status. - # @param [WEBrick::HTTPResponse] response The request to send to the browser. - def dispatch_request(request, response) - @token = @response_handler ? @response_handler.call(self, request, response) : default_response_handler(self, request, response) - - if @status == :waiting then - if @token.present? then - @status = :success - response.status = 200 - else - @status = :denied - response.status = 403 - end - - response.body = @compiled_template.result(binding) - @server.shutdown - end - end - - # Cleans up resources - def cleanup - @timeout_thread.exit if @timeout_thread - @server.shutdown if @server - ["USR2", "INT", "TERM", "KILL"].each {|signal| Kernel.trap(signal, "DEFAULT") } + # Handle timeouts. + def handle_timeout + EM.stop end end end \ No newline at end of file