/
app.rb
274 lines (239 loc) · 7.78 KB
/
app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
require 'sinatra/base'
require 'sinatra/contrib'
require 'pg'
require 'tilt/erubis'
require 'erubis'
require 'json'
require 'httpclient'
require 'openssl'
require 'expeditor'
# bundle config build.pg --with-pg-config=<path to pg_config>
# bundle install
module Isucon5f
module TimeWithoutZone
def to_s
strftime("%F %H:%M:%S")
end
end
::Time.prepend TimeWithoutZone
end
class Isucon5f::Endpoint
LIST = {}
def self.get(name)
LIST[name]
end
def initialize(name, method, token_type, token_key, uri)
@name, @method, @token_type, @token_key, @uri = name, method, token_type, token_key, uri
@ssl = uri.start_with?('https://')
LIST[@name] = self
end
attr_reader :name, :method, :token_type, :token_key, :uri
def fetch(conf)
headers = {}
params = (conf['params'] && conf['params'].dup) || {}
case token_type
when 'header' then headers[token_key] = conf['token']
when 'param' then params[token_key] = conf['token']
end
call_uri = sprintf(uri, *conf['keys'])
client = HTTPClient.new
if @ssl
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
fetcher = case method
when 'GET' then client.method(:get_content)
when 'POST' then client.method(:post_content)
else
raise "unknown method #{method}"
end
res = fetcher.call(call_uri, params, headers)
JSON.parse(res)
end
end
Isucon5f::Endpoint.new('ken', 'GET', nil, nil, 'http://api.five-final.isucon.net:8080/%s')
Isucon5f::Endpoint.new('ken2', 'GET', nil, nil, 'http://api.five-final.isucon.net:8080/')
Isucon5f::Endpoint.new('surname', 'GET', nil, nil, 'http://api.five-final.isucon.net:8081/surname')
Isucon5f::Endpoint.new('givenname', 'GET', nil, nil, 'http://api.five-final.isucon.net:8081/givenname')
Isucon5f::Endpoint.new('tenki', 'GET', 'param', 'zipcode', 'http://api.five-final.isucon.net:8988/')
Isucon5f::Endpoint.new('perfectsec', 'GET', 'header', 'X-PERFECT-SECURITY-TOKEN', 'https://api.five-final.isucon.net:8443/tokens')
Isucon5f::Endpoint.new('perfectsec_attacked', 'GET', 'header', 'X-PERFECT-SECURITY-TOKEN', 'https://api.five-final.isucon.net:8443/attacked_list')
class Isucon5f::WebApp < Sinatra::Base
use Rack::Session::Cookie, secret: (ENV['ISUCON5_SESSION_SECRET'] || 'tonymoris')
set :erb, escape_html: true
set :public_folder, File.expand_path('../../static', __FILE__)
SALT_CHARS = [('a'..'z'),('A'..'Z'),('0'..'9')].map(&:to_a).reduce(&:+)
helpers do
def config
@config ||= {
db: {
host: ENV['ISUCON5_DB_HOST'] || 'localhost',
port: ENV['ISUCON5_DB_PORT'] && ENV['ISUCON5_DB_PORT'].to_i,
username: ENV['ISUCON5_DB_USER'] || 'isucon',
password: ENV['ISUCON5_DB_PASSWORD'],
database: ENV['ISUCON5_DB_NAME'] || 'isucon5f',
},
}
end
def db
return Thread.current[:isucon5_db] if Thread.current[:isucon5_db]
conn = PG.connect(
host: config[:db][:host],
port: config[:db][:port],
user: config[:db][:username],
password: config[:db][:password],
dbname: config[:db][:database],
connect_timeout: 3600
)
Thread.current[:isucon5_db] = conn
conn
end
def authenticate(email, password)
query = <<SQL
SELECT id, email, grade FROM users WHERE email=$1 AND passhash=digest(salt || $2, 'sha512')
SQL
user = nil
db.exec_params(query, [email, password]) do |result|
result.each do |tuple|
user = {id: tuple['id'].to_i, email: tuple['email'], grade: tuple['grade']}
end
end
session[:user_id] = user[:id] if user
user
end
def current_user
return @user if @user
return nil unless session[:user_id]
@user = nil
db.exec_params('SELECT id,email,grade FROM users WHERE id=$1', [session[:user_id]]) do |r|
r.each do |tuple|
@user = {id: tuple['id'].to_i, email: tuple['email'], grade: tuple['grade']}
end
end
session.clear unless @user
@user
end
def generate_salt
salt = ''
32.times do
salt << SALT_CHARS[rand(SALT_CHARS.size)]
end
salt
end
end
get '/signup' do
session.clear
erb :signup
end
post '/signup' do
email, password, grade = params['email'], params['password'], params['grade']
salt = generate_salt
insert_user_query = <<SQL
INSERT INTO users (email,salt,passhash,grade) VALUES ($1,$2,digest($3 || $4, 'sha512'),$5) RETURNING id
SQL
default_arg = {}
insert_subscription_query = <<SQL
INSERT INTO subscriptions (user_id,arg) VALUES ($1,$2)
SQL
db.transaction do |conn|
user_id = conn.exec_params(insert_user_query, [email,salt,salt,password,grade]).values.first.first
conn.exec_params(insert_subscription_query, [user_id, default_arg.to_json])
end
redirect '/login'
end
post '/cancel' do
redirect '/signup'
end
get '/login' do
session.clear
erb :login
end
post '/login' do
authenticate params['email'], params['password']
halt 403 unless current_user
redirect '/'
end
get '/logout' do
session.clear
redirect '/login'
end
get '/' do
unless current_user
return redirect '/login'
end
erb :main, locals: {user: current_user}
end
get '/user.js' do
halt 403 unless current_user
erb :userjs, content_type: 'application/javascript', locals: {grade: current_user[:grade]}
end
get '/modify' do
user = current_user
halt 403 unless user
query = <<SQL
SELECT arg FROM subscriptions WHERE user_id=$1
SQL
arg = db.exec_params(query, [user[:id]]).values.first[0]
erb :modify, locals: {user: user, arg: arg}
end
post '/modify' do
user = current_user
halt 403 unless user
service = params["service"]
token = params.has_key?("token") ? params["token"].strip : nil
keys = params.has_key?("keys") ? params["keys"].strip.split(/\s+/) : nil
param_name = params.has_key?("param_name") ? params["param_name"].strip : nil
param_value = params.has_key?("param_value") ? params["param_value"].strip : nil
select_query = <<SQL
SELECT arg FROM subscriptions WHERE user_id=$1 FOR UPDATE
SQL
update_query = <<SQL
UPDATE subscriptions SET arg=$1 WHERE user_id=$2
SQL
db.transaction do |conn|
arg_json = conn.exec_params(select_query, [user[:id]]).values.first[0]
arg = JSON.parse(arg_json)
arg[service] ||= {}
arg[service]['token'] = token if token
arg[service]['keys'] = keys if keys
if param_name && param_value
arg[service]['params'] ||= {}
arg[service]['params'][param_name] = param_value
end
conn.exec_params(update_query, [arg.to_json, user[:id]])
end
redirect '/modify'
end
def fetch_api(method, uri, headers, params)
client = HTTPClient.new
if uri.start_with? "https://"
client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
fetcher = case method
when 'GET' then client.method(:get_content)
when 'POST' then client.method(:post_content)
else
raise "unknown method #{method}"
end
res = fetcher.call(uri, params, headers)
JSON.parse(res)
end
get '/data' do
unless user = current_user
halt 403
end
arg_json = db.exec_params("SELECT arg FROM subscriptions WHERE user_id=$1", [user[:id]]).values.first[0]
arg = JSON.parse(arg_json)
data = arg.map do |service, conf|
#Expeditor::Command.new do
endpoint = Isucon5f::Endpoint.get(service)
{"service" => service, "data" => endpoint.fetch(conf)}
#end
end
#data.each(&:start)
json data#.map(&:get)
end
get '/initialize' do
file = File.expand_path("../../sql/initialize.sql", __FILE__)
system("psql", "-f", file, "isucon5f")
end
end