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 +11 -2
- data/lib/rhack.rb +2 -0
- data/lib/rhack/services.rb +2 -0
- data/lib/rhack/services/oauth.rb +221 -0
- data/lib/rhack/services/storage.rb +13 -0
- data/lib/rhack/storage.rb +106 -0
- data/lib/rhack/version.rb +1 -1
- metadata +5 -2
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
|
data/lib/rhack.rb
CHANGED
@@ -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"
|
data/lib/rhack/services.rb
CHANGED
@@ -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
|
data/lib/rhack/version.rb
CHANGED
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.
|
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-
|
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
|