rhack 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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