Diff options
Diffstat (limited to '')
-rw-r--r--custom/extensions/about-render.lua259
-rw-r--r--custom/extensions/auth-file.lua127
-rw-r--r--custom/extensions/auth-inline.lua123
-rw-r--r--custom/extensions/email-gravatar.lua11
-rw-r--r--custom/extensions/email-libravatar.lua11
-rw-r--r--custom/extensions/link-commits.lua10
-rw-r--r--custom/extensions/link-trailers.lua14
-rw-r--r--custom/extensions/syntax-highlight.lua67
8 files changed, 183 insertions, 439 deletions
diff --git a/custom/extensions/about-render.lua b/custom/extensions/about-render.lua
index 808dd08..5738b00 100644
--- a/custom/extensions/about-render.lua
+++ b/custom/extensions/about-render.lua
@@ -3,8 +3,9 @@
-- interpreter so a readme costs no extra process per request. Markdown, man
-- pages and plain text are the three formats, chosen from the readme's file
-- extension, and anything that fails to parse falls back to escaped plain
--- text. The wrappers it emits carry the classes that assets/cgit.css styles.
--- It runs on Lua 5.1 through 5.5 and LuaJIT.
+-- text. Markdown and man pages come out in div.markdown and plain text in
+-- pre.plaintext, the two wrappers assets/cgit.css styles. It runs on Lua 5.1
+-- through 5.5 and LuaJIT.
--
-- about-filter=lua:/usr/local/lib/cgit/filters/about-render.lua
@@ -190,9 +191,7 @@ end
-- first. html() only ever gets literal tag scaffolding and the character
-- references whose shape the markdown grammar checked.
-local emit_inline
-
-emit_inline = function(list)
+local function emit_inline(list)
for _, node in ipairs(list) do
local kind = node.kind
if kind == "text" then
@@ -271,9 +270,7 @@ local function emit_lines(lines)
end
end
-local emit_blocks
-
-emit_blocks = function(blocks)
+local function emit_blocks(blocks)
for _, block in ipairs(blocks) do
local kind = block.kind
if kind == "heading" then
@@ -285,13 +282,7 @@ emit_blocks = function(blocks)
elseif kind == "hr" then
html("<hr>")
elseif kind == "code" then
- html("<pre><code")
- if block.lang and block.lang ~= "" then
- html(" data-lang='")
- html_attr(block.lang)
- html("'")
- end
- html(">")
+ html("<pre><code>")
html_txt(block.text)
html("</code></pre>")
elseif kind == "blockquote" then
@@ -348,10 +339,23 @@ end
-- Both parse into a node tree before anything is emitted, so a failed parse
-- falls back to plain text without leaving half a page behind.
+local function render_blocks(text, parse)
+ local ok, blocks = pcall(parse, text)
+ if not ok then
+ return render_plaintext(text)
+ end
+ html("<div class='markdown'>")
+ -- Emitting is wrapped so a bug there can at worst truncate the page,
+ -- never end the request in an error.
+ pcall(emit_blocks, blocks)
+ html("</div>")
+end
+
local render_markdown, render_man
if has_lpeg then
- local P, R, S, B, C, Ct = lpeg.P, lpeg.R, lpeg.S, lpeg.B, lpeg.C, lpeg.Ct
+ local P, R, S, B = lpeg.P, lpeg.R, lpeg.S, lpeg.B
+ local C, Cc, Ct = lpeg.C, lpeg.Cc, lpeg.Ct
local space = S(" \t")
local whitespace = S(" \t\r\n\f\v")
@@ -447,28 +451,27 @@ if has_lpeg then
return { node_text(s) }
end
inline_depth = inline_depth + 1
- local nodes = inline_grammar:match(s) or { node_text(s) }
+ local nodes = inline_grammar:match(s)
inline_depth = inline_depth - 1
return nodes
end
- local lang_char = R("az", "AZ", "09") + S("_.+#-")
local heading_line = C(P("#") * P("#") ^ -5) * space ^ 1 * C(P(1) ^ 0)
-- A run of equals signs or dashes under a paragraph turns it into a
-- heading, as on GitHub, rather than reading as more of its text.
- local setext_line = space ^ 0 * (C(P("=") ^ 1) + C(P("-") ^ 1))
+ local setext_line = space ^ 0 * (P("=") ^ 1 * Cc(1) + P("-") ^ 1 * Cc(2))
* space ^ 0 * eol
local code_indent = P(" ") + P("\t")
local function thematic(mark)
return space ^ 0 * P(mark) * (space ^ 0 * P(mark)) ^ 2 * space ^ 0 * eol
end
local break_line = thematic("-") + thematic("*") + thematic("_")
- local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3)) * space ^ 0 * C(lang_char ^ 0)
+ local fence_line = space ^ 0 * (C(P("`") ^ 3) + C(P("~") ^ 3))
local close_backtick = space ^ 0 * P("`") ^ 3 * space ^ 0 * eol
local close_tilde = space ^ 0 * P("~") ^ 3 * space ^ 0 * eol
local quote_line = space ^ 0 * P(">")
local bullet = S("-*+")
- local number = R("09") ^ 1 * S(".)")
+ local number = digit ^ 1 * S(".)")
local item_line = space ^ 0 * C(bullet + number) * space ^ 1 * C(P(1) ^ 0)
local dash_cell = space ^ 0 * P(":") ^ -1 * P("-") ^ 1 * P(":") ^ -1 * space ^ 0
-- A delimiter row is dash-cells joined by pipes. Require at least one
@@ -480,16 +483,10 @@ if has_lpeg then
pipe * dash_cell * (pipe * dash_cell) ^ 0 * pipe ^ -1
+ dash_cell * (pipe * dash_cell) ^ 1 * pipe ^ -1
) * space ^ 0 * eol
- local block_start = space ^ 0 * ((P("#") * P("#") ^ -5 * space) + P(">")
- + P("`") ^ 3 + P("~") ^ 3 + ((bullet + number) * space))
+ local block_start = space ^ 0 * heading_line + quote_line + fence_line + item_line
local function ordered(mark) return mark:match("%d") ~= nil end
- local function follows_list(blocks)
- local last = blocks[#blocks]
- return last ~= nil and last.kind == "list"
- end
-
-- An indented block runs on through blank lines, and up to four spaces
-- or one tab come off each of its lines.
local function continues_code(line)
@@ -500,13 +497,6 @@ if has_lpeg then
return line:match("^\t(.*)$") or line:match("^ ? ? ? ?(.*)$")
end
- local function setext_level(line)
- local run = setext_line:match(line)
- if run then
- return run:sub(1, 1) == "=" and 1 or 2
- end
- end
-
local function table_starts(lines, i)
return i < #lines and lines[i]:find("|", 1, true) ~= nil
and table_delimiter:match(lines[i + 1]) ~= nil
@@ -517,7 +507,15 @@ if has_lpeg then
-- paragraph short.
local function ends_paragraph(lines, i)
return lines[i]:match("^%s*$") or block_start:match(lines[i])
- or table_starts(lines, i)
+ or break_line:match(lines[i]) or table_starts(lines, i)
+ end
+
+ local function code_block(lines)
+ local text = table.concat(lines, "\n")
+ if #lines > 0 then
+ text = text .. "\n"
+ end
+ return { kind = "code", text = text }
end
-- A paragraph's lines run together the way GitHub shows them, unless one
@@ -542,13 +540,11 @@ if has_lpeg then
return lines
end
- local parse_blocks
-
- parse_blocks = function(lines, depth)
+ local function parse_blocks(lines, depth)
local blocks, i, n = {}, 1, #lines
while i <= n do
local line = lines[i]
- local fence, lang = fence_line:match(line)
+ local fence = fence_line:match(line)
local hashes, title = heading_line:match(line)
local mark = item_line:match(line)
if line:match("^%s*$") then
@@ -562,15 +558,7 @@ if has_lpeg then
i = i + 1
end
i = i + 1
- local body = table.concat(code, "\n")
- if #code > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = {
- kind = "code",
- lang = lang,
- text = body,
- }
+ blocks[#blocks + 1] = code_block(code)
elseif hashes then
blocks[#blocks + 1] = {
kind = "heading",
@@ -628,10 +616,8 @@ if has_lpeg then
end
local item = { content }
i = i + 1
- -- A wrapped line carries the item on, whatever its
- -- indent, while a rule still ends the list.
- while i <= n and not ends_paragraph(lines, i)
- and not break_line:match(lines[i]) do
+ -- A wrapped line carries the item on, whatever its indent.
+ while i <= n and not ends_paragraph(lines, i) do
item[#item + 1] = lines[i]
i = i + 1
end
@@ -645,7 +631,8 @@ if has_lpeg then
-- Four spaces or a tab start a code block, except straight
-- after a list, where an indented paragraph more likely
-- carries an item on.
- elseif code_indent:match(line) and not follows_list(blocks) then
+ elseif code_indent:match(line)
+ and (blocks[#blocks] or {}).kind ~= "list" then
local code = {}
while i <= n and continues_code(lines[i]) do
code[#code + 1] = unindent(lines[i])
@@ -654,17 +641,13 @@ if has_lpeg then
while code[#code]:match("^%s*$") do
code[#code] = nil
end
- blocks[#blocks + 1] = {
- kind = "code",
- text = table.concat(code, "\n") .. "\n",
- }
+ blocks[#blocks + 1] = code_block(code)
else
local paragraph, level = { line }, nil
i = i + 1
while i <= n do
- level = setext_level(lines[i])
- if level or ends_paragraph(lines, i)
- or break_line:match(lines[i]) then
+ level = setext_line:match(lines[i])
+ if level or ends_paragraph(lines, i) then
break
end
paragraph[#paragraph + 1] = lines[i]
@@ -693,19 +676,9 @@ if has_lpeg then
end
render_markdown = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_blocks, split_lines(text), 0)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown'>")
- -- Emitting is wrapped so a bug there can at worst
- -- truncate the page, never raise and let the outer
- -- fallback duplicate what has already been sent.
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, function(t)
+ return parse_blocks(split_lines(t), 0)
+ end)
end
-- Roff fonts are a current state carried across a run rather than
@@ -733,15 +706,8 @@ if has_lpeg then
}
local backslash = P("\\")
- local function text_token(text) return { kind = "text", text = text } end
- local skip_token = { kind = "skip" }
- local function skip() return skip_token end
local function font_token(name)
- return { kind = "font", font = fonts[name] or "R" }
- end
- local function char_token(name) return text_token(man_chars[name] or "") end
- local function string_token(name)
- return text_token(man_strings[name] or "")
+ return { font = fonts[name] or "R" }
end
-- \( takes a two character name and \[ a longer one, bounded so that a
@@ -754,50 +720,43 @@ if has_lpeg then
-- the escapes that only steer hyphenation or breaks.
local man_inline_grammar = Ct((
(backslash * P("f") * (named + C(P(1)))) / font_token
- + (backslash * S('"#') * P(1) ^ 0) / skip
- + (backslash * P("-")) / function() return text_token("-") end
- + (backslash * P("e")) / function() return text_token("\\") end
- + (backslash * S(" 0~")) / function() return text_token(" ") end
- + (backslash * S("&|^%:cud{}")) / skip
- + (backslash * named) / char_token
- + (backslash * P("*") * (named + C(P(1)))) / string_token
- + (backslash * P("s") * S("+-") ^ -1 * (named + R("09") * R("09") ^ -1)) / skip
- + (backslash * S("mM") * (named + P(1))) / skip
- + (backslash * C(P(1))) / text_token
- + backslash / skip
- + C((1 - backslash) ^ 1) / text_token
+ + backslash * S('"#') * P(1) ^ 0
+ + (backslash * P("-")) / "-"
+ + (backslash * P("e")) / "\\"
+ + (backslash * S(" 0~")) / " "
+ + backslash * S("&|^%:cud{}")
+ + (backslash * named) / man_chars
+ + (backslash * P("*") * (named + C(P(1)))) / man_strings
+ + (backslash * P("s") * S("+-") ^ -1 * (named + digit * digit ^ -1)) / 0
+ + (backslash * S("mM") * (named + P(1))) / 0
+ + backslash * C(P(1))
+ + backslash
+ + C((1 - backslash) ^ 1)
) ^ 0)
+ local function in_font(font, nodes)
+ if font == "B" then
+ return { { kind = "strong", kids = nodes } }
+ elseif font == "I" then
+ return { { kind = "em", kids = nodes } }
+ end
+ return nodes
+ end
+
local function man_inline(text)
- local tokens = man_inline_grammar:match(text) or {}
+ local tokens = man_inline_grammar:match(text)
local nodes, parts, font = {}, {}, "R"
local function flush()
- if #parts == 0 then
- return
- end
local run = table.concat(parts)
parts = {}
- if run == "" then
- return
- end
- if font == "B" then
- nodes[#nodes + 1] = {
- kind = "strong",
- kids = { { kind = "text", text = run } },
- }
- elseif font == "I" then
- nodes[#nodes + 1] = {
- kind = "em",
- kids = { { kind = "text", text = run } },
- }
- else
- nodes[#nodes + 1] = { kind = "text", text = run }
+ if run ~= "" then
+ nodes[#nodes + 1] = in_font(font, { node_text(run) })[1]
end
end
for _, token in ipairs(tokens) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- elseif token.kind == "font" then
+ if type(token) == "string" then
+ parts[#parts + 1] = token
+ else
flush()
font = token.font
end
@@ -809,13 +768,7 @@ if has_lpeg then
-- A no fill block keeps its lines as they are, so only the text of
-- each one is wanted there, without its fonts.
local function man_plain(text)
- local parts = {}
- for _, token in ipairs(man_inline_grammar:match(text) or {}) do
- if token.kind == "text" then
- parts[#parts + 1] = token.text
- end
- end
- return table.concat(parts)
+ return table.concat(plain_text(man_inline(text), {}))
end
-- A macro's arguments divide at blanks, except inside double quotes.
@@ -838,15 +791,6 @@ if has_lpeg then
end
end
- local function in_font(font, nodes)
- if font == "B" then
- return { { kind = "strong", kids = nodes } }
- elseif font == "I" then
- return { { kind = "em", kids = nodes } }
- end
- return nodes
- end
-
-- Each alternating macro sets its arguments in its two fonts by turns
-- with nothing between them, as in .BR ls (1).
local single_font = { B = "B", I = "I" }
@@ -883,11 +827,7 @@ if has_lpeg then
end
local function flush_nofill()
if nofill_lines then
- local body = table.concat(nofill_lines, "\n")
- if #nofill_lines > 0 then
- body = body .. "\n"
- end
- blocks[#blocks + 1] = { kind = "code", text = body }
+ blocks[#blocks + 1] = code_block(nofill_lines)
nofill_lines = nil
end
end
@@ -1000,22 +940,8 @@ if has_lpeg then
end
render_man = function(text)
- if #text > max_bytes then
- return render_plaintext(text)
- end
- local ok, blocks = pcall(parse_man, text)
- if not ok then
- return render_plaintext(text)
- end
- html("<div class='markdown manpage'>")
- pcall(emit_blocks, blocks)
- html("</div>")
+ render_blocks(text, parse_man)
end
-else
- -- No lpeg, so markdown and man readmes read as escaped source, not
- -- nothing.
- render_markdown = render_plaintext
- render_man = render_plaintext
end
@@ -1043,32 +969,13 @@ end
-- cgit takes filter output through a C string sink that stops at the first
-- NUL byte, so each one becomes U+FFFD, as a browser would show it, and the
--- text past it survives. A plain find works on Lua 5.1, where a NUL cannot
--- sit in a pattern.
-local function replace_nul(text)
- if not text:find("\0", 1, true) then
- return text
- end
- local parts, start = {}, 1
- while true do
- local at = text:find("\0", start, true)
- if not at then
- parts[#parts + 1] = text:sub(start)
- return table.concat(parts, "\239\191\189")
- end
- parts[#parts + 1] = text:sub(start, at - 1)
- start = at + 1
- end
-end
-
+-- text past it survives. The class names NUL as the one byte outside \1 to
+-- \255, since on Lua 5.1 a zero byte in a pattern ends it.
function filter_close()
- local text = replace_nul(table.concat(chunks))
+ local text = table.concat(chunks):gsub("[^\1-\255]", "\239\191\189")
chunks = {}
local ext = (filename:match("%.([^.]+)$") or ""):lower()
- local render = handlers[ext] or render_plaintext
- local ok = pcall(render, text)
- if not ok then
- render_plaintext(text)
- end
+ local render = #text <= max_bytes and handlers[ext] or render_plaintext
+ render(text)
return 0
end
diff --git a/custom/extensions/auth-file.lua b/custom/extensions/auth-file.lua
index a03e1ea..ab56ee1 100644
--- a/custom/extensions/auth-file.lua
+++ b/custom/extensions/auth-file.lua
@@ -53,7 +53,6 @@
-- still lists a protected repository with its description unless cgitrc sets
-- repo.hide=1 for it.
--- The values that follow are the configuration and are meant to be edited.
-- Accounts live one per line as username:hash, where the hash is any kind this
-- system's crypt(3) checks. yescrypt ($y$) is what libxcrypt recommends, as on
@@ -79,8 +78,8 @@ local groups_filename = "/etc/cgit-auth/groups"
-- Per-repository access lives one per line as reponame:group1,group2 and so
-- on. A repository named here is protected and one that is not named is
-- public. The repository name has to match exactly, while group and user names
--- match regardless of case. While this file cannot be read every repository
--- is refused, since nothing then tells the protected ones apart.
+-- match regardless of case. While this file cannot be read every page, the
+-- index too, is refused, since nothing then tells the protected ones apart.
local repos_filename = "/etc/cgit-auth/repos"
-- Where the cookie-signing secret is stored, created on first use. It must be
@@ -105,6 +104,7 @@ local cookie_insecure = false
-- the password's length, and one longer than this is turned down unchecked.
local max_password_bytes = 256
+
-- A browser accepts a cookie named __Host- only when it is Secure, covers the
-- whole host and was set by this host, so no sibling subdomain can plant a
-- session in its place. Either cookie setting above can rule that out.
@@ -113,12 +113,18 @@ if not cookie_insecure and cookie_path == "/" then
session_cookie = "__Host-" .. cookie_name
end
+-- A cookie carries its expiry and Max-Age as whole seconds, while on Lua 5.3
+-- and later a length worked out with a fraction, such as 1.5 days, is a float
+-- that tostring would write with a trailing ".0".
+session_seconds = math.floor(session_seconds)
+
-- One request reaches this script as an open, a write and a close, so what the
-- open decodes is kept here for the calls that follow.
local action, http, cgit, post
--- The two lookups below, account_hash and repo_userset, are the only part
--- of this script that differs from auth-inline.lua.
+-- Apart from the configuration, the lookups below, account_hash and
+-- repo_userset with their helpers, are the only code that differs from
+-- auth-inline.lua, which defines trim later, beside get_cookie.
-- Lua patterns backtrack, so "^%s*(.-)%s*$" costs the square of a run of
-- spaces inside the text. Finding each end directly stays linear, which
@@ -152,9 +158,6 @@ end
-- a failed login can spend the same work on a hash of the same kind and
-- cost, and its timing does not tell whether the name exists.
function account_hash(user)
- if user == nil then
- return nil
- end
local wanted = user:lower()
local users_file = io.open(users_filename, "r")
if users_file == nil then
@@ -180,9 +183,6 @@ end
-- repository whose groups resolve to nobody comes back as an empty table
-- instead, so it denies everyone rather than falling through to public.
function repo_userset(repo)
- if repo == nil then
- return nil
- end
local groups = nil
local repos_file, err = io.open(repos_filename, "r")
if repos_file == nil then
@@ -215,25 +215,17 @@ function repo_userset(repo)
return users
end
--- The URL helpers below are adapted from keplerproject/wsapi.
+-- The url helpers below are adapted from keplerproject/wsapi.
function url_decode(str)
- if not str then
- return ""
- end
str = string.gsub(str, "+", " ")
str = string.gsub(str, "%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
- str = string.gsub(str, "\r\n", "\n")
return str
end
function url_encode(str)
- if not str then
- return ""
- end
- str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])", function(char)
return string.format("%%%02X", string.byte(char))
end)
@@ -245,13 +237,13 @@ end
-- '=', for example a base64 password, so the value runs to the next '&'.
function parse_query(query)
local params = {}
- for key, value in string.gmatch(query or "", "([^&=]+)=([^&]*)") do
+ for key, value in string.gmatch(query, "([^&=]+)=([^&]*)") do
params[url_decode(key)] = url_decode(value)
end
return params
end
--- The token comes back still URL encoded by secure_value. Decoding it here
+-- The token comes back still url encoded by secure_value. Decoding it here
-- would break the signature check for any value carrying a percent escape.
function get_cookie(cookies, name)
for raw in string.gmatch(cookies or "", "[^;]+") do
@@ -350,46 +342,9 @@ end
-- or to nil to turn it down, which is how a session meets its account's
-- current hash.
function validate_value(expected_field, cookie, bind)
- local i = 0
- local value = ""
- local field = ""
- local expiration = 0
- local salt = ""
- local signature = ""
-
- if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then
- return nil
- end
-
- for component in string.gmatch(cookie, "[^|]+") do
- if i == 0 then
- field = component
- elseif i == 1 then
- value = component
- elseif i == 2 then
- -- The expiration must be a plain integer, since
- -- tonumber and tostring of "1e9" or "100.0" differ
- -- across Lua versions and the signed bytes have to
- -- come back byte for byte.
- if not string.match(component, "^%d+$") then
- return nil
- end
- expiration = tonumber(component)
- elseif i == 3 then
- salt = component
- elseif i == 4 then
- signature = component
- else
- break
- end
- i = i + 1
- end
-
- if signature == nil or signature:len() == 0 then
- return nil
- end
-
- if url_decode(field) ~= expected_field then
+ local field, value, expiration, salt, signature =
+ string.match(cookie or "", "^([^|]+)|([^|]+)|(%d+)|([^|]+)|([^|]+)$")
+ if signature == nil or url_decode(field) ~= expected_field then
return nil
end
@@ -408,12 +363,13 @@ function validate_value(expected_field, cookie, bind)
end
end
- local payload = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
+ local payload = field .. "|" .. value .. "|" .. expiration .. "|" .. salt
if not constant_equals(signature, sign(payload, binding)) then
return nil
end
-- An expiration of 0 never expires and is used for the redirect token.
+ expiration = tonumber(expiration)
if expiration ~= 0 and expiration <= os.time() then
return nil
end
@@ -435,12 +391,6 @@ function secure_value(field, value, expiration, binding)
return payload .. "|" .. sign(payload, binding)
end
--- A control character in a header value would let that value split the
--- response, so it is dropped rather than escaped.
-function strip_controls(s)
- return (string.gsub(s or "", "%c", ""))
-end
-
-- Compare two strings in time that does not depend on how many leading bytes
-- match, so a mismatch position is not revealed by timing.
function constant_equals(a, b)
@@ -456,7 +406,8 @@ function constant_equals(a, b)
end
-- An empty value is how a login is cleared, since Max-Age=0 tells the browser
--- to drop the cookie it already holds.
+-- to drop the cookie it already holds. Any other value is a token secure_value
+-- built, which holds no control character to split the response with.
function set_cookie(cookie, value)
local attributes = "; HttpOnly; SameSite=Lax; Path=" .. cookie_path
if not cookie_insecure then
@@ -464,16 +415,18 @@ function set_cookie(cookie, value)
end
if value == "" then
attributes = attributes .. "; Max-Age=0"
- elseif session_seconds > 0 then
+ else
attributes = attributes .. "; Max-Age=" .. tostring(session_seconds)
end
- html("Set-Cookie: " .. cookie .. "=" .. strip_controls(value) .. attributes .. "\n")
+ html("Set-Cookie: " .. cookie .. "=" .. value .. attributes .. "\n")
end
+-- A control character in the location would let it split the response, so
+-- it is dropped rather than escaped.
function redirect_to(url)
html("Status: 302 Found\n")
html("Cache-Control: no-cache, no-store\n")
- html("Location: " .. strip_controls(url) .. "\n")
+ html("Location: " .. url:gsub("%c", "") .. "\n")
end
-- A failed login lands back on the page it came from with this flag, which
@@ -501,7 +454,7 @@ end
function authenticate_post()
local redirect = validate_value("redirect", post["redirect"])
- if redirect == nil or not is_safe_redirect(redirect) then
+ if not is_safe_redirect(redirect) then
not_found()
return 0
end
@@ -581,33 +534,17 @@ function body()
return 0
end
--- filter_open unpacks the action and request fields cgit passes in fixed
--- order, filter_write collects a post body, and filter_close runs the action.
-
local actions = {}
actions["authenticate-post"] = authenticate_post
actions["authenticate-cookie"] = authenticate_cookie
actions["body"] = body
-function filter_open(...)
- action = actions[select(1, ...)]
-
+function filter_open(name, cookie, method, query, referer, path, host, https,
+ repo, page, url, login)
+ action = actions[name]
post = {}
-
- http = {}
- http["cookie"] = select(2, ...)
- http["method"] = select(3, ...)
- http["query"] = select(4, ...)
- http["referer"] = select(5, ...)
- http["path"] = select(6, ...)
- http["host"] = select(7, ...)
- http["https"] = select(8, ...)
-
- cgit = {}
- cgit["repo"] = select(9, ...)
- cgit["page"] = select(10, ...)
- cgit["url"] = select(11, ...)
- cgit["login"] = select(12, ...)
+ http = { cookie = cookie }
+ cgit = { repo = repo, url = url, login = login }
end
function filter_close()
diff --git a/custom/extensions/auth-inline.lua b/custom/extensions/auth-inline.lua
index c4b87f6..e795717 100644
--- a/custom/extensions/auth-inline.lua
+++ b/custom/extensions/auth-inline.lua
@@ -53,7 +53,6 @@
-- still lists a protected repository with its description unless cgitrc sets
-- repo.hide=1 for it.
--- The values that follow are the configuration and are meant to be edited.
-- Protected repositories and the users allowed into each. A repository named
-- here is protected and one that is not named is public. The repository key
@@ -104,6 +103,7 @@ local cookie_insecure = false
-- the password's length, and one longer than this is turned down unchecked.
local max_password_bytes = 256
+
-- A browser accepts a cookie named __Host- only when it is Secure, covers the
-- whole host and was set by this host, so no sibling subdomain can plant a
-- session in its place. Either cookie setting above can rule that out.
@@ -112,12 +112,18 @@ if not cookie_insecure and cookie_path == "/" then
session_cookie = "__Host-" .. cookie_name
end
+-- A cookie carries its expiry and Max-Age as whole seconds, while on Lua 5.3
+-- and later a length worked out with a fraction, such as 1.5 days, is a float
+-- that tostring would write with a trailing ".0".
+session_seconds = math.floor(session_seconds)
+
-- One request reaches this script as an open, a write and a close, so what the
-- open decodes is kept here for the calls that follow.
local action, http, cgit, post
--- The two lookups below, account_hash and repo_userset, are the only part
--- of this script that differs from auth-file.lua.
+-- Apart from the configuration, the lookups below, account_hash and
+-- repo_userset with the fold that feeds them, are the only code that
+-- differs from auth-file.lua, which defines trim among its lookups.
-- The configured tables are folded to lowercased user names once, so that a
-- lookup matches whatever case the login form was filled in with, the way
@@ -141,9 +147,6 @@ end
-- a failed login can spend the same work on a hash of the same kind and
-- cost, and its timing does not tell whether the name exists.
function account_hash(user)
- if user == nil then
- return nil
- end
local hash = users[user:lower()]
if hash ~= nil then
return hash
@@ -155,31 +158,20 @@ end
-- The users allowed into a repository come back keyed by lowercased name, and
-- a repository the table does not name comes back as nil.
function repo_userset(repo)
- if repo == nil then
- return nil
- end
return protected_repos[repo]
end
--- The URL helpers below are adapted from keplerproject/wsapi.
+-- The url helpers below are adapted from keplerproject/wsapi.
function url_decode(str)
- if not str then
- return ""
- end
str = string.gsub(str, "+", " ")
str = string.gsub(str, "%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
- str = string.gsub(str, "\r\n", "\n")
return str
end
function url_encode(str)
- if not str then
- return ""
- end
- str = string.gsub(str, "\n", "\r\n")
str = string.gsub(str, "([^%w ])", function(char)
return string.format("%%%02X", string.byte(char))
end)
@@ -191,7 +183,7 @@ end
-- '=', for example a base64 password, so the value runs to the next '&'.
function parse_query(query)
local params = {}
- for key, value in string.gmatch(query or "", "([^&=]+)=([^&]*)") do
+ for key, value in string.gmatch(query, "([^&=]+)=([^&]*)") do
params[url_decode(key)] = url_decode(value)
end
return params
@@ -212,7 +204,7 @@ local function trim(text)
return text:sub(first, last)
end
--- The token comes back still URL encoded by secure_value. Decoding it here
+-- The token comes back still url encoded by secure_value. Decoding it here
-- would break the signature check for any value carrying a percent escape.
function get_cookie(cookies, name)
for raw in string.gmatch(cookies or "", "[^;]+") do
@@ -311,46 +303,9 @@ end
-- or to nil to turn it down, which is how a session meets its account's
-- current hash.
function validate_value(expected_field, cookie, bind)
- local i = 0
- local value = ""
- local field = ""
- local expiration = 0
- local salt = ""
- local signature = ""
-
- if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then
- return nil
- end
-
- for component in string.gmatch(cookie, "[^|]+") do
- if i == 0 then
- field = component
- elseif i == 1 then
- value = component
- elseif i == 2 then
- -- The expiration must be a plain integer, since
- -- tonumber and tostring of "1e9" or "100.0" differ
- -- across Lua versions and the signed bytes have to
- -- come back byte for byte.
- if not string.match(component, "^%d+$") then
- return nil
- end
- expiration = tonumber(component)
- elseif i == 3 then
- salt = component
- elseif i == 4 then
- signature = component
- else
- break
- end
- i = i + 1
- end
-
- if signature == nil or signature:len() == 0 then
- return nil
- end
-
- if url_decode(field) ~= expected_field then
+ local field, value, expiration, salt, signature =
+ string.match(cookie or "", "^([^|]+)|([^|]+)|(%d+)|([^|]+)|([^|]+)$")
+ if signature == nil or url_decode(field) ~= expected_field then
return nil
end
@@ -369,12 +324,13 @@ function validate_value(expected_field, cookie, bind)
end
end
- local payload = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt
+ local payload = field .. "|" .. value .. "|" .. expiration .. "|" .. salt
if not constant_equals(signature, sign(payload, binding)) then
return nil
end
-- An expiration of 0 never expires and is used for the redirect token.
+ expiration = tonumber(expiration)
if expiration ~= 0 and expiration <= os.time() then
return nil
end
@@ -396,12 +352,6 @@ function secure_value(field, value, expiration, binding)
return payload .. "|" .. sign(payload, binding)
end
--- A control character in a header value would let that value split the
--- response, so it is dropped rather than escaped.
-function strip_controls(s)
- return (string.gsub(s or "", "%c", ""))
-end
-
-- Compare two strings in time that does not depend on how many leading bytes
-- match, so a mismatch position is not revealed by timing.
function constant_equals(a, b)
@@ -417,7 +367,8 @@ function constant_equals(a, b)
end
-- An empty value is how a login is cleared, since Max-Age=0 tells the browser
--- to drop the cookie it already holds.
+-- to drop the cookie it already holds. Any other value is a token secure_value
+-- built, which holds no control character to split the response with.
function set_cookie(cookie, value)
local attributes = "; HttpOnly; SameSite=Lax; Path=" .. cookie_path
if not cookie_insecure then
@@ -425,16 +376,18 @@ function set_cookie(cookie, value)
end
if value == "" then
attributes = attributes .. "; Max-Age=0"
- elseif session_seconds > 0 then
+ else
attributes = attributes .. "; Max-Age=" .. tostring(session_seconds)
end
- html("Set-Cookie: " .. cookie .. "=" .. strip_controls(value) .. attributes .. "\n")
+ html("Set-Cookie: " .. cookie .. "=" .. value .. attributes .. "\n")
end
+-- A control character in the location would let it split the response, so
+-- it is dropped rather than escaped.
function redirect_to(url)
html("Status: 302 Found\n")
html("Cache-Control: no-cache, no-store\n")
- html("Location: " .. strip_controls(url) .. "\n")
+ html("Location: " .. url:gsub("%c", "") .. "\n")
end
-- A failed login lands back on the page it came from with this flag, which
@@ -462,7 +415,7 @@ end
function authenticate_post()
local redirect = validate_value("redirect", post["redirect"])
- if redirect == nil or not is_safe_redirect(redirect) then
+ if not is_safe_redirect(redirect) then
not_found()
return 0
end
@@ -542,33 +495,17 @@ function body()
return 0
end
--- filter_open unpacks the action and request fields cgit passes in fixed
--- order, filter_write collects a post body, and filter_close runs the action.
-
local actions = {}
actions["authenticate-post"] = authenticate_post
actions["authenticate-cookie"] = authenticate_cookie
actions["body"] = body
-function filter_open(...)
- action = actions[select(1, ...)]
-
+function filter_open(name, cookie, method, query, referer, path, host, https,
+ repo, page, url, login)
+ action = actions[name]
post = {}
-
- http = {}
- http["cookie"] = select(2, ...)
- http["method"] = select(3, ...)
- http["query"] = select(4, ...)
- http["referer"] = select(5, ...)
- http["path"] = select(6, ...)
- http["host"] = select(7, ...)
- http["https"] = select(8, ...)
-
- cgit = {}
- cgit["repo"] = select(9, ...)
- cgit["page"] = select(10, ...)
- cgit["url"] = select(11, ...)
- cgit["login"] = select(12, ...)
+ http = { cookie = cookie }
+ cgit = { repo = repo, url = url, login = login }
end
function filter_close()
diff --git a/custom/extensions/email-gravatar.lua b/custom/extensions/email-gravatar.lua
index aebb27c..467deff 100644
--- a/custom/extensions/email-gravatar.lua
+++ b/custom/extensions/email-gravatar.lua
@@ -47,7 +47,7 @@ local base_url = "https://gravatar.com/avatar/"
-- One name reaches this filter as an open, writes and a close, so what the
-- open works out is kept here for the close.
-local buffer = ""
+local chunks = {}
local avatar_hash = nil
local function hash_hex(input)
@@ -96,7 +96,7 @@ local function normalize_email(email)
end
function filter_open(email, page)
- buffer = ""
+ chunks = {}
local address = normalize_email(email)
if address == nil or not has_digest then
avatar_hash = nil
@@ -106,7 +106,7 @@ function filter_open(email, page)
end
function filter_write(text)
- buffer = buffer .. text
+ chunks[#chunks + 1] = text
end
local function avatar_url(size)
@@ -118,14 +118,15 @@ end
-- off the critical path. The icon only decorates the name beside it, so its
-- alt text is empty and a screen reader goes straight to the name.
function filter_close()
+ local text = table.concat(chunks)
if avatar_hash == nil then
- html(buffer)
+ html(text)
else
html("<img src='" .. avatar_url(avatar_size) ..
"' srcset='" .. avatar_url(avatar_size * 2) .. " 2x" ..
"' alt='' width='" .. avatar_size ..
"' height='" .. avatar_size ..
- "' loading='lazy' decoding='async'> " .. buffer)
+ "' loading='lazy' decoding='async'> " .. text)
end
return 0
end
diff --git a/custom/extensions/email-libravatar.lua b/custom/extensions/email-libravatar.lua
index fca67a7..0a6e10c 100644
--- a/custom/extensions/email-libravatar.lua
+++ b/custom/extensions/email-libravatar.lua
@@ -46,7 +46,7 @@ local base_url = "https://seccdn.libravatar.org/avatar/"
-- One name reaches this filter as an open, writes and a close, so what the
-- open works out is kept here for the close.
-local buffer = ""
+local chunks = {}
local avatar_hash = nil
local function hash_hex(input)
@@ -95,7 +95,7 @@ local function normalize_email(email)
end
function filter_open(email, page)
- buffer = ""
+ chunks = {}
local address = normalize_email(email)
if address == nil or not has_digest then
avatar_hash = nil
@@ -105,7 +105,7 @@ function filter_open(email, page)
end
function filter_write(text)
- buffer = buffer .. text
+ chunks[#chunks + 1] = text
end
local function avatar_url(size)
@@ -117,14 +117,15 @@ end
-- off the critical path. The icon only decorates the name beside it, so its
-- alt text is empty and a screen reader goes straight to the name.
function filter_close()
+ local text = table.concat(chunks)
if avatar_hash == nil then
- html(buffer)
+ html(text)
else
html("<img src='" .. avatar_url(avatar_size) ..
"' srcset='" .. avatar_url(avatar_size * 2) .. " 2x" ..
"' alt='' width='" .. avatar_size ..
"' height='" .. avatar_size ..
- "' loading='lazy' decoding='async'> " .. buffer)
+ "' loading='lazy' decoding='async'> " .. text)
end
return 0
end
diff --git a/custom/extensions/link-commits.lua b/custom/extensions/link-commits.lua
index 3666140..fc095a4 100644
--- a/custom/extensions/link-commits.lua
+++ b/custom/extensions/link-commits.lua
@@ -28,8 +28,8 @@ local objects = {
}
-- Text-reference rules, each one a Lua pattern with a single capture and a
--- URL where %s is replaced by that capture, percent-encoded. The whole match
--- is what gets shown and the capture is only what goes into the URL. Rules are
+-- Url where %s is replaced by that capture, percent-encoded. The whole match
+-- is what gets shown and the capture is only what goes into the url. Rules are
-- tried in order and the leftmost match wins, so put the more specific
-- patterns first, and an empty list leaves only object names linked. The
-- examples are commented out, so an unedited copy links object names alone,
@@ -54,15 +54,15 @@ local rules = {
local chunks = {}
--- Percent-encode everything but the URL unreserved characters, so a captured
--- value cannot break out of the href attribute or out of the URL itself.
+-- Percent-encode everything but the url unreserved characters, so a captured
+-- value cannot break out of the href attribute or out of the url itself.
local function url_encode(s)
return (string.gsub(s, "[^%w._~-]", function(c)
return string.format("%%%02X", string.byte(c))
end))
end
--- Build one anchor from a URL template holding %s and the text to show. The
+-- Build one anchor from a url template holding %s and the text to show. The
-- replacement is a function so that a '%' in the encoded value is not taken
-- for a gsub reference.
local function make_link(url_template, capture, display)
diff --git a/custom/extensions/link-trailers.lua b/custom/extensions/link-trailers.lua
index b281150..df7c661 100644
--- a/custom/extensions/link-trailers.lua
+++ b/custom/extensions/link-trailers.lua
@@ -13,9 +13,9 @@
-- Rules are tried in order and the first whose key matches the trailer, case
-- insensitively, and whose pattern matches the value wins. Each pattern is a
--- Lua pattern with a single capture and a URL where %s is replaced by that
+-- Lua pattern with a single capture and a url where %s is replaced by that
-- capture, percent-encoded. The matched run is what gets wrapped and the
--- capture is only what goes into the URL. A rule without a key applies to
+-- capture is only what goes into the url. A rule without a key applies to
-- every trailer.
--
-- Lua patterns are not regular expressions. The reference is
@@ -38,7 +38,7 @@ local rules = {
-- url = "https://www.cve.org/CVERecord?id=CVE-%s" },
}
--- A value that is nothing but one http or https URL, under any key, links to
+-- A value that is nothing but one http or https url, under any key, links to
-- itself. Set false to leave such values as text.
local link_urls = true
@@ -46,15 +46,15 @@ local link_urls = true
local key = ""
local chunks = {}
--- Percent-encode everything but the URL unreserved characters, so a captured
--- value cannot break out of the href attribute or out of the URL itself.
+-- Percent-encode everything but the url unreserved characters, so a captured
+-- value cannot break out of the href attribute or out of the url itself.
local function url_encode(s)
return (string.gsub(s, "[^%w._~-]", function(c)
return string.format("%%%02X", string.byte(c))
end))
end
--- Build one anchor from a URL template holding %s and the text to show. The
+-- Build one anchor from a url template holding %s and the text to show. The
-- replacement is a function so that a '%' in the encoded value is not taken
-- for a gsub reference.
local function make_link(url_template, capture, display)
@@ -66,7 +66,7 @@ local function make_link(url_template, capture, display)
end
-- The value is already escaped, which is also what an attribute wants, so a
--- URL only has to be kept away from the quote that closes the attribute.
+-- Url only has to be kept away from the quote that closes the attribute.
local function link_url(text)
if not link_urls or not string.find(text, "^https?://[^%s'\"]+$") then
return nil
diff --git a/custom/extensions/syntax-highlight.lua b/custom/extensions/syntax-highlight.lua
index 8a1e7ab..7414b63 100644
--- a/custom/extensions/syntax-highlight.lua
+++ b/custom/extensions/syntax-highlight.lua
@@ -1,4 +1,4 @@
--- Server-side syntax highlighting for cgit's tree and blob views, named by
+-- Server-side syntax highlighting for cgit's tree and blame pages, named by
-- the source-filter setting and run inside cgit's embedded Lua interpreter
-- so a coloured blob costs no extra process per request. Tokens come from the
-- Scintillua lexers and reach the page wrapped in span elements carrying the
@@ -28,7 +28,8 @@ local scintillua_env = "CGIT_SCINTILLUA_PATH"
-- directory next to cgitrc is enough for it to be found. The lexers come
-- from https://orbitalquark.github.io/scintillua/ and need lpeg, built for
-- the Lua cgit is linked against. Forgetting that is the usual reason
--- nothing is coloured.
+-- nothing is coloured. A file is coloured only when Scintillua's detect()
+-- names its lexer, so an older Scintillua without detect() colours nothing.
--
-- # Debian and Ubuntu
-- sudo apt install lua-lpeg
@@ -74,25 +75,6 @@ local css = {
pseudoelement = "hl-func",
}
--- Extension to lexer name fixes for the fallback path, reached only when
--- this Scintillua has no detect(). Most extensions already equal their lexer
--- name and these are the frequent exceptions.
-local ext_lexer = {
- py = "python",
- js = "javascript",
- ts = "typescript",
- rb = "ruby",
- pl = "perl",
- pm = "perl",
- sh = "bash",
- md = "markdown",
- htm = "html",
- yml = "yaml",
- rs = "rust",
- c = "ansi_c",
- h = "ansi_c",
-}
-
local scintillua = nil
local filename = ""
@@ -170,37 +152,13 @@ local function load_scintillua()
return nil
end
--- Loading a lexer runs its Lua, and one written for another Scintillua can
--- raise, so a failure here just leaves this file uncoloured.
-local function load_lexer_name(name)
- if name == nil then
- return nil
- end
- local ok, lexer = pcall(scintillua.load, name)
- if ok and lexer then
- return lexer
- end
- return nil
-end
-
+-- The first line goes along to detect(), so a script with no extension is
+-- still known by its shebang. detect() matches an extension by its case, so
+-- a name in capitals such as MAIN.C is tried again in lower case.
local function lexer_for(name, text)
- if type(scintillua.detect) == "function" then
- -- The first line goes along, so a script with no extension is still
- -- known by its shebang.
- local ok, lang = pcall(scintillua.detect, name, text:match("^[^\n]*"))
- if ok and lang then
- local lexer = load_lexer_name(lang)
- if lexer then
- return lexer
- end
- end
- end
- local ext = string.match(name, "%.([^.]+)$")
- if not ext then
- return nil
- end
- ext = string.lower(ext)
- return load_lexer_name(ext_lexer[ext]) or load_lexer_name(ext)
+ local first = string.match(text, "^[^\n]*")
+ local lang = scintillua.detect(name, first) or scintillua.detect(string.lower(name), first)
+ return lang and scintillua.load(lang)
end
local function highlight(text)
@@ -208,8 +166,8 @@ local function highlight(text)
if not lexer then
return nil
end
- local ok, tokens = pcall(lexer.lex, lexer, text)
- if not ok or type(tokens) ~= "table" then
+ local tokens = lexer:lex(text)
+ if type(tokens) ~= "table" then
return nil
end
local out = {}
@@ -262,6 +220,9 @@ function filter_close()
scintillua = load_scintillua() or false
end
if scintillua then
+ -- Every step of highlight runs Scintillua's own Lua, and code
+ -- written for another version of it can raise, so a failure
+ -- anywhere there just leaves this file uncoloured.
local ok, marked = pcall(highlight, text)
if ok and marked then
html(marked)