rest-gw2 0.4.0 → 0.5.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.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +30 -0
  3. data/README.md +7 -3
  4. data/Rakefile +1 -0
  5. data/TODO.md +26 -0
  6. data/config.ru +6 -0
  7. data/lib/rest-gw2.rb +1 -0
  8. data/lib/rest-gw2/client.rb +249 -110
  9. data/lib/rest-gw2/client/item_detail.rb +80 -0
  10. data/lib/rest-gw2/server.rb +5 -583
  11. data/lib/rest-gw2/server/action.rb +283 -0
  12. data/lib/rest-gw2/server/cache.rb +14 -4
  13. data/lib/rest-gw2/server/imp.rb +270 -0
  14. data/lib/rest-gw2/server/runner.rb +1 -0
  15. data/lib/rest-gw2/server/view.rb +309 -0
  16. data/lib/rest-gw2/{view → server/view}/characters.erb +10 -6
  17. data/lib/rest-gw2/server/view/check_list.erb +10 -0
  18. data/lib/rest-gw2/server/view/check_percentage.erb +9 -0
  19. data/lib/rest-gw2/server/view/commerce.erb +24 -0
  20. data/lib/rest-gw2/{view → server/view}/dyes.erb +7 -7
  21. data/lib/rest-gw2/server/view/error.erb +1 -0
  22. data/lib/rest-gw2/server/view/exchange.erb +29 -0
  23. data/lib/rest-gw2/server/view/guild_info.erb +37 -0
  24. data/lib/rest-gw2/{view → server/view}/index.erb +0 -0
  25. data/lib/rest-gw2/{view → server/view}/info.erb +1 -1
  26. data/lib/rest-gw2/{view → server/view}/item_list.erb +0 -0
  27. data/lib/rest-gw2/{view/items.erb → server/view/item_section.erb} +0 -0
  28. data/lib/rest-gw2/{view → server/view}/item_show.erb +7 -1
  29. data/lib/rest-gw2/server/view/items.erb +8 -0
  30. data/lib/rest-gw2/server/view/items_from.erb +35 -0
  31. data/lib/rest-gw2/{view → server/view}/layout.erb +14 -4
  32. data/lib/rest-gw2/server/view/members.erb +23 -0
  33. data/lib/rest-gw2/server/view/menu.erb +13 -0
  34. data/lib/rest-gw2/server/view/menu_armors.erb +17 -0
  35. data/lib/rest-gw2/server/view/menu_commerce.erb +7 -0
  36. data/lib/rest-gw2/server/view/menu_guild.erb +11 -0
  37. data/lib/rest-gw2/server/view/menu_unlocks.erb +11 -0
  38. data/lib/rest-gw2/{view → server/view}/menu_weapons.erb +2 -1
  39. data/lib/rest-gw2/{view → server/view}/pages.erb +2 -2
  40. data/lib/rest-gw2/{view → server/view}/profile.erb +7 -7
  41. data/lib/rest-gw2/server/view/skins.erb +15 -0
  42. data/lib/rest-gw2/server/view/stash.erb +10 -0
  43. data/lib/rest-gw2/server/view/titles.erb +5 -0
  44. data/lib/rest-gw2/server/view/unlock_percentage.erb +1 -0
  45. data/lib/rest-gw2/server/view/unlocks_items.erb +5 -0
  46. data/lib/rest-gw2/server/view/unlocks_list.erb +3 -0
  47. data/lib/rest-gw2/{view → server/view}/wallet.erb +1 -1
  48. data/lib/rest-gw2/server/view/wip.erb +1 -0
  49. data/lib/rest-gw2/version.rb +2 -1
  50. data/rest-gw2.gemspec +43 -25
  51. data/task/README.md +8 -8
  52. data/task/gemgem.rb +29 -7
  53. metadata +42 -25
  54. data/lib/rest-gw2/view/error.erb +0 -1
  55. data/lib/rest-gw2/view/guild.erb +0 -14
  56. data/lib/rest-gw2/view/items_from.erb +0 -30
  57. data/lib/rest-gw2/view/menu.erb +0 -16
  58. data/lib/rest-gw2/view/menu_armors.erb +0 -11
  59. data/lib/rest-gw2/view/skins.erb +0 -14
  60. data/lib/rest-gw2/view/transactions.erb +0 -28
  61. data/lib/rest-gw2/view/wip.erb +0 -1
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-gw2/server/imp'
4
+
5
+ module RestGW2
6
+ class ServerAction
7
+ def self.weapons
8
+ %w[Greatsword Sword Hammer Mace Axe Dagger
9
+ Staff Scepter
10
+ Longbow Shortbow Rifle Pistol
11
+ Shield Torch Focus Warhorn
12
+ Spear Speargun Trident]
13
+ end
14
+
15
+ def self.armors
16
+ %w[Helm Shoulders Coat Gloves Leggings Boots HelmAquatic]
17
+ end
18
+
19
+ def self.armors_weight
20
+ %w[Light Medium Heavy Clothing]
21
+ end
22
+
23
+ def self.crafting
24
+ %w[Weaponsmith Huntsman Artificer
25
+ Armorsmith Leatherworker Tailor
26
+ Jeweler Chef Scribe]
27
+ end
28
+
29
+ include Jellyfish
30
+ controller_include NormalizedPath, ServerImp
31
+
32
+ handle Timeout::Error do
33
+ status 504
34
+ render :error, 'Timeout. Please try again.'
35
+ end
36
+
37
+ handle RestGW2::Error do |e|
38
+ status 502
39
+ render :error, e.error['text']
40
+ end
41
+
42
+ post '/access_token' do
43
+ t = encrypt(request.POST['access_token'])
44
+ r = request.POST['referrer']
45
+ u = if r == view.path('/') then view.path('/account') else r end
46
+ found "#{u}?t=#{t}"
47
+ end
48
+
49
+ get '/' do
50
+ render :index
51
+ end
52
+
53
+ get '/account' do
54
+ info = gw2_request(:account_with_detail).dup
55
+ %w[guilds guild_leader].each do |guild|
56
+ info[guild] = info[guild].map(&view.method(:show_guild))
57
+ end
58
+
59
+ render :info, info
60
+ end
61
+
62
+ get %r{\A/guilds/(?<uuid>[^/]+)\z} do |m|
63
+ guild_request(m[:uuid]) do |arg|
64
+ render :guild_info, arg.merge(:guild => arg.dig(:guilds, arg[:gid]))
65
+ end
66
+ end
67
+
68
+ get %r{\A/guilds/(?<uuid>[^/]+)/members\z} do |m|
69
+ guild_request(m[:uuid]) do |arg|
70
+ members = gw2_defer(:guild_members, arg[:gid])
71
+
72
+ render :members, arg.merge(:members => members)
73
+ end
74
+ end
75
+
76
+ get %r{\A/guilds/(?<uuid>[^/]+)/items\z} do |m|
77
+ guild_request(m[:uuid]) do |arg|
78
+ stash = gw2_defer( :stash_with_detail, arg[:gid])
79
+ treasury = gw2_defer(:treasury_with_detail, arg[:gid])
80
+
81
+ render :stash, arg.merge(:stash => stash, :treasury => treasury)
82
+ end
83
+ end
84
+
85
+ get '/characters' do
86
+ chars = gw2_request(:characters_with_detail)
87
+ total = chars.inject(0){ |t, c| t + c['age'] }
88
+ craftings = group_by_crafting(chars)
89
+
90
+ render :characters, :chars => chars, :total => total,
91
+ :craftings => craftings
92
+ end
93
+
94
+ get %r{\A/characters/(?<name>[\w ]+)\z} do |m|
95
+ characters = gw2_request(:characters_with_detail)
96
+ names = characters.map { |c| c['name'] }
97
+ name = m[:name]
98
+ char = characters.find{ |c| c['name'] == name }
99
+ equi = gw2_defer(:expand_item_detail, char['equipment'])
100
+ bags = gw2_defer(:bags_with_detail , char['bags'])
101
+
102
+ equi_buy, equi_sell = view.sum_items(equi)
103
+ bags_buy, bags_sell = view.sum_items(bags +
104
+ bags.flat_map{ |c| c && c['inventory'] })
105
+
106
+ render :profile, :names => names, :equi => equi, :bags => bags,
107
+ :equi_buy => equi_buy, :equi_sell => equi_sell,
108
+ :bags_buy => bags_buy, :bags_sell => bags_sell
109
+ end
110
+
111
+ get '/items' do
112
+ render :items, gw2_request(:with_item_detail, 'v2/account/inventory')
113
+ end
114
+
115
+ get '/items/bank' do
116
+ render :items, gw2_request(:with_item_detail, 'v2/account/bank')
117
+ end
118
+
119
+ get '/items/materials' do
120
+ render :items, gw2_request(:with_item_detail, 'v2/account/materials')
121
+ end
122
+
123
+ get '/items/all' do
124
+ render :items, all_items
125
+ end
126
+
127
+ get %r{\A/items/(?<id>\d+)\z} do |m|
128
+ acct, bank, materials, chars = find_my_item(m[:id].to_i)
129
+ buy, sell = view.sum_items(acct + bank + materials +
130
+ chars.values.flatten)
131
+
132
+ render :items_from, :acct => acct, :bank => bank,
133
+ :materials => materials, :chars => chars,
134
+ :buy => buy, :sell => sell
135
+ end
136
+
137
+ get '/wallet' do
138
+ render :wallet, gw2_request(:wallet_with_detail)
139
+ end
140
+
141
+ get '/unlocks/skins/backpacks' do
142
+ skin_request('Back')
143
+ end
144
+
145
+ weapons.each do |weapon|
146
+ get "/unlocks/skins/weapons/#{weapon.downcase}" do
147
+ skin_request('Weapon', weapon)
148
+ end
149
+ end
150
+
151
+ get '/unlocks/skins/weapons/other' do
152
+ subtype = Regexp.new("\\A(?:#{Regexp.union(*ServerAction.weapons)})\\z")
153
+
154
+ skin_request('Weapon', 'Other') do |item|
155
+ item.dig('details', 'type') !~ subtype
156
+ end
157
+ end
158
+
159
+ armors.each do |armor|
160
+ armors_weight.each do |weight|
161
+ get "/unlocks/skins/armors/#{armor.downcase}/#{weight.downcase}" do
162
+ skin_request('Armor', armor, weight)
163
+ end
164
+ end
165
+ end
166
+
167
+ get '/unlocks/skins/armors/other' do
168
+ subtype = Regexp.new("\\A(?:#{Regexp.union(*ServerAction.armors)})\\z")
169
+
170
+ skin_request('Armor', 'Other') do |item|
171
+ item.dig('details', 'type') !~ subtype
172
+ end
173
+ end
174
+
175
+ get '/unlocks/skins/gathering' do
176
+ skin_request('Gathering')
177
+ end
178
+
179
+ get '/unlocks/dyes' do
180
+ dyes = gw2_request(:dyes_with_detail)
181
+ buy, sell = view.sum_items(dyes)
182
+ unlocked = dyes.count{ |d| d['count'] > 0 }
183
+
184
+ render :dyes, :dyes => dyes,
185
+ :buy => buy, :sell => sell,
186
+ :unlocked => unlocked
187
+ end
188
+
189
+ get '/unlocks/outfits' do
190
+ render :unlocks_items, gw2_request(:outfits_with_detail)
191
+ end
192
+
193
+ get '/unlocks/minis' do
194
+ render :unlocks_items, gw2_request(:minis_with_detail)
195
+ end
196
+
197
+ get '/unlocks/finishers' do
198
+ render :unlocks_items, gw2_request(:finishers_with_detail)
199
+ end
200
+
201
+ get '/unlocks/mailcarriers' do
202
+ render :unlocks_items, gw2_request(:mailcarriers_with_detail)
203
+ end
204
+
205
+ get '/unlocks/gliders' do
206
+ render :unlocks_items, gw2_request(:gliders_with_detail)
207
+ end
208
+
209
+ get '/unlocks/cats' do
210
+ render :unlocks_list, gw2_request(:cats_with_detail)
211
+ end
212
+
213
+ get '/unlocks/nodes' do
214
+ render :unlocks_list, gw2_request(:nodes_with_detail)
215
+ end
216
+
217
+ get '/achievements/titles' do
218
+ render :titles, gw2_request(:titles_with_detail)
219
+ end
220
+
221
+ get '/commerce/delivery' do
222
+ items = gw2_request(:delivery_with_detail, :page => view.query_p)
223
+ total = items.shift['price']
224
+
225
+ render :commerce, :items => items, :total => total
226
+ end
227
+
228
+ get '/commerce/buying' do
229
+ trans_request(:transactions_with_detail_compact, 'current/buys')
230
+ end
231
+
232
+ get '/commerce/selling' do
233
+ trans_request(:transactions_with_detail_compact, 'current/sells')
234
+ end
235
+
236
+ get '/commerce/bought' do
237
+ trans_request(:transactions_with_detail_compact, 'history/buys')
238
+ end
239
+
240
+ get '/commerce/sold' do
241
+ trans_request(:transactions_with_detail_compact, 'history/sells')
242
+ end
243
+
244
+ get '/exchange' do
245
+ # We try to spend 800 gems to buy golds as the standard for buying gold,
246
+ # and try to spend 100 golds to buy gems as the standard for buying gems
247
+ # Then we try to use those ratios to calculate the prices and match
248
+ # the information in the game. We also try to include the standard
249
+ # to the list, sorted it to the proper place.
250
+ # This is trying to match whatever the game is showing, and also show
251
+ # the real responses from the API.
252
+ gold_std, gem_std =
253
+ [stub_gold(800), stub_gem(100_00_00)].map(&method(:resolve_count))
254
+
255
+ buy_gold_coins_per_gem = gold_std['coins_per_gem']
256
+ buy_gold = [gold_std, 1, 10, 50, 100, 250].map do |gold|
257
+ case gold
258
+ when Numeric
259
+ coins = gold * 100_00
260
+ stub_gold((coins.to_f / buy_gold_coins_per_gem).round, coins)
261
+ else
262
+ gold
263
+ end
264
+ end.sort_by{ |g| g['count'] }
265
+
266
+ buy_gem_coins_per_gem = gem_std['coins_per_gem']
267
+ buy_gem = [gem_std, 400, 800, 1200, 2000].map do |gem|
268
+ case gem
269
+ when Numeric
270
+ stub_gem(gem * buy_gem_coins_per_gem, gem)
271
+ else
272
+ gem
273
+ end
274
+ end.sort_by{ |g| g['count'] }
275
+
276
+ render :exchange, :buy_gold => buy_gold, :buy_gem => buy_gem
277
+ end
278
+
279
+ get '/tokeninfo' do
280
+ render :info, gw2_request(:get, 'v2/tokeninfo')
281
+ end
282
+ end
283
+ end
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module RestGW2
3
4
  module Cache
5
+ EXPIRES_IN = 600
6
+ LRU_SIZE = 8192
7
+
4
8
  module_function
5
9
  def default logger
6
10
  @cache ||= Cache.pick(logger)
@@ -12,8 +16,8 @@ module RestGW2
12
16
 
13
17
  def memcache logger
14
18
  require 'dalli'
15
- client = Dalli::Client.new
16
- File.open(IO::NULL) do |null|
19
+ client = Dalli::Client.new(nil, :expires_in => EXPIRES_IN)
20
+ File.open(IO::NULL, 'w') do |null|
17
21
  Dalli.logger = Logger.new(null)
18
22
  client.alive!
19
23
  Dalli.logger = logger
@@ -28,8 +32,14 @@ module RestGW2
28
32
 
29
33
  def lru_cache logger
30
34
  require 'lru_redux'
31
- logger.info("LRU cache size: 100")
32
- LruRedux::ThreadSafeCache.new(100)
35
+ logger.info("LRU cache size: #{LRU_SIZE}")
36
+ cache = LruRedux::ThreadSafeCache.new(LRU_SIZE)
37
+ cache.extend(Module.new{
38
+ def fetch key # original fetch could deadlock
39
+ self[key] || self[key] = yield
40
+ end
41
+ })
42
+ cache
33
43
  rescue LoadError => e
34
44
  logger.debug("Skip LRU cache because: #{e}")
35
45
  nil
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-gw2/server/cache'
4
+ require 'rest-gw2/server/view'
5
+ require 'rest-gw2/client'
6
+
7
+ require 'rest-core'
8
+ require 'jellyfish'
9
+
10
+ require 'uri'
11
+ require 'timeout'
12
+ require 'openssl'
13
+
14
+ module RestGW2
15
+ module ServerImp
16
+ SECRET = ENV['RESTGW2_SECRET'] || 'RESTGW2_SECRET'*2
17
+
18
+ GemIcon = 'https://render.guildwars2.com/' \
19
+ 'file/220061640ECA41C0577758030357221B4ECCE62C/502065.png'
20
+ GoldIcon = 'https://render.guildwars2.com/' \
21
+ 'file/98457F504BA2FAC8457F532C4B30EDC23929ACF9/619316.png'
22
+
23
+ # VIEW
24
+ def view
25
+ @view ||= View.new(request, query_t)
26
+ end
27
+
28
+ def render *args
29
+ view.render(*args)
30
+ end
31
+
32
+ def all_items
33
+ acct, bank, mats, chars = all_items_defer
34
+ flatten_chars = chars.flat_map do |c|
35
+ c['equipment'] +
36
+ c['bags'] +
37
+ c['bags'].flat_map{ |c| c && c['inventory'] }
38
+ end
39
+ (acct + bank + mats + flatten_chars).compact.
40
+ sort_by{ |i| i['name'] || i['id'].to_s }.inject([]) do |r, i|
41
+ last = r.last
42
+ if last && last['id'] == i['id'] &&
43
+ last.values_at('skin', 'upgrades', 'infusions').compact.empty?
44
+ last['count'] += i['count']
45
+ else
46
+ r << i
47
+ end
48
+ r
49
+ end
50
+ end
51
+
52
+ def find_my_item id
53
+ acct, bank, mats, chars = all_items_defer
54
+ [select_item(acct.compact, id),
55
+ select_item(bank.compact, id),
56
+ select_item(mats.compact, id),
57
+ chars.inject({}){ |r, c|
58
+ equi = select_item(c['equipment'].compact, id)
59
+ bags = c['bags'].reject(&:nil?).map do |b|
60
+ selected = select_item(b['inventory'].compact, id)
61
+ b.merge('inventory' => selected) if selected.any? ||
62
+ b['id'] == id
63
+ end.compact
64
+ r[c['name']] = [equi, bags] if equi.any? || bags.any?
65
+ r
66
+ }]
67
+ end
68
+
69
+ def select_item items, id
70
+ items.select{ |i| i['id'] == id }
71
+ end
72
+
73
+ def all_items_defer
74
+ acct = gw2_defer(:with_item_detail, 'v2/account/inventory')
75
+ bank = gw2_defer(:with_item_detail, 'v2/account/bank')
76
+ mats = gw2_defer(:with_item_detail, 'v2/account/materials')
77
+ chars = gw2_defer(:characters_with_detail).map do |c|
78
+ c['equipment'] = gw2_defer(:expand_item_detail, c['equipment'])
79
+ c['bags'] = gw2_defer(:bags_with_detail , c['bags'])
80
+ c
81
+ end
82
+ [acct, bank, mats, chars]
83
+ end
84
+
85
+ # CONTROLLER
86
+ def gw2_request msg, *args
87
+ block ||= :itself.to_proc
88
+ refresh = !!request.GET['r']
89
+ opts = {'cache.update' => refresh, 'expires_in' => Cache::EXPIRES_IN}
90
+ args << {} if msg == :with_item_detail
91
+ key = cache_key(msg, args)
92
+ cache.delete(key) if refresh
93
+ cache.fetch(key) do
94
+ PromisePool::Future.resolve(gw2.public_send(msg, *args, opts))
95
+ end
96
+ end
97
+
98
+ def gw2_defer msg, *args
99
+ PromisePool::Promise.new.defer do
100
+ gw2_request(msg, *args)
101
+ end.future
102
+ end
103
+
104
+ def guild_request gid
105
+ guilds = gw2_request(:account_with_detail)['guilds'].
106
+ group_by{ |g| g['id'] }.inject({}){ |r, (id, v)| r[id] = v.first; r }
107
+
108
+ if guilds[gid]
109
+ yield(:gid => gid, :guilds => guilds)
110
+ else
111
+ status 404
112
+ render :error, "Cannot find guild id: #{gid}"
113
+ end
114
+ end
115
+
116
+ def skin_request type, subtype=nil, weight=nil, &block
117
+ items = gw2_request(:skins_with_detail).select do |i|
118
+ filter_skin(i, type, subtype, weight, &block)
119
+ end
120
+ skin_submenu = "menu_#{type.downcase}s" if subtype
121
+ subtype = subtype.downcase if subtype
122
+ weight = weight.downcase if weight
123
+ unlocked = items.count{ |i| i['count'] > 0 }
124
+
125
+ render :skins, :items => items,
126
+ :skin_submenu => skin_submenu,
127
+ :subtype => subtype,
128
+ :weight => weight,
129
+ :unlocked => unlocked
130
+ end
131
+
132
+ def filter_skin item, type, subtype, weight
133
+ item['type'] == type &&
134
+ if block_given?
135
+ yield(item)
136
+ else
137
+ (subtype.nil? || subtype == item.dig('details', 'type')) &&
138
+ (weight.nil? || weight == item.dig('details', 'weight_class'))
139
+ end
140
+ end
141
+
142
+ def trans_request msg, path
143
+ items = gw2_request(msg, path, :page => view.query_p)
144
+ total = view.sum_trans(items)
145
+ pages = calculate_pages("v2/commerce/transactions/#{path}")
146
+
147
+ render :commerce, :items => items, :total => total, :pages => pages
148
+ end
149
+
150
+ def group_by_crafting characters
151
+ characters.inject(Hash.new{|h,k|h[k]=[]}) do |group, char|
152
+ char['crafting'].each do |crafting|
153
+ group[crafting['discipline']] <<
154
+ [crafting['rating'], char['name'], crafting['active']]
155
+ end
156
+ group
157
+ end
158
+ end
159
+
160
+ def calculate_pages path
161
+ link = gw2.get(path, {:page_size => 200},
162
+ RC::RESPONSE_KEY => RC::RESPONSE_HEADERS)['LINK']
163
+ pages = RC::ParseLink.parse_link(link)
164
+ parse_page(pages['first']['uri'])..parse_page(pages['last']['uri'])
165
+ end
166
+
167
+ def parse_page uri
168
+ RC::ParseQuery.parse_query(URI.parse(uri).query)['page'].to_i
169
+ end
170
+
171
+ def stub_gold num, count=nil
172
+ count ||=
173
+ gw2_request(:get, 'v2/commerce/exchange/gems', :quantity => num)
174
+
175
+ {'name' => 'Gold', 'icon' => GoldIcon, 'price' => num, 'count' => count}
176
+ end
177
+
178
+ def stub_gem num, count=nil
179
+ count ||=
180
+ gw2_request(:get, 'v2/commerce/exchange/coins', :quantity => num)
181
+
182
+ {'name' => 'Gem', 'icon' => GemIcon, 'price' => num, 'count' => count}
183
+ end
184
+
185
+ def resolve_count item
186
+ item['coins_per_gem'] = item['count']['coins_per_gem']
187
+ item['count'] = item['count']['quantity']
188
+ item
189
+ end
190
+
191
+ def gw2
192
+ @gw2 ||= Client.new(:access_token => access_token,
193
+ :log_method => logger(env).method(:info),
194
+ :cache => cache)
195
+ end
196
+
197
+ def cache
198
+ @cache ||= RestGW2::Cache.default(logger(env))
199
+ end
200
+
201
+ def cache_key msg, args
202
+ [msg, *args, access_token].join(':')
203
+ end
204
+
205
+ # ACCESS TOKEN
206
+ def access_token
207
+ query_t && decrypt(query_t) || ENV['RESTGW2_ACCESS_TOKEN']
208
+ rescue ArgumentError, OpenSSL::Cipher::CipherError => e
209
+ raise RestGW2::Error.new({'text' => e.message}, 0)
210
+ end
211
+
212
+ def query_t
213
+ @query_t ||= begin
214
+ r = request.GET['t']
215
+ r if r && !r.strip.empty?
216
+ end
217
+ end
218
+
219
+ # UTILITIES
220
+ def encrypt data
221
+ cipher = new_cipher
222
+ cipher.encrypt
223
+ cipher.key = SECRET
224
+ iv = cipher.random_iv
225
+ encrypted = cipher.update(data) + cipher.final
226
+ tag = auth_tag(cipher)
227
+ encode_base64(iv, encrypted, tag)
228
+ end
229
+
230
+ def decrypt data
231
+ iv, encrypted, tag = decode_base64(data)
232
+ cipher = new_cipher
233
+ cipher.decrypt
234
+ cipher.key = SECRET
235
+ cipher.iv = iv
236
+ set_auth_tag(cipher, tag)
237
+ cipher.update(encrypted) + cipher.final
238
+ end
239
+
240
+ def encode_base64 *data
241
+ data.map{ |d| [d].pack('m0') }.join('.').tr('+/=', '-_~')
242
+ end
243
+
244
+ def decode_base64 str
245
+ str.split('.').map{ |d| d.tr('-_~', '+/=').unpack('m0').first }
246
+ end
247
+
248
+ def new_cipher
249
+ OpenSSL::Cipher.new(ENV['CIPHER_ALGO'] || 'aes-128-gcm')
250
+ rescue OpenSSL::Cipher::CipherError
251
+ OpenSSL::Cipher.new('aes-128-cbc')
252
+ end
253
+
254
+ def auth_tag cipher
255
+ cipher.respond_to?(:auth_tag) && cipher.auth_tag || ''
256
+ end
257
+
258
+ def set_auth_tag cipher, tag
259
+ cipher.respond_to?(:auth_tag=) && cipher.auth_tag = tag
260
+ end
261
+
262
+ # MISC
263
+ def logger env
264
+ env['rack.logger'] || begin
265
+ require 'logger'
266
+ Logger.new(env['rack.errors'])
267
+ end
268
+ end
269
+ end
270
+ end