all repos — cgit @ d6e9200cc35411f3f27426b608bcfdef9348e6d3

a hyperfast web frontend for git written in c

filters/simple-authentication.lua (view raw)

  1-- This script may be used with the auth-filter. Be sure to configure it as you wish.
  2--
  3-- Requirements:
  4-- 	luacrypto >= 0.3
  5-- 	<http://mkottman.github.io/luacrypto/>
  6--
  7
  8
  9--
 10--
 11-- Configure these variables for your settings.
 12--
 13--
 14
 15local protected_repos = {
 16	glouglou	= { laurent = true, jason = true },
 17	qt		= { jason = true, bob = true }
 18}
 19
 20local users = {
 21	jason		= "secretpassword",
 22	laurent		= "s3cr3t",
 23	bob		= "ilikelua"
 24}
 25
 26local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM"
 27
 28
 29
 30--
 31--
 32-- Authentication functions follow below. Swap these out if you want different authentication semantics.
 33--
 34--
 35
 36-- Sets HTTP cookie headers based on post
 37function authenticate_post()
 38	local password = users[post["username"]]
 39	-- TODO: Implement time invariant string comparison function to mitigate against timing attack.
 40	if password == nil or password ~= post["password"] then
 41		construct_cookie("", "cgitauth")
 42	else
 43		construct_cookie(post["username"], "cgitauth")
 44	end
 45	return 0
 46end
 47
 48
 49-- Returns 1 if the cookie is valid and 0 if it is not.
 50function authenticate_cookie()
 51	accepted_users = protected_repos[cgit["repo"]]
 52	if accepted_users == nil then
 53		-- We return as valid if the repo is not protected.
 54		return 1
 55	end
 56
 57	local username = validate_cookie(get_cookie(http["cookie"], "cgitauth"))
 58	if username == nil or not accepted_users[username] then
 59		return 0
 60	else
 61		return 1
 62	end
 63end
 64
 65-- Prints the html for the login form.
 66function body()
 67	html("<h2>Authentication Required</h2>")
 68	html("<form method='post' action='")
 69	html_attr(cgit["login"])
 70	html("'>")
 71	html("<table>")
 72	html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>")
 73	html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>")
 74	html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>")
 75	html("</table></form>")
 76
 77	return 0
 78end
 79
 80
 81--
 82--
 83-- Cookie construction and validation helpers.
 84--
 85--
 86
 87local crypto = require("crypto")
 88
 89-- Returns username of cookie if cookie is valid. Otherwise returns nil.
 90function validate_cookie(cookie)
 91	local i = 0
 92	local username = ""
 93	local expiration = 0
 94	local salt = ""
 95	local hmac = ""
 96
 97	if cookie:len() < 3 or cookie:sub(1, 1) == "|" then
 98		return nil
 99	end
100
101	for component in string.gmatch(cookie, "[^|]+") do
102		if i == 0 then
103			username = component
104		elseif i == 1 then
105			expiration = tonumber(component)
106			if expiration == nil then
107				expiration = 0
108			end
109		elseif i == 2 then
110			salt = component
111		elseif i == 3 then
112			hmac = component
113		else
114			break
115		end
116		i = i + 1
117	end
118
119	if hmac == nil or hmac:len() == 0 then
120		return nil
121	end
122
123	-- TODO: implement time invariant comparison to prevent against timing attack.
124	if hmac ~= crypto.hmac.digest("sha1", username .. "|" .. tostring(expiration) .. "|" .. salt, secret) then
125		return nil
126	end
127
128	if expiration <= os.time() then
129		return nil
130	end
131
132	return username:lower()
133end
134
135function construct_cookie(username, cookie)
136	local authstr = ""
137	if username:len() > 0 then
138		-- One week expiration time
139		local expiration = os.time() + 604800
140		local salt = crypto.hex(crypto.rand.bytes(16))
141
142		authstr = username .. "|" .. tostring(expiration) .. "|" .. salt
143		authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret)
144	end
145
146	html("Set-Cookie: " .. cookie .. "=" .. authstr .. "; HttpOnly")
147	if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then
148		html("; secure")
149	end
150	html("\n")
151end
152
153--
154--
155-- Wrapper around filter API follows below, exposing the http table, the cgit table, and the post table to the above functions.
156--
157--
158
159local actions = {}
160actions["authenticate-post"] = authenticate_post
161actions["authenticate-cookie"] = authenticate_cookie
162actions["body"] = body
163
164function filter_open(...)
165	action = actions[select(1, ...)]
166
167	http = {}
168	http["cookie"] = select(2, ...)
169	http["method"] = select(3, ...)
170	http["query"] = select(4, ...)
171	http["referer"] = select(5, ...)
172	http["path"] = select(6, ...)
173	http["host"] = select(7, ...)
174	http["https"] = select(8, ...)
175
176	cgit = {}
177	cgit["repo"] = select(9, ...)
178	cgit["page"] = select(10, ...)
179	cgit["url"] = select(11, ...)
180
181	cgit["login"] = ""
182	for _ in cgit["url"]:gfind("/") do
183		cgit["login"] = cgit["login"] .. "../"
184	end
185	cgit["login"] = cgit["login"] .. "?p=login"
186
187end
188
189function filter_close()
190	return action()
191end
192
193function filter_write(str)
194	post = parse_qs(str)
195end
196
197
198--
199--
200-- Utility functions follow below, based on keplerproject/wsapi.
201--
202--
203
204function url_decode(str)
205	if not str then
206		return ""
207	end
208	str = string.gsub(str, "+", " ")
209	str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
210	str = string.gsub(str, "\r\n", "\n")
211	return str
212end
213
214function parse_qs(qs)
215	local tab = {}
216	for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
217		tab[url_decode(key)] = url_decode(val)
218	end
219	return tab
220end
221
222function get_cookie(cookies, name)
223	cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";")
224	return url_decode(string.match(cookies, ";" .. name .. "=(.-);"))
225end