Diff options
Diffstat (limited to 'custom')
| -rw-r--r-- | custom/cgitrc | 103 | |
| -rw-r--r-- | custom/extensions/about-render.lua | 259 | |
| -rw-r--r-- | custom/extensions/auth-file.lua | 127 | |
| -rw-r--r-- | custom/extensions/auth-inline.lua | 123 | |
| -rw-r--r-- | custom/extensions/email-gravatar.lua | 11 | |
| -rw-r--r-- | custom/extensions/email-libravatar.lua | 11 | |
| -rw-r--r-- | custom/extensions/link-commits.lua | 10 | |
| -rw-r--r-- | custom/extensions/link-trailers.lua | 14 | |
| -rw-r--r-- | custom/extensions/syntax-highlight.lua | 67 | |
| -rwxr-xr-x | custom/hooks/post-receive.cgit-age | 15 | |
| -rwxr-xr-x | custom/hooks/post-receive.cgit-cache | 4 | |
| -rwxr-xr-x | custom/hooks/post-receive.commit-graph | 2 | |
| -rw-r--r-- | custom/servers/apache.conf | 49 | |
| -rw-r--r-- | custom/servers/lighttpd.conf | 22 | |
| -rw-r--r-- | custom/servers/nginx.conf | 52 |
15 files changed, 290 insertions, 579 deletions
diff --git a/custom/cgitrc b/custom/cgitrc index 86f00aa..c1a5b68 100644 --- a/custom/cgitrc +++ b/custom/cgitrc @@ -10,10 +10,6 @@ # list repositories by hand in the per-repository section at the end of this # file. # -# About pages (markdown, man and plain-text readmes) can render through the -# bundled about-render.lua about-filter in the filter section below. Source -# syntax highlighting is optional and ships as a source-filter there too. -# # One key=value pair per line. Lines starting with # or ; are comments. # Maximum number of entries in the cgit output cache. A value of 0 disables @@ -23,36 +19,33 @@ cache-size=0 # Directory used to store cgit cache entries. Value is a filesystem path that # may use macros. Default is /var/cache/cgit. The directory must be owned by the # account the web server runs cgit as, with mode 0700, since the cache holds -# every rendered page and the URLs visitors asked for. +# every rendered page and the urls visitors asked for. cache-root=/var/cache/cgit -# Minutes to cache the repository index page. Value is an integer number of -# minutes. Default is 5. +# Minutes to cache the repository index page. Value is an integer. Default is 5. cache-index-ttl=5 -# Minutes to cache the repository summary page. Value is an integer number of -# minutes. Default is 5. +# Minutes to cache the repository summary page. Value is an integer. Default is +# 5. cache-summary-ttl=5 # Minutes to cache the result of scanning a path for repositories. Value is an -# integer number of minutes. Default is 15. +# integer. Default is 15. cache-scan-ttl=15 # Minutes to cache repository pages requested with a fixed object id. A negative -# value keeps the page forever. Value is an integer number of minutes. Default -# is -1. +# value keeps the page forever. Value is an integer. Default is -1. cache-static-ttl=-1 # Minutes to cache repository pages requested without a fixed object id. Value -# is an integer number of minutes. Default is 5. +# is an integer. Default is 5. cache-dynamic-ttl=5 -# Minutes to cache the repository about page. Value is an integer number of -# minutes. Default is 15. +# Minutes to cache the repository about page. Value is an integer. Default is +# 15. cache-about-ttl=15 -# Minutes to cache snapshots. Value is an integer number of minutes. Default is -# 5. +# Minutes to cache snapshots. Value is an integer. Default is 5. cache-snapshot-ttl=5 # Largest response, in kilobytes, that a cache slot keeps. A larger one is @@ -82,12 +75,12 @@ root-desc=a fast webinterface for the git dscm #readme=:README.md # CSS document urls added to the head of every page. An empty value disables it. -# Value is one or more urls. Default is /cgit.css. +# Value is a url, and each css line adds one. Default is /cgit.css. css=/cgit.css # JavaScript document urls included on every page, loaded with defer in the -# order given. An empty value disables it. Value is one or more urls. Default -# is /cgit.js. +# order given. An empty value disables it. Value is a url, and each js line +# adds one. Default is /cgit.js. js=/cgit.js # Shortcut icon url for all pages. Value is a url. Default is /favicon.ico. @@ -124,8 +117,8 @@ enable-header=1 # or 1. Default is 0. embedded=0 -# Root url for all cgit links, enabling virtual urls. Value is a url path. -# Default is none. +# Root url for all cgit links. Value is a url path. Default is SCRIPT_NAME +# followed by a slash. #virtual-root=/ # Link format string for submodules in directory listings. Value is a format @@ -133,24 +126,21 @@ embedded=0 # Default is none. #module-link=/%s/commit/?id=%s -# Directory scanned for git repositories at parse time. Value is a filesystem -# path that may use macros. Default is none. -#scan-path=/var/lib/git - # When scanning, recurse into dot-directories. Must precede scan-path. Values # are 0 or 1. Default is 0. scan-hidden-path=0 -# Derive default section names from N leading path elements. Must precede -# scan-path. Value is an integer that may be negative. Default is 0. +# Derive default section names from the first N path elements, or from all +# but the last -N when N is negative. Must precede scan-path. Value is an +# integer. Default is 0. section-from-path=0 # Strip the .git suffix from a scanned repository's url and name. Must precede # scan-path. Values are 0 or 1. Default is 0. remove-suffix=0 -# File listing subdirs of scan-path to load. Must precede scan-path. Value is a -# filesystem path that may use macros. Default is none. +# File listing subdirectories of scan-path to load. Must precede scan-path. +# Value is a filesystem path that may use macros. Default is none. #project-list=/var/lib/git/projects.list # Serve a repository only if this file exists inside it. Emulates gitweb @@ -233,18 +223,17 @@ enable-remote-branches=0 # 1. Default is 0. enable-subject-links=0 -# Allow /plain to serve HTML mimetypes instead of downgrading to text/plain. -# Values are 0 or 1. Default is 0. +# Allow /plain to serve HTML mimetypes instead of downgrading to text/plain or +# application/octet-stream. Values are 0 or 1. Default is 0. enable-html-serving=0 # Generate line-number links for plaintext blobs in the tree and blame pages. # Values are 0 or 1. Default is 1. enable-tree-linenumbers=1 -# Name the cgit and git versions in the page footer, the generator tag and -# the signature of patches. Exact versions help someone match a site against -# advisories, so they stay out unless asked for. Values are 0 or 1. Default -# is 0. +# Name the cgit version in the page footer, the generator tag and patch +# signatures, and the git version in the footer. Values are 0 or 1. +# Default is 0. enable-version-info=0 # List directories first, then files, in the tree page. Values are 0 or 1. @@ -281,44 +270,38 @@ enable-http-clone=1 # repository ships under custom/extensions/. The ones written as # /path/to/your-command mark where your own command goes. -# Honour every setting in a repository's own cgitrc and git config found by -# scan-path. Those files belong to whoever can push, so without this the -# settings that run a command, put raw markup on the page, serve a file as -# markup, place a link or read a file off the disk are ignored. The repo.* lines -# below never need it. Values are 0 or 1. Default is 0. +# Honour every setting in a scanned repository's own cgitrc and git config, +# including those that run a command or put markup on the page. Those files +# belong to whoever can push. The repo.* lines below never need it. Values are +# 0 or 1. Default is 0. trust-scan-config=0 # Filter command used to format about-page content. The bundled about-render.lua -# renders markdown, man pages and plain text. Value is a command optionally -# prefixed with exec or lua. Default is none. +# renders markdown, man pages and plain text. Value is a command. Default is +# none. #about-filter=lua:/usr/local/lib/cgit/filters/about-render.lua # Filter command used to format commit messages, for example to turn object -# names and issue numbers into links. Value is a command optionally prefixed -# with exec or lua. Default is none. +# names and issue numbers into links. Value is a command. Default is none. #commit-filter=lua:/usr/local/lib/cgit/filters/link-commits.lua # Filter command used to format author and committer emails, for example to add -# avatar images. Value is a command optionally prefixed with exec or lua. -# Default is none. +# avatar images. Value is a command. Default is none. #email-filter=lua:/usr/local/lib/cgit/filters/email-gravatar.lua # Filter command used to format the value of a commit trailer when # enable-commit-trailers is 1. The shipped filter links Fixes, Reverts, Closes, -# Bug and URL values. Value is a command optionally prefixed with exec or lua. -# Default is none. +# Bug and URL values. Value is a command. Default is none. #trailer-filter=lua:/usr/local/lib/cgit/filters/link-trailers.lua # Filter command used to format plaintext blobs in the tree and blame pages. The -# shipped filter uses the Scintillua lexers and needs lpeg, and missing either -# means plain uncolored text, not an error. Value is a command optionally -# prefixed with exec or lua. Default is none. +# comments in custom/extensions/syntax-highlight.lua list what the shipped +# filter needs. Value is a command. Default is none. #source-filter=lua:/usr/local/lib/cgit/filters/syntax-highlight.lua # Filter command invoked to authenticate access, gating repositories behind a # login. See custom/extensions/auth-inline.lua and -# custom/extensions/auth-file.lua. Value is a command optionally prefixed with -# exec or lua. Default is none. +# custom/extensions/auth-file.lua. Value is a command. Default is none. #auth-filter=lua:/usr/local/lib/cgit/filters/auth-inline.lua # Number of items to display in atom feeds. Value is an integer. Default is 10. @@ -332,7 +315,7 @@ max-message-length=80 # integer. Default is 80. max-repodesc-length=80 -# Max blob size in KBytes read into memory for html, plain and blob output. A +# Max blob size in KBytes read into memory for HTML, plain and blob output. A # value of 0 disables the limit. Value is an integer. Default is 10240. max-blob-size=10240 @@ -361,8 +344,7 @@ max-diff-lines=1000 # Branches and tags listed per section on the refs page, and per page on the # dedicated branch and tag pages. A value of 0 or less lists everything on one -# page. -# Value is an integer. Default is 200. +# page. Value is an integer. Default is 200. max-ref-count=200 # Max files considered when detecting renames. A value of -1 uses the git @@ -426,6 +408,11 @@ age-file=info/web/last-modified #mimetype.png=image/png #mimetype.svg=image/svg+xml +# Directory scanned for git repositories at parse time. Each repository found +# inherits the global settings above this line, so it comes after them all. +# Value is a filesystem path that may use macros. Default is none. +#scan-path=/var/lib/git + # Each repository is introduced by a repo.url line. All following repo.* # settings apply to that repository until the next repo.url line. When # repositories are discovered via scan-path the repo. prefix is dropped inside @@ -462,7 +449,7 @@ age-file=info/web/last-modified #repo.section=main # Prefix for snapshot filenames instead of the repository basename. Value is a -# prefix string. Default is none. +# prefix string. Default is the repository basename. #repo.snapshot-prefix=example # Extra content added verbatim to the head of each page for this repository. 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) diff --git a/custom/hooks/post-receive.cgit-age b/custom/hooks/post-receive.cgit-age index a87e325..82a13f0 100755 --- a/custom/hooks/post-receive.cgit-age +++ b/custom/hooks/post-receive.cgit-age @@ -2,8 +2,13 @@ # Updates the age-file cgit reads for the idle column. The path below is the # default age-file, so a cgitrc that changes the setting needs the same change -# here. To install the hook, copy or link it to hooks/post-receive in each -# repository. +# here. A file the pusher cannot write stays as it was, and nothing here +# complains or exits non-zero, so the pusher is not shown an error over a stale +# idle column. +# +# To install the hook, copy or link it to hooks/post-receive in each +# repository. Git runs one post-receive per repository, so fold this together +# with the other hooks here to have more than one. set -eu @@ -32,10 +37,10 @@ date="$(git for-each-ref --sort=-committerdate --count=1 \ # being handed an empty file. test -n "$date" || exit 0 -mkdir -p "$(dirname "$agefile")" +mkdir -p "$(dirname "$agefile")" 2>/dev/null || exit 0 # Write through a temporary file so a reader never sees a half written date. tmpfile="$agefile.$$" trap 'rm -f "$tmpfile"' EXIT -printf '%s\n' "$date" >"$tmpfile" -mv "$tmpfile" "$agefile" +{ printf '%s\n' "$date" >"$tmpfile"; } 2>/dev/null || exit 0 +mv "$tmpfile" "$agefile" 2>/dev/null || : diff --git a/custom/hooks/post-receive.cgit-cache b/custom/hooks/post-receive.cgit-cache index f1a4b40..61ac1ea 100755 --- a/custom/hooks/post-receive.cgit-cache +++ b/custom/hooks/post-receive.cgit-cache @@ -19,12 +19,12 @@ # The path below is the default cache-root, so a cgitrc that changes the # setting needs the same change here. The web server owns the cache root, so # the pushing user needs write access to it. Nothing here complains or exits -# nonzero when that access is missing, so the pusher is not shown an error +# non-zero when that access is missing, so the pusher is not shown an error # over a stale page. # # To install the hook, copy or link it to hooks/post-receive in each # repository. Git runs one post-receive per repository, so fold this together -# with the age-file hook to have both. +# with the other hooks here to have more than one. set -eu diff --git a/custom/hooks/post-receive.commit-graph b/custom/hooks/post-receive.commit-graph index 51d8c3c..d2d711c 100755 --- a/custom/hooks/post-receive.commit-graph +++ b/custom/hooks/post-receive.commit-graph @@ -10,7 +10,7 @@ # --split writes each push's new commits as a small layer over the last, and # git merges the layers as they pile up, so a push costs little. A git too old # for these options leaves the graph as it was, and nothing here complains or -# exits nonzero, so the pusher is not shown an error over a missing speedup. +# exits non-zero, so the pusher is not shown an error over a missing speedup. # # To install the hook, copy or link it to hooks/post-receive in each # repository. Git runs one post-receive per repository, so fold this together diff --git a/custom/servers/apache.conf b/custom/servers/apache.conf index 7778c7f..3d65d5e 100644 --- a/custom/servers/apache.conf +++ b/custom/servers/apache.conf @@ -17,16 +17,14 @@ # cgit CGI binary /usr/lib/cgit/cgit.cgi # static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt) # cgit config /etc/cgitrc -# public URL https://git.example.org/ (cgit at the domain root) +# public url https://git.example.org/ (cgit at the domain root) # # cgit is one CGI executable. It learns the repository and the page from -# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. -# ScriptAlias runs the binary and forwards the trailing path as PATH_INFO, so -# no extra path tuning is needed. cgit builds its own link base from -# SCRIPT_NAME. The five static assets are served straight off disk. mod_alias -# resolves Alias and ScriptAlias in order and the first match wins, so the -# static Alias lines come before the catch-all ScriptAlias to stop the two -# routes from shadowing each other. +# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. The +# five static assets are served straight off disk. mod_alias resolves Alias and +# ScriptAlias in order and the first match wins, so the static Alias lines come +# before the catch-all ScriptAlias to stop the two routes from shadowing each +# other. # ServerRoot is what every relative path below resolves against, including the @@ -138,8 +136,8 @@ AddType text/plain .txt Require all granted # These assets rarely change, so let browsers cache them. Keep the - # caching scoped to this directory. Ordinary cgit pages carry no caching - # headers, so a server-wide ExpiresDefault would stamp freshness onto + # caching scoped to this directory. Ordinary cgit pages carry no expiry + # time, so a server-wide ExpiresDefault would stamp freshness onto # them, fight the no-store cgit puts on the login page, and could let # one visitor's page be served to another from a shared cache. <IfModule mod_expires.c> @@ -150,7 +148,6 @@ AddType text/plain .txt # The cgit binary. <Directory "/usr/lib/cgit"> - # Allow CGI execution here. Options +ExecCGI # Run cgit.cgi as a CGI even if it is ever reached through a plain Alias # rather than ScriptAlias. @@ -204,13 +201,8 @@ AddType text/plain .txt LimitRequestBody 65536 # Site-wide security headers are set here so they also cover the static - # assets Apache serves. cgit itself sends only the headers the proxy - # cannot supply. Those are Status, Content-Type, Content-Length and - # Content-Disposition on downloads, Location on redirects, a Cache-Control - # marking the login page no-store and a page behind an auth filter private, - # the auth filter's Set-Cookie, and on raw repository bytes a nosniff of - # its own next to the stricter policy "default-src 'none'". Everything - # else, this policy included, is the proxy's job. + # assets Apache serves. On raw repository bytes cgit sends a nosniff and + # the stricter policy "default-src 'none'" of its own. # # The word setifempty is load bearing on the two headers cgit can also # emit. "Header always set" replaces a same-named header even when the @@ -248,23 +240,22 @@ AddType text/plain .txt # once browsers have seen it. Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains" - # These five files are the only things served off disk. cgit.css and - # cgit.js are the paths cgit's HTML points at by default, so if you - # relocate the assets update both these Alias targets and the css, js, - # logo and favicon settings in cgitrc to agree. + # These five files are the only things served off disk. cgit's default + # css, js, logo and favicon settings point at these paths, so if you + # relocate the assets update both these Alias targets and those settings + # in cgitrc to agree. Alias /cgit.css /usr/share/cgit/cgit.css Alias /cgit.js /usr/share/cgit/cgit.js Alias /cgit.png /usr/share/cgit/cgit.png Alias /favicon.ico /usr/share/cgit/favicon.ico Alias /robots.txt /usr/share/cgit/robots.txt - # ScriptAlias maps a URL prefix to a path, marks it executable, and - # forwards the rest of the URL as PATH_INFO. Mapping / makes cgit the - # handler for every URL the static Aliases above did not already claim. - # The trailing slash on cgit.cgi/ is load bearing, telling Apache that - # cgit.cgi is the program and the rest of the URL is PATH_INFO. cgit - # derives its link base from SCRIPT_NAME, which at the domain root is / - # and needs no tuning. For a sub-path install see the note below. + # Mapping / makes cgit the handler for every url the static Aliases above + # did not already claim. The trailing slash on cgit.cgi/ is load bearing, + # telling Apache that cgit.cgi is the program and the rest of the url is + # PATH_INFO. cgit derives its link base from SCRIPT_NAME, which at the + # domain root is / and needs no tuning. For a sub-path install see the + # note below. ScriptAlias / /usr/lib/cgit/cgit.cgi/ </VirtualHost> diff --git a/custom/servers/lighttpd.conf b/custom/servers/lighttpd.conf index 8fef3c4..edbf162 100644 --- a/custom/servers/lighttpd.conf +++ b/custom/servers/lighttpd.conf @@ -9,19 +9,18 @@ # lighttpd -D -f /path/to/lighttpd.conf # run it in the foreground # # lighttpd runs the cgit.cgi binary directly through mod_cgi, so no FastCGI -# bridge is needed. This is cgit's classic reference deployment, mod_cgi with -# mod_alias and mod_setenv. +# bridge is needed. # # Paths assumed below, edit them to match your install. # cgit CGI binary /usr/lib/cgit/cgit.cgi # static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt) # cgit config /etc/cgitrc -# public URL https://git.example.org/ (cgit at the domain root) +# public url https://git.example.org/ (cgit at the domain root) # # cgit is one CGI executable. It learns the repository and the page from -# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. cgit -# builds its own link base from SCRIPT_NAME. The five static assets are served -# straight off disk and must never be routed through cgit. +# PATH_INFO and reads page options such as h= and id= from QUERY_STRING. The +# five static assets are served straight off disk and must never be routed +# through cgit. # A plain assignment rather than "+=", since this config stands on its own @@ -75,13 +74,8 @@ $HTTP["host"] == "git.example.org" { setenv.add-environment = ( "CGIT_CONFIG" => "/etc/cgitrc" ) # Site-wide security headers are set here so they also cover the static - # assets lighttpd serves. cgit itself sends only the headers the server - # cannot supply. Those are Status, Content-Type, Content-Length and - # Content-Disposition on downloads, Location on redirects, a Cache-Control - # marking the login page no-store and a page behind an auth filter private, - # the auth filter's Set-Cookie, and on raw repository bytes a nosniff of - # its own next to the stricter policy "default-src 'none'". Everything - # else, this policy included, is the server's job. + # assets lighttpd serves. On raw repository bytes cgit sends a nosniff and + # the stricter policy "default-src 'none'" of its own. # # The add in add-response-header is load bearing. It appends a second # copy next to what cgit emitted, so a raw page carries both policies and @@ -128,7 +122,7 @@ $HTTP["host"] == "git.example.org" { # hands everything else to cgit. # # The trailing slash on cgit.cgi/ is load bearing. lighttpd builds the - # physical path by stripping the matched key off the front of the URL and + # physical path by stripping the matched key off the front of the url and # appending the rest to the value. For the key / the remainder carries no # leading slash, so without the trailing slash a request for # /linux/tree/kernel/sched.c glues onto the binary name as diff --git a/custom/servers/nginx.conf b/custom/servers/nginx.conf index d18a109..7f09682 100644 --- a/custom/servers/nginx.conf +++ b/custom/servers/nginx.conf @@ -12,13 +12,12 @@ # cgit CGI binary /usr/lib/cgit/cgit.cgi # static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt) # cgit config /etc/cgitrc -# public URL https://git.example.org/ (cgit at the domain root) +# public url https://git.example.org/ (cgit at the domain root) # # cgit is one CGI executable. It learns the repository and the page from # PATH_INFO and reads page options such as h= and id= from QUERY_STRING, so -# nginx must pass PATH_INFO through to the binary. cgit builds its own link -# base from SCRIPT_NAME. The five static assets are served straight off disk -# and must never be routed through cgit. +# nginx must pass PATH_INFO through to the binary. The five static assets are +# served straight off disk and must never be routed through cgit. # # nginx cannot run CGI programs itself, so a small bridge called fcgiwrap runs # the cgit.cgi binary and speaks FastCGI to nginx. Nothing below works until @@ -120,7 +119,6 @@ http { root /var/www/html; } - # Everything else moves to HTTPS. location / { return 301 https://$host$request_uri; } @@ -152,15 +150,9 @@ http { ssl_session_timeout 1d; ssl_session_tickets off; - # Site-wide security headers sit here because they must also cover the - # static assets nginx serves directly. cgit itself sends only the - # headers the proxy cannot supply. Those are Status, Content-Type, - # Content-Length and Content-Disposition on downloads, Location on - # redirects, a Cache-Control marking the login page no-store and a page - # behind an auth filter private, the auth filter's Set-Cookie, and on - # raw repository bytes a nosniff of its own next to the stricter policy - # "default-src 'none'". Everything else, this policy included, is the - # proxy's job. + # Site-wide security headers are set here so they also cover the + # static assets nginx serves. On raw repository bytes cgit sends a + # nosniff and the stricter policy "default-src 'none'" of its own. # # add_header appends and never replaces what cgit sent, so a raw page # carries both policies and the browser enforces the stricter one, @@ -199,9 +191,9 @@ http { # hard to undo once browsers have seen it. add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; - # The document root is the directory that holds the static assets. cgit - # emits absolute links to /cgit.css and /cgit.png by default, so those - # files must resolve at the root of the URL space. + # The document root is the directory that holds the static assets. + # cgit's default css, js, logo and favicon settings are absolute links + # to these files, so they must resolve at the root of the url space. root /usr/share/cgit; # The only request body cgit ever reads is the auth-filter login form, @@ -214,18 +206,17 @@ http { # Match the assets by their exact root-level names, never by bare # extension. A repository can hold files ending in .css or .png, and - # /myrepo/tree/style.css is a real cgit URL a broad match would + # /myrepo/tree/style.css is a real cgit url a broad match would # capture and 404. The regex anchored at ^/ matches /cgit.css but # never /myrepo/tree/cgit.css, and a regex location beats the prefix - # location below, so assets win their exact URLs and cgit the rest. + # location below, so assets win their exact urls and cgit the rest. location ~ ^/(cgit\.css|cgit\.js|cgit\.png|favicon\.ico|robots\.txt)$ { expires 30d; access_log off; try_files $uri =404; } - # Everything that is not a static asset above is a cgit URL, the repo - # index, a repository, a page within a repository, a snapshot, a feed. + # Everything else is a cgit url. location / { # The CGI environment, normally "include fastcgi_params", written # out here so this config stands alone. The cookie and referer @@ -249,7 +240,7 @@ http { # whole request path is the PATH_INFO. fastcgi_param PATH_INFO $uri; - # Page options such as h=branch, id=sha and the snapshot format. + # Page options such as h=branch and id=sha. fastcgi_param QUERY_STRING $query_string; # Which config cgit reads. It checks CGIT_CONFIG and falls back to @@ -257,11 +248,11 @@ http { # explicit and lets you move cgitrc without recompiling. fastcgi_param CGIT_CONFIG /etc/cgitrc; - # The browser's Host header, so cgit builds clone URLs against the + # The browser's Host header, so cgit builds clone urls against the # name the visitor used rather than server_name. fastcgi_param HTTP_HOST $http_host; - # The real scheme, so cgit builds correct https clone URLs. + # The real scheme, so cgit builds correct https clone urls. fastcgi_param HTTPS $https if_not_empty; # The routine remainder, needed by fcgiwrap and by the login form. @@ -281,7 +272,6 @@ http { fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; - # Hand off to the fcgiwrap socket. fastcgi_pass unix:/run/fcgiwrap.socket; # Large outputs such as snapshot tarballs and blame on big files @@ -289,7 +279,7 @@ http { fastcgi_read_timeout 300s; fastcgi_buffering off; - # Ordinary cgit pages carry no caching headers, so browsers + # Ordinary cgit pages carry no expiry time, so browsers # refetch them and cgit's internal cache keeps that cheap. Do not # add a blanket expires or Cache-Control in this location. It # would fight the no-store cgit puts on the login page and could @@ -300,7 +290,7 @@ http { # To serve cgit at https://git.example.org/cgit/ instead of the root, split -# the URL so SCRIPT_NAME is the prefix and PATH_INFO is the rest. Keep every +# the url so SCRIPT_NAME is the prefix and PATH_INFO is the rest. Keep every # other fastcgi_param from the location above. # # location /cgit/ { @@ -320,7 +310,7 @@ http { # access_log off; # } # -# cgit's default css=/cgit.css and logo=/cgit.png point at the domain root, so -# under a sub-path also set css=/cgit/cgit.css and logo=/cgit/cgit.png in -# cgitrc. cgit derives the /cgit prefix from SCRIPT_NAME. If links come out -# wrong, pin it in cgitrc with virtual-root=/cgit/. +# cgit's default css, js, logo and favicon settings point at the domain root, +# so under a sub-path also set them to the /cgit/ paths in cgitrc. cgit +# derives the /cgit prefix from SCRIPT_NAME. If links come out wrong, pin it +# in cgitrc with virtual-root=/cgit/. |
