session.rb 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. require 'openssl'
  2. require 'rack'
  3. module ShopifyAPI
  4. class ValidationException < StandardError
  5. end
  6. class Session
  7. cattr_accessor :api_key, :secret, :myshopify_domain
  8. self.myshopify_domain = 'myshopify.com'
  9. attr_accessor :domain, :token, :name, :extra
  10. attr_reader :api_version
  11. alias_method :url, :domain
  12. class << self
  13. def setup(params)
  14. params.each { |k,value| public_send("#{k}=", value) }
  15. end
  16. def temp(domain:, token:, api_version: ShopifyAPI::Base.api_version, &block)
  17. session = new(domain: domain, token: token, api_version: api_version)
  18. with_session(session, &block)
  19. end
  20. def with_session(session, &_block)
  21. original_session = extract_current_session
  22. original_user = ShopifyAPI::Base.user
  23. original_password = ShopifyAPI::Base.password
  24. begin
  25. ShopifyAPI::Base.clear_session
  26. ShopifyAPI::Base.activate_session(session)
  27. yield
  28. ensure
  29. ShopifyAPI::Base.activate_session(original_session)
  30. ShopifyAPI::Base.user = original_user
  31. ShopifyAPI::Base.password = original_password
  32. end
  33. end
  34. def with_version(api_version, &block)
  35. original_session = extract_current_session
  36. session = new(domain: original_session.site, token: original_session.token, api_version: api_version)
  37. with_session(session, &block)
  38. end
  39. def prepare_domain(domain)
  40. return nil if domain.blank?
  41. # remove http:// or https://
  42. domain = domain.strip.gsub(%r{\Ahttps?://}, '')
  43. # extract host, removing any username, password or path
  44. shop = URI.parse("https://#{domain}").host
  45. # extract subdomain of .myshopify.com
  46. if (idx = shop.index("."))
  47. shop = shop.slice(0, idx)
  48. end
  49. return nil if shop.empty?
  50. "#{shop}.#{myshopify_domain}"
  51. rescue URI::InvalidURIError
  52. nil
  53. end
  54. def validate_signature(params)
  55. params = (params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params).with_indifferent_access
  56. return false unless (signature = params[:hmac])
  57. calculated_signature = OpenSSL::HMAC.hexdigest(
  58. OpenSSL::Digest.new('SHA256'), secret, encoded_params_for_signature(params)
  59. )
  60. Rack::Utils.secure_compare(calculated_signature, signature)
  61. end
  62. private
  63. def encoded_params_for_signature(params)
  64. params = params.except(:signature, :hmac, :action, :controller)
  65. params.map { |k,v| "#{URI.escape(k.to_s, '&=%')}=#{URI.escape(v.to_s, '&%')}" }.sort.join('&')
  66. end
  67. def extract_current_session
  68. site = ShopifyAPI::Base.site.to_s
  69. token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
  70. version = ShopifyAPI::Base.api_version
  71. new(domain: site, token: token, api_version: version)
  72. end
  73. end
  74. def initialize(domain:, token:, api_version: ShopifyAPI::Base.api_version, extra: {})
  75. self.domain = self.class.prepare_domain(domain)
  76. self.api_version = api_version
  77. self.token = token
  78. self.extra = extra
  79. end
  80. def create_permission_url(scope, redirect_uri, options = {})
  81. params = { client_id: api_key, scope: scope.join(','), redirect_uri: redirect_uri }
  82. params[:state] = options[:state] if options[:state]
  83. construct_oauth_url("authorize", params)
  84. end
  85. def request_token(params)
  86. return token if token
  87. unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
  88. raise ShopifyAPI::ValidationException, "Invalid Signature: Possible malicious login"
  89. end
  90. response = access_token_request(params[:code])
  91. if response.code == "200"
  92. self.extra = JSON.parse(response.body)
  93. self.token = extra.delete('access_token')
  94. if (expires_in = extra.delete('expires_in'))
  95. extra['expires_at'] = Time.now.utc.to_i + expires_in
  96. end
  97. token
  98. else
  99. raise RuntimeError, response.msg
  100. end
  101. end
  102. def shop
  103. Shop.current
  104. end
  105. def site
  106. "https://#{domain}"
  107. end
  108. def api_version=(version)
  109. @api_version = if ApiVersion::NullVersion.matches?(version)
  110. ApiVersion::NullVersion
  111. else
  112. ApiVersion.find_version(version)
  113. end
  114. end
  115. def valid?
  116. domain.present? && token.present? && api_version.is_a?(ApiVersion)
  117. end
  118. def expires_in
  119. return unless expires_at.present?
  120. [0, expires_at.to_i - Time.now.utc.to_i].max
  121. end
  122. def expires_at
  123. return unless extra.present?
  124. @expires_at ||= Time.at(extra['expires_at']).utc
  125. end
  126. def expired?
  127. return false if expires_in.nil?
  128. expires_in <= 0
  129. end
  130. def hash
  131. state.hash
  132. end
  133. def ==(other)
  134. self.class == other.class && state == other.state
  135. end
  136. alias_method :eql?, :==
  137. protected
  138. def state
  139. [domain, token, api_version, extra]
  140. end
  141. private
  142. def parameterize(params)
  143. URI.escape(params.collect { |k, v| "#{k}=#{v}" }.join('&'))
  144. end
  145. def access_token_request(code)
  146. uri = URI.parse(construct_oauth_url('access_token'))
  147. https = Net::HTTP.new(uri.host, uri.port)
  148. https.use_ssl = true
  149. request = Net::HTTP::Post.new(uri.request_uri)
  150. request.set_form_data('client_id' => api_key, 'client_secret' => secret, 'code' => code)
  151. https.request(request)
  152. end
  153. def construct_oauth_url(path, query_params = {})
  154. query_string = "?#{parameterize(query_params)}" unless query_params.empty?
  155. "https://#{domain}/admin/oauth/#{path}#{query_string}"
  156. end
  157. end
  158. end