From d906407a60e24b355489cbea13bb8d5dd0094a3b Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 18 Jun 2015 19:24:11 +0200 Subject: modules/tv/git{.nix -> /default.nix} --- modules/cd/default.nix | 2 +- modules/tv/git.nix | 347 --------------------------------------------- modules/tv/git/default.nix | 347 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 348 deletions(-) delete mode 100644 modules/tv/git.nix create mode 100644 modules/tv/git/default.nix diff --git a/modules/cd/default.nix b/modules/cd/default.nix index 1d621e03d..468d20448 100644 --- a/modules/cd/default.nix +++ b/modules/cd/default.nix @@ -11,7 +11,7 @@ ../tv/base-cac-CentOS-7-64bit.nix ../tv/ejabberd.nix # XXX echtes modul ../tv/exim-smarthost.nix - ../tv/git.nix + ../tv/git ../tv/retiolum.nix ../tv/sanitize.nix ]; diff --git a/modules/tv/git.nix b/modules/tv/git.nix deleted file mode 100644 index d264125dc..000000000 --- a/modules/tv/git.nix +++ /dev/null @@ -1,347 +0,0 @@ -{ config, lib, pkgs, ... }: - -let - inherit (builtins) - attrNames attrValues concatLists filter hasAttr head lessThan removeAttrs - tail toJSON typeOf; - inherit (lib) - concatMapStringsSep concatStringsSep escapeShellArg hasPrefix - literalExample makeSearchPath mapAttrsToList mkIf mkOption optionalString - removePrefix singleton sort types unique; - inherit (pkgs) linkFarm writeScript writeText; - - - ensureList = x: - if typeOf x == "list" then x else [x]; - - getName = x: x.name; - - makeAuthorizedKey = command-script: user@{ name, pubkey }: - # TODO assert name - # TODO assert pubkey - let - options = concatStringsSep "," [ - ''command="exec ${command-script} ${name}"'' - "no-agent-forwarding" - "no-port-forwarding" - "no-pty" - "no-X11-forwarding" - ]; - in - "${options} ${pubkey}"; - - # [case-pattern] -> shell-script - # Create a shell script that succeeds (exit 0) when all its arguments - # match the case patterns (in the given order). - makeAuthorizeScript = - let - # TODO escape - to-pattern = x: concatStringsSep "|" (ensureList x); - go = i: ps: - if ps == [] - then "exit 0" - else '' - case ''$${toString i} in ${to-pattern (head ps)}) - ${go (i + 1) (tail ps)} - esac''; - in - patterns: '' - #! /bin/sh - set -euf - ${concatStringsSep "\n" (map (go 1) patterns)} - exit -1 - ''; - - reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); - - # TODO makeGitHooks that uses runCommand instead of scriptFarm? - scriptFarm = - farm-name: scripts: - let - makeScript = script-name: script-string: { - name = script-name; - path = writeScript "${farm-name}_${script-name}" script-string; - }; - in - linkFarm farm-name (mapAttrsToList makeScript scripts); - - writeJSON = name: data: writeText name (toJSON data); - - - cfg = config.services.git; -in - -# TODO unify logging of shell scripts to user and journal -# TODO move all scripts to ${etcDir}, so ControlMaster connections -# immediately pick up new authenticators -# TODO when authorized_keys changes, then restart ssh -# (or kill already connected users somehow) - -{ - options.services.git = { - enable = mkOption { - type = types.bool; - default = false; - description = "Enable Git repository hosting."; - }; - dataDir = mkOption { - type = types.str; - default = "/var/lib/git"; - description = "Directory used to store repositories."; - }; - etcDir = mkOption { - type = types.str; - default = "/etc/git-ssh"; - }; - rules = mkOption { - type = types.unspecified; - }; - repos = mkOption { - type = types.attrsOf (types.submodule ({ - options = { - name = mkOption { - type = types.str; - description = '' - Repository name. - ''; - }; - hooks = mkOption { - type = types.attrsOf types.str; - description = '' - Repository-specific hooks. - ''; - }; - }; - })); - - default = {}; - - example = literalExample '' - { - testing = { - name = "testing"; - hooks.post-update = ''' - #! /bin/sh - set -euf - echo post-update hook: $* >&2 - '''; - }; - testing2 = { name = "testing2"; }; - } - ''; - - description = '' - Repositories. - ''; - }; - users = mkOption { - type = types.unspecified; - }; - }; - - config = - let - command-script = writeScript "git-ssh-command" '' - #! /bin/sh - set -euf - - PATH=${makeSearchPath "bin" (with pkgs; [ - coreutils - git - gnugrep - gnused - systemd - ])} - - abort() { - echo "error: $1" >&2 - systemd-cat -p err -t git-ssh echo "error: $1" - exit -1 - } - - GIT_SSH_USER=$1 - - systemd-cat -p info -t git-ssh echo \ - "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" - - # References: The Base Definitions volume of - # POSIX.1‐2013, Section 3.278, Portable Filename Character Set - portable_filename_bre="^[A-Za-z0-9._-]\\+$" - - command=$(echo "$SSH_ORIGINAL_COMMAND" \ - | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ - | grep "$portable_filename_bre" \ - || abort 'cannot read command') - - GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ - | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ - | grep "$portable_filename_bre" \ - || abort 'cannot read reponame') - - ${cfg.etcDir}/authorize-command \ - "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ - || abort 'access denied' - - repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO - - systemd-cat -p info -t git-ssh \ - echo "authorized exec $command $repodir" - - export GIT_SSH_USER - export GIT_SSH_REPO - exec "$command" "$repodir" - ''; - - init-script = writeScript "git-ssh-init" '' - #! /bin/sh - set -euf - - PATH=${makeSearchPath "bin" (with pkgs; [ - coreutils - findutils - gawk - git - gnugrep - gnused - ])} - - dataDir=${escapeShellArg cfg.dataDir} - mkdir -p "$dataDir" - - # Notice how the presence of hooks symlinks determine whether - # we manage a repositry or not. - - # Make sure that no existing repository has hooks. We can delete - # symlinks because we assume we created them. - find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete - bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) - if echo "$bad_hooks" | grep -q .; then - printf 'error: unknown hooks:\n%s\n' \ - "$(echo "$bad_hooks" | sed 's/^/ /')" \ - >&2 - exit -1 - fi - - # Initialize repositories. - ${concatMapStringsSep "\n" (repo: - let - hooks = scriptFarm "git-ssh-hooks" (makeHooks repo); - in - '' - reponame=${escapeShellArg repo.name} - repodir=$dataDir/$reponame - if ! test -d "$repodir"; then - mkdir -m 0700 "$repodir" - git init --bare --template=/var/empty "$repodir" - chown -R git: "$repodir" - fi - ln -s ${hooks} "$repodir/hooks" - '' - ) (attrValues cfg.repos)} - - # Warn about repositories that exist but aren't mentioned in the - # current configuration (and thus didn't receive a hooks symlink). - unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ - -type d \! -exec test -e '{}/hooks' \; -print) - if echo "$unknown_repos" | grep -q .; then - printf 'warning: stale repositories:\n%s\n' \ - "$(echo "$unknown_repos" | sed 's/^/ /')" \ - >&2 - fi - ''; - - makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { - pre-receive = '' - #! /bin/sh - set -euf - - PATH=${makeSearchPath "bin" (with pkgs; [ - coreutils # env - git - systemd - ])} - - accept() { - #systemd-cat -p info -t git-ssh echo "authorized $1" - accept_string="''${accept_string+$accept_string - }authorized $1" - } - reject() { - #systemd-cat -p err -t git-ssh echo "denied $1" - #echo 'access denied' >&2 - #exit_code=-1 - reject_string="''${reject_string+$reject_string - }access denied: $1" - } - - empty=0000000000000000000000000000000000000000 - - accept_string= - reject_string= - while read oldrev newrev ref; do - - if [ $oldrev = $empty ]; then - receive_mode=create - elif [ $newrev = $empty ]; then - receive_mode=delete - elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then - receive_mode=fast-forward - else - receive_mode=non-fast-forward - fi - - if ${cfg.etcDir}/authorize-push \ - "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then - accept "$receive_mode $ref" - else - reject "$receive_mode $ref" - fi - done - - if [ -n "$reject_string" ]; then - systemd-cat -p err -t git-ssh echo "$reject_string" - exit -1 - fi - - systemd-cat -p info -t git-ssh echo "$accept_string" - - ${optionalString (hasAttr "post-receive" repo.hooks) '' - # custom post-receive hook - ${repo.hooks.post-receive}''} - ''; - }; - - etc-base = - assert (hasPrefix "/etc/" cfg.etcDir); - removePrefix "/etc/" cfg.etcDir; - in - mkIf cfg.enable { - system.activationScripts.git-ssh-init = "${init-script}"; - - # TODO maybe put all scripts here and then use PATH? - environment.etc."${etc-base}".source = - scriptFarm "git-ssh-authorizers" { - authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ - (map getName (ensureList user)) - (map getName (ensureList repo)) - (map getName perm.allow-commands) - ]) cfg.rules); - - authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ - (map getName (ensureList user)) - (map getName (ensureList repo)) - (ensureList perm.allow-receive-ref) - (map getName perm.allow-receive-modes) - ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); - }; - - users.extraUsers = singleton { - description = "Git repository hosting user"; - name = "git"; - shell = "/bin/sh"; - openssh.authorizedKeys.keys = - mapAttrsToList (_: makeAuthorizedKey command-script) cfg.users; - uid = 112606723; # genid git - }; - }; -} diff --git a/modules/tv/git/default.nix b/modules/tv/git/default.nix new file mode 100644 index 000000000..d264125dc --- /dev/null +++ b/modules/tv/git/default.nix @@ -0,0 +1,347 @@ +{ config, lib, pkgs, ... }: + +let + inherit (builtins) + attrNames attrValues concatLists filter hasAttr head lessThan removeAttrs + tail toJSON typeOf; + inherit (lib) + concatMapStringsSep concatStringsSep escapeShellArg hasPrefix + literalExample makeSearchPath mapAttrsToList mkIf mkOption optionalString + removePrefix singleton sort types unique; + inherit (pkgs) linkFarm writeScript writeText; + + + ensureList = x: + if typeOf x == "list" then x else [x]; + + getName = x: x.name; + + makeAuthorizedKey = command-script: user@{ name, pubkey }: + # TODO assert name + # TODO assert pubkey + let + options = concatStringsSep "," [ + ''command="exec ${command-script} ${name}"'' + "no-agent-forwarding" + "no-port-forwarding" + "no-pty" + "no-X11-forwarding" + ]; + in + "${options} ${pubkey}"; + + # [case-pattern] -> shell-script + # Create a shell script that succeeds (exit 0) when all its arguments + # match the case patterns (in the given order). + makeAuthorizeScript = + let + # TODO escape + to-pattern = x: concatStringsSep "|" (ensureList x); + go = i: ps: + if ps == [] + then "exit 0" + else '' + case ''$${toString i} in ${to-pattern (head ps)}) + ${go (i + 1) (tail ps)} + esac''; + in + patterns: '' + #! /bin/sh + set -euf + ${concatStringsSep "\n" (map (go 1) patterns)} + exit -1 + ''; + + reponames = rules: sort lessThan (unique (map (x: x.repo.name) rules)); + + # TODO makeGitHooks that uses runCommand instead of scriptFarm? + scriptFarm = + farm-name: scripts: + let + makeScript = script-name: script-string: { + name = script-name; + path = writeScript "${farm-name}_${script-name}" script-string; + }; + in + linkFarm farm-name (mapAttrsToList makeScript scripts); + + writeJSON = name: data: writeText name (toJSON data); + + + cfg = config.services.git; +in + +# TODO unify logging of shell scripts to user and journal +# TODO move all scripts to ${etcDir}, so ControlMaster connections +# immediately pick up new authenticators +# TODO when authorized_keys changes, then restart ssh +# (or kill already connected users somehow) + +{ + options.services.git = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable Git repository hosting."; + }; + dataDir = mkOption { + type = types.str; + default = "/var/lib/git"; + description = "Directory used to store repositories."; + }; + etcDir = mkOption { + type = types.str; + default = "/etc/git-ssh"; + }; + rules = mkOption { + type = types.unspecified; + }; + repos = mkOption { + type = types.attrsOf (types.submodule ({ + options = { + name = mkOption { + type = types.str; + description = '' + Repository name. + ''; + }; + hooks = mkOption { + type = types.attrsOf types.str; + description = '' + Repository-specific hooks. + ''; + }; + }; + })); + + default = {}; + + example = literalExample '' + { + testing = { + name = "testing"; + hooks.post-update = ''' + #! /bin/sh + set -euf + echo post-update hook: $* >&2 + '''; + }; + testing2 = { name = "testing2"; }; + } + ''; + + description = '' + Repositories. + ''; + }; + users = mkOption { + type = types.unspecified; + }; + }; + + config = + let + command-script = writeScript "git-ssh-command" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + git + gnugrep + gnused + systemd + ])} + + abort() { + echo "error: $1" >&2 + systemd-cat -p err -t git-ssh echo "error: $1" + exit -1 + } + + GIT_SSH_USER=$1 + + systemd-cat -p info -t git-ssh echo \ + "authorizing $GIT_SSH_USER $SSH_CONNECTION $SSH_ORIGINAL_COMMAND" + + # References: The Base Definitions volume of + # POSIX.1‐2013, Section 3.278, Portable Filename Character Set + portable_filename_bre="^[A-Za-z0-9._-]\\+$" + + command=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\1/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read command') + + GIT_SSH_REPO=$(echo "$SSH_ORIGINAL_COMMAND" \ + | sed -n 's/^\([^ ]*\) '"'"'\(.*\)'"'"'/\2/p' \ + | grep "$portable_filename_bre" \ + || abort 'cannot read reponame') + + ${cfg.etcDir}/authorize-command \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$command" \ + || abort 'access denied' + + repodir=${escapeShellArg cfg.dataDir}/$GIT_SSH_REPO + + systemd-cat -p info -t git-ssh \ + echo "authorized exec $command $repodir" + + export GIT_SSH_USER + export GIT_SSH_REPO + exec "$command" "$repodir" + ''; + + init-script = writeScript "git-ssh-init" '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + findutils + gawk + git + gnugrep + gnused + ])} + + dataDir=${escapeShellArg cfg.dataDir} + mkdir -p "$dataDir" + + # Notice how the presence of hooks symlinks determine whether + # we manage a repositry or not. + + # Make sure that no existing repository has hooks. We can delete + # symlinks because we assume we created them. + find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks -type l -delete + bad_hooks=$(find "$dataDir" -mindepth 2 -maxdepth 2 -name hooks) + if echo "$bad_hooks" | grep -q .; then + printf 'error: unknown hooks:\n%s\n' \ + "$(echo "$bad_hooks" | sed 's/^/ /')" \ + >&2 + exit -1 + fi + + # Initialize repositories. + ${concatMapStringsSep "\n" (repo: + let + hooks = scriptFarm "git-ssh-hooks" (makeHooks repo); + in + '' + reponame=${escapeShellArg repo.name} + repodir=$dataDir/$reponame + if ! test -d "$repodir"; then + mkdir -m 0700 "$repodir" + git init --bare --template=/var/empty "$repodir" + chown -R git: "$repodir" + fi + ln -s ${hooks} "$repodir/hooks" + '' + ) (attrValues cfg.repos)} + + # Warn about repositories that exist but aren't mentioned in the + # current configuration (and thus didn't receive a hooks symlink). + unknown_repos=$(find "$dataDir" -mindepth 1 -maxdepth 1 \ + -type d \! -exec test -e '{}/hooks' \; -print) + if echo "$unknown_repos" | grep -q .; then + printf 'warning: stale repositories:\n%s\n' \ + "$(echo "$unknown_repos" | sed 's/^/ /')" \ + >&2 + fi + ''; + + makeHooks = repo: removeAttrs repo.hooks [ "pre-receive" ] // { + pre-receive = '' + #! /bin/sh + set -euf + + PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils # env + git + systemd + ])} + + accept() { + #systemd-cat -p info -t git-ssh echo "authorized $1" + accept_string="''${accept_string+$accept_string + }authorized $1" + } + reject() { + #systemd-cat -p err -t git-ssh echo "denied $1" + #echo 'access denied' >&2 + #exit_code=-1 + reject_string="''${reject_string+$reject_string + }access denied: $1" + } + + empty=0000000000000000000000000000000000000000 + + accept_string= + reject_string= + while read oldrev newrev ref; do + + if [ $oldrev = $empty ]; then + receive_mode=create + elif [ $newrev = $empty ]; then + receive_mode=delete + elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then + receive_mode=fast-forward + else + receive_mode=non-fast-forward + fi + + if ${cfg.etcDir}/authorize-push \ + "$GIT_SSH_USER" "$GIT_SSH_REPO" "$ref" "$receive_mode"; then + accept "$receive_mode $ref" + else + reject "$receive_mode $ref" + fi + done + + if [ -n "$reject_string" ]; then + systemd-cat -p err -t git-ssh echo "$reject_string" + exit -1 + fi + + systemd-cat -p info -t git-ssh echo "$accept_string" + + ${optionalString (hasAttr "post-receive" repo.hooks) '' + # custom post-receive hook + ${repo.hooks.post-receive}''} + ''; + }; + + etc-base = + assert (hasPrefix "/etc/" cfg.etcDir); + removePrefix "/etc/" cfg.etcDir; + in + mkIf cfg.enable { + system.activationScripts.git-ssh-init = "${init-script}"; + + # TODO maybe put all scripts here and then use PATH? + environment.etc."${etc-base}".source = + scriptFarm "git-ssh-authorizers" { + authorize-command = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (map getName perm.allow-commands) + ]) cfg.rules); + + authorize-push = makeAuthorizeScript (map ({ repo, user, perm }: [ + (map getName (ensureList user)) + (map getName (ensureList repo)) + (ensureList perm.allow-receive-ref) + (map getName perm.allow-receive-modes) + ]) (filter (x: hasAttr "allow-receive-ref" x.perm) cfg.rules)); + }; + + users.extraUsers = singleton { + description = "Git repository hosting user"; + name = "git"; + shell = "/bin/sh"; + openssh.authorizedKeys.keys = + mapAttrsToList (_: makeAuthorizedKey command-script) cfg.users; + uid = 112606723; # genid git + }; + }; +} -- cgit v1.2.3 From 15ede8a3a4e64f80416d70084d463a09286b5040 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 18 Jun 2015 20:12:05 +0200 Subject: module/tv/git: add cgit --- modules/tv/git/cgit.nix | 110 +++++++++++++++++++++++++++++++++++++++++++++ modules/tv/git/default.nix | 33 ++++++++++++-- 2 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 modules/tv/git/cgit.nix diff --git a/modules/tv/git/cgit.nix b/modules/tv/git/cgit.nix new file mode 100644 index 000000000..edee19909 --- /dev/null +++ b/modules/tv/git/cgit.nix @@ -0,0 +1,110 @@ +{ config, lib, pkgs, ... }: + +let + inherit (builtins) attrValues filter getAttr; + inherit (lib) concatMapStringsSep mkIf optionalString; + + cfg = config.services.git; + + isPublicRepo = getAttr "public"; # TODO this is also in ./default.nix +in + +{ + config = mkIf cfg.cgit { + + users.extraUsers = lib.singleton { + name = "fcgiwrap"; + uid = 2851179180; # genid fcgiwrap + group = "fcgiwrap"; + home = "/var/empty"; + }; + + users.extraGroups = lib.singleton { + name = "fcgiwrap"; + gid = 2851179180; # genid fcgiwrap + }; + + services.fcgiwrap = { + enable = true; + user = "fcgiwrap"; + group = "fcgiwrap"; + # socketAddress = "/run/fcgiwrap.sock" (default) + # socketType = "unix" (default) + }; + + environment.etc."cgitrc".text = '' + css=/cgit-static/cgit.css + logo=/cgit-static/cgit.png + + # if you do not want that webcrawler (like google) index your site + robots=noindex, nofollow + + virtual-root=/cgit + + # TODO make this nicer + cache-root=/tmp/cgit + + cache-size=1000 + enable-commit-graph=1 + enable-index-links=1 + enable-index-owner=0 + enable-log-filecount=1 + enable-log-linecount=1 + enable-remote-branches=1 + + root-title=repositories at ${config.networking.hostName} + root-desc=keep calm and engage + + snapshots=0 + max-stats=year + + ${concatMapStringsSep "\n" (repo: '' + repo.url=${repo.name} + repo.path=${cfg.dataDir}/${repo.name} + ${optionalString (repo.desc != null) "repo.desc=${repo.desc}"} + '') (filter isPublicRepo (attrValues cfg.repos))} + ''; + + # TODO modular nginx configuration + services.nginx = + let + name = config.networking.hostName; + qname = "${name}.retiolum"; + in + { + enable = true; + httpConfig = '' + include ${pkgs.nginx}/conf/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + gzip on; + server { + listen 80; + server_name ${name} ${qname} localhost; + root ${pkgs.cgit}/cgit; + + location /cgit-static { + rewrite ^/cgit-static(/.*)$ $1 break; + #expires 30d; + } + + location /cgit { + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/cgit.cgi; + #fastcgi_param PATH_INFO $uri; + fastcgi_split_path_info ^(/cgit/?)(.+)$; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_pass unix:${config.services.fcgiwrap.socketAddress}; + } + + location / { + return 404; + } + } + ''; + }; + }; +} diff --git a/modules/tv/git/default.nix b/modules/tv/git/default.nix index d264125dc..50e2f9206 100644 --- a/modules/tv/git/default.nix +++ b/modules/tv/git/default.nix @@ -2,8 +2,8 @@ let inherit (builtins) - attrNames attrValues concatLists filter hasAttr head lessThan removeAttrs - tail toJSON typeOf; + attrNames attrValues concatLists getAttr filter hasAttr head lessThan + removeAttrs tail toJSON typeOf; inherit (lib) concatMapStringsSep concatStringsSep escapeShellArg hasPrefix literalExample makeSearchPath mapAttrsToList mkIf mkOption optionalString @@ -16,6 +16,8 @@ let getName = x: x.name; + isPublicRepo = getAttr "public"; # TODO this is also in ./cgit.nix + makeAuthorizedKey = command-script: user@{ name, pubkey }: # TODO assert name # TODO assert pubkey @@ -78,12 +80,21 @@ in # (or kill already connected users somehow) { + imports = [ + ./cgit.nix + ]; + options.services.git = { enable = mkOption { type = types.bool; default = false; description = "Enable Git repository hosting."; }; + cgit = mkOption { + type = types.bool; + default = true; + description = "Enable cgit."; # TODO better desc; talk about nginx + }; dataDir = mkOption { type = types.str; default = "/var/lib/git"; @@ -99,6 +110,13 @@ in repos = mkOption { type = types.attrsOf (types.submodule ({ options = { + desc = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Repository description. + ''; + }; name = mkOption { type = types.str; description = '' @@ -111,6 +129,14 @@ in Repository-specific hooks. ''; }; + public = mkOption { + type = types.bool; + default = false; + description = '' + Allow everybody to read the repository via HTTP if cgit enabled. + ''; + # TODO allow every configured user to fetch the repository via SSH. + }; }; })); @@ -230,8 +256,9 @@ in '' reponame=${escapeShellArg repo.name} repodir=$dataDir/$reponame + mode=${toString (if isPublicRepo repo then 0711 else 0700)} if ! test -d "$repodir"; then - mkdir -m 0700 "$repodir" + mkdir -m "$mode" "$repodir" git init --bare --template=/var/empty "$repodir" chown -R git: "$repodir" fi -- cgit v1.2.3 From 94488da95f3161bc791efa7a5c4d74ce2ebfab21 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 18 Jun 2015 21:21:54 +0200 Subject: lib/git: add irc-announce --- lib/default.nix | 3 +- lib/git.nix | 140 +++++++++++++++++++++++++++++++++++++++++++++++-- modules/cd/default.nix | 2 +- modules/wu/default.nix | 2 +- modules/wu/users.nix | 5 +- 5 files changed, 144 insertions(+), 8 deletions(-) diff --git a/lib/default.nix b/lib/default.nix index 27cf0e250..164a6a1aa 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, pkgs, ... }: with builtins; @@ -11,6 +11,7 @@ rec { lib = lib // { inherit addNames; }; + inherit pkgs; }; addName = name: set: diff --git a/lib/git.nix b/lib/git.nix index 5916cf83c..b28d89413 100644 --- a/lib/git.nix +++ b/lib/git.nix @@ -1,7 +1,7 @@ -{ lib, ... }: +{ lib, pkgs, ... }: let - inherit (lib) addNames; + inherit (lib) addNames escapeShellArg makeSearchPath; commands = addNames { git-receive-pack = {}; @@ -37,5 +37,139 @@ let master = "refs/heads/master"; all-heads = "refs/heads/*"; }; + + irc-announce-script = pkgs.writeScript "irc-announce-script" '' + #! /bin/sh + set -euf + + export PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + gawk + gnused + netcat + ])} + + IRC_SERVER="$1" + IRC_PORT="$2" + IRC_NICK="$3" + IRC_CHANNEL="$4" + message=$5 + + export IRC_CHANNEL # for privmsg_cat + + # echo2 and cat2 are used output to both, stdout and stderr + # This is used to see what we send to the irc server. (debug output) + echo2() { echo "$*"; echo "$*" >&2; } + cat2() { tee /dev/stderr; } + + # privmsg_cat transforms stdin to a privmsg + privmsg_cat() { awk '{ print "PRIVMSG "ENVIRON["IRC_CHANNEL"]" :"$0 }'; } + + # ircin is used to feed the output of netcat back to the "irc client" + # so we can implement expect-like behavior with sed^_^ + # XXX mkselfdestructingtmpfifo would be nice instead of this cruft + tmpdir="$(mktemp -d irc-announce_XXXXXXXX)" + cd "$tmpdir" + mkfifo ircin + trap " + rm ircin + cd '$OLDPWD' + rmdir '$tmpdir' + trap - EXIT INT QUIT + " EXIT INT QUIT + + { + echo2 "USER $LOGNAME 0 * :$LOGNAME@$(hostname)" + echo2 "NICK $IRC_NICK" + + # wait for MODE message + sed -n '/^:[^ ]* MODE /q' + + echo2 "JOIN $IRC_CHANNEL" + + printf '%s' "$message" \ + | privmsg_cat \ + | cat2 + + echo2 "PART $IRC_CHANNEL" + + # wait for PART confirmation + sed -n '/:'"$IRC_NICK"'![^ ]* PART /q' + + echo2 'QUIT :Gone to have lunch' + } < ircin \ + | nc "$IRC_SERVER" "$IRC_PORT" | tee -a ircin + ''; + + hooks = { + # TODO make this a package? + irc-announce = { nick, channel, server, port ? 6667 }: '' + #! /bin/sh + set -euf + + export PATH=${makeSearchPath "bin" (with pkgs; [ + coreutils + git + gnused + ])} + + nick=${escapeShellArg nick} + channel=${escapeShellArg channel} + server=${escapeShellArg server} + port=${toString port} + + empty=0000000000000000000000000000000000000000 + + unset message + while read oldrev newrev ref; do + + if [ $oldrev = $empty ]; then + receive_mode=create + elif [ $newrev = $empty ]; then + receive_mode=delete + elif [ "$(git merge-base $oldrev $newrev)" = $oldrev ]; then + receive_mode=fast-forward + else + receive_mode=non-fast-forward + fi + + h=$(echo $ref | sed 's:^refs/heads/::') + + # empty_tree=$(git hash-object -t tree /dev/null + empty_tree=4b825dc6 + + id=$(echo $oldrev | cut -b-7) + id2=$(echo $newrev | cut -b-7) + if [ $oldrev = $empty ]; then id=$empty_tree; fi + if [ $newrev = $empty ]; then id2=$empty_tree; fi + + case $receive_mode in + create) + #git log --oneline $id2 + link="http://cd/cgit/$GIT_SSH_REPO/" + ;; + fast-forward|non-fast-forward) + #git diff --stat $id..$id2 + link="http://cd/cgit/$GIT_SSH_REPO/diff/?h=$h&id=$id&id2=$id2" + ;; + esac + + #host=$nick + #$host $GIT_SSH_REPO $ref $link + message="''${message+$message + }$GIT_SSH_USER $receive_mode pushed $link" + done + + if test -n "''${message-}"; then + exec ${irc-announce-script} \ + "$server" \ + "$port" \ + "$nick" \ + "$channel" \ + "$message" + fi + ''; + }; + in -commands // receive-modes // permissions // refs +commands // receive-modes // permissions // refs // hooks diff --git a/modules/cd/default.nix b/modules/cd/default.nix index 468d20448..ac32795ef 100644 --- a/modules/cd/default.nix +++ b/modules/cd/default.nix @@ -48,7 +48,7 @@ let inherit (builtins) readFile; # TODO lib should already include our stuff - inherit (import ../../lib { inherit lib; }) addNames git; + inherit (import ../../lib { inherit lib pkgs; }) addNames git; in rec { enable = true; diff --git a/modules/wu/default.nix b/modules/wu/default.nix index 84a8361af..68475ad5b 100644 --- a/modules/wu/default.nix +++ b/modules/wu/default.nix @@ -1,7 +1,7 @@ { config, pkgs, ... }: let - lib = import ../../lib { inherit pkgs; }; + lib = import ../../lib { lib = pkgs.lib; inherit pkgs; }; inherit (lib) majmin; in diff --git a/modules/wu/users.nix b/modules/wu/users.nix index 88f2b658e..4c8631489 100644 --- a/modules/wu/users.nix +++ b/modules/wu/users.nix @@ -1,4 +1,4 @@ -{ config, pkgs, ... }: +{ config, lib, pkgs, ... }: let inherit (builtins) attrValues; @@ -194,7 +194,8 @@ let sudoers = let inherit (builtins) filter hasAttr; - inherit (import ../../lib { inherit pkgs; }) concat isSuffixOf removeSuffix setToList; + inherit (import ../../lib { inherit lib pkgs; }) + concat isSuffixOf removeSuffix setToList; hasMaster = { group ? "", ... }: isSuffixOf "-sub" group; -- cgit v1.2.3 From 1325e5a6757c5825704e74b641e829c25c810124 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 18 Jun 2015 21:22:21 +0200 Subject: host cd: use irc-announce and cgit --- modules/cd/default.nix | 33 ++++++++++++++++++++------------- modules/cd/iptables.nix | 1 + 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/modules/cd/default.nix b/modules/cd/default.nix index ac32795ef..7223203a0 100644 --- a/modules/cd/default.nix +++ b/modules/cd/default.nix @@ -59,31 +59,38 @@ makefu = { pubkey = "xxx"; }; }; - # TODO warn about stale repodirs repos = addNames { + shitment = { + desc = "shitment repository"; + hooks = { + post-receive = git.irc-announce { + nick = config.networking.hostName; # TODO make this the default + channel = "#retiolum"; + server = "ire.retiolum"; + }; + }; + public = true; + }; testing = { + desc = "testing repository"; hooks = { - update = '' - #! /bin/sh - set -euf - echo update hook: $* >&2 - ''; - post-update = '' - #! /bin/sh - set -euf - echo post-update hook: $* >&2 - ''; + post-receive = git.irc-announce { + nick = config.networking.hostName; # TODO make this the default + channel = "#repository"; + server = "ire.retiolum"; + }; }; + public = true; }; }; rules = with git; with users; with repos; [ { user = tv; - repo = testing; + repo = [ testing shitment ]; perm = push master [ non-fast-forward create delete merge ]; } { user = [ lass makefu ]; - repo = testing; + repo = [ testing shitment ]; perm = fetch; } ]; diff --git a/modules/cd/iptables.nix b/modules/cd/iptables.nix index 48425e8dc..950aa8472 100644 --- a/modules/cd/iptables.nix +++ b/modules/cd/iptables.nix @@ -63,6 +63,7 @@ ip4tables -A Retiolum -j ACCEPT -p icmp --icmp-type echo-request ip6tables -A Retiolum -j ACCEPT -p ipv6-icmp -m icmp6 --icmpv6-type echo-request + ipXtables -A Retiolum -j ACCEPT -p tcp --dport http -m conntrack --ctstate NEW ${when log "ipXtables -A Retiolum -j LOG --log-level info --log-prefix 'REJECT '"} ipXtables -A Retiolum -j REJECT -p tcp --reject-with tcp-reset -- cgit v1.2.3