rhack 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -19,8 +19,6 @@ It's still randomly documented since it's just my working tool.
19
19
 
20
20
  #### Main goals for 1.x
21
21
 
22
- * Client subclass for OAuth2 with a full set of abstract authorizaztion and API methods. Main idea is a per-user key-value token storage with a handling of tokens expiration.
23
- * Redis-based cache storage for scrapers data.
24
22
  * More agile response postprocessing configuration. Instead of using :json, :hash etc as a flag, define some "before filters" in the Page and chain them.
25
23
  * Route :xhr option to the Scout; add some transparent control on user-agents: desktop, mobile, randomly predefined...
26
24
 
@@ -33,6 +31,17 @@ It's still randomly documented since it's just my working tool.
33
31
 
34
32
  ### CHANGES
35
33
 
34
+ ##### Version 1.1.0
35
+
36
+ * ::OAuthClient < ::Client
37
+ * A full set of abstract OAuth2 authorizaztion and API methods
38
+ * Per-user key-value oauth_token storage
39
+ * Handling of tokens expiration
40
+ * Fits for, at least, facebook.com, linkedin.com and vk.com
41
+
42
+ * ::Storage
43
+ * Wrapper of Redis-based storage to handily store/cache scrapers data
44
+
36
45
  ##### Version 1.0.0
37
46
 
38
47
  * ::Frame
@@ -15,6 +15,7 @@ module RHACK
15
15
  @@config = cfgfile ? YAML.load(IO.read(cfgfile)) : {}
16
16
 
17
17
  L = RMTools::RMLogger.new(config.logger || {})
18
+ # L is automatically included in any module under RHACK
18
19
 
19
20
  db = config.db || {}
20
21
  @@redis = nil
@@ -85,3 +86,4 @@ require "rhack/scout"
85
86
  require "rhack/scout_squad"
86
87
  require "rhack/frame"
87
88
  require "rhack/page"
89
+ require "rhack/storage"
@@ -1,5 +1,7 @@
1
1
  require 'rhack'
2
2
  require 'rhack/services/base'
3
+ require 'rhack/services/storage'
4
+ require 'rhack/services/oauth'
3
5
 
4
6
  module RHACK
5
7
  for name in [:Service, :ServiceError]
@@ -0,0 +1,221 @@
1
+ # encoding: utf-8
2
+ module RHACK
3
+
4
+ class OAuthError < ClientError; end
5
+ class StateError < OAuthError; end
6
+ class NoTokenError < OAuthError; end
7
+
8
+ class CodeIndiffirentPage < Page
9
+ def process(c, opts={})
10
+ c.res.instance_variable_set :@code, 200
11
+ super
12
+ end
13
+ end
14
+
15
+ class OAuthClient < Client
16
+ attr_reader :oauth_tokens
17
+
18
+ # just a buffer for one action
19
+ # @ {user_id => hash<last used user data>}
20
+ attr_accessor :users_data
21
+
22
+ def initialize *args
23
+ @users_data = {}
24
+ @oauth_tokens = {}
25
+ @oauth_states_users = {}
26
+ @storage = self.class.storage
27
+ service, frame, opts = args.get_opts [:api, nil], {}
28
+ super service, frame, {cp: false, wait: true, json: true, scouts: 1}.merge(opts)
29
+ end
30
+
31
+ alias_constant :OAUTH
32
+ alias_constant :API
33
+
34
+ # TODO:
35
+ # * hierarchical url_params[:scope] (?)
36
+ def validate(user_id, url_params={})
37
+ if action = url_params.delete(:action)
38
+ if action_params = API(action)
39
+ url_params = action_params.slice(:scope).merge(url_params)
40
+ end
41
+ end
42
+ if data = user_data([user_id, url_params[:scope]])
43
+ token, expires = data
44
+ if user_id != '__app__'
45
+ if token and expires
46
+ # substracting a minute so that "last moment" request wouldn't fail
47
+ if expires - 60 < Time.now.to_i
48
+ token = false
49
+ end
50
+ else
51
+ token = false
52
+ end
53
+ end
54
+ end
55
+ if token
56
+ block_given? ? yield(token) : token
57
+ else
58
+ {oauth_url: get_oauth_url(user_id, url_params)}
59
+ end
60
+ end
61
+
62
+ # @ state_params : [string<user_id>, (strings*",")<scope>]
63
+ # persistent: state_params -> [string<token>, int<expires>]
64
+ def user_data(state_params, data=nil)
65
+ key = "#{self.class.name.sub('RHACK::', '').underscore}:tokens"
66
+ if data
67
+ if data == :clear
68
+ @oauth_tokens.delete state_params
69
+ $redis.hdel key, state_params*':'
70
+ else
71
+ @oauth_tokens[state_params] = data
72
+ $redis.hset key, state_params*':', data*','
73
+ end
74
+ elsif !@oauth_tokens[state_params]
75
+ if data = $redis.hget(key, state_params*':')
76
+ token, expire = data/','
77
+ @oauth_tokens[state_params] = [token, expire.to_i]
78
+ end
79
+ end
80
+ @users_data[state_params[0]] = @oauth_tokens[state_params]
81
+ end
82
+
83
+ # usually called internally
84
+ def get_oauth_url(user_id='__default__', url_params={})
85
+ state = String.rand(64)
86
+ @oauth_states_users[state] = [user_id, url_params[:scope]]
87
+ # TODO: change it with something more consious
88
+ url_params[:redirect_uri] = OAUTH(:landing).dup
89
+ L.debug url_params
90
+ if redirect_protocol = url_params.delete(:redirect_protocol)
91
+ url_params[:redirect_uri].sub!(/^\w+/, redirect_protocol)
92
+ end
93
+ L.debug url_params
94
+ @oauth_url = URI(:oauth)[:auth] + {
95
+ response_type: 'code',
96
+ client_id: OAUTH(:id),
97
+ state: state
98
+ }.merge(url_params).to_params
99
+ end
100
+
101
+ # @ url_params : {:code, :state, ...}
102
+ def get_oauth_token(url_params={}, &block)
103
+ state = url_params.delete :state
104
+ L.debug state
105
+ if state_params = @oauth_states_users[state]
106
+ if data = user_data(state_params)
107
+ # code is allready used, return token
108
+ return data[0]
109
+ end
110
+ else
111
+ raise StateError, "Couldn't find user authentication state. Please, retry authorization from start"
112
+ end
113
+
114
+ url_params[:redirect_uri] = OAUTH(:landing).dup
115
+ L.debug url_params
116
+ if redirect_protocol = url_params.delete(:redirect_protocol)
117
+ url_params[:redirect_uri].sub!(/^\w+/, redirect_protocol)
118
+ end
119
+ L.debug url_params
120
+ @f.run({}, URI(:oauth)[:token] + {
121
+ grant_type: 'authorization_code',
122
+ client_id: OAUTH(:id),
123
+ client_secret: OAUTH(:secret)
124
+ }.merge(url_params).to_params, raw: true, proc_result: block) {|curl|
125
+ L.debug curl.res
126
+ L.debug curl.res.body
127
+ # TODO: refactor parse type selector: raw, json, hash, xml...
128
+ # from_json -> (symbolize_keys: true)
129
+ if curl.res.code == 200
130
+ body = curl.res.body
131
+ hash = '{['[body[0]] ? body.from_json(symbolize_keys: true) : body.to_params
132
+ token = hash.access_token
133
+ data = [token, Time.now.to_i + (hash.expires || hash.expires_in).to_i]
134
+ L.debug token
135
+ user_data(state_params, data)
136
+ token
137
+ else
138
+ raise OAuthError, curl.res.body
139
+ end
140
+ }
141
+ end
142
+
143
+ def get_application_oauth_token(&block)
144
+ @f.run(URI(:oauth)[:token] + {
145
+ grant_type: 'client_credentials',
146
+ client_id: OAUTH(:id),
147
+ client_secret: OAUTH(:secret)
148
+ }.to_params, raw: true, proc_result: block) {|curl|
149
+ if curl.res.code == 200
150
+ body = curl.res.body
151
+ hash = '{['[body[0]] ? body.from_json(symbolize_keys: true) : body.to_params
152
+ user_data(['__app__', nil], [hash.access_token])[0]
153
+ else
154
+ raise OAuthError, curl.res.body
155
+ end
156
+ }
157
+ end
158
+
159
+ # Если придёт мысль делать враппер клиента по запросу
160
+ #
161
+ # @ action : url or reference to ::API
162
+ # @ args :
163
+ # token : token or state_params
164
+ # action_params : smth to append to url
165
+ def api(action, *args, &block)
166
+ if action_data = API(action)
167
+ action, scope = action_data.values_at :path, :scope
168
+ app_token = action_data[:token] == :application
169
+ end
170
+ token, opts = args.get_opts [app_token ? ['__app__'] : ['__default__']]
171
+ opts = opts.symbolize_keys
172
+ action_params = opts.delete(:params) || {}
173
+ redirect_params = opts.extract!(:redirect_protocol)
174
+
175
+ if token.is Array
176
+ token[1] ||= scope
177
+ state_params = token
178
+ L.debug state_params
179
+ request_params = [token[0], (token[1] ? {scope: token[1]} : {}).merge(redirect_params)]
180
+ L.debug request_params
181
+ token = validate(*request_params)
182
+ if token.is Hash
183
+ L.debug token
184
+ if block
185
+ return {res: block.(token)}
186
+ else
187
+ return token
188
+ end
189
+ end
190
+ end
191
+ unless token
192
+ raise NoTokenError
193
+ end
194
+
195
+ L.debug state_params
196
+ action += '?' if !action['?']
197
+ action += action_params.to_params
198
+ L.debug [action_data, action, token]
199
+ opts = {proc_result: block, headers: {'Referer' => nil}, result: CodeIndiffirentPage}.merge(opts)
200
+ # TODO: option to
201
+ @f.run(URI(:api) % {action: action} + token, opts) {|page|
202
+ if page.hash and page.hash != true and error = page.hash.error
203
+ L.debug state_params
204
+ if error.code.in([190, 100]) and state_params
205
+ user_data state_params, :clear
206
+ L.warn error.message
207
+ if request_params
208
+ {oauth_url: get_oauth_url(*request_params)}
209
+ end
210
+ else
211
+ raise OAuthError, error.message
212
+ end
213
+ else
214
+ page
215
+ end
216
+ }
217
+ end
218
+
219
+ end
220
+
221
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module RHACK
4
+ class Client
5
+ class_attribute :storage
6
+ self.storage = {}
7
+ attr_reader :storage
8
+
9
+ def self.store(type, name, opts={})
10
+ storage[name] = RHACK::Storage(type, (opts[:prefix] || self.name.sub('RHACK::', '').underscore)+':'+name)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+ module RHACK
3
+
4
+ class Storage
5
+ __init__
6
+ include Redis::Objects
7
+ class TypeError < ::TypeError; end
8
+
9
+ def initialize(type, namespace)
10
+ @namespace = namespace
11
+ @type = type
12
+ end
13
+
14
+ def inspect
15
+ "<#Storage #{@namespace}: #{@type}>"
16
+ end
17
+ alias :to_s :inspect
18
+
19
+ # set пригодится как для массового переноса или обновления всего неймспейса (реже),
20
+ # так и для &-проверки на стороне руби, какие ключи нужных данных вообще стоит дёргать (чаще)
21
+ def keys
22
+ Set redis.smembers(@namespace)
23
+ end
24
+
25
+ # TODO:
26
+ # @ opts should apply to redis.command, e.g. "use zadd instead of sadd"
27
+ def __store(key, data, opts={})
28
+ item_key = "#{@namespace}:#{key}"
29
+ case @type
30
+ when :hash
31
+ redis.hmset item_key, *data.to_a
32
+ when :set
33
+ redis.sadd item_key, data.to_a
34
+ when :zset
35
+ redis.zadd item_key, data.to_a
36
+ end
37
+ data
38
+ end
39
+
40
+ def store(key, data)
41
+ redis.sadd(@namespace, key)
42
+ __store(key, data)
43
+ end
44
+ alias :[]= :store
45
+
46
+ def storenx(key, data)
47
+ if redis.sadd(@namespace, key)
48
+ __store(key, data)
49
+ true
50
+ else false
51
+ end
52
+ end
53
+
54
+ def fetch(key)
55
+ item_key = "#{@namespace}:#{key}"
56
+ case @type
57
+ when :hash
58
+ redis.hgetall item_key
59
+ when :set
60
+ redis.smembers item_key
61
+ when :zset
62
+ # it will become better if I'll find use case for it
63
+ redis.zrange item_key, 0, -1
64
+ end
65
+ end
66
+ alias :[] :fetch
67
+
68
+ def fetchex(key, overwrite=nil)
69
+ exists = overwrite.nil? ? exists?(key) : !overwrite
70
+ if exists
71
+ fetch(key)
72
+ else
73
+ if res = yield
74
+ store key, res
75
+ res
76
+ end
77
+ end
78
+ end
79
+
80
+ def exists?(key)
81
+ redis.type("#{@namespace}:#{key}") != 'none'
82
+ end
83
+ alias :ex :exists?
84
+
85
+ def all
86
+ redis.smembers(@namespace).map_hash {|key|
87
+ [key, redis.fetch(key)]
88
+ }
89
+ end
90
+
91
+ def del(key)
92
+ if redis.srem(@namespace, key)
93
+ redis.del "#{@namespace}:#{key}"
94
+ end
95
+ end
96
+
97
+ def wipe!
98
+ redis.smembers(@namespace).each {|key|
99
+ redis.del "#{@namespace}:#{key}"
100
+ }
101
+ redis.del @namespace
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -1,3 +1,3 @@
1
1
  module RHACK
2
- VERSION = '1.0.0'
2
+ VERSION = '1.1.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-29 00:00:00.000000000 Z
12
+ date: 2013-06-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -182,6 +182,9 @@ files:
182
182
  - lib/rhack/services/base.rb
183
183
  - lib/rhack/services/compatibility.rb
184
184
  - lib/rhack/services/examples.rb
185
+ - lib/rhack/services/oauth.rb
186
+ - lib/rhack/services/storage.rb
187
+ - lib/rhack/storage.rb
185
188
  - lib/rhack/version.rb
186
189
  - lib/rhack_in.rb
187
190
  - rhack.gemspec