Diffstat (limited to 'filters')
| -rwxr-xr-x | filters/ci-jenkins.sh | 58 | ||||
| -rwxr-xr-x | filters/commit-links.sh | 2 | ||||
| -rw-r--r-- | filters/email-gravatar.lua | 17 | ||||
| -rw-r--r-- | filters/email-libravatar.lua | 17 | ||||
| -rw-r--r-- | filters/file-authentication.lua | 359 | ||||
| -rw-r--r-- | filters/gentoo-ldap-authentication.lua | 78 | ||||
| -rwxr-xr-x | filters/html-converters/md2html | 119 | ||||
| -rw-r--r-- | filters/simple-authentication.lua | 90 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.py | 49 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.sh | 16 |
10 files changed, 706 insertions, 99 deletions
diff --git a/filters/ci-jenkins.sh b/filters/ci-jenkins.sh new file mode 100755 index 0000000..842af21 --- /dev/null +++ b/filters/ci-jenkins.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# This script may be used with the ci-filter or repo.ci-filter setting in +# cgitrc to hide the "ci" tab for refs which have no pipeline on a Jenkins +# instance. +# +# Arguments: +# $1 the name of the branch or tag being viewed +# $2 "branch" or "tag" +# $3 the ci url which the "ci" tab would redirect to +# +# Exit with a zero status to show the tab, non-zero to hide it. This script +# must not write anything to standard output, as that would end up in the +# middle of the page cgit is rendering. +# +# The filter is consulted while rendering every repository page, so the +# verdict is cached on disk to keep Jenkins from being hammered, and the +# probe is given a short timeout so that an unreachable Jenkins degrades +# into a missing tab rather than a hanging web server. +# +# Set CI_NETRC to a netrc(5) file if the Jenkins instance requires +# authentication; without it a private job answers 403 and the tab is +# hidden even though the pipeline exists. + +CI_CACHE_DIR="${CI_CACHE_DIR:-/var/cache/cgit/ci-filter}" +CI_CACHE_TTL_MINUTES="${CI_CACHE_TTL_MINUTES:-5}" +CI_TIMEOUT="${CI_TIMEOUT:-2}" + +url="$3" +test -n "$url" || exit 1 + +# Jenkins job pages are often not readable anonymously, so query the REST +# API rather than the page the tab points at. +probe="$url/api/json?tree=name" + +key="$(printf '%s' "$url" | cksum | tr -cd '0-9')" +cache="$CI_CACHE_DIR/$key" + +mkdir -p "$CI_CACHE_DIR" 2>/dev/null + +if test -f "$cache" && + test -z "$(find "$cache" -mmin "+$CI_CACHE_TTL_MINUTES" 2>/dev/null)" +then + exit "$(cat "$cache")" +fi + +status=0 +curl --silent --fail --head --output /dev/null \ + --max-time "$CI_TIMEOUT" \ + ${CI_NETRC:+--netrc-file "$CI_NETRC"} \ + "$probe" >/dev/null 2>&1 || status=1 + +if test -d "$CI_CACHE_DIR" +then + printf '%s\n' "$status" >"$cache.$$" 2>/dev/null && + mv "$cache.$$" "$cache" 2>/dev/null +fi + +exit "$status" diff --git a/filters/commit-links.sh b/filters/commit-links.sh index 5881952..796ac30 100755 --- a/filters/commit-links.sh +++ b/filters/commit-links.sh @@ -19,7 +19,7 @@ regex='' # This expression generates links to commits referenced by their SHA1. regex=$regex' -s|\b([0-9a-fA-F]{7,40})\b|<a href="./?id=\1">\1</a>|g' +s|\b([0-9a-fA-F]{7,64})\b|<a href="./?id=\1">\1</a>|g' # This expression generates links to a fictional bugtracker. regex=$regex' diff --git a/filters/email-gravatar.lua b/filters/email-gravatar.lua index 52cf426..c39b490 100644 --- a/filters/email-gravatar.lua +++ b/filters/email-gravatar.lua @@ -3,15 +3,24 @@ -- prefix in filters. It is much faster than the corresponding python script. -- -- Requirements: --- luacrypto >= 0.3 --- <http://mkottman.github.io/luacrypto/> +-- luaossl +-- <http://25thandclement.com/~william/projects/luaossl.html> -- -local crypto = require("crypto") +local digest = require("openssl.digest") + +function md5_hex(input) + local b = digest.new("md5"):final(input) + local x = "" + for i = 1, #b do + x = x .. string.format("%.2x", string.byte(b, i)) + end + return x +end function filter_open(email, page) buffer = "" - md5 = crypto.digest("md5", email:sub(2, -2):lower()) + md5 = md5_hex(email:sub(2, -2):lower()) end function filter_close() diff --git a/filters/email-libravatar.lua b/filters/email-libravatar.lua index b0e2447..7336baf 100644 --- a/filters/email-libravatar.lua +++ b/filters/email-libravatar.lua @@ -3,15 +3,24 @@ -- prefix in filters. -- -- Requirements: --- luacrypto >= 0.3 --- <http://mkottman.github.io/luacrypto/> +-- luaossl +-- <http://25thandclement.com/~william/projects/luaossl.html> -- -local crypto = require("crypto") +local digest = require("openssl.digest") + +function md5_hex(input) + local b = digest.new("md5"):final(input) + local x = "" + for i = 1, #b do + x = x .. string.format("%.2x", string.byte(b, i)) + end + return x +end function filter_open(email, page) buffer = "" - md5 = crypto.digest("md5", email:sub(2, -2):lower()) + md5 = md5_hex(email:sub(2, -2):lower()) end function filter_close() diff --git a/filters/file-authentication.lua b/filters/file-authentication.lua new file mode 100644 index 0000000..0248804 --- /dev/null +++ b/filters/file-authentication.lua @@ -0,0 +1,359 @@ +-- This script may be used with the auth-filter. +-- +-- Requirements: +-- luaossl +-- <http://25thandclement.com/~william/projects/luaossl.html> +-- luaposix +-- <https://github.com/luaposix/luaposix> +-- +local sysstat = require("posix.sys.stat") +local unistd = require("posix.unistd") +local rand = require("openssl.rand") +local hmac = require("openssl.hmac") + +-- This file should contain a series of lines in the form of: +-- username1:hash1 +-- username2:hash2 +-- username3:hash3 +-- ... +-- Hashes can be generated using something like `mkpasswd -m sha-512 -R 300000`. +-- This file should not be world-readable. +local users_filename = "/etc/cgit-auth/users" + +-- This file should contain a series of lines in the form of: +-- groupname1:username1,username2,username3,... +-- ... +local groups_filename = "/etc/cgit-auth/groups" + +-- This file should contain a series of lines in the form of: +-- reponame1:groupname1,groupname2,groupname3,... +-- ... +local repos_filename = "/etc/cgit-auth/repos" + +-- Set this to a path this script can write to for storing a persistent +-- cookie secret, which should not be world-readable. +local secret_filename = "/var/cache/cgit/auth-secret" + +-- +-- +-- Authentication functions follow below. Swap these out if you want different authentication semantics. +-- +-- + +-- Looks up a hash for a given user. +function lookup_hash(user) + local line + for line in io.lines(users_filename) do + local u, h = string.match(line, "(.-):(.+)") + if u:lower() == user:lower() then + return h + end + end + return nil +end + +-- Looks up users for a given repo. +function lookup_users(repo) + local users = nil + local groups = nil + local line, group, user + for line in io.lines(repos_filename) do + local r, g = string.match(line, "(.-):(.+)") + if r == repo then + groups = { } + for group in string.gmatch(g, "([^,]+)") do + groups[group:lower()] = true + end + break + end + end + if groups == nil then + return nil + end + for line in io.lines(groups_filename) do + local g, u = string.match(line, "(.-):(.+)") + if groups[g:lower()] then + if users == nil then + users = { } + end + for user in string.gmatch(u, "([^,]+)") do + users[user:lower()] = true + end + end + end + return users +end + + +-- Sets HTTP cookie headers based on post and sets up redirection. +function authenticate_post() + local hash = lookup_hash(post["username"]) + local redirect = validate_value("redirect", post["redirect"]) + + if redirect == nil then + not_found() + return 0 + end + + redirect_to(redirect) + + if hash == nil or hash ~= unistd.crypt(post["password"], hash) then + set_cookie("cgitauth", "") + else + -- One week expiration time + local username = secure_value("username", post["username"], os.time() + 604800) + set_cookie("cgitauth", username) + end + + html("\n") + return 0 +end + + +-- Returns 1 if the cookie is valid and 0 if it is not. +function authenticate_cookie() + accepted_users = lookup_users(cgit["repo"]) + if accepted_users == nil then + -- We return as valid if the repo is not protected. + return 1 + end + + local username = validate_value("username", get_cookie(http["cookie"], "cgitauth")) + if username == nil or not accepted_users[username:lower()] then + return 0 + else + return 1 + end +end + +-- Prints the html for the login form. +function body() + html("<h2>Authentication Required</h2>") + html("<form method='post' action='") + html_attr(cgit["login"]) + html("'>") + html("<input type='hidden' name='redirect' value='") + html_attr(secure_value("redirect", cgit["url"], 0)) + html("' />") + html("<table>") + html("<tr><td><label for='username'>Username:</label></td><td><input id='username' name='username' autofocus /></td></tr>") + html("<tr><td><label for='password'>Password:</label></td><td><input id='password' name='password' type='password' /></td></tr>") + html("<tr><td colspan='2'><input value='Login' type='submit' /></td></tr>") + html("</table></form>") + + return 0 +end + + + +-- +-- +-- Wrapper around filter API, exposing the http table, the cgit table, and the post table to the above functions. +-- +-- + +local actions = {} +actions["authenticate-post"] = authenticate_post +actions["authenticate-cookie"] = authenticate_cookie +actions["body"] = body + +function filter_open(...) + action = actions[select(1, ...)] + + 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, ...) + +end + +function filter_close() + return action() +end + +function filter_write(str) + post = parse_qs(str) +end + + +-- +-- +-- Utility functions based on keplerproject/wsapi. +-- +-- + +function url_decode(str) + if not str then + return "" + end + str = string.gsub(str, "+", " ") + str = string.gsub(str, "%%(%x%x)", function(h) return string.char(tonumber(h, 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(c) return string.format("%%%02X", string.byte(c)) end) + str = string.gsub(str, " ", "+") + return str +end + +function parse_qs(qs) + local tab = {} + for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do + tab[url_decode(key)] = url_decode(val) + end + return tab +end + +function get_cookie(cookies, name) + cookies = string.gsub(";" .. cookies .. ";", "%s*;%s*", ";") + return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) +end + +function tohex(b) + local x = "" + for i = 1, #b do + x = x .. string.format("%.2x", string.byte(b, i)) + end + return x +end + +-- +-- +-- Cookie construction and validation helpers. +-- +-- + +local secret = nil + +-- Loads a secret from a file, creates a secret, or returns one from memory. +function get_secret() + if secret ~= nil then + return secret + end + local secret_file = io.open(secret_filename, "r") + if secret_file == nil then + local old_umask = sysstat.umask(63) + local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) + local temporary_file = io.open(temporary_filename, "w") + if temporary_file == nil then + os.exit(177) + end + temporary_file:write(tohex(rand.bytes(32))) + temporary_file:close() + unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. + unistd.unlink(temporary_filename) + sysstat.umask(old_umask) + secret_file = io.open(secret_filename, "r") + end + if secret_file == nil then + os.exit(177) + end + secret = secret_file:read() + secret_file:close() + if secret:len() ~= 64 then + os.exit(177) + end + return secret +end + +-- Returns value of cookie if cookie is valid. Otherwise returns nil. +function validate_value(expected_field, cookie) + local i = 0 + local value = "" + local field = "" + local expiration = 0 + local salt = "" + local chmac = "" + + 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 + expiration = tonumber(component) + if expiration == nil then + expiration = -1 + end + elseif i == 3 then + salt = component + elseif i == 4 then + chmac = component + else + break + end + i = i + 1 + end + + if chmac == nil or chmac:len() == 0 then + return nil + end + + -- Lua hashes strings, so these comparisons are time invariant. + if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then + return nil + end + + if expiration == -1 or (expiration ~= 0 and expiration <= os.time()) then + return nil + end + + if url_decode(field) ~= expected_field then + return nil + end + + return url_decode(value) +end + +function secure_value(field, value, expiration) + if value == nil or value:len() <= 0 then + return "" + end + + local authstr = "" + local salt = tohex(rand.bytes(16)) + value = url_encode(value) + field = url_encode(field) + authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt + authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) + return authstr +end + +function set_cookie(cookie, value) + html("Set-Cookie: " .. cookie .. "=" .. value .. "; HttpOnly") + if http["https"] == "yes" or http["https"] == "on" or http["https"] == "1" then + html("; secure") + end + html("\n") +end + +function redirect_to(url) + html("Status: 302 Redirect\n") + html("Cache-Control: no-cache, no-store\n") + html("Location: " .. url .. "\n") +end + +function not_found() + html("Status: 404 Not Found\n") + html("Cache-Control: no-cache, no-store\n\n") +end diff --git a/filters/gentoo-ldap-authentication.lua b/filters/gentoo-ldap-authentication.lua index fce5632..673c88d 100644 --- a/filters/gentoo-ldap-authentication.lua +++ b/filters/gentoo-ldap-authentication.lua @@ -1,12 +1,18 @@ -- This script may be used with the auth-filter. Be sure to configure it as you wish. -- -- Requirements: --- luacrypto >= 0.3 --- <http://mkottman.github.io/luacrypto/> +-- luaossl +-- <http://25thandclement.com/~william/projects/luaossl.html> -- lualdap >= 1.2 --- <http://git.zx2c4.com/lualdap/about/> +-- <https://git.zx2c4.com/lualdap/about/> +-- luaposix +-- <https://github.com/luaposix/luaposix> -- - +local sysstat = require("posix.sys.stat") +local unistd = require("posix.unistd") +local lualdap = require("lualdap") +local rand = require("openssl.rand") +local hmac = require("openssl.hmac") -- -- @@ -21,11 +27,9 @@ local protected_repos = { portage = "dev" } - --- All cookies will be authenticated based on this secret. Make it something --- totally random and impossible to guess. It should be large. -local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" - +-- Set this to a path this script can write to for storing a persistent +-- cookie secret, which should be guarded. +local secret_filename = "/var/cache/cgit/auth-secret" -- @@ -102,11 +106,9 @@ end -- -- -local lualdap = require("lualdap") - function gentoo_ldap_user_groups(username, password) -- Ensure the user is alphanumeric - if username:match("%W") then + if username == nil or username:match("%W") then return nil end @@ -224,6 +226,13 @@ function get_cookie(cookies, name) return string.match(cookies, ";" .. name .. "=(.-);") end +function tohex(b) + local x = "" + for i = 1, #b do + x = x .. string.format("%.2x", string.byte(b, i)) + end + return x +end -- -- @@ -231,7 +240,38 @@ end -- -- -local crypto = require("crypto") +local secret = nil + +-- Loads a secret from a file, creates a secret, or returns one from memory. +function get_secret() + if secret ~= nil then + return secret + end + local secret_file = io.open(secret_filename, "r") + if secret_file == nil then + local old_umask = sysstat.umask(63) + local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) + local temporary_file = io.open(temporary_filename, "w") + if temporary_file == nil then + os.exit(177) + end + temporary_file:write(tohex(rand.bytes(32))) + temporary_file:close() + unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. + unistd.unlink(temporary_filename) + sysstat.umask(old_umask) + secret_file = io.open(secret_filename, "r") + end + if secret_file == nil then + os.exit(177) + end + secret = secret_file:read() + secret_file:close() + if secret:len() ~= 64 then + os.exit(177) + end + return secret +end -- Returns value of cookie if cookie is valid. Otherwise returns nil. function validate_value(expected_field, cookie) @@ -240,7 +280,7 @@ function validate_value(expected_field, cookie) local field = "" local expiration = 0 local salt = "" - local hmac = "" + local chmac = "" if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then return nil @@ -259,19 +299,19 @@ function validate_value(expected_field, cookie) elseif i == 3 then salt = component elseif i == 4 then - hmac = component + chmac = component else break end i = i + 1 end - if hmac == nil or hmac:len() == 0 then + if chmac == nil or chmac:len() == 0 then return nil end -- Lua hashes strings, so these comparisons are time invariant. - if hmac ~= crypto.hmac.digest("sha1", field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt, secret) then + if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then return nil end @@ -292,11 +332,11 @@ function secure_value(field, value, expiration) end local authstr = "" - local salt = crypto.hex(crypto.rand.bytes(16)) + local salt = tohex(rand.bytes(16)) value = url_encode(value) field = url_encode(field) authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt - authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) + authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) return authstr end diff --git a/filters/html-converters/md2html b/filters/html-converters/md2html index c8ee7d9..4f6ad37 100755 --- a/filters/html-converters/md2html +++ b/filters/html-converters/md2html @@ -1,7 +1,50 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import markdown -from pygments.formatters import HtmlFormatter -print(''' +import sys +import io +from markdown.extensions.toc import TocExtension + +# Pygments' token classes, mapped onto the --syn-* custom properties that +# cgit.css defines. The fallbacks are the palette those properties default to, +# so that the output stays readable under a stylesheet without them. +SYNTAX_COLORS = ( + (('k', 'kc', 'kd', 'kn', 'kr'), '--syn-keyword', '#000', + 'font-weight: bold'), + (('kp',), '--syn-keyword', '#000', None), + (('kt',), '--syn-type', '#010181', None), + (('nc', 'nf', 'nn', 'ne'), '--syn-keyword-alt', '#0057ae', None), + (('s', 'sa', 'sb', 'sc', 'dl', 'sd', 's2', 'sh', 'sx', 'sr', 's1', 'ss'), + '--syn-string', '#bf0303', None), + (('se',), '--syn-escape', '#f0f', None), + (('si',), '--syn-interpolation', '#0057ae', None), + (('c', 'ch', 'cm', 'c1', 'cs'), '--syn-comment', '#838183', + 'font-style: italic'), + (('cp', 'cpf'), '--syn-preproc', '#008200', None), + (('m', 'mb', 'mf', 'mh', 'mi', 'mo', 'il'), '--syn-number', '#b07e00', + None), + (('o', 'ow', 'p'), '--syn-operator', '#000', None), + (('err',), '--syn-error', '#bf0303', None), + (('gd',), '--red', 'red', None), + (('gi',), '--green', 'green', None), + (('lineno', 'linenos'), '--syn-linenum', '#555', None), +) + + +def style_defs(prefix='.highlight'): + rules = [] + for classes, prop, fallback, extra in SYNTAX_COLORS: + selector = ', '.join('%s .%s' % (prefix, c) for c in classes) + decls = 'color: var(%s, %s)' % (prop, fallback) + if extra: + decls += '; ' + extra + rules.append('%s { %s }' % (selector, decls)) + rules.append('%s .hll { background-color: var(--syn-mark-bg, #ffb) }' + % prefix) + return '\n'.join(rules) + '\n' + +sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stdout.write(''' <style> .markdown-body { font-size: 14px; @@ -15,7 +58,7 @@ print(''' margin-bottom: 0 !important; } .markdown-body a.absent { - color: #c00; + color: var(--red-mid, #c00); } .markdown-body a.anchor { display: block; @@ -37,28 +80,32 @@ print(''' } .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { display: none; - color: #000; + color: var(--fg-strong, #000); } .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { text-decoration: none; line-height: 1; padding-left: 0; margin-left: -22px; - top: 15%} + top: 15%; +} .markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { display: inline-block; } +div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div#cgit .markdown-body h3 a.toclink, div#cgit .markdown-body h4 a.toclink, div#cgit .markdown-body h5 a.toclink, div#cgit .markdown-body h6 a.toclink { + color: var(--fg-strong, #000); +} .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { font-size: inherit; } .markdown-body h1 { font-size: 28px; - color: #000; + color: var(--fg-strong, #000); } .markdown-body h2 { font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; + border-bottom: 1px solid var(--border, #ccc); + color: var(--fg-strong, #000); } .markdown-body h3 { font-size: 18px; @@ -70,18 +117,14 @@ print(''' font-size: 14px; } .markdown-body h6 { - color: #777; + color: var(--fg-mute, #777); font-size: 14px; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { margin: 15px 0; } .markdown-body hr { - background: transparent url("/dirty-shade.png") repeat-x 0 0; - border: 0 none; - color: #ccc; - height: 4px; - padding: 0; + border: 2px solid var(--border, #ccc); } .markdown-body>h2:first-child, .markdown-body>h1:first-child, .markdown-body>h1:first-child+h2, .markdown-body>h3:first-child, .markdown-body>h4:first-child, .markdown-body>h5:first-child, .markdown-body>h6:first-child { margin-top: 0; @@ -143,9 +186,9 @@ print(''' margin-bottom: 0px; } .markdown-body blockquote { - border-left: 4px solid #DDD; + border-left: 4px solid var(--border, #ddd); padding: 0 15px; - color: #777; + color: var(--fg-mute, #777); } .markdown-body blockquote>:first-child { margin-top: 0px; @@ -157,15 +200,15 @@ print(''' font-weight: bold; } .markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; + border: 1px solid var(--border, #ccc); padding: 6px 13px; } .markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; + border-top: 1px solid var(--border, #ccc); + background-color: var(--bg, #fff); } .markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; + background-color: var(--bg-zebra, #f8f8f8); } .markdown-body img { max-width: 100%; @@ -177,7 +220,7 @@ print(''' overflow: hidden; } .markdown-body span.frame>span { - border: 1px solid #ddd; + border: 1px solid var(--border, #ddd); display: block; float: left; overflow: hidden; @@ -191,7 +234,7 @@ print(''' } .markdown-body span.frame span span { clear: both; - color: #333; + color: var(--fg, #333); display: block; padding: 5px 0 0; } @@ -249,8 +292,8 @@ print(''' .markdown-body code, .markdown-body tt { margin: 0 2px; padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; + border: 1px solid var(--border, #eaeaea); + background-color: var(--bg-zebra, #f8f8f8); border-radius: 3px; } .markdown-body code { @@ -264,8 +307,8 @@ print(''' background: transparent; } .markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; + background-color: var(--bg-zebra, #f8f8f8); + border: 1px solid var(--border, #ccc); font-size: 13px; line-height: 19px; overflow: auto; @@ -279,11 +322,21 @@ print(''' border: none; } ''') -print(HtmlFormatter(style='pastie').get_style_defs('.highlight')) -print(''' -</style> +sys.stdout.write(style_defs('.highlight')) +sys.stdout.write(''' +</style> ''') -print("<div class='markdown-body'>") +sys.stdout.write("<div class='markdown-body'>") +sys.stdout.flush() # Note: you may want to run this through bleach for sanitization -markdown.markdownFromFile(output_format="html5", extensions=["markdown.extensions.fenced_code", "markdown.extensions.codehilite", "markdown.extensions.tables"], extension_configs={"markdown.extensions.codehilite":{"css_class":"highlight"}}) -print("</div>") +markdown.markdownFromFile( + output_format="html5", + extensions=[ + "markdown.extensions.fenced_code", + "markdown.extensions.codehilite", + "markdown.extensions.tables", + "markdown.extensions.sane_lists", + TocExtension(anchorlink=True)], + extension_configs={ + "markdown.extensions.codehilite":{"css_class":"highlight"}}) +sys.stdout.write("</div>") diff --git a/filters/simple-authentication.lua b/filters/simple-authentication.lua index de34d09..23d3457 100644 --- a/filters/simple-authentication.lua +++ b/filters/simple-authentication.lua @@ -1,10 +1,15 @@ -- This script may be used with the auth-filter. Be sure to configure it as you wish. -- -- Requirements: --- luacrypto >= 0.3 --- <http://mkottman.github.io/luacrypto/> +-- luaossl +-- <http://25thandclement.com/~william/projects/luaossl.html> +-- luaposix +-- <https://github.com/luaposix/luaposix> -- - +local sysstat = require("posix.sys.stat") +local unistd = require("posix.unistd") +local rand = require("openssl.rand") +local hmac = require("openssl.hmac") -- -- @@ -18,24 +23,16 @@ local protected_repos = { qt = { jason = true, bob = true } } --- Please note that, in production, you'll want to replace this simple lookup --- table with either a table of salted and hashed passwords (using something --- smart like scrypt), or replace this table lookup with an external support, --- such as consulting your system's pam / shadow system, or an external --- database, or an external validating web service. For testing, or for --- extremely low-security usage, you may be able, however, to get away with --- compromising on hardcoding the passwords in cleartext, as we have done here. +-- A list of users and hashes, generated with `mkpasswd -m sha-512 -R 300000`. local users = { - jason = "secretpassword", - laurent = "s3cr3t", - bob = "ilikelua" + jason = "$6$rounds=300000$YYJct3n/o.ruYK$HhpSeuCuW1fJkpvMZOZzVizeLsBKcGA/aF2UPuV5v60JyH2MVSG6P511UMTj2F3H75.IT2HIlnvXzNb60FcZH1", + laurent = "$6$rounds=300000$dP0KNHwYb3JKigT$pN/LG7rWxQ4HniFtx5wKyJXBJUKP7R01zTNZ0qSK/aivw8ywGAOdfYiIQFqFhZFtVGvr11/7an.nesvm8iJUi.", + bob = "$6$rounds=300000$jCLCCt6LUpTz$PI1vvd1yaVYcCzqH8QAJFcJ60b6W/6sjcOsU7mAkNo7IE8FRGW1vkjF8I/T5jt/auv5ODLb1L4S2s.CAyZyUC" } --- All cookies will be authenticated based on this secret. Make it something --- totally random and impossible to guess. It should be large. -local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" - - +-- Set this to a path this script can write to for storing a persistent +-- cookie secret, which should be guarded. +local secret_filename = "/var/cache/cgit/auth-secret" -- -- @@ -45,7 +42,7 @@ local secret = "BE SURE TO CUSTOMIZE THIS STRING TO SOMETHING BIG AND RANDOM" -- Sets HTTP cookie headers based on post and sets up redirection. function authenticate_post() - local password = users[post["username"]] + local hash = users[post["username"]] local redirect = validate_value("redirect", post["redirect"]) if redirect == nil then @@ -55,8 +52,7 @@ function authenticate_post() redirect_to(redirect) - -- Lua hashes strings, so these comparisons are time invariant. - if password == nil or password ~= post["password"] then + if hash == nil or hash ~= unistd.crypt(post["password"], hash) then set_cookie("cgitauth", "") else -- One week expiration time @@ -184,6 +180,13 @@ function get_cookie(cookies, name) return url_decode(string.match(cookies, ";" .. name .. "=(.-);")) end +function tohex(b) + local x = "" + for i = 1, #b do + x = x .. string.format("%.2x", string.byte(b, i)) + end + return x +end -- -- @@ -191,7 +194,38 @@ end -- -- -local crypto = require("crypto") +local secret = nil + +-- Loads a secret from a file, creates a secret, or returns one from memory. +function get_secret() + if secret ~= nil then + return secret + end + local secret_file = io.open(secret_filename, "r") + if secret_file == nil then + local old_umask = sysstat.umask(63) + local temporary_filename = secret_filename .. ".tmp." .. tohex(rand.bytes(16)) + local temporary_file = io.open(temporary_filename, "w") + if temporary_file == nil then + os.exit(177) + end + temporary_file:write(tohex(rand.bytes(32))) + temporary_file:close() + unistd.link(temporary_filename, secret_filename) -- Intentionally fails in the case that another process is doing the same. + unistd.unlink(temporary_filename) + sysstat.umask(old_umask) + secret_file = io.open(secret_filename, "r") + end + if secret_file == nil then + os.exit(177) + end + secret = secret_file:read() + secret_file:close() + if secret:len() ~= 64 then + os.exit(177) + end + return secret +end -- Returns value of cookie if cookie is valid. Otherwise returns nil. function validate_value(expected_field, cookie) @@ -200,7 +234,7 @@ function validate_value(expected_field, cookie) local field = "" local expiration = 0 local salt = "" - local hmac = "" + local chmac = "" if cookie == nil or cookie:len() < 3 or cookie:sub(1, 1) == "|" then return nil @@ -219,19 +253,19 @@ function validate_value(expected_field, cookie) elseif i == 3 then salt = component elseif i == 4 then - hmac = component + chmac = component else break end i = i + 1 end - if hmac == nil or hmac:len() == 0 then + if chmac == nil or chmac:len() == 0 then return nil end -- Lua hashes strings, so these comparisons are time invariant. - if hmac ~= crypto.hmac.digest("sha1", field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt, secret) then + if chmac ~= tohex(hmac.new(get_secret(), "sha256"):final(field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt)) then return nil end @@ -252,11 +286,11 @@ function secure_value(field, value, expiration) end local authstr = "" - local salt = crypto.hex(crypto.rand.bytes(16)) + local salt = tohex(rand.bytes(16)) value = url_encode(value) field = url_encode(field) authstr = field .. "|" .. value .. "|" .. tostring(expiration) .. "|" .. salt - authstr = authstr .. "|" .. crypto.hmac.digest("sha1", authstr, secret) + authstr = authstr .. "|" .. tohex(hmac.new(get_secret(), "sha256"):final(authstr)) return authstr end diff --git a/filters/syntax-highlighting.py b/filters/syntax-highlighting.py index 1ca4108..d757e2d 100755 --- a/filters/syntax-highlighting.py +++ b/filters/syntax-highlighting.py @@ -30,11 +30,50 @@ from pygments.lexers import guess_lexer_for_filename from pygments.formatters import HtmlFormatter -sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +# Pygments' token classes, mapped onto the --syn-* custom properties that +# cgit.css defines. The fallbacks are the palette those properties default to, +# so that the output stays readable under a stylesheet without them. +SYNTAX_COLORS = ( + (('k', 'kc', 'kd', 'kn', 'kr'), '--syn-keyword', '#000', + 'font-weight: bold'), + (('kp',), '--syn-keyword', '#000', None), + (('kt',), '--syn-type', '#010181', None), + (('nc', 'nf', 'nn', 'ne'), '--syn-keyword-alt', '#0057ae', None), + (('s', 'sa', 'sb', 'sc', 'dl', 'sd', 's2', 'sh', 'sx', 'sr', 's1', 'ss'), + '--syn-string', '#bf0303', None), + (('se',), '--syn-escape', '#f0f', None), + (('si',), '--syn-interpolation', '#0057ae', None), + (('c', 'ch', 'cm', 'c1', 'cs'), '--syn-comment', '#838183', + 'font-style: italic'), + (('cp', 'cpf'), '--syn-preproc', '#008200', None), + (('m', 'mb', 'mf', 'mh', 'mi', 'mo', 'il'), '--syn-number', '#b07e00', + None), + (('o', 'ow', 'p'), '--syn-operator', '#000', None), + (('err',), '--syn-error', '#bf0303', None), + (('gd',), '--red', 'red', None), + (('gi',), '--green', 'green', None), + (('lineno', 'linenos'), '--syn-linenum', '#555', None), +) + + +def style_defs(prefix='.highlight'): + rules = [] + for classes, prop, fallback, extra in SYNTAX_COLORS: + selector = ', '.join('%s .%s' % (prefix, c) for c in classes) + decls = 'color: var(%s, %s)' % (prop, fallback) + if extra: + decls += '; ' + extra + rules.append('%s { %s }' % (selector, decls)) + rules.append('%s .hll { background-color: var(--syn-mark-bg, #ffb) }' + % prefix) + return '\n'.join(rules) + '\n' + + +sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8', errors='replace') +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') data = sys.stdin.read() filename = sys.argv[1] -formatter = HtmlFormatter(style='pastie') +formatter = HtmlFormatter(nobackground=True) try: lexer = guess_lexer_for_filename(filename, data) @@ -48,8 +87,8 @@ except TypeError: lexer = TextLexer() # highlight! :-) -# printout pygments' css definitions as well +# printout the css definitions for pygments' token classes as well sys.stdout.write('<style>') -sys.stdout.write(formatter.get_style_defs('.highlight')) +sys.stdout.write(style_defs('.highlight')) sys.stdout.write('</style>') sys.stdout.write(highlight(data, lexer, formatter, outfile=None)) diff --git a/filters/syntax-highlighting.sh b/filters/syntax-highlighting.sh index 4fa7928..bb8d60c 100755 --- a/filters/syntax-highlighting.sh +++ b/filters/syntax-highlighting.sh @@ -1,6 +1,6 @@ #!/bin/sh # This script can be used to implement syntax highlighting in the cgit -# tree-view by refering to this file with the source-filter or repo.source- +# tree-view by referring to this file with the source-filter or repo.source- # filter options in cgitrc. # # This script requires a shell supporting the ${var##pattern} syntax. @@ -8,8 +8,10 @@ # might have to use an external call to sed instead. # # Note: the highlight command (http://www.andre-simon.de/) uses css for syntax -# highlighting, so you'll probably want something like the following included -# in your css file: +# highlighting. cgit.css defines these classes in terms of its --syn-* custom +# properties, so nothing has to be added when using the default stylesheet. +# The palette below, from highlight 3.13, is what those properties default to; +# a custom stylesheet wanting the same colors can use it directly: # # Style definition file generated by highlight 2.4.8, http://www.andre-simon.de/ # @@ -114,8 +116,12 @@ EXTENSION="${BASENAME##*.}" # Version 2 can be found (for example) on EPEL 5, while version 3 can be # found (for example) on EPEL 6. # +# Note that -I (--include-style) is not used: the stylesheet it embeds in the +# output would override the .hl rules in cgit.css and with them any theming +# done through the --syn-* custom properties. +# # This is for version 2 -exec highlight --force -f -I -X -S "$EXTENSION" 2>/dev/null +exec highlight --force -f -X -S "$EXTENSION" 2>/dev/null # This is for version 3 -#exec highlight --force -f -I -O xhtml -S "$EXTENSION" 2>/dev/null +#exec highlight --force -f -O xhtml -S "$EXTENSION" 2>/dev/null |