-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinit.lua
More file actions
727 lines (621 loc) · 21.9 KB
/
Copy pathinit.lua
File metadata and controls
727 lines (621 loc) · 21.9 KB
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
local _M = {
resources = nil,
}
ophal.modules.oauth2 = _M
local seawolf = require 'seawolf'.__build('contrib', 'text', 'other')
local config = settings.oauth2
local xtable, time, base = seawolf.contrib.seawolf_table, os.time, base
local explode, env, tonumber = seawolf.text.explode, env, tonumber
local empty, goto, site = seawolf.variable.empty, goto, settings.site
local header, route_arg, sleep = header, route_arg, socket.sleep
local request_uri, escape = request_uri, socket.url.escape
local hash_hmac, string = seawolf.other.hash_hmac, string
local _SESSION = _SESSION
local db_query, user_mod
function _M.init()
db_query = env.db_query
user_mod = ophal.modules.user
-- Oauth2 session fingerprint
if _SESSION and _SESSION.oauth2 == nil then
_SESSION.oauth2 = {}
end
end
function _M.cron()
db_query('DELETE FROM oauth2_google_nonce WHERE ? > created + ?', time(), config.google.nonce_ttl or 2*60)
db_query('DELETE FROM oauth2_facebook_nonce WHERE ? > created + ?', time(), config.facebook.nonce_ttl or 2*60)
end
function _M.route()
local items = {}
items['oauth2/google'] = {
page_callback = 'google_callback',
format = 'json',
}
items['oauth2/facebook'] = {
page_callback = 'facebook_callback',
format = 'json',
}
return items
end
function _M.google_api_path(path)
return 'https://www.googleapis.com/oauth2/' .. (config.google.api_version or 'v4') .. (path or '')
end
function _M.google_valid_nonce(nonce)
local rs = db_query('SELECT COUNT(*) total FROM oauth2_google_nonce WHERE id = ? AND ? <= created + ?', nonce, time(), config.google.nonce_ttl or 2*60)
local count = rs:fetch(true)
if count then
return tonumber(count.total) > 0
end
end
function _M.google_create_authcodes(entity)
local rs = db_query('INSERT INTO oauth2_google_token(id, resource, access_token, refresh_token, user_id, created, changed, ttl) VALUES(?, ?, ?, ?, ?, ?, ?, ?)', entity.id, entity.resource, entity.access_token, entity.refresh_token, entity.user_id, time(), time(), entity.ttl)
end
function _M.google_load_authcodes(id)
local rs = db_query('SELECT * FROM oauth2_google_token WHERE id = ?', id)
return rs:fetch(true)
end
function _M.google_update_authcodes(entity)
local rs = db_query('UPDATE oauth2_google_token SET access_token = ?, changed = ?, ttl = ? WHERE id = ?', entity.access_token, time(), entity.ttl, entity.id)
end
function _M.google_get_resource(id)
if empty(_M.resources) then
_M.resources = module_invoke_all 'oauth2_resource'
end
if not empty(_M.resources) then
return _M.resources[id]
end
end
function _M.google_get_authcodes(code, client_id, client_secret)
local json = require 'dkjson'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local authentication_result = {}
local body, token_url, authentication_values
-- Use the default App ID and App Secret if not specified.
client_id = client_id or config.google.client_id
client_secret = client_secret or config.google.client_secret
-- Note that the "code" provided by Google is a hash based on the client_id,
-- client_secret, and redirect_url. All of these things must be IDENTICAL to
-- the same values that were passed to Google in the approval request.
body = _M.build_params{
code = code,
client_id = client_id,
client_secret = client_secret,
redirect_uri = url('oauth2/google/', {absolute = true}),
grant_type = 'authorization_code',
}
token_url = _M.google_api_path '/token'
for i = 1, 5 do
local response = xtable()
local r, c, h, s = https.request{
url = token_url,
method = 'POST',
headers = {
['content-length'] = #body,
['content-type'] = 'application/x-www-form-urlencoded',
},
source = ltn12.source.string(body),
sink = ltn12.sink.table(response),
}
authentication_result.res = r
authentication_result.code = c
authentication_result.headers = h
authentication_result.status = s
authentication_result.data = response:concat()
if authentication_result.code == 200 then
break
end
-- Google access code generation seems to take a lot of time. That's why we have to wait
-- some seconds before the code can be acquired.
sleep(1)
end
if 200 ~= authentication_result.code then
if authentication_result.data then
error(authentication_result.data)
elseif authentication_result.error then
error(authentication_result.error)
else
error 'Unknown error.'
end
else
return json.decode(authentication_result.data)
end
end
--[[ Execute a Google API query.
@see https://developers.google.com/identity/protocols/googlescopes
]]
function _M.google_api_query(api_url, access_token, params, method)
params = params or {}
method = method or 'GET'
local json = require 'dkjson'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local result = {}
local post_data, output
if access_token then
params.access_token = access_token
end
if method == 'GET' or method == 'DELETE' then
local response = xtable()
local r, c, h, s = https.request{
url = api_url .. '?' .. _M.build_params(params),
method = 'GET',
sink = ltn12.sink.table(response),
}
result.res = r
result.code = c
result.headers = h
result.status = s
result.data = response:concat()
elseif method == 'POST' then
post_data = _M.build_params(params)
local response = xtable()
local r, c, h, s = https.request{
url = api_url,
method = 'GET',
headers = {
['content-length'] = #body,
['content-type'] = 'application/x-www-form-urlencoded',
},
source = ltn12.source.string(body),
sink = ltn12.sink.table(response),
}
result.res = r
result.code = c
result.headers = h
result.status = s
result.data = response:concat()
else
error 'Unsupported request method. Google supports whether GET, DELETE or POST requests.'
end
-- If the response contains a redirect (such as to an image), return the
-- redirect as the data.
if
(301 == result.code or 302 == result.code or 307 == result.code) and
not empty(result.headers.location)
then
output.data = {
data = result.data,
redirect_code = result.code,
redirect_url = result.headers.location,
}
else
output = json.decode(result.data)
end
return output
end
--[[ Return an Ophal User ID given a Facebook ID.
]]
function _M.google_get_user_id(g_id)
local rs = db_query('SELECT user_id FROM oauth2_google_users WHERE g_id = ?', g_id)
local row = rs:fetch(true)
return (row and row.user_id) and tonumber(row.user_id) or nil
end
function _M.google_login_user(account)
module_invoke_all('user_login', account, output)
_SESSION.user_id = account.id
end
--[[ Save a Ophal User ID to Facebook ID pairing.
]]
function _M.google_save_user(user_id, g_id)
if not empty(user_id) and not empty(g_id) then
-- Delete the existing Google ID if present for this Ophal user and
-- make sure no other Ophal account is connected with this Google ID.
db_query('DELETE FROM oauth2_google_users WHERE user_id = ? OR g_id = ?', user_id, g_id)
db_query('INSERT INTO oauth2_google_users(user_id, g_id, created) VALUES(?, ?, ?)', user_id, g_id, time())
end
end
function _M.google_callback()
local output = {}
local state = explode('|', _GET.state)
local resource_id = state[1]
local nonce = state[2]
local resource, res, g_data, user_id, account
if empty(_M.google_valid_nonce(nonce)) or empty(_GET.code) then
error 'ERROR: Invalid state returned.'
end
if resource_id == 'website' then
res = _M.google_get_authcodes(_GET.code)
if res and not empty(res.access_token) then
_SESSION.oauth2.google = res
g_data = _M.google_api_query('https://www.googleapis.com/oauth2/v2/userinfo', res.access_token)
module_invoke_all('oauth2_google_login', g_data)
-- Use fake email if user email not available.
if empty(g_data.email) then
g_data.email = g_data.id .. '@google.com'
end
user_id = _M.google_get_user_id(g_data.id)
if empty(user_id) then
-- Lookup user from email address
account = user_mod.load_by_field('mail', g_data.email)
if account then
user_id = account.id
else
user_id = user_mod.create{name = g_data.name,
mail = g_data.email,
pass = '',
active = true,
}
end
end
if not empty(user_id) then
_M.facebook_login_user(user_mod.load(user_id))
_SESSION.oauth2.google.data = g_data
else
error 'ERROR: Can not create requested user account.'
end
if user_mod.is_logged_in() then
-- The user is already logged in to Ophal.
-- So just associate the two accounts.
_M.google_save_user(user_mod.current().id, g_data.id)
else
error 'ERROR: Authentication failed!'
end
goto()
end
else
resource = _M.google_get_resource(resource_id)
if empty(resource) then
res = _M.google_get_authcodes(_GET.code)
res.id = _GET.code
res.resource = resource_id
res.user_id = 1
res.ttl = res.expires_in
_M.google_create_authcodes(res)
end
goto(resource.path)
end
return ''
end
function _M.build_params(params)
local output = xtable()
for k, v in pairs(params) do
output:append(k .. '=' .. escape(v))
end
return output:concat '&'
end
function _M.google_get_nonce(resource)
local uuid = require 'uuid'
local nonce = uuid.new()
local rs, err = db_query('INSERT INTO oauth2_google_nonce(id, created) VALUES(?, ?)', nonce, time())
return resource .. '|' .. nonce
end
function _M.google_refresh_authcodes(id)
local token = _M.google_load_authcodes(id)
if not empty(token) then
local json = require 'dkjson'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local body = _M.build_params{
refresh_token = token.refresh_token,
client_id = config.google.client_id,
client_secret = config.google.client_secret,
grant_type = 'refresh_token',
}
local response = {}
local request = {
url = _M.google_api_path '/token',
method = 'POST',
headers = {
['content-length'] = #body,
['content-type'] = 'application/x-www-form-urlencoded',
},
source = ltn12.source.string(body),
sink = ltn12.sink.table(response),
}
local s, c, h, hs = https.request(request)
if c == 200 then
local response = xtable(response):concat()
return json.decode(response)
end
end
end
function _M.google_get_token(resource, user_id)
local rs, err = db_query('SELECT id, access_token, refresh_token, changed, ttl FROM oauth2_google_token WHERE resource = ? AND user_id = ? ORDER BY changed DESC', resource, user_id)
local token = rs:fetch(true)
if not empty(token) then
if token.changed + token.ttl > time() then
return token.access_token
else
res = _M.google_refresh_authcodes(token.id)
if res then
res.id = token.id
res.ttl = res.expires_in
_M.google_update_authcodes(res)
return res.access_token
end
end
end
end
function _M.facebook_api_path(path)
return 'https://graph.facebook.com/' .. (config.facebook.api_version or 'v2.4') .. (path or '')
end
function _M.facebook_get_nonce()
local uuid = require 'uuid'
local nonce = uuid.new()
local rs, err = db_query('INSERT INTO oauth2_facebook_nonce(id, created) VALUES(?, ?)', nonce, time())
return nonce
end
function _M.facebook_valid_nonce(nonce)
local rs = db_query('SELECT COUNT(*) total FROM oauth2_facebook_nonce WHERE id = ? AND ? <= created + ?', nonce, time(), config.facebook.nonce_ttl or 2*60)
local count = rs:fetch(true)
if count then
return tonumber(count.total) > 0
end
end
--[[ Given an approval code from Facebook, return an access token and related.
The approval code is generated by Facebook when a user grants access to our
site application to use their data. We use this approval code to get an
access token from Facebook. The access token usually is valid for about
15 minutes, allowing us to pull as much information as we want about the
user.
@param string $code
An approval code from Facebook. Usually pulled from the ?code GET parameter
after a user has approved our application's access to their information.
@param string $action_name
The action is the directory name underneath the "fboauth" path. This value
must be the same between the page originally provided to Facebook as the
"redirect" URL and when requesting an access token.
@return string
An access token that can be used in REST queries against Facebook's Graph
API, which will provide us with info about the Facebook user.
]]
function _M.facebook_get_authcodes(code, client_id, client_secret)
local json = require 'dkjson'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local authentication_result = {}
local query, token_url, authentication_values
-- Use the default App ID and App Secret if not specified.
client_id = client_id or config.facebook.client_id
client_secret = client_secret or config.facebook.client_secret
-- Note that the "code" provided by Facebook is a hash based on the client_id,
-- client_secret, and redirect_url. All of these things must be IDENTICAL to
-- the same values that were passed to Facebook in the approval request.
query = {
client_id = client_id,
client_secret = client_secret,
--~ auth_type = 'request',
redirect_uri = url('oauth2/facebook/', {absolute = true}),
code = code,
}
token_url = _M.facebook_api_path('/oauth/access_token') .. '?' .. _M.build_params(query)
for i = 1, 5 do
local response = xtable()
local r, c, h, s = https.request{
url = token_url,
method = 'GET',
sink = ltn12.sink.table(response),
}
authentication_result.res = r
authentication_result.code = c
authentication_result.headers = h
authentication_result.status = s
authentication_result.data = response:concat()
if authentication_result.code == 200 then
break
end
-- Facebook access code generation seems to take a lot of time. That's why we have to wait
-- some seconds before the code can be acquired.
sleep(1)
end
if 200 ~= authentication_result.code then
if authentication_result.data then
error(authentication_result.data)
elseif authentication_result.error then
error(authentication_result.error)
else
error 'Unknown error.'
end
else
return json.decode(authentication_result.data)
end
end
--[[ Execute a Graph API query through Facebook.
@see http://developers.facebook.com/docs/reference/api/
]]
function _M.facebook_graph_query(id, access_token, params, method)
params = params or {}
method = method or 'GET'
local json = require 'dkjson'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
local graph_result = {}
local graph_url, post_data, output
if access_token then
params.access_token = access_token
if config.facebook.secret_required then
params.appsecret_proof = hash_hmac('sha256', access_token, config.facebook.client_secret)
end
end
if method == 'GET' or method == 'DELETE' then
graph_url = _M.facebook_api_path('/' .. id) .. '?' .. _M.build_params(params)
local response = xtable()
local r, c, h, s = https.request{
url = graph_url,
method = 'GET',
sink = ltn12.sink.table(response),
}
graph_result.res = r
graph_result.code = c
graph_result.headers = h
graph_result.status = s
graph_result.data = response:concat()
elseif method == 'POST' then
graph_url = _M.facebook_api_path('/' .. id)
post_data = _M.build_params(params)
local response = xtable()
local r, c, h, s = https.request{
url = graph_url,
method = 'GET',
headers = {
['content-length'] = #body,
['content-type'] = 'application/x-www-form-urlencoded',
},
source = ltn12.source.string(body),
sink = ltn12.sink.table(response),
}
graph_result.res = r
graph_result.code = c
graph_result.headers = h
graph_result.status = s
graph_result.data = response:concat()
else
error 'Unsupported request method. Facebook supports whether GET, DELETE or POST requests.'
end
-- If the response contains a redirect (such as to an image), return the
-- redirect as the data.
if
(301 == graph_result.code or 302 == graph_result.code or 307 == graph_result.code) and
not empty(graph_result.headers.location)
then
output.data = {
data = graph_result.data,
redirect_code = graph_result.code,
redirect_url = graph_result.headers.location,
}
else
output = json.decode(graph_result.data)
end
return output
end
--[[ Return an Ophal User ID given a Facebook ID.
]]
function _M.facebook_get_user_id(fb_id)
local rs = db_query('SELECT user_id FROM oauth2_facebook_users WHERE fb_id = ?', fb_id)
local row = rs:fetch(true)
return (row and row.user_id) and tonumber(row.user_id) or nil
end
function _M.facebook_login_user(account)
module_invoke_all('user_login', account, output)
_SESSION.user_id = account.id
end
--[[ Save a Ophal User ID to Facebook ID pairing.
]]
function _M.facebook_save_user(user_id, fb_id)
if not empty(user_id) and not empty(fb_id) then
-- Delete the existing Facebook ID if present for this Ophal user and
-- make sure no other Ophal account is connected with this Facebook ID.
db_query('DELETE FROM oauth2_facebook_users WHERE user_id = ? OR fb_id = ?', user_id, fb_id)
db_query('INSERT INTO oauth2_facebook_users(user_id, fb_id, created) VALUES(?, ?, ?)', user_id, fb_id, time())
end
end
function _M.facebook_callback()
local output = {}
local access_token, res, fb_data, user_id, account
if empty(_M.facebook_valid_nonce(_GET.state)) or empty(_GET.code) then
error 'ERROR: Invalid state returned.'
end
if _GET.error then
error 'ERROR: User has denied to allow access.'
elseif config.facebook.client_id and config.facebook.client_secret then
res = _M.facebook_get_authcodes(_GET.code)
if res and not empty(res.access_token) then
_SESSION.oauth2.facebook = res
local id = 'me' -- Versions prior to v2.4 and v2.5 are working with just this.
local _, _, major, minor = string.find(config.facebook.api_version, 'v(%d+)%.(%d+)')
major = tonumber(major)
minor = tonumber(minor)
if major == 2 and minor >= 4 then
fb_data = _M.facebook_graph_query(id, res.access_token, {fields = config.facebook.fields or 'email,name'})
else
fb_data = _M.facebook_graph_query(id, res.access_token)
end
module_invoke_all('oauth2_facebook_login', fb_data)
-- Use fake email if user email not available.
if empty(fb_data.email) then
fb_data.email = fb_data.id .. '@facebook.com'
end
user_id = _M.facebook_get_user_id(fb_data.id)
if empty(user_id) then
-- Lookup user from email address
account = user_mod.load_by_field('mail', fb_data.email)
if account then
user_id = account.id
else
user_id = user_mod.create{name = fb_data.name,
mail = fb_data.email,
pass = '',
active = true,
}
end
end
if not empty(user_id) then
_M.facebook_login_user(user_mod.load(user_id))
_SESSION.oauth2.facebook.data = fb_data
else
error 'ERROR: Can not create requested user account.'
end
if user_mod.is_logged_in() then
-- The user is already logged in to Ophal.
-- So just associate the two accounts.
_M.facebook_save_user(user_mod.current().id, fb_data.id)
else
error 'ERROR: Authentication failed!'
end
goto()
end
end
return output
end
function theme.oauth2_google_connect(variables)
local variables = variables or {}
local base_url = 'https://accounts.google.com/o/oauth2'
local params = {
scope = 'https://www.googleapis.com/auth/analytics.readonly',
state = _M.google_get_nonce(variables.resource),
redirect_uri = ('%s://%s/oauth2/callback'):format(site.scheme or 'http', _SERVER 'SERVER_NAME'),
response_type = 'code',
client_id = config.google.client_id,
access_type = 'offline',
approval_prompt = 'force',
}
local connect_url = base_url .. '/auth?' .. _M.build_params(params)
return l('Connect to Google API', connect_url, {external = true})
end
function theme.oauth2_google_connect_link(variables)
local variables = variables or {}
local base_url = 'https://accounts.google.com/o/oauth2'
local params = {
client_id = config.google.client_id,
redirect_uri = url('oauth2/google/', {absolute = true}),
scope = config.google.scope or 'profile email',
state = _M.google_get_nonce 'website',
response_type = 'code',
access_type = 'offline',
prompt = 'select_account',
include_granted_scopes = 'true',
}
local connect_url = base_url .. '/auth?' .. _M.build_params(params)
local options = {
external = true,
}
for k, v in pairs(variables.options or {}) do
options[k] = v
end
return l(
variables.label and variables.label or 'Connect to Google',
connect_url, options
)
end
function theme.oauth2_facebook_connect_link(variables)
local variables = variables or {}
local base_url = 'https://www.facebook.com/' .. (config.facebook.api_version or 'v2.4') .. '/dialog/oauth'
local params = {
client_id = config.facebook.client_id,
redirect_uri = url('oauth2/facebook/', {absolute = true}),
scope = config.facebook.scope,
state = _M.facebook_get_nonce(),
}
local connect_url = base_url .. '?' .. _M.build_params(params)
local options = {
external = true,
}
for k, v in pairs(variables.options or {}) do
options[k] = v
end
return l(
variables.label and variables.label or 'Connect to Facebook',
connect_url, options
)
end
return _M