-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.rb
executable file
·354 lines (306 loc) · 11.3 KB
/
main.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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'sinatra/base'
require 'sinatra/json'
require 'webrick'
require 'webrick/https'
require 'openssl'
require 'rack/throttle'
require 'rack/protection'
require 'rack/session/encrypted_cookie'
require 'securerandom'
require 'slop'
require_relative 'classes/users'
require_relative 'classes/book'
require_relative 'classes/helper'
if File.exists?('/etc/letsencrypt/live/socialread.tk/fullchain.pem')
webrick_options = {
Host: '0.0.0.0',
Port: 2048,
Logger: WEBrick::Log.new($stderr, WEBrick::Log::DEBUG),
DocumentRoot: '/ruby/htdocs',
SSLEnable: true,
SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
SSLCertificate: OpenSSL::X509::Certificate.new(File.open('/etc/letsencrypt/live/socialread.tk/fullchain.pem').read),
SSLPrivateKey: OpenSSL::PKey::RSA.new(File.open('/etc/letsencrypt/live/socialread.tk/privkey.pem').read),
SSLCertName: [['CN', WEBrick::Utils.getservername]]
}
else
webrick_options = {
Host: '0.0.0.0',
Port: 2048,
Logger: WEBrick::Log.new($stderr, WEBrick::Log::DEBUG),
DocumentRoot: '/ruby/htdocs',
SSLEnable: false
}
end
# The server
class MyReadServer < Sinatra::Base
# Accept command-line arguments for easy configuration
@arguments = Slop.parse do |o|
o.on '-h', '--help' do
puts o
exit
end
o.string '-l', '--limit', '(optional) specify the maximum hourly amount of POST requests allowed per user', default: 10
o.bool '-p', '--production', 'run the server in production mode', default: false
end
# Set up logging
logging_path = 'db/server.log'
Dir.mkdir('db') unless Dir.exist?('db')
File.new(logging_path, 'w').close unless File.exist?(logging_path)
$stdout.reopen(logging_path, 'w')
$stdout.sync = true
$stderr.reopen($stdout)
# Configure rate limiting
rules = [
{ method: 'POST', limit: @arguments[:limit]}
# { method: 'GET', limit: 10 },
]
# IP Whitelist - only works when running server in development environment
ip_whitelist = [
'127.0.0.1',
'0.0.0.0'
]
# Enable production environment when specified
set :environment, :production if @arguments.production?
# Configure the following rules for production environment
configure :production do
use Rack::Throttle::Rules, rules: rules, time_window: :hour
use Rack::Protection
end
# Configure the following rules for production environment (IP Whitelist is on)
configure :development do
use Rack::Throttle::Rules, rules: rules, ip_whitelist: ip_whitelist, time_window: :hour
use Rack::Protection
end
# Enable (encrypted) Sinatra session storage, sessions are reset after 1800 seconds (30 min)
enable :sessions
set :sessions, key_size: 32, salt: SecureRandom.hex(32), signed_salt: SecureRandom.hex(32)
set :session_store, Rack::Session::EncryptedCookie
set :sessions, expire_after: 1800
set force_ssl: true
# Create a new database in memory for the users, sync them to file every three seconds
users = Users.new
users.write_every('3s')
# Books cache - store books in RAM when they have already been fetched.
# Cache for a book is refreshed if it is requested 24 hours after creation of the cache for the specified book
$books = {}
# #############
# API Endpoints
# #############
# Home
get '/' do
erb :index
end
# Register
post '/register' do
!params[:name].nil? && !params[:pass].nil? && users.add(params[:name], params[:pass]) ? status(201) : halt(400)
end
# Login
post '/login' do
if !params[:name].nil? && !params[:pass].nil? && users.login(params[:name], params[:pass])
session[:id] = params[:name]
redirect '/'
else
halt 401
end
end
# Sign out
get '/signout' do
session.delete(:id)
redirect '/'
end
# Will return 200 if logged in as this user
get '/user/:name' do
users.exists?(params[:name]) && params[:name] == session[:id] ? status(200) : halt(401)
end
# Change password for user, need to be logged in as specified user
post '/user/:name/change_password' do
if !params[:name].nil? && !params[:old_pass].nil? && !params[:new_pass].nil?
if params[:name] == session[:id]
users.chpass(params[:name], params[:old_pass], params[:new_pass]) ? status(200) : halt(401)
else
halt 401
end
else
halt 400
end
end
# Delete users account, need to be logged in as specified user
post '/user/:name/delete' do
if !params[:name].nil? && !params[:pass].nil?
if params[:name] == session[:id]
if users.del(params[:name], params[:pass])
session.delete(:id)
status 200
else
halt 401
end
else
halt 401
end
else
halt 400
end
end
# Get book collections from user, need to be logged in as specified user
get '/user/:name/book_collections' do
if users.exists?(params[:name]) && params[:name] == session[:id]
json users.users[params[:name]][1]
else
halt 401
end
end
# Add book to book collection, need to be logged in as specified user
get '/user/:name/add_book_to_collection/:collection_name/:book_id' do
if users.exists?(params[:name]) && params[:name] == session[:id]
users.add_book_to_collection(params[:name], params[:collection_name], params[:book_id])
else
halt 401
end
end
# Delete book from book collection, need to be logged in as specified user
get '/user/:name/del_book_from_collection/:collection_name/:book_id' do
if users.exists?(params[:name]) && params[:name] == session[:id]
users.add_book_to_collection(params[:name], params[:collection_name], params[:book_id])
else
halt 401
end
end
# Create new book collection, need to be logged in as specified user
get '/user/:name/add_book_collection/:collection_name' do
if users.exists?(params[:name]) && params[:name] == session[:id]
users.add_collection(params[:name], params[:collection_name]) ? status(201) : halt(400)
else
halt 401
end
end
# Create new book collection with books (separated by ;), need to be logged in as specified user
get '/user/:name/add_book_collection/:collection_name/:book_ids' do
if users.exists?(params[:name]) && params[:name] == session[:id]
book_ids = params[:book_ids].to_s.split(';') unless params[:book_ids].nil?
users.add_collection(params[:name], params[:collection_name], book_ids) ? status(201) : halt(400)
else
halt 401
end
end
# Delete book collection, need to be logged in as specified user
get '/user/:name/del_book_collection/:collection_name' do
if users.exists?(params[:name]) && params[:name] == session[:id]
users.del_collection(params[:name], params[:collection_name]) ? status(200) : halt(400)
else
halt 401
end
end
# Rename book collection, need to be logged in as specified user
get '/user/:name/chname_book_collection/:collection_name/:new_collection_name' do
if users.exists?(params[:name]) && params[:name] == session[:id]
users.chname_collection(params[:name], params[:collection_name], params[:new_collection_name]) ? status(200) : halt(400)
else
halt 401
end
end
# Get book collection by name from user, need to be logged in as specified user
get '/user/:name/book_collections/:book_collection' do
session[:id] && params[:name] == session[:id] ? json(users.get_collection(params[:name], params[:book_collection])) : halt(401)
end
# Recommend books based on user info
get '/user/:name/recommend_books' do
session[:id] && params[:name] == session[:id] ? json(users.recommend_personal(session[:id])) : halt(401)
end
# Search for books, need to be logged in
get '/search_book/:search' do
if session[:id]
book_ids = search(params[:search])
halt 400 if !book_ids || book_ids.nil? || book_ids == ''
# Launch a thread per Book ID to retrieve details about this book
# Results in much faster execution if there are a lot of threads available on the CPU
# Disabled because it's slower on the Azure system (4 threads)
# Kept in the code base because it's faster on systems with > 10 threads
# books_to_return = Parallel.map(book_ids) do |book_id|
# Book.new(book_id, true).to_hash
# end
# Cache book results instead of using multithreading to search for each book
# This will be faster when not having a lot of threads and searching multiple times
current_time = Time.now
books_to_return = book_ids.map do |book_id|
$books[book_id] = [Book.new(book_id, true), current_time] if $books[book_id].nil? || (current_time - $books[book_id][1]) > 86_400
$books[book_id][0].to_hash
end
json books_to_return
else
halt 401
end
end
# Recommend a book based on author and subject(s - can be an array), need to be logged in
get '/recommend_book_via_author/:author' do
session[:id] ? json(recommend(params[:author])) : halt(401)
end
get '/recommend_book_via_subject/:subject' do
session[:id] ? json(recommend('', params[:subject])) : halt(401)
end
get '/recommend_book_via_author_subject/:author/:subject' do
session[:id] ? json(recommend(params[:author], params[:subject])) : halt(401)
end
# Get book information via id, need to be logged in
get '/book/:book' do
halt 400 if params[:book].nil?
if session[:id]
# Use book cache, refresh cache if the current book cache is older than 24 hours (86400s)
current_time = Time.now
$books[params[:book]] = [Book.new(params[:book]), current_time] if $books[params[:book]].nil? || (current_time - $books[params[:book]][1]) > 86_400
json ($books[params[:book]])[0].to_hash
else
halt 401
end
end
# Get data entry from book, need to be logged in
get '/book/:book/:param' do
if session[:id]
$books[params[:book]] ||= [Book.new(params[:book]), Time.now] unless params[:book].nil?
b = $books[params[:book]][0]
b.instance_variable_get(params[:param]) if b.instance_variable_defined?(params[:param])
else
halt 401
end
end
# Get book information via id, need to be logged in
get '/isbn/:isbn' do
halt 400 if params[:isbn].nil?
if session[:id]
# Use book cache, refresh cache if the current book cache is older than 24 hours (86400s)
current_time = Time.now
$books[params[:isbn]] = [Book.new(params[:isbn], true), current_time] if $books[params[:isbn]].nil? || (current_time - $books[params[:isbn]][1]) > 86_400
json ($books[params[:isbn]])[0].to_hash
else
halt 401
end
end
# Get data entry from book, need to be logged in
get '/isbn/:isbn/:param' do
if session[:id]
$books[params[:isbn]] ||= [Book.new(params[:isbn], true), Time.now] unless params[:isbn].nil?
b = $books[params[:isbn]][0]
b.instance_variable_get(params[:param]) if b.instance_variable_defined?(params[:param])
else
halt 401
end
end
# ##############
# Error Handling
# ##############
# 201 - Created
error 201 do
erb :error, locals: { message: body[0], response_code: 201 }
end
# 400 - Bad Request
error 400 do
erb :error, locals: { message: body[0], response_code: 400 }
end
# 401 - Unauthorized
error 401 do
erb :error, locals: { message: body[0], response_code: 401 }
end
end
Rack::Handler::WEBrick.run MyReadServer, webrick_options