commit ac19c0ce064461b808ffa9a9a6276ca0ae050591
Author: Jan Pobrislo <ccx@webprojekty.cz>
Date: Tue, 7 May 2019 03:25:47 +0200
First commit
Diffstat:
10 files changed, 478 insertions(+), 0 deletions(-)
diff --git a/bin/cat_pass_fd b/bin/cat_pass_fd
@@ -0,0 +1,4 @@
+#!/command/execlineb
+importas -i LOGINPASS_FD LOGINPASS_FD
+fdmove 0 $LOGINPASS_FD
+cat
diff --git a/bin/startsshagent b/bin/startsshagent
@@ -0,0 +1,14 @@
+#!/command/execlineb -s1
+piperw 5 4
+piperw 6 7
+fdmove -c 3 0
+s6-env DISPLAY=tty ZSHASKPASS_READFD=6 ZSHASKPASS_WRITEFD=4 SSH_ASKPASS=/home/ccx/bin/zshaskpass
+ssh-agent -a $1
+fdclose 3
+fdclose 4
+fdclose 6
+unexport ZSHASKPASS_READFD
+unexport ZSHASKPASS_WRITEFD
+if { env DISPLAY=terminal SSH_ASKPASS=/home/ccx/bin/zshaskpass ssh-add -c }
+s6-env ZSHASKPASS_PASSFD=7 ZSHASKPASS_PROMPTFD=5
+$@
diff --git a/bin/startsshagent.passfd b/bin/startsshagent.passfd
@@ -0,0 +1,14 @@
+#!/command/execlineb -s1
+piperw 5 4
+piperw 6 7
+s6-env DISPLAY=tty ZSHASKPASS_READFD=6 ZSHASKPASS_WRITEFD=4 SSH_ASKPASS=/home/ccx/bin/zshaskpass
+ssh-agent -a $1
+fdclose 4
+fdclose 6
+unexport ZSHASKPASS_READFD
+unexport ZSHASKPASS_WRITEFD
+if { redirfd -r 0 /dev/null env DISPLAY=pipe SSH_ASKPASS=/home/ccx/bin/zshaskpass ssh-add -c }
+importas -i LOGINPASS_FD LOGINPASS_FD
+fdclose $LOGINPASS_FD
+s6-env ZSHASKPASS_PASSFD=7 ZSHASKPASS_PROMPTFD=5
+$@
diff --git a/bin/zshaskpass b/bin/zshaskpass
@@ -0,0 +1,37 @@
+#!/bin/zsh
+if [[ -n $LOGINPASS_FD && $(</proc/$PPID/cmdline) == ssh-add* ]]; then
+ exec cat_pass_fd
+fi
+if [[ $+ZSHASKPASS_READFD -eq 0 || -z $ZSHASKPASS_READFD ]]; then
+ echo >&2 'ZSHASKPASS_READFD not set'
+ exit 2
+fi
+
+if [[ $+ZSHASKPASS_WRITEFD -eq 0 || -z $ZSHASKPASS_WRITEFD ]]; then
+ echo >&2 'ZSHASKPASS_WRITEFD not set'
+ exit 2
+fi
+
+prompt_color=%F{yellow}
+accept_color=%F{green}
+cancel_color=%F{red}
+
+exec <& $ZSHASKPASS_READFD
+#exec 2>&$ZSHASKPASS_WRITEFD
+#exec 2>/run/user/$USER/zshaskpass.new
+#mv /run/user/$USER/zshaskpass.new /run/user/$USER/zshaskpass
+#set -x
+#filan >&2
+
+prompt="$* (^D cancels): "
+printf "${(%)prompt_color}%s\n" ${(f)prompt} >& $ZSHASKPASS_WRITEFD
+read -s PASS
+RET=$?
+[[ $PASS == $'\0' ]] && RET=1
+if (($RET)); then
+ printf '%s\n' ${(%)cancel_color}Cancelled >& $ZSHASKPASS_WRITEFD
+else
+ printf '%s\n' ${(%)accept_color}OK >& $ZSHASKPASS_WRITEFD
+ cat <<<$PASS || exit $?
+fi
+exit $RET
diff --git a/bin/zshaskpass_mux b/bin/zshaskpass_mux
@@ -0,0 +1,129 @@
+#!/bin/zsh
+if [[ $+ZSHASKPASS_PROMPTFD -eq 0 || -z $ZSHASKPASS_PROMPTFD ]]; then
+ echo >&2 'ZSHASKPASS_PROMPTFD not set'
+ exit 2
+fi
+
+if [[ $+ZSHASKPASS_PASSFD -eq 0 || -z $ZSHASKPASS_PASSFD ]]; then
+ echo >&2 'ZSHASKPASS_PASSFD not set'
+ exit 2
+fi
+
+setopt no_unset warn_create_global
+zmodload zsh/zselect
+
+typeset -g COPROC_OUT fd newfd prompt_end
+typeset -a coproc_args
+typeset -gA fds
+
+#if [[ $ZSHASKPASS_PROMPTFD -gt $ZSHASKPASS_PASSFD ]]; then
+# COPROC_OUT=$[ $ZSHASKPASS_PROMPTFD + 1 ]
+#else
+# COPROC_OUT=$[ $ZSHASKPASS_PASSFD + 1 ]
+#fi
+
+prompt_end=%f
+coproc_args=(
+ fdmove -c 2 1
+ unexport ZSHASKPASS_PROMPTFD
+ unexport ZSHASKPASS_PASSFD
+ unexport SSH_ASKPASS
+ "$@"
+)
+if (($+LOGINCAP_READFD)); then
+ coproc_args=( fdclose $LOGINCAP_READFD unexport LOGINCAP_FD "$coproc_args[@]" )
+fi
+if (($+LOGINCAP_WRITEFD)); then
+ coproc_args=( fdclose $LOGINCAP_WRITEFD unexport LOGINCAP_FD "$coproc_args[@]" )
+fi
+
+logincap() {
+ if ! (( $+LOGINCAP_READFD & $+LOGINCAP_WRITEFD )); then
+ echo "logincap FDs not defined"
+ return
+ fi
+ printf >&$LOGINCAP_WRITEFD '%s\n' "$*"
+ local line result_color
+ read -u $LOGINCAP_READFD line
+ result_color=%F{cyan}
+ printf '%s\n' ${(%)result_color}$line${(%)prompt_end}
+}
+
+cmd() {
+ case $1 in
+ ('') ;;
+ ('c '*)
+ logincap ${1#c };;
+ (off|poweroff)
+ logincap o;;
+ (reboot)
+ logincap b;;
+ (root)
+ logincap "terminal rxvt-unicode importas -i PTY_FD PTY_FD s6-envdir /run/user/ccx/X$[ ${TTY#/dev/tty} + 4 ]/env urxvt -pty-fd \$PTY_FD" ;;
+ (exit)
+ printf '\n' >/run/user/ccx/X$[ ${TTY#/dev/tty} + 4 ]/fifo;;
+ (shell)
+ zsh -i; printf "shell exited with %d\n" $?;;
+ (*) printf '%s: unknown command\n' ${(qqq)REPLY};;
+ esac
+}
+
+# typeset -f -t cmd
+
+coproc "$coproc_args[@]"
+trap 'kill %1' EXIT
+trap 'kill %1; ssh-add -D; exit' INT HUP TERM QUIT
+trap 'printf "\nALRM!\n"' ALRM
+(($+TMOUT)) && printf 'TMOUT=%s\n' ${(qqq)TMOUT}
+#coproc "$@"
+exec {COPROC_OUT}<& p
+select_fds=( $ZSHASKPASS_PROMPTFD $COPROC_OUT )
+
+while zselect -A fds -r 0 $select_fds; do
+ if ! (($#jobstates)); then
+ echo >&2 "Session exited."
+ exit 0
+ fi
+ for fd in ${(k)fds}; do
+ if [[ $fds[$fd] != r ]]; then
+ echo >&2 "Error on filedescriptor $fd"
+ exit 1
+ fi
+ if [[ $fd == $ZSHASKPASS_PROMPTFD ]]; then
+ # zshaskpass
+ if IFS= read -u $fd; then
+ if [[ $REPLY == *': ' ]]; then
+ printf '%s' "$REPLY${(%)prompt_end}"
+ read -s
+ RET=$?
+ echo
+ if (($RET)); then
+ REPLY=$'\0'
+ fi
+ cat <<<$REPLY >& $ZSHASKPASS_PASSFD
+ else
+ printf '%s\n' "$REPLY${(%)prompt_end}"
+ fi
+ else
+ echo >&2 "Read from askpass failed"
+ fi
+ elif [[ $fd == $COPROC_OUT ]]; then
+ # coprocess
+ if IFS= read -u $COPROC_OUT -t; then
+ if [[ $REPLY == "XFIFO: "* ]]; then
+ printf '< opening: %s\n' ${REPLY#*: }
+ exec {newfd}>>${REPLY#*: }
+ else
+ printf '> %s\n' $REPLY
+ fi
+ fi
+ else
+ # stdin
+ if IFS= read -u $fd -s; then
+ cmd "$REPLY"
+ else
+ echo >&2 "Read from filedescriptor $fd failed"
+ fi
+ fi
+ done
+done
diff --git a/etc/loginexec b/etc/loginexec
@@ -0,0 +1,45 @@
+#!/command/execlineb -P
+multisubstitute {
+ importas HOME HOME
+ importas USER USER
+ importas SHELL SHELL
+}
+
+ifthenelse -s { test -d $HOME } {
+ cd $HOME
+} {
+ cd /
+}
+
+ifelse -n { test -x ${HOME}/loginexec } {
+ s6-setuidgid $USER
+ exec -l $SHELL
+}
+
+foreground { printf "%s: running %s\n" /etc/loginexec ${HOME}/loginexec }
+piperw 10 8
+piperw 9 11
+background {
+ fdmove 0 10
+ fdmove 1 11
+ fdclose 8
+ fdclose 9
+ fdclose 10
+ fdclose 11
+ #sleep 365d
+ importas -D "" LOGINPASS_FD LOGINPASS_FD
+ ifelse {
+ test -n $LOGINPASS_FD
+ } {
+ fdclose $LOGINPASS_FD
+ unexport LOGINPASS_FD↵
+ /root/logincaps
+ }
+ /root/logincaps
+}
+fdclose 10
+fdclose 11
+export LOGINCAP_READFD 9
+export LOGINCAP_WRITEFD 8
+s6-setuidgid $USER
+${HOME}/loginexec
diff --git a/home/.xinitrc b/home/.xinitrc
@@ -0,0 +1,76 @@
+#!/bin/sh
+#set -x
+
+# Per-user supervision tree is expected in /run/user/$USER/service
+# Files specific to each X11 session are put in /run/user/$USER/X<display-number>
+DNAME=X${DISPLAY#:}
+RUNDIR=/run/user/$USER
+
+cd "$RUNDIR" || exit $?
+mkdir -p "$DNAME" || exit $?
+cd "$DNAME" || exit $?
+
+# Set up lockfile and exit-fifo
+touch ./lock || exit $?
+exec 3<./lock
+flock -w 2 3 || exit $?
+if ! test -p fifo; then
+ mkfifo fifo || exit $?
+fi
+
+# Share the necessary environment variables
+# Runscripts should start with: s6-envdir ../../../env
+export SX_RC_FIFO=$RUNDIR/$DNAME/fifo
+rm -r ./env
+s6-dumpenv ./env || exit $?
+# mkdir -p ./env || exit $?
+# printf '%s' >../env/"$DNAME"/DISPLAY "$DISPLAY" || exit $?
+# printf '%s' >../env/"$DNAME"/XAUTHORITY "$XAUTHORITY" || exit $?
+
+# Check if s6-rc was already initialized
+if test -e ./s6-rc; then
+ # Make sure the session is down
+ s6-rc -l "$PWD"/s6-rc -da -t 1500 change
+
+ # Update the session so it uses the current compiled state
+ s6-rc-update -l "$PWD"/s6-rc "$(s6-linkname -f "$HOME"/s6-rc/xsession/compiled)" || exit $?
+else
+ # Check for any leftover service symlinks which would prevent s6-rc-init from succeeding
+ if stat >/dev/null ../service/"$DNAME"-*; then
+ for sv in ../service/"$DNAME"-*; do
+ dst=$(s6-linkname -f "$sv")
+ rm "$sv"
+ test -e "$dst/supervise" && s6-svc -wD -T 300 -x "$dst"
+ done
+ fi
+
+ if ! s6-rc-init -p "$DNAME"- -d -l "$PWD"/s6-rc -c "$HOME"/s6-rc/xsession/compiled "$RUNDIR"/service; then
+ # s6-rc-init failed, clean up livedir symlink so it's not considered initialized successfully
+ rm -f ./s6-rc
+ exit 1
+ fi
+fi
+
+set -x
+# Start up the session
+s6-svscanctl -a "$RUNDIR"/service || exit $?
+s6-rc -l "$PWD"/s6-rc -- change ok-all || exit $?
+
+# Run until we get message on fifo to exit or until this process dies
+# Cleanup process is forked until parent dies and then stops s6-rc
+exec env PWD="$PWD" execlineb -c '
+importas -i PWD PWD
+pipeline -w {
+ foreground { redirfd -w 1 /dev/null cat }
+ fdclose 0
+ foreground { s6-echo "X session terminated" }
+ if -n { s6-rc -l $PWD/s6-rc -da change }
+ foreground { s6-echo "`s6-rc -da change` failed" }
+}
+cat fifo
+'
+#foreground {
+# importas -i SX_RC_FIFO SX_RC_FIFO
+# fdmove 1 2
+# s6-echo XFIFO: $SX_RC_FIFO
+#}
diff --git a/home/loginexec b/home/loginexec
@@ -0,0 +1,9 @@
+#!/bin/zsh -l
+set -x
+if [[ $TTY = /dev/tty[1-4] ]]; then
+ X=$[ ${TTY#/dev/tty} + 4 ]
+ exec startsshagent.passfd /run/user/$USER/ssh_agent.${${TTY#/dev/}//\//.} =zshaskpass_mux xinit -- :$X tty$X -quiet
+else
+ exec ssh-agent -a /run/user/$USER/ssh_agent.${${TTY#/dev/}//\//.} /bin/execlineb -c \
+ 'if { redirfd -r 0 /dev/null env DISPLAY=pipe SSH_ASKPASS=/home/ccx/bin/cat_pass_fd ssh-add } zsh -l'
+fi
diff --git a/sbin/logincaps b/sbin/logincaps
@@ -0,0 +1,36 @@
+#!/bin/zsh
+setopt no_unset warn_create_global
+#set -x
+
+trap 'printf "\nlogincaps: ALRM!\n"' ALRM
+
+pretendrun() { : "$@" }
+typeset -f -t pretendrun
+
+main() {
+ local line term_cmd REPLY
+ while read line; do
+ case $line in
+ (o) printf >&2 "%s\n" "Would power-off."
+ pretendrun s6-poweroff
+ printf 'OK\n'
+ ;;
+ (b) printf >&2 "%s\n" "Would reboot."
+ pretendrun s6-reboot
+ printf 'OK\n'
+ ;;
+ (terminal *)
+ term_cmd=( "${(Q@)${(z)${line#terminal }}}" )
+ printf >&2 "%s\n" "Would launch root terminal."
+ pretendrun /root/su-term.py -d 1 -u $USER -- "$term_cmd[@]" <&2
+ #{ /root/su-term.py -d 1 -u $USER -- "$term_cmd[@]" <&2 &!
+ #} | read
+ printf 'OK\n'
+ ;;
+
+ (*) printf 'ECMD\n';;
+ esac
+ done
+}
+
+main
diff --git a/sbin/su-term.py b/sbin/su-term.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+
+from __future__ import (
+ generators, division, absolute_import, with_statement, print_function
+)
+import sys
+import os
+import os.path
+import pwd
+import spwd
+import crypt
+import getpass
+import argparse
+import re
+
+parser = argparse.ArgumentParser(description="Check root password and run terminal as given user with root shell")
+parser.add_argument('-u', '--user', required=True, help="User name or ID to run terminal as")
+parser.add_argument('-g', '--group', type=int, help="Group ID to run terminal as")
+parser.add_argument('-d', '--notify-fd', type=int, help="Write newline to this filedescriptor after reading password")
+parser.add_argument('term_env', help='The TERM variable used by the shell')
+parser.add_argument('exe', nargs='+', help='Terminal application to execute')
+
+
+def exec_terminal(uid, gid, home, terminal, term_env, shell, shell_workdir):
+ assert isinstance(uid, int)
+ assert isinstance(gid, int)
+ assert isinstance(home, str)
+ assert isinstance(terminal, (list, tuple))
+ assert all(isinstance(s, str) for s in terminal)
+ assert len(terminal)
+ assert isinstance(shell, str)
+ master, slave = os.openpty()
+ if os.fork():
+ # parent
+ os.close(slave)
+ os.setgid(gid)
+ os.setuid(uid)
+ os.chdir(home)
+ env = dict(os.environ)
+ env['PTY_FD'] = str(master)
+ env['HOME'] = home
+ if '/' in terminal[0]:
+ os.execve(terminal[0], terminal, env)
+ else:
+ # TODO: proper search path
+ os.execve('/bin/exec', ['exec'] + terminal, env)
+ else:
+ # child
+ os.close(master)
+ os.dup2(slave, 0)
+ os.dup2(slave, 1)
+ os.dup2(slave, 2)
+ os.close(slave)
+ os.setsid()
+ os.chdir(shell_workdir)
+ env = dict(os.environ)
+ env['USER'] = 'root'
+ env['TERM'] = term_env
+ env['HOME'] = shell_workdir
+ env['SHELL'] = shell
+ env.pop('LOGNAME', None)
+ os.execve(shell, ['-' + os.path.basename(shell)], env)
+
+
+def constant_time_compare(val1, val2):
+ """
+ Returns True if the two strings are equal, False otherwise.
+
+ The time taken is independent of the number of characters that match.
+
+ For the sake of simplicity, this function executes in constant time only
+ when the two strings have the same length. It short-circuits when they
+ have different lengths.
+ """
+ if len(val1) != len(val2):
+ return False
+ result = 0
+ for x, y in zip(val1, val2):
+ result |= ord(x) ^ ord(y)
+ return result == 0
+
+
+def main():
+ args = parser.parse_args()
+ if re.match('[0-9]+', args.user):
+ uid = int(args.user)
+ pwent = pwd.getpwuid(uid)
+ else:
+ pwent = pwd.getpwnam(args.user)
+ uid = pwent.pw_uid
+ gid = pwent.pw_gid if args.group is None else int(args.group)
+ root_hash = spwd.getspnam('root').sp_pwd
+ root_pwent = pwd.getpwnam('root')
+ p = getpass.getpass()
+ if constant_time_compare(root_hash, crypt.crypt(p, root_hash)):
+ # OK
+ if args.notify_fd is not None:
+ os.write(args.notify_fd, '\n')
+ exec_terminal(
+ uid=uid,
+ gid=gid,
+ home=pwent.pw_dir,
+ terminal=args.exe,
+ term_env=args.term_env,
+ shell=root_pwent.pw_shell,
+ shell_workdir=root_pwent.pw_dir,
+ )
+ else:
+ sys.stderr.write("Incorrect password\n")
+ sys.exit(2)
+
+
+if __name__ == '__main__':
+ main()