diff options
Diffstat (limited to 'filters/simple-authentication.lua')
-rw-r--r-- | filters/simple-authentication.lua | 314 |
1 files changed, 0 insertions, 314 deletions
diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua deleted file mode 100644 index 23d3457..0000000 --- a/filters/simple-authentication.lua +++ /dev/null | |||
@@ -1,314 +0,0 @@ | |||
1 | -- This script may be used with the auth-filter. Be sure to configure it as you wish. | ||
2 | -- | ||
3 | -- Requirements: | ||
4 | -- luaossl | ||
5 | -- <http://25thandclement.com/~william/projects/luaossl.html> | ||
6 | -- luaposix | ||
7 | -- <https://github.com/luaposix/luaposix> | ||
8 | -- | ||
9 | local sysstat = require("posix.sys.stat") | ||
10 | local unistd = require("posix.unistd") | ||
11 | local rand = require("openssl.rand") | ||
12 | local hmac = require("openssl.hmac") | ||
13 | |||
14 | -- | ||
15 | -- | ||
16 | -- Configure these variables for your settings. | ||
17 | -- | ||
18 | -- | ||
19 | |||
20 | -- A list of password protected repositories along with the users who can access them. | ||
21 | local protected_repos = { | ||
22 | glouglou = { laurent = true, jason = true }, | ||
23 | qt = { jason = true, bob = true } | ||
24 | } | ||
25 | |||
26 | -- A list of users and hashes, generated with `mkpasswd -m sha-512 -R 300000`. | ||
27 | local users = { | ||
28 | jason = "$6$rounds=300000$YYJct3n/o.ruYK$HhpSeuCuW1fJkpvMZOZzVizeLsBKcGA/aF2UPuV5v60JyH2MVSG6P511UMTj2F3H75.IT2HIlnvXzNb60FcZH1", | ||
29 | laurent = "$6$rounds=300000$dP0KNHwYb3JKigT$pN/LG7rWxQ4HniFtx5wKyJXBJUKP7R01zTNZ0qSK/aivw8ywGAOdfYiIQFqFhZFtVGvr11/7an.nesvm8iJUi.", | ||
30 | bob = "$6$rounds=300000$jCLCCt6LUpTz$PI1vvd1yaVYcCzqH8QAJFcJ60b6W/6sjcOsU7mAkNo7IE8FRGW1vkjF8I/T5jt/auv5ODLb1L4S2s.CAyZyUC" | ||
31 | } | ||
32 | |||
33 | -- Set this to a path this script can write to for storing a persistent | ||
34 | -- cookie secret, which should be guarded. | ||
35 | local secret_filename = "/var/cache/cgit/auth-secret" | ||
36 | |||
37 | -- | ||
38 | -- | ||
39 | -- Authentication functions follow below. Swap these out if you want different authentication semantics. | ||
40 | -- | ||
41 | -- | ||
42 | |||
43 | -- Sets HTTP cookie headers based on post and sets up redirection. | ||
44 | function authenticate_post() | ||
45 | local hash = users[post["username"]] | ||
46 | local redirect = validate_value("redirect", post["redirect"]) | ||
47 | |||
48 | if redirect == nil then | ||
49 | not_found() | ||
50 | return 0 | ||
51 | end | ||
52 | |||
53 | redirect_to(redirect) | ||
54 | |||
55 | if hash == nil or hash ~= unistd.crypt(post["password"], hash) then | ||
56 | set_cookie("cgitauth", "") | ||
57 | else | ||
58 | -- One week expiration time | ||
59 | local username = secure_value("username", post["username"], os.time() + 604800) | ||
60 | set_cookie("cgitauth", username) | ||
61 | end | ||
62 | |||
63 | html("\n") | ||
64 | return 0 | ||
65 | end | ||
66 | |||
67 | |||
68 | -- Returns 1 if the cookie is valid and 0 if it is not. | ||
69 | function authenticate_cookie() | ||
70 | accepted_users = protected_repos[cgit["repo"]] | ||
71 | if accepted_users == nil then | ||
72 | -- We return as valid if the repo is not protected. | ||
73 | return 1 | ||
74 | end | ||
75 | |||
76 | local username = validate_value("username", get_cookie(http["cookie"], "cgitauth")) | ||
77 | if username == nil or not accepted_users[username:lower()] then | ||
78 | return 0 | ||
79 | else | ||
80 | return 1 | ||
81 | end | ||
82 | end | ||
83 | |||
84 | -- Prints the html for the login form. | ||
85 | function body() | ||
86 | html("<h2>Authentication Required</h2>") | ||
87 | html("<form method='post' action='") | ||
88 | html_attr(cgit["login"]) | ||
89 | html("'>") | ||
90 | html("<input type='hidden' name='redirect' value='") | ||
91 | html_attr(secure_value("redirect", cgit["url"], 0)) | ||
92 | html("' />") | ||
93 | html("<table>") | ||
94 | html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") | ||
95 | html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") | ||
96 | html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") | ||
97 | html("</table></form>") | ||
98 | |||
99 | return 0 | ||
100 | end | ||
101 | |||
102 | |||
103 | |||
104 | -- | ||
105 | -- | ||
106 | -- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. | ||
107 | -- | ||
108 | -- | ||
109 | |||
110 | local actions = {} | ||
111 | actions["authenticate-post"] = authenticate_post | ||
112 | actions["authenticate-cookie"] = authenticate_cookie | ||
113 | actions["body"] = body | ||
114 | |||
115 | function filter_open(...) | ||
116 | action = actions[select(1, ...)] | ||
117 | |||
118 | http = {} | ||
119 | http["cookie"] = select(2, ...) | ||
120 | http["method"] = select(3, ...) | ||
121 | http["query"] = select(4, ...) | ||
122 | http["referer"] = select(5, ...) | ||
123 | http["path"] = select(6, ...) | ||
124 | http["host"] = select(7, ...) | ||
125 | http["https"] = select(8, ...) | ||
126 | |||
127 | cgit = {} | ||
128 | cgit["repo"] = select(9, ...) | ||
129 | cgit["page"] = select(10, ...) | ||
130 | cgit["url"] = select(11, ...) | ||
131 | cgit["login"] = select(12, ...) | ||
132 | |||
133 | end | ||
134 | |||
135 | function filter_close() | ||
136 | return action() | ||
137 | end | ||
138 | |||
139 | function filter_write(str) | ||
140 | post = parse_qs(str) | ||
141 | end | ||
142 | |||
143 | |||
144 | -- | ||
145 | -- | ||
146 | -- Utility functions based on keplerproject/wsapi. | ||
147 | -- | ||
148 | -- | ||
149 | |||
150 | function url_decode(str) | ||
151 | if not str then | ||
152 | return "" | ||
153 | end | ||
154 | str = string.gsub(str, "+", " ") | ||
155 | str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end) | ||
156 | str = string.gsub(str, "\r\n", "\n") | ||
157 | return str | ||
158 | end | ||
159 | |||
160 | function url_encode(str) | ||
161 | if not str then | ||
162 | return "" | ||
163 | end | ||
164 | str = string.gsub(str, "\n", "\r\n") | ||
165 | str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end) | ||
166 | str = string.gsub(str, " ", "+") | ||
167 | return str | ||
168 | end | ||
169 | |||
170 | function parse_qs(qs) | ||
171 | local tab = {} | ||
172 | for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do | ||
173 | tab[url_decode(key)] = url_decode(val) | ||
174 | end | ||
175 | return tab | ||
176 | end | ||
177 | |||
178 | function get_cookie(cookies, name) | ||
179 | cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") | ||
180 | return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) | ||
181 | end | ||
182 | |||
183 | function tohex(b) | ||
184 | local x = "" | ||
185 | for i = 1, #b do | ||
186 | x = x .. string.format("%.2x", string.byte(b, i)) | ||
187 | end | ||
188 | return x | ||
189 | end | ||
190 | |||
191 | -- | ||
192 | -- | ||
193 | -- Cookie construction and validation helpers. | ||
194 | -- | ||
195 | -- | ||
196 | |||
197 | local secret = nil | ||
198 | |||
199 | -- Loads a secret from a file, creates a secret, or returns one from memory. | ||
200 | function get_secret() | ||
201 | if secret ~= nil then | ||
202 | return secret | ||
203 | end | ||
204 | local secret_file = io.open(secret_filename, "r") | ||
205 | if secret_file == nil then | ||
206 | local old_umask = sysstat.umask(63) | ||
207 | local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) | ||
208 | local temporary_file = io.open(temporary_filename, "w") | ||
209 | if temporary_file == nil then | ||
210 | os.exit(177) | ||
211 | end | ||
212 | temporary_file:write(tohex(rand.bytes(32))) | ||
213 | temporary_file:close() | ||
214 | unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. | ||
215 | unistd.unlink(temporary_filename) | ||
216 | sysstat.umask(old_umask) | ||
217 | secret_file = io.open(secret_filename, "r") | ||
218 | end | ||
219 | if secret_file == nil then | ||
220 | os.exit(177) | ||
221 | end | ||
222 | secret = secret_file:read() | ||
223 | secret_file:close() | ||
224 | if secret:len() ~= 64 then | ||
225 | os.exit(177) | ||
226 | end | ||
227 | return secret | ||
228 | end | ||
229 | |||
230 | -- Returns value of cookie if cookie is valid. Otherwise returns nil. | ||
231 | function validate_value(expected_field, cookie) | ||
232 | local i = 0 | ||
233 | local value = "" | ||
234 | local field = "" | ||
235 | local expiration = 0 | ||
236 | local salt = "" | ||
237 | local chmac = "" | ||
238 | |||
239 | if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then | ||
240 | return nil | ||
241 | end | ||
242 | |||
243 | for component in string.gmatch(cookie, "[^|]+") do | ||
244 | if i == 0 then | ||
245 | field = component | ||
246 | elseif i == 1 then | ||
247 | value = component | ||
248 | elseif i == 2 then | ||
249 | expiration = tonumber(component) | ||
250 | if expiration == nil then | ||
251 | expiration = -1 | ||
252 | end | ||
253 | elseif i == 3 then | ||
254 | salt = component | ||
255 | elseif i == 4 then | ||
256 | chmac = component | ||
257 | else | ||
258 | break | ||
259 | end | ||
260 | i = i + 1 | ||
261 | end | ||
262 | |||
263 | if chmac == nil or chmac:len() == 0 then | ||
264 | return nil | ||
265 | end | ||
266 | |||
267 | -- Lua hashes strings, so these comparisons are time invariant. | ||
268 | if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then | ||
269 | return nil | ||
270 | end | ||
271 | |||
272 | if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then | ||
273 | return nil | ||
274 | end | ||
275 | |||
276 | if url_decode(field) ~= expected_field then | ||
277 | return nil | ||
278 | end | ||
279 | |||
280 | return url_decode(value) | ||
281 | end | ||
282 | |||
283 | function secure_value(field, value, expiration) | ||
284 | if value == nil or value:len() <= 0 then | ||
285 | return "" | ||
286 | end | ||
287 | |||
288 | local authstr = "" | ||
289 | local salt = tohex(rand.bytes(16)) | ||
290 | value = url_encode(value) | ||
291 | field = url_encode(field) | ||
292 | authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt | ||
293 | authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) | ||
294 | return authstr | ||
295 | end | ||
296 | |||
297 | function set_cookie(cookie, value) | ||
298 | html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") | ||
299 | if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then | ||
300 | html("; secure") | ||
301 | end | ||
302 | html("\n") | ||
303 | end | ||
304 | |||
305 | function redirect_to(url) | ||
306 | html("Status: 302 Redirect\n") | ||
307 | html("Cache-Control: no-cache, no-store\n") | ||
308 | html("Location: " .. url .. "\n") | ||
309 | end | ||
310 | |||
311 | function not_found() | ||
312 | html("Status: 404 Not Found\n") | ||
313 | html("Cache-Control: no-cache, no-store\n\n") | ||
314 | end | ||