module Twurl class OAuthClient class << self def rcfile(reload = false) if reload || @rcfile.nil? @rcfile = RCFile.new end @rcfile end def load_from_options(options) if options.command == 'request' && has_oauth_options?(options) load_new_client_from_oauth_options(options) elsif options.command == 'request' && options.app_only && options.consumer_key load_client_for_non_profile_app_only_auth(options) elsif rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key) load_client_for_username_and_consumer_key(options.username, options.consumer_key) elsif options.username load_client_for_username(options.username) elsif options.command == 'authorize' && options.app_only load_client_for_app_only_auth(options, options.consumer_key) elsif options.command == 'authorize' load_new_client_from_options(options) else load_default_client(options) end end def has_oauth_options?(options) (options.consumer_key && options.consumer_secret && options.access_token && options.token_secret) ? true : false end def load_client_for_username_and_consumer_key(username, consumer_key) user_profiles = rcfile[username] if user_profiles && attributes = user_profiles[consumer_key] new(attributes) else raise Exception, "No profile for #{username}" end end def load_client_for_username(username) if user_profiles = rcfile[username] if user_profiles.values.size == 1 new(user_profiles.values.first) else raise Exception, "There is more than one consumer key associated with #{username}. Please specify which consumer key you want as well." end else raise Exception, "No profile for #{username}" end end def load_new_client_from_options(options) new(options.oauth_client_options) end def load_new_client_from_oauth_options(options) new(options.oauth_client_options.merge( 'token' => options.access_token, 'secret' => options.token_secret ) ) end def load_client_for_app_only_auth(options, consumer_key) if options.command == 'authorize' AppOnlyOAuthClient.new(options) else AppOnlyOAuthClient.new( options.oauth_client_options.merge( 'bearer_token' => rcfile.bearer_tokens.to_hash[consumer_key] ) ) end end def load_client_for_non_profile_app_only_auth(options) AppOnlyOAuthClient.new( options.oauth_client_options.merge( 'bearer_token' => rcfile.bearer_tokens.to_hash[options.consumer_key] ) ) end def load_default_client(options) return if options.command == 'bearer_tokens' exception_message = "You must authorize first." app_only_exception_message = "To use --bearer option, you need to authorize (OAuth1.0a) and create at least one user profile (~/.twurlrc):\n\n" \ "twurl authorize -c key -s secret\n" \ "\nor, you can specify issued token's consumer_key directly:\n" \ "(to see your issued tokens: 'twurl bearer_tokens')\n\n" \ "twurl --bearer -c key '/path/to/api'" raise Exception, "#{options.app_only ? app_only_exception_message : exception_message}" unless rcfile.default_profile if options.app_only raise Exception, "No available bearer token found for consumer_key:#{rcfile.default_profile_consumer_key}" \ unless rcfile.has_bearer_token_for_consumer_key?(rcfile.default_profile_consumer_key) load_client_for_app_only_auth(options, rcfile.default_profile_consumer_key) else load_client_for_username_and_consumer_key(*rcfile.default_profile) end end end OAUTH_CLIENT_OPTIONS = %w[username consumer_key consumer_secret token secret] attr_reader *OAUTH_CLIENT_OPTIONS attr_reader :username def initialize(options = {}) @username = options['username'] @consumer_key = options['consumer_key'] @consumer_secret = options['consumer_secret'] @token = options['token'] @secret = options['secret'] configure_http! end METHODS = { :post => Net::HTTP::Post, :get => Net::HTTP::Get, :put => Net::HTTP::Put, :delete => Net::HTTP::Delete, :options => Net::HTTP::Options, :head => Net::HTTP::Head, :copy => Net::HTTP::Copy } def build_request_from_options(options, &block) request_class = METHODS.fetch(options.request_method.to_sym) request = request_class.new(options.path, options.headers) if options.upload && options.upload['file'].count > 0 boundary = "00Twurl" + rand(1000000000000000000).to_s + "lruwT99" multipart_body = [] file_field = options.upload['filefield'] ? options.upload['filefield'] : 'media[]' options.data.each {|key, value| multipart_body << "--#{boundary}\r\n" multipart_body << "Content-Disposition: form-data; name=\"#{key}\"\r\n" multipart_body << "\r\n" multipart_body << value multipart_body << "\r\n" } options.upload['file'].each {|filename| multipart_body << "--#{boundary}\r\n" multipart_body << "Content-Disposition: form-data; name=\"#{file_field}\"; filename=\"#{File.basename(filename)}\"\r\n" multipart_body << "Content-Type: application/octet-stream\r\n" multipart_body << "Content-Transfer-Encoding: base64\r\n" if options.upload['base64'] multipart_body << "\r\n" if options.upload['base64'] enc = Base64.encode64(File.binread(filename)) multipart_body << enc else multipart_body << File.binread(filename) end } multipart_body << "\r\n--#{boundary}--\r\n" request.body = multipart_body.join request.content_type = "multipart/form-data, boundary=\"#{boundary}\"" elsif options.json_data request.body = options.data elsif options.data request.content_type = "application/x-www-form-urlencoded" unless request.content_type if options.raw_data request.body = options.data else begin request.body = options.data.map do |key, value| "#{key}" + (value.nil? ? "" : "=#{CGI.escape(value)}") end.join("&") rescue raise Exception, "ERROR: failed to parse POST request body" end end end request end def perform_request_from_options(options, &block) request = build_request_from_options(options) request.oauth!(consumer.http, consumer, access_token) request['user-agent'] = user_agent consumer.http.request(request, &block) end def user_agent "twurl version: #{Version} " \ "platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" end def exchange_credentials_for_access_token response = begin consumer.token_request(:post, consumer.access_token_path, nil, {}) rescue OAuth::Unauthorized perform_pin_authorize_workflow end @token = response[:oauth_token] @secret = response[:oauth_token_secret] end def perform_pin_authorize_workflow @request_token = consumer.get_request_token CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN") begin pin = STDIN.gets.chomp rescue SystemExit, Interrupt raise Exception, "Operation cancelled" end access_token = @request_token.get_access_token(:oauth_verifier => pin.chomp) {:oauth_token => access_token.token, :oauth_token_secret => access_token.secret} end def generate_authorize_url request = consumer.create_signed_request(:get, consumer.authorize_path, @request_token, pin_auth_parameters) params = request['Authorization'].sub(/^OAuth\s+/, '').split(/,\s+/).map { |p| k, v = p.split('=') v =~ /"(.*?)"/ "#{k}=#{CGI.escape($1)}" }.join('&') "#{Twurl.options.base_url}#{request.path}?#{params}" end def pin_auth_parameters {'oauth_callback' => 'oob'} end def fetch_verify_credentials access_token.get('/1.1/account/verify_credentials.json?include_entities=false&skip_status=true') end def authorized? oauth_response = fetch_verify_credentials oauth_response.class == Net::HTTPOK end def needs_to_authorize? token.nil? || secret.nil? end def save verify_has_username self.class.rcfile << self end def verify_has_username if username.nil? || username == '' oauth_response = fetch_verify_credentials oauth_response.body =~ /"screen_name"\s*:\s*"(.*?)"/ @username = $1 end end def to_hash OAUTH_CLIENT_OPTIONS.inject({}) do |hash, attribute| if value = send(attribute) hash[attribute] = value end hash end end def configure_http! consumer.http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace consumer.http.read_timeout = consumer.http.open_timeout = Twurl.options.timeout || 60 consumer.http.open_timeout = Twurl.options.connection_timeout if Twurl.options.connection_timeout consumer.http.max_retries = 0 consumer.http.use_ssl = true consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE end def consumer @consumer ||= OAuth::Consumer.new( consumer_key, consumer_secret, :site => Twurl.options.base_url, :proxy => Twurl.options.proxy ) end def access_token @access_token ||= OAuth::AccessToken.new(consumer, token, secret) end end end