rest-gw2 0.4.0 → 0.5.0

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