-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathapp.lua
313 lines (274 loc) · 10.9 KB
/
app.lua
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
-- Snap!Cloud
-- ==========
--
-- A cloud backend for Snap!
--
-- Written by Bernat Romagosa and Michael Ball
--
-- Copyright (C) 2019 by Bernat Romagosa and Michael Ball
--
-- This file is part of Snap Cloud.
--
-- Snap Cloud is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of
-- the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
-- Mute annoying _G guard warnings
require 'writeguardmuter'
-- Packaging everything so it can be accessed from other modules
local lapis = require('lapis')
package.loaded.app = lapis.Application()
package.loaded.db = require('lapis.db')
package.loaded.validate = require('lapis.validate')
package.loaded.util = require('lapis.util')
package.loaded.resty_sha512 = require('resty.sha512')
package.loaded.resty_string = require('resty.string')
package.loaded.resty_random = require('resty.random')
package.loaded.config = require('lapis.config').get()
package.loaded.cjson = require('cjson')
package.loaded.app_helpers = require('lapis.application')
package.loaded.json_params = package.loaded.app_helpers.json_params
package.loaded.yield_error = package.loaded.app_helpers.yield_error
package.loaded.respond_to = package.loaded.app_helpers.respond_to
package.loaded.html = require('lapis.html')
local date = require('date')
package.loaded.disk = require('disk')
package.loaded.locale = require('locale')
local app = package.loaded.app
local config = package.loaded.config
-- Snap!Cloud Utilities
local utils = require('lib.util')
-- Track exceptions, exposes raven, rollbar, and normalize_error
local exceptions = require('lib.exceptions')
local domain_allowed = require('cors')
-- Snap!Cloud overrides
-- Provides debug_print, string.from_sql_date
require('lib.global')
-- wrap the lapis capture errors to provide our own custom error handling
-- just do: yield_error({msg = 'oh no', status = 401})
local lapis_capture_errors = package.loaded.app_helpers.capture_errors
package.loaded.capture_errors = function(fn)
return lapis_capture_errors({
on_error = function(self)
local error = self.errors[1]
if type(error) == 'table' then
return errorResponse(self, error.msg, error.status)
else
return errorResponse(self, error, 400)
end
end,
fn
})
end
require 'models'
require 'responses'
app.cookie_attributes = function (self)
-- Cookies are 'session cookies' unless they have an expiration date.
-- Cookies have a Max-Age of 35 days, because this is continually reset
-- using the Snap!Cloud will continue to extend the user's cookie. (See before_filter)
-- Any update to `self.session.x` will extend the cookie's life.
-- See https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html
local attributes = "Domain=" .. ngx.var.host .. "; Path=/;"
if (config._name == 'development') then
attributes = attributes .. " HttpOnly; SameSite=Lax; "
else
-- SameSite must be None on production to allow extensions (and CORS) to work right.
attributes = attributes .. " Secure; HttpOnly; SameSite=None;"
end
if self.session.persist_session == 'true' then
local max_seconds = 35 * 24 * 60 * 60 -- 35 days, 24 hours, 60 minutes, 60 seconds
attributes = "Max-Age=" .. max_seconds .. "; " .. attributes
end
return attributes
end
-- CACHING UTILITIES
-- Custom caching to take in account current locale
local lapis_cached = require('lapis.cache').cached
package.loaded.cached = function (func, options)
local options = options or {}
local cache_key = function (path, params, request)
local key = path
local param_keys = {}
for k, _ in pairs(params) do table.insert(param_keys, k) end
table.sort(param_keys)
for _, v in ipairs(param_keys) do
key = key .. '#' .. v .. '=' .. params[v]
end
return key .. '@' .. (request.session.locale or 'en') ..
'~' .. (request.session.username)
end
local function no_cache (request) return request.params.no_cache ~= true end
return lapis_cached({
dict_name = 'page_cache', -- default dictionary, unchanged
exptime = options.exptime or 30,
cache_key = cache_key,
when = options.when or no_cache,
func
})
end
-- cache for SQL queries so we're not constantly bombarding the DB
package.loaded.cached_query = function (key_table, category, model, on_miss)
local cache = ngx.shared.query_cache
local sorted_keys = {}
for _, v in pairs(key_table) do table.insert(sorted_keys, tostring(v)) end
table.sort(sorted_keys)
local key = ''
for _, v in ipairs(sorted_keys) do key = key .. '#' .. v end
local contents = cache:get(key)
if contents == nil then
-- run the function that was passed for when there's a cache miss
contents = on_miss()
cache:set(key, package.loaded.util.to_json(contents))
if category then
ngx.shared.query_cache_categories:set(category, key)
end
else
contents = package.loaded.util.from_json(contents)
if model then
for _, item in ipairs(contents) do
setmetatable(item, model.__index)
end
end
end
return contents
end
package.loaded.uncache_category = function (category)
local query = ngx.shared.query_cache_categories:get(category)
if query then ngx.shared.query_cache:delete(query) end
end
-- Before filter
app:before_filter(function (self)
-- Temporarily disable IP bans because of too many false positives
--[[
local ip_entry = package.loaded.BannedIPs:find(ngx.var.remote_addr)
if (ip_entry and ip_entry.offense_count > 2) then
self:write(
errorResponse(self, 'Your IP has been banned from the system', 403)
)
return
end
]]--
-- Make locale available to all routes and templates
self.locale = package.loaded.locale
self.locale.language = self.session.locale or 'en'
self.req.source =
(self.req.headers['content-type'] and
self.req.headers['content-type']:find('json')) and 'snap' or 'site'
-- Set Access Control header
local domain = utils.domain_name(self.req.headers.origin)
if self.req.headers.origin and domain_allowed[domain] then
self.res.headers['Access-Control-Allow-Origin'] =
self.req.headers.origin
self.res.headers['Access-Control-Allow-Credentials'] = 'true'
self.res.headers['Vary'] = 'Origin'
end
if ngx.req.get_method() == 'OPTIONS' then
self.res.headers['access-control-allow-headers'] = 'Content-Type'
self.res.headers['access-control-allow-methods'] =
'GET, POST, DELETE, OPTIONS'
self:write(rawResponse('preflight processed'))
return
end
if ngx.req.get_method() == 'POST' then
-- read body params for all POST requests
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- try to decode it, if it fails it's not proper JSON
if pcall(function () package.loaded.util.from_json(body) end) then
local post_params = package.loaded.util.from_json(body)
for k, v in pairs(post_params) do
self.params[k] = v
end
else
self.params.body = body
end
end
if self.params.username and self.params.username ~= '' then
self.params.username =
package.loaded.util.unescape(
tostring(self.params.username)):lower()
self.queried_user =
package.loaded.Users:find({ username = self.params.username })
end
-- unescape all parameters and JSON-decode them
for k, v in pairs(self.params) do
if type(v) == 'string' then
-- leave strings alone
self.params[k] = package.loaded.util.unescape(v)
elseif pcall(function () package.loaded.util.from_json(v) end) then
-- try to decode it, if it fails it's not proper JSON
self.params[k] =
package.loaded.util.from_json(
package.loaded.util.unescape(v)
)
end
end
if self.session.username and self.session.username ~= '' then
self.current_user =
package.loaded.Users:find({ username = self.session.username })
self.session.last_access_at = date(true):fmt('${http}')
else
self.session.username = ''
self.current_user = nil
end
if self.params.matchtext then
self.params.matchtext = '%' .. self.params.matchtext .. '%'
end
end)
function app:default_route()
ngx.log(ngx.NOTICE, "User hit unknown path " .. self.req.parsed_url.path)
-- handle an open redirect vuln so nas not to redirect to different domains
self.req.parsed_url.path = string.gsub(self.req.parsed_url.path, '//', '/')
-- call the original implementaiton to preserve the functionality it provides
return lapis.Application.default_route(self)
end
function app:handle_404()
return errorResponse(self, 'Failed to find resource: ' .. self.req.cmd_url, 404)
end
function app:handle_error(err, trace)
if config._name == 'development' then
debug_print(err, trace)
local msg = '<pre style="text-align: left; width: 80ch">'
.. err .. '<br>' .. trace .. '</pre>'
return errorResponse(self, msg, 500)
end
local err_msg = exceptions.normalize_error(err)
local user_info = exceptions.get_user_info(self.session)
if config.sentry_dsn then
local _, send_err = exceptions.rvn:captureException({{
type = err_msg,
value = err .. "\n\n" .. trace,
trace_level = 2, -- skip `handle_error`
}}, { user = user_info })
if send_err then
ngx.log(ngx.ERR, send_err)
end
end
return errorResponse(self, "An unexpected error occurred: " .. err_msg, 500)
end
-- Enable the ability to have a maintenance mode
-- No routes are served, and a generic error is returned.
if config.maintenance_mode == 'true' then
local msg = 'The Snap!Cloud is currently down for maintenance.'
app:match('/*', function(self)
return errorResponse(self, msg, 500)
end)
return app
end
-- The API for the Snap! editor is implemented in the api.lua file
require 'api'
require 'discourse'
-- We don't keep spam/exploit paths in the API
-- Disabled for now to prevent false positives
-- require 'spambots'
-- The community site is handled in the site.lua file
require 'site'
return app