pax_global_header00006660000000000000000000000064146763336330014530gustar00rootroot0000000000000052 comment=b46a4985c057a4ddd4a9319194628b5786b8ad74 emptty-0.13.0/000077500000000000000000000000001467633363300131335ustar00rootroot00000000000000emptty-0.13.0/.github/000077500000000000000000000000001467633363300144735ustar00rootroot00000000000000emptty-0.13.0/.github/workflows/000077500000000000000000000000001467633363300165305ustar00rootroot00000000000000emptty-0.13.0/.github/workflows/main.yaml000066400000000000000000000044271467633363300203470ustar00rootroot00000000000000name: Build on: push: branches: master pull_request: branches: master jobs: build-void: name: Build runs-on: ubuntu-latest strategy: fail-fast: false matrix: go-version: ['1.20', '1.21', '1.22'] container: image: 'voidlinux/voidlinux:latest' steps: - name: Prepare container run: | # Redefine current main repo mirror echo 'repository=https://repo-default.voidlinux.org/current' > /usr/share/xbps.d/00-repository-main.conf # Update system xbps xbps-install -Syu xbps # Update system xbps-install -Syu # Install dependencies xbps-install -y git make gcc pam-devel libX11-devel - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout uses: actions/checkout@v4 - name: Git Safe Directory run: git config --global --add safe.directory /__w/emptty/emptty - name: Test without tags run: make test - name: Test with noxlib tag run: make test TAGS=noxlib - name: Test with noutmp tag run: make test TAGS=noutmp - name: Test with nopam tag run: make test TAGS=nopam - name: Build with noxlib tag run: | # Make clean make clean # Make build make build TAGS=noxlib - name: Build with noutmp tag run: | # Make clean make clean # Make build make build TAGS=noutmp - name: Build with nopam tag run: | # Make clean make clean # Make build make build TAGS=nopam - name: Build without tags run: | # Make clean make clean # Make build make build - name: Test install-pam run: make install-pam - name: Test install-manual run: make install-manual - name: Test install-config run: make install-config - name: Test install-runit run: make install-runit - name: Test install-motd-gen run: make install-motd-gen - name: Test install run: make install - name: Test uninstall run: make uninstall emptty-0.13.0/.github/workflows/release.yaml000066400000000000000000000027121467633363300210360ustar00rootroot00000000000000name: Release on: push: tags: '*' jobs: release-for-arch: name: Relase for Arch runs-on: ubuntu-latest outputs: package-name: ${{ steps.build.outputs.package-name }} container: image: 'archlinux:base' steps: - name: Prepare container run: | # Update system pacman -Syu --noconfirm # Install dependencies pacman -S --noconfirm git make go gcc pam libx11 - name: Checkout uses: actions/checkout@v4 - name: Git Safe Directory run: git config --global --add safe.directory /__w/emptty/emptty - id: build name: Package and Upload run: | export TAG=`git describe --tags` export DISTDIR=emptty-bin-x86_64-${TAG:1} echo "package-name=$DISTDIR.tar.gz" >> $GITHUB_OUTPUT # Test and Build make test build # Install into distdir DESTDIR=$DISTDIR make install install-manual install-config install-pam install-systemd mkdir -p $DISTDIR/usr/share/licenses/emptty/ cp LICENSE $DISTDIR/usr/share/licenses/emptty/LICENSE # Package distdir ls $DISTDIR | xargs tar -czf $DISTDIR.tar.gz -C $DISTDIR/ - name: Upload to release uses: softprops/action-gh-release@v1 with: files: ${{ steps.build.outputs.package-name }} draft: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} emptty-0.13.0/.gitignore000066400000000000000000000000201467633363300151130ustar00rootroot00000000000000dist/ cover.out emptty-0.13.0/LICENSE000066400000000000000000000020471467633363300141430ustar00rootroot00000000000000MIT License Copyright (c) 2020 tvrzna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. emptty-0.13.0/Makefile000066400000000000000000000064551467633363300146050ustar00rootroot00000000000000DISTFILE=emptty BUILD_VERSION=`git describe --tags` GOVERSION=`go version | grep -Eo 'go[0-9]+\.[0-9]+'` ifdef TAGS TAGS_ARGS = -tags ${TAGS} endif test: @echo "Testing..." @go test -coverprofile cover.out ${TAGS_ARGS} ./... @echo "Done" clean: @echo "Cleaning..." @rm -f dist/${DISTFILE} @rm -f dist/emptty.1.gz @rm -rf dist @echo "Done" build: @echo "Building${TAGS_ARGS}..." @mkdir -p dist @go build ${TAGS_ARGS} -o dist/${DISTFILE} -ldflags "-X github.com/tvrzna/emptty/src.buildVersion=${BUILD_VERSION}" -buildvcs=false @gzip -cn res/emptty.1 > dist/emptty.1.gz @echo "Done" install: @echo "Installing..." @install -DZs dist/${DISTFILE} -m 755 -t ${DESTDIR}/usr/bin @echo "Done" install-config: @echo "Installing config..." @install -DZ res/conf -m 644 -T ${DESTDIR}/etc/${DISTFILE}/conf @echo "Done" install-manual: @echo "Installing manual..." @install -D dist/emptty.1.gz -t ${DESTDIR}/usr/share/man/man1 @echo "Done" install-motd-gen: @echo "Installing motd-gen.sh..." @install -DZ res/motd-gen.sh -m 744 -t ${DESTDIR}/etc/${DISTFILE}/ @echo "Done" install-pam: @echo "Installing pam file..." @install -DZ res/pam -m 644 -T ${DESTDIR}/etc/pam.d/${DISTFILE} @echo "Done" install-pam-debian: @echo "Installing pam-debian file..." @install -DZ res/pam-debian -m 644 -T ${DESTDIR}/etc/pam.d/${DISTFILE} @echo "Done" install-pam-fedora: @echo "Installing pam-fedora file..." @install -DZ res/pam-fedora -m 644 -T ${DESTDIR}/etc/pam.d/${DISTFILE} @echo "Done" install-pam-suse: @echo "Installing pam-suse file..." @install -DZ res/pam-suse -m 644 -T ${DESTDIR}/etc/pam.d/${DISTFILE} @echo "Done" install-runit: @echo "Installing runit service..." @install -DZ res/runit-run -m 755 -T ${DESTDIR}/etc/sv/${DISTFILE}/run @echo "Done" install-runit-artix: @echo "Installing Artix runit service..." @install -DZ res/runit-run -m 755 -T ${DESTDIR}/etc/runit/sv/${DISTFILE}/run @echo "Done" install-systemd: @echo "Installing systemd service..." @install -DZ res/systemd-service -m 644 -T ${DESTDIR}/usr/lib/systemd/system/${DISTFILE}.service @echo "Done" install-openrc: @echo "Installing OpenRC service..." @install -DZ res/openrc-service -m 755 -T ${DESTDIR}/etc/init.d/${DISTFILE} @echo "Done" install-s6: @echo "Installing S6 service..." @install -DZ res/s6-dependencies -m 644 -T ${DESTDIR}/etc/s6/sv/${DISTFILE}/dependencies @install -DZ res/s6-type -m 644 -T ${DESTDIR}/etc/s6/sv/${DISTFILE}/type @install -DZ res/s6-run -m 755 -T ${DESTDIR}/etc/s6/sv/${DISTFILE}/run @echo "Done. Please recompile your S6 database." install-dinit: @echo "Installing dinit service..." @install -DZ res/dinit-service -m 644 -T ${DESTDIR}/etc/dinit.d/${DISTFILE} @install -DZ res/dinit-script -m 755 -T ${DESTDIR}/etc/dinit.d/scripts/${DISTFILE} @echo "Done" install-all: install install-manual install-pam uninstall: @echo "Uninstalling..." @rm -rf ${DESTDIR}/etc/sv/${DISTFILE} @rm -rf ${DESTDIR}/etc/runit/sv/${DISTFILE} @rm -f ${DESTDIR}/usr/lib/systemd/system/${DISTFILE}.service @rm -f ${DESTDIR}/etc/init.d/${DISTFILE} @rm -f ${DESTDIR}/usr/share/man/man1/emptty.1.gz @rm -f ${DESTDIR}/etc/pam.d/emptty @rm -rf ${DESTDIR}/etc/s6/sv/${DISTFILE} @rm -rf ${DESTDIR}/usr/bin/${DISTFILE} @rm -rf ${DESTDIR}/etc/dinit.d/${DISTFILE} @rm -rf ${DESTDIR}/etc/dinit.d/scripts/${DISTFILE} @echo "Done" emptty-0.13.0/README.md000066400000000000000000000353061467633363300144210ustar00rootroot00000000000000# emptty [![Release](https://img.shields.io/github/release/tvrzna/emptty.svg?style=flat-square)](https://github.com/tvrzna/emptty/releases/latest) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/tvrzna/emptty/.github/workflows/main.yaml?branch=master&style=flat-square)](https://github.com/tvrzna/emptty/actions?query=workflow:Build) [![Go Report Card](https://goreportcard.com/badge/github.com/tvrzna/emptty?style=flat-square)](https://goreportcard.com/report/github.com/tvrzna/emptty) Dead simple CLI Display Manager on TTY ![](screenshot.png) [![Packaging status](https://repology.org/badge/vertical-allrepos/emptty.svg)](https://repology.org/project/emptty/versions) ## Configuration __NOTE__: Please be aware that emptty does not source any kind of `.profile` scripts by default. If you want to use them, please see [samples](SAMPLES.md). --- #### /etc/emptty/conf Default startup configuration. On each change it requires to restart emptty. `TTY_NUMBER` TTY, where emptty will start. `SWITCH_TTY` Enables switching to defined TTY number. Default is true. `PRINT_ISSUE` Enables printing of /etc/issue in daemon mode. `PRINT_MOTD` Enables printing of default motd, static motd or dynamic motd. `DEFAULT_USER` Preselected user, if AUTOLOGIN is enabled, this user is logged in. `DEFAULT_SESSION` Preselected desktop session, if user does not use `emptty` file. Has lower priority than `AUTOLOGIN_SESSION` `DEFAULT_SESSION_ENV` Optional environment of preselected desktop session, if user does not use `emptty` file. Possible values are "xorg" and "wayland". `AUTOLOGIN` Enables Autologin, if DEFAULT_USER is defined. Possible values are "true" or "false". Default value is false. __NOTE:__ to enable autologin DEFAULT_USER must be in group nopasswdlogin, otherwise user will NOT be authorized. `AUTOLOGIN_SESSION` The default session used, if Autologin is enabled. If session is not found in list of session, it proceeds to manual selection. `AUTOLOGIN_SESSION_ENV` Optional environment of autologin desktop session. Possible values are "xorg" and "wayland". `AUTOLOGIN_MAX_RETRY` If Autologin is enabled and session does not start correctly, the number of retries in short period is kept to eventually stop the infinite loop of restarts. -1 is for infinite retries, 0 is for no retry. Default value is 2. `LANG` defines locale for all users. Default value is "en_US.UTF-8" `DBUS_LAUNCH` Starts "dbus-launch" before desktop command. After end of session "dbus-daemon" is interrupted. Default value is true. If user config is handled as script (does not contain `Exec` option), this config is overridden to false. `ALWAYS_DBUS_LAUNCH` Starts "dbus-launch" before desktop command in any case, `DBUS_LAUNCH` value is ignored. It also starts even if `XINITRC_LAUNCH` is set to `true`. After end of session "dbus-daemon" is interrupted. Default value is false. `XINITRC_LAUNCH` Starts Xorg desktop with calling "\~/.xinitrc" script with session exec as argument, if is true, file exists and selected WM/DE is Xorg session, it overrides DBUS_LAUNCH. If user config is handled as script (does not contain `Exec` option), this config is overridden to false. `VERTICAL_SELECTION` Prints available WM/DE each on new line instead of printing on single line. `LOGGING` Defines the way, how is logging handled. Possible values are "rotate", "appending" or "disabled". Default value is "rotate". `LOGGING_FILE` Overrides path of log file. Default value is `/var/log/emptty/[TTY_NUMBER].log`. __NOTE:__ It expects existence of directories to defined logging file. `XORG_ARGS` Arguments passed to Xorg server. `DYNAMIC_MOTD` Allows to use dynamic motd script to generate custom MOTD. Possible values are "true" or "false". Default value is false. __NOTE:__ Be sure, that dynamic motd has correct content and permissions (e.g. 744), the script is started as default user; in daemon mode it means `root`. `DYNAMIC_MOTD_PATH` Allows to override default path to dynamic motd. Default value is `/etc/emptty/motd-gen.sh` `MOTD_PATH` Allows to override default path to static motd. Default value is `/etc/emptty/motd` `FG_COLOR` Foreground color, available only in daemon mode. List of colors is listed below. `BG_COLOR` Background color, available only in daemon mode. List of colors is listed below. `DISPLAY_START_SCRIPT` Script started before Display (Xorg/Wayland) starts. __NOTE:__ The script is started as default user; in daemon mode it means `root`. `DISPLAY_STOP_SCRIPT` Script started after Display (Xorg/Wayland) stops. __NOTE:__ The script is started as default user; in daemon mode it means `root`. `ENABLE_NUMLOCK` Enables numlock in daemon mode. Possible values are "true" or "false". Default value is false. `SESSION_ERROR_LOGGING` Defines how logging of session errors is handled. Possible values are "rotate", "appending" or "disabled". Default value is "disabled". `SESSION_ERROR_LOGGING_FILE` Overrides path of session errors log file. Default value is `/var/log/emptty/session-errors.[TTY_NUMBER].log`. __NOTE:__ It expects existence of directories to defined logging file. `NO_XDG_FALLBACK` Disallows setting of fallback values for all XDG environmental variables and leaves it on Login Controls. Possible values are "true" or "false". Default value is false. __NOTE:__ Be aware, that setting to "true" could lead to unexpected behaviour. `DEFAULT_XAUTHORITY` If set true, it will not use `.emptty-xauth` file, but the standard `~/.Xauthority` file. This allows to handle xauth issues. Possible values are "true" or "false". Default value is false. `ROOTLESS_XORG` If set true, Xorg will be started as rootless, if system allows and emptty is running in daemon mode. Possible values are "true" or "false". Default value is false. __NOTE:__ Rootless Xorg requires additional [changes](#rootless-xorg) changes in Xorg config. `IDENTIFY_ENVS` If set true, environmental groups are printed to differ Xorg/Wayland/Custom/UserCustom desktops. Possible values are "true" or "false". Default value is false. `HIDE_ENTER_LOGIN` If set true, "hostname login:" is not displayed. Possible values are "true" or "false". Default value is false. `HIDE_ENTER_PASSWORD` If set true, "Password:" is not displayed. Possible values are "true" or "false". Default value is false. `XORG_SESSIONS_PATH` Path to directory, where Xorg sessions' desktop files are stored. Default value is "/usr/share/xsessions/". `WAYLAND_SESSIONS_PATH` Path to directory, where Wayland sessions' desktop files are stored. Default value is "/usr/share/wayland-sessions/". `SELECT_LAST_USER` Enables funtionality of saving last successfully logged in user for next login. Possible values are "false", "per-tty" or "global". Default value is false. `AUTO_SELECTION` If set to "true" and only one desktop is available, it automatically select that desktop. Possible values are "true" or "false". Default value is false. `ALLOW_COMMANDS` If set to "true" and no default user is selected, it allows to enter [commands](#commands) into login input. Possible values are "true" or "false", Default value is true. `CMD_POWEROFF` Command to be used to perform poweroff. Default value is "poweroff". `CMD_REBOOT` Command to be used to perform reboot. Default value is "reboot". `CMD_SUSPEND` Command to be used to perform suspend. Default value is blank, but it tries to use "systemctl suspend", "loginctl suspend" or "zzz". #### Commands If commands are allowed and default user is not defined, there could be used commands in login input. All of these commands need to start with colon `:`. Escape characters are ignored to prevent issues with muscle memory from VI. - `:help`, `:?` prints available commands - `:poweroff`, `:shutdown` processess poweroff command - `:reboot` processes reboot command - `:suspend`, `:zzz` processes suspend command #### Dynamic MOTD If `DYNAMIC_MOTD` is set to `true`, this file exists and is executable for its owner, the result is printed as your own MOTD. Be very careful with this script! #### Static MOTD Custom file, that prints your own MOTD. Reading this file supports colors (e.g. `\x1b[31m` or `\033[32m`). #### User Config `(${HOME}/.config/emptty or ${HOME}/.emptty)` Optional configuration file, that could be also handled as shell script. If is not presented, emptty shows selection of installed desktops. Configuration file stored as `${HOME}/.config/emptty` has higher priority on loading. See [samples](SAMPLES.md#emptty-as-config) `Name` Optional name to be used as Session Name. `Exec` Defines command to start Desktop Environment/Window Manager. It could contain multiple arguments same as in \*.desktop files. This value does not need to be defined, if user config file is presented as shell script (with shebang at the start and execution permissions). `Environment` Selects, which environment should be defined for following command. Possible values are "xorg" and "wayland", "xorg" is default. `Lang` Defines locale for logged user, has higher priority than LANG from global configuration `Selection` Requires selection of desktop, basically turns `emptty` file into `.xinitrc` for Xorg and Wayland. In this case `Exec` is skipped. Possible values are "false" for never using selection, "true" for always showing selection or "auto" for showing selection or first option autoselect, if there is no other desktop. Defauls value is false. `LoginShell` Defines custom shell to be used to start the session. This allows to start the session with non-interactive shell e.g. `/bin/bash --login` `DesktopNames` Value passed into `XDG_CURRENT_DESKTOP` variable. #### User Exit Script `${HOME}/.config/emptty-exit` Optional script file, that is handled as shell script and is started, when session is going end. Script is started even if emptty is being terminated. The default timeout to finish script is 3 seconds, but it is configurable from the script itself by setting variable `Timeout`. `Timeout` Optional custom timeout for script to finish its run, number represents seconds. Default is 3. #### `/etc/emptty/custom-sessions/` or `${HOME}/.config/emptty-custom-sessions/` Optional folders for custom sessions, that could be available system-wide (in case of `/etc/emptty/custom-sessions/`) or user-specific (in case of `${HOME}/.config/emptty-custom-sessions/`), but do not have .desktop file stored on standard paths for Xorg or Wayland sessions. Expected suffix of each file is ".desktop". See [samples](SAMPLES.md#custom-sessions) `Name` Defines name of Desktop Environment/Window Manager. `Exec` Defines command to start Desktop Environment/Window Manager. `Environment` Selects, which environment should be defined for following command. Possible values are "xorg" and "wayland", "xorg" is default. `DesktopNames` Value passed into `XDG_CURRENT_DESKTOP` variable. `NoDisplay` / `Hidden` Boolean value, that controls visibility of desktop session. #### `${HOME}./xinitrc` If config `XINITRC_LAUNCH` is set to true, it enables possibility to use .xinitrc script. See [samples](SAMPLES.md#xinitrc) #### Colors Please, be aware that `LIGHT_` colors could be unavailable as background color.
List of colors
#### Rootless Xorg If Rootless Xorg does not work as expected, make sure you have set following lines in your `/etc/X11/Xwrapper.config`. ``` needs_root_rights = no allowed_users = anybody ``` ## Logging As it is mentioned in configuration, there are three options to handle logging of emptty. The logs contains not just logs from emptty, but also from Xorg (if used) and user's WM/DE. Described log location could differ according configuration `LOGGING_FILE`, that is stored in `/etc/emptty/conf`. #### rotate This option provides simple solution, when current instance of `emptty` logs into `/var/log/emptty/[TTY_NUMBER].log` and the previous version is stored as `/var/log/emptty/[TTY_NUMBER].log.old`. __NOTE:__ Current instance always move previous log into old file, if `emptty` crashes and is started again, previous log is in `/var/log/emptty/[TTY_NUMBER].log.old`. #### appending This option provides functionality that logs everything into `/var/log/emptty/[TTY_NUMBER].log` and does not handle log rotation by itself. It leaves the option for user to handle it themselves (e.g. with logrotate). __NOTE:__ Appending without roration could cause large log file, be sure that log file is rotated. #### disabled This option points all log into `/dev/null`, so no log is available. __NOTE:__ If any issue starts to appear and you want to report it, ensure you do not use this option. ## Build & install ### Build dependencies - go (>= 1.20) - gcc - pam-devel - libx11-devel (libx11) ### Dependencies - pam - libx11 - xorg / xorg-server (optional) - xauth / xorg-xauth (required for xorg) - mcookie (required for xorg) - wayland (optional) ### Make Commands --- - `make clean` to cleanup already built binary. - `make build` to build binary and gzip man page. --- - `make install` to install binary. - `make install-pam` to install pam module. - `make install-pam-debian` to install pam module for Debian. - `make install-pam-fedora` to install pam module for Fedora. - `make install-pam-suse` to install pam module for openSUSE. - `make install-manual` to install man page. - `make install-all` to install binary, pam module and man page. --- - `make install-config` to create default conf file in /etc/emptty/. - `make install-dinit` to install dinit service. - `make install-runit` to install runit service. - `make install-runit-artix` to install runit to Artix service folder. - `make install-openrc` to install openrc service. - `make install-s6` to install s6 service. - `make install-systemd` to install systemd service. - `make install-motd-gen` to create default motd-gen.sh in /etc/emptty/. --- - `make uninstall` to remove emptty from your system --- ### Build tags Different distros could handle libc dependencies in different ways and `emptty` have direct references to these libc functions. For these cases there are Build tags to disable incompatible functionality or just to avoid some unwanted dependency. The usage during build is really simple, just add parameter and optional tags split with ",". ``` $ make build TAGS=tag1,tag2 ``` #### nopam This tag disables dependency on PAM. In Linux it switch to basic authentication with `shadow`. #### noxlib This tag disables dependency on libx11, could be useful, if only Wayland desktop is expected to be used. #### noutmp This tag disables dependency on UTMP/UTMPX. Its implementation is different by each libc/distro, this provides ability to build if incompatibility occurs. emptty-0.13.0/SAMPLES.md000066400000000000000000000056121467633363300145650ustar00rootroot00000000000000# emptty - Samples ## \~/.config/emptty or \~/.emptty as init script In your `.config` folder you have to create 'emptty' file or in your home folder you have to create `.emptty` file. This variant allows to treat your script in similar way as your `.xinitrc`, however this is common to both Xorg and Wayland. The magic option is `Selection=true` or `Selection=auto`. You can define your own environmental variables and keep the possibility to select any desktop. As it is mentioned in [README](README.md), no `.profile` scripts are sourced by default. However following scripts contains few examples, how it could be done inside `emptty` file. #### Script with sourced `/etc/profile` ``` #!/bin/sh Selection=true xrandr --output eDP1 --mode 1920x1080 xrdb -merge ~/.Xresources # source /etc/profile does not have any effect . /etc/profile . ~/.bashrc export BROWSER=firefox export EDITOR=vim exec dbus-launch $@ ``` #### Script with sourced `/etc/profile` using LoginShell ``` #!/bin/sh Selection=true LoginShell=/bin/bash --login # /etc/profile is sourced by using non-interactive shell exec dbus-launch $@ ``` #### Script with fish support in LoginShell Emptty supports simplified fish support, if shebang is set to fish, properties could be set in its basic way. ``` #!/bin/fish set Selection true set LoginShell /bin/fish --login exec dbus-launch $argv ``` ## \~/.config/emptty or \~/.emptty as config In your .config folder you have to create 'emptty' file or in your home folder you have to create `.emptty` file. If `environment` is not defined, it assumes xorg. #### Xorg session ``` Name=Custom Optional Name Exec=/usr/bin/openbox-session Environment=xorg ``` #### Wayland session ``` Name=Custom Optional Name Exec=/usr/bin/sway Environment=wayland ``` ## \~/.config/emptty or \~/.emptty as script In your .config folder you have to create 'emptty' file or in your home folder you have to create `.emptty` file. This file needs to have execution permission (`chmod +x ~/.config/emptty` or `chmod +x ~/.emptty`). ``` #!/bin/sh Environment=xorg # source /etc/profile does not have any effect . /etc/profile . ~/.bashrc exec dbus-launch i3 ``` ## \~/.xinitrc In your home folder you have to create `.xinitrc` file. This file needs to have execution permission (`chmod +x ~/.xinitrc`). ``` #!/bin/sh . ~/.xprofile xrdb -merge ~/.Xresources xmodmap ~/.Xmodmap exec dbus-launch $@ ``` ## Custom sessions #### User-specific Create folder custom-sessions as super user ``` $ mkdir -p ~/.config/emptty-custom-sessions/ ``` #### System-wide Create folder custom-sessions as super user ``` $ sudo mkdir -p /etc/emptty/custom-sessions ``` In these folders you can paste your desktop files. If `environment` is not defined, it assumes xorg. ### Xorg session sowm.desktop ``` Name=sowm Exec=/usr/bin/sowm Environment=xorg ``` ### Wayland session sway.desktop ``` Name=My custom Sway Exec=/usr/bin/sway Environment=wayland ```emptty-0.13.0/go.mod000066400000000000000000000002041467633363300142350ustar00rootroot00000000000000module github.com/tvrzna/emptty go 1.20 require github.com/msteinert/pam/v2 v2.0.0 replace github.com/tvrzna/emptty/src => ./src emptty-0.13.0/go.sum000066400000000000000000000007351467633363300142730ustar00rootroot00000000000000github.com/msteinert/pam/v2 v2.0.0 h1:jnObb8MT6jvMbmrUQO5J/puTUjxy7Av+55zVJRJsCyE= github.com/msteinert/pam/v2 v2.0.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= emptty-0.13.0/main.go000066400000000000000000000001331467633363300144030ustar00rootroot00000000000000package main import emptty "github.com/tvrzna/emptty/src" func main() { emptty.Main() } emptty-0.13.0/res/000077500000000000000000000000001467633363300137245ustar00rootroot00000000000000emptty-0.13.0/res/conf000066400000000000000000000052151467633363300145770ustar00rootroot00000000000000# TTY, where emptty will start. TTY_NUMBER=7 # Enables switching to defined TTY number. SWITCH_TTY=true # Enables printing of /etc/issue in daemon mode. PRINT_ISSUE=true # Enables printing of default motd, /etc/emptty/motd or /etc/emptty/motd-gen.sh. PRINT_MOTD=true # Preselected user, if AUTOLOGIN is enabled, this user is logged in. #DEFAULT_USER=user # Enables Autologin, if DEFAULT_USER is defined and part of nopasswdlogin group. Possible values are "true" or "false". AUTOLOGIN=false # The default session used, if Autologin is enabled. If session is not found in list of session, it proceeds to manual selection. # AUTOLOGIN_SESSION=i3 # If Autologin is enabled and session does not start correctly, the number of retries in short period is kept to eventually stop the infinite loop of restarts. -1 is for infinite retries, 0 is for no retry. # AUTOLOGIN_MAX_RETRY=2 # Default LANG, if user does not have set own in init script. #LANG=en_US.UTF-8 # Starts desktop with calling "dbus-launch". DBUS_LAUNCH=true # Starts Xorg desktop with calling "~/.xinitrc" script, if is true, file exists and selected WM/DE is Xorg session, it overrides DBUS_LAUNCH. XINITRC_LAUNCH=false # Prints available WM/DE each on new line instead of printing on single line. VERTICAL_SELECTION=false # Defines the way, how is logging handled. Possible values are "rotate", "appending" or "disabled". #LOGGING=rotate # Overrides path of log file #LOGGING_FILE=/var/log/emptty/[TTY_NUMBER].log # Arguments passed to Xorg server. #XORG_ARGS= # Allows to use dynamic motd script to generate custom MOTD. #DYNAMIC_MOTD=false # Allows to override default path to dynamic motd. #DYNAMIC_MOTD_PATH=/etc/emptty/motd-gen.sh # Allows to override default path to static motd. #MOTD_PATH=/etc/emptty/motd # Foreground color, available only in daemon mode. #FG_COLOR=LIGHT_BLACK # Background color, available only in daemon mode. #BG_COLOR=BLACK # Enables numlock in daemon mode. Possible values are "true" or "false". #ENABLE_NUMLOCK=false # Defines the way, how is logging of session errors handled. Possible values are "rotate", "appending" or "disabled". #SESSION_ERROR_LOGGING=disabled # Overrides path of session errors log file #SESSION_ERROR_LOGGING_FILE=/var/log/emptty/session-errors.[TTY_NUMBER].log # If set true, it will not use `.emptty-xauth` file, but the standard `~/.Xauthority` file. This allows to handle xauth issues. #DEFAULT_XAUTHORITY=false #If set true, Xorg will be started as rootless, if system allows and emptty is running in daemon mode. #ROOTLESS_XORG=false #If set true, environmental groups are printed to differ Xorg/Wayland/Custom/UserCustom desktops. IDENTIFY_ENVS=false emptty-0.13.0/res/dinit-script000066400000000000000000000000461467633363300162600ustar00rootroot00000000000000#!/bin/sh -l exec /usr/bin/emptty -d emptty-0.13.0/res/dinit-service000066400000000000000000000002321467633363300164110ustar00rootroot00000000000000type = process command = /etc/dinit.d/scripts/emptty restart = true smooth-recovery = true depends-on = logind waits-for = loginready waits-for = rclocal emptty-0.13.0/res/emptty.1000066400000000000000000000305201467633363300153300ustar00rootroot00000000000000.TH EMPTTY 1 "August 2024" "emptty 0.13.0" emptty .SH NAME emptty \- Dead simple CLI Display Manager on TTY .SH SYNOPSIS .B emptty [-v] [--version] [-d] [--daemon] [-c PATH] [--config PATH] [-i] [--ignore-config] [-t TTY] [--tty TTY] [-u defaultUser] [--default-user defaultUser] [-a [session]] [--autologin [session]] .SH DESCRIPTION .B emptty Simple CLI Display Manager, that allows one to select DE/WM after login, use predefined config or allows autologin, if selected user is part of .I nopasswdlogin group. .SH OPTIONS .IP "\-v, \-\-version" Display the version of the program. .IP "\-d, \-\-daemon" Starts emptty as daemon, that does not require agetty. .IP "\-c, \-\-config PATH" Loads configuration from specified path. .IP "\-i, \-\-ignore-config" Skips loading of configuration from file, loads only argument configuration. .IP "\-t, \-\-tty TTY" Overrides loaded configuration by setting defined TTY. May be specified as a number (e.g. "7") or a TTY name (e.g. "tty7"). .IP "\-u, \-\-default-user defaultUser" Overrides loaded configuration by setting defined defaultUser. .IP "\-a, \-\-autologin [session]" Overrides loaded configuration by enabling autologin. If session is defined, it overrides autologin session. .SH CONFIG /etc/emptty/conf .IP TTY_NUMBER TTY, where emptty will start. .IP SWITCH_TTY Enables switching to defined TTY number. Default is true. .IP PRINT_ISSUE Enables printing of /etc/issue in daemon mode. .IP PRINT_MOTD Enables printing of default motd, static motd or dynamic motd. .IP DEFAULT_USER Preselected user, if AUTOLOGIN is enabled, this user is logged in. .IP DEFAULT_SESSION Preselected desktop session, if user does not use `emptty` file. Has lower priority than .I AUTOLOGIN_SESSION .IP DEFAULT_SESSION_ENV Optional environment of preselected desktop session, if user does not use `emptty` file. Possible values are "xorg" and "wayland". .IP AUTOLOGIN Enables Autologin, if DEFAULT_USER is defined. Possible values are "true" or "false". Default value is false. .B NOTE: to enable autologin DEFAULT_USER must be in group .I nopasswdlogin , otherwise user will NOT be authorized. .IP AUTOLOGIN_SESSION The default session used, if Autologin is enabled. If session is not found in list of session, it proceeds to manual selection. .IP AUTOLOGIN_SESSION_ENV Optional environment of autologin desktop session. Possible values are "xorg" and "wayland". .IP AUTOLOGIN_MAX_RETRY If Autologin is enabled and session does not start correctly, the number of retries in short period is kept to eventually stop the infinite loop of restarts. -1 is for infinite retries, 0 is for no retry. Default value is 2. .IP LANG defines locale for all users. Default value is "en_US.UTF-8" .IP DBUS_LAUNCH Starts "dbus-launch" before desktop command. After end of session "dbus-daemon" is interrupted. Default value is true. If .I user config is handled as script (does not contain .I Exec option), this config is overridden to false. .IP ALWAYS_DBUS_LAUNCH Starts "dbus-launch" before desktop command in any case, .I DBUS_LAUNCH value is ignored. It also starts even if .I XINITRC_LAUNCH is set to `true`. After end of session "dbus-daemon" is interrupted. Default value is false. .IP XINITRC_LAUNCH Starts Xorg desktop with calling .I ~/.xinitrc script with session exec as argument, if is true, file exists and selected WM/DE is Xorg session, it overrides DBUS_LAUNCH. If .I user config is handled as script (does not contain .I Exec option), this config is overridden to false. .IP VERTICAL_SELECTION Prints available WM/DE each on new line instead of printing on single line. .IP LOGGING Defines how logging is handled. Possible values are "rotate", "appending" or "disabled". Default value is "rotate". .IP LOGGING_FILE Overrides path of log file. Default value is .I /var/log/emptty/[TTY_NUMBER].log .B NOTE: It expects existence of directories to defined logging file. .IP XORG_ARGS Arguments passed to Xorg server. .IP DYNAMIC_MOTD Allows using dynamic motd script to generate custom MOTD. Possible values are "true" or "false". Default value is false. .B NOTE: Be sure, that dynamic motd has correct content and permissions (e.g. 744), the script is started as default user; in daemon mode it means .I root .IP DYNAMIC_MOTD_PATH Allows overriding default path to dynamic motd. Default value is "/etc/emptty/motd-gen.sh". .IP MOTD_PATH Allows overriding default path to static motd. Default value is "/etc/emptty/motd". .IP FG_COLOR Foreground color, available only in daemon mode. List of colors is listed below. .IP BG_COLOR Background color, available only in daemon mode. List of colors is listed below. .IP DISPLAY_START_SCRIPT Script started before Display (Xorg/Wayland) starts. .B NOTE: The script is started as default user; in daemon mode it means .I root .IP DISPLAY_STOP_SCRIPT Script started after Display (Xorg/Wayland) stops. .B NOTE: The script is started as default user; in daemon mode it means .I root .IP ENABLE_NUMLOCK Enables numlock in daemon mode. Possible values are "true" or "false". Default value is false. .IP SESSION_ERROR_LOGGING Defines how logging of session errors is handled. Possible values are "rotate", "appending" or "disabled". Default value is "disabled". .IP SESSION_ERROR_LOGGING_FILE Overrides path of session errors log file. Default value is .I /var/log/emptty/session-errors.[TTY_NUMBER].log .B NOTE: It expects existence of directories to defined logging file. .IP NO_XDG_FALLBACK Disallows setting of fallback values for all XDG environmental variables and leaves it on Login Controls. Possible values are "true" or "false". Default value is false. .B NOTE: Be aware, that setting to "true" could lead to unexpected behaviour. .IP DEFAULT_XAUTHORITY If set true, it will not use .emptty-xauth file, but the standard ~/.Xauthority file. This allows handling xauth issues. Possible values are "true" or "false". Default value is false. .IP ROOTLESS_XORG If set true, Xorg will be started as rootless, if system allows and emptty is running in daemon mode. Possible values are "true" or "false". Default value is false. .IP IDENTIFY_ENVS If set true, environmental groups are printed to differ Xorg/Wayland/Custom/UserCustom desktops. Possible values are "true" or "false". Default value is false. .IP HIDE_ENTER_LOGIN If set true, "hostname login:" is not displayed. Possible values are "true" or "false". Default value is false. .IP HIDE_ENTER_PASSWORD If set true, "Password:" is not displayed. Possible values are "true" or "false". Default value is false. .IP XORG_SESSIONS_PATH Path to directory, where Xorg sessions' desktop files are stored. Default value is "/usr/share/xsessions/". .IP WAYLAND_SESSIONS_PATH Path to directory, where Wayland sessions' desktop files are stored. Default value is "/usr/share/wayland-sessions/". .IP SELECT_LAST_USER Enables funtionality of saving last successfully logged in user for next login. Possible values are "false", "per-tty" or "global". Default value is false. .IP AUTO_SELECTION If set to "true" and only one desktop is available, it automatically select that desktop. Possible values are "true" or "false". Default value is false. .IP ALLOW_COMMANDS If set to "true" and no default user is selected, it allows to enter commands into login input. Possible values are "true" or "false", Default value is true. .IP CMD_POWEROFF Command to be used to perform poweroff. Default value is "poweroff". .IP CMD_REBOOT Command to be used to perform reboot. Default value is "reboot". .IP CMD_SUSPEND Command to be used to perform suspend. Default value is blank, but it tries to use "systemctl suspend", "loginctl suspend" or "zzz". .SH COMMANDS If commands are allowed and default user is not defined, there could be used commands in login input. All of these commands need to start with colon ":". Escape characters are ignored to prevent issues with muscle memory from VI. - :help, :? - prints available commands - :poweroff, :shutdown - processess poweroff command - :reboot - processes reboot command - :suspend, :zzz - processes suspend command .SH DYNAMIC MOTD Optional file stored by default as /etc/emptty/motd-gen.sh. Could be overridden. If .I DYNAMIC_MOTD is set to true, this file exists and is executable for its owner, the result is printed as your own MOTD. Be very careful with this script! .SH CUSTOM MOTD Optional file stored by default as /etc/emptty/motd. Could be overridden. Custom file, that prints your own MOTD. Reading this file supports colors (e.g. .I \\\x1b[31m or .I \\\033[32m ) .SH USER CONFIG Optional file stored as ${HOME}/.config/emptty or ${HOME}/.emptty Configuration file stored as ${HOME}/.config/emptty has higher priority on loading. .IP Name Optional name to be used as Session Name. .IP Exec Defines command to start Desktop Environment/Window Manager. This value does not need to be defined, if user config is presented as shell script (with shebang at the start and execution permissions). .IP Environment Selects, which environment should be defined for following command. Possible values are "xorg" and "wayland", "xorg" is default. .IP Lang Defines locale for logged user, has higher priority than LANG from global configuration .IP Selection Requires selection of desktop, basically turns .I emptty file into .I .xinitrc for Xorg and Wayland. In this case .I Exec is skipped. Possible values are "false" for never using selection, "true" for always showing selection or "auto" for showing selection or first option autoselect, if there is no other desktop. Default value is false. .IP LoginShell Defines custom shell to be used to start the session. This allows starting the session with non-interactive shell e.g. "/bin/bash --login" .IP DesktopNames Value passed into .I XDG_CURRENT_DESKTOP variable. .SH USER EXIT SCRIPT Optional script file stored as ${HOME}/.config/emptty-exit, that is handled as shell script and is started, when session is going to end. Script is started even if emptty is being terminated. The default timeout to finish script is 3 seconds, but it is configurable from the script itself. .IP Timeout Optional custom timeout for script to finish its run, number represents seconds. Default is 3. .SH CUSTOM SESSIONS Optional folders for custom sessions, that could be available system-wide (in case of /etc/emptty/custom-sessions/) or user-specific (in case of ${HOME}/.config/emptty-custom-sessions/), but do not have .desktop file stored on standard paths for Xorg or Wayland sessions. Expected suffix of each file is ".desktop". .IP Name Defines name of Desktop Environment/Window Manager. .IP Exec Defines command to start Desktop Environment/Window Manager. It could contain multiple arguments same as in *.desktop files. .IP Environment Selects, which environment should be defined for following command. Possible values are "xorg" and "wayland", "xorg" is default. .IP DesktopNames Value passed into .I XDG_CURRENT_DESKTOP variable. .IP NoDisplay/Hidden Boolean value, that controls visibility of desktop session. .SH LAST SESSION The last user selection of session is stored into ~/.cache/emptty/last-session .SH LOGGING As it is mentioned in configuration, there are three options to handle logging of emptty. The logs contains not just logs from emptty, but also from Xorg (if used) and user's WM/DE. Described log location could differ according configuration .I LOGGING_FILE , that is stored in .I /etc/emptty/conf .IP default This option provides simple solution, when current instance of emptty logs into .I /var/log/emptty/[TTY_NUMBER].log and the previous version is stored as .I /var/log/emptty/[TTY_NUMBER].log.old .B NOTE: Current instance always move previous log into old file, if emptty crashes and is started again, previous log is in .I /var/log/emptty/[TTY_NUMBER].log.old .IP appending This option provides functionality that logs everything into .I /var/log/emptty/[TTY_NUMBER].log and does not handle log rotation by itself. It leaves the option for user to handle it themselves (e.g. with logrotate). .B NOTE: Appending without roration could cause large log file, be sure that log file is rotated. .IP disabled This option points all log into .I /dev/null , so no log is available. .B NOTE: If any issue starts to appear and you want to report it, ensure you do not use this option. .SH COLORS Please, be aware that .I LIGHT_ colors could be unavailable as background color. BLACK, RED, GREEN, YELLOW, BLUE, PURPLE, CYAN, WHITE LIGHT_BLACK, LIGHT_RED, LIGHT_GREEN, LIGHT_YELLOW, LIGHT_BLUE, LIGHT_PURPLE, LIGHT_CYAN, LIGHT_WHITE emptty-0.13.0/res/motd-gen.sh000066400000000000000000000002461467633363300157740ustar00rootroot00000000000000#!/bin/sh echo "┌─┐┌┬┐┌─┐┌┬┐┌┬┐┬ ┬" echo "├┤ │││├─┘ │ │ └┬┘" echo "└─┘┴ ┴┴ ┴ ┴ ┴ "emptty-0.13.0/res/openrc-service000066400000000000000000000005101467633363300165670ustar00rootroot00000000000000#!/sbin/openrc-run supervisor=supervise-daemon description="emptty Display Manager" command=/usr/bin/emptty command_args="-d" pidfile="/run/${RC_SVCNAME}.pid" respawn_period=${respawn_period-60} EMPTTY_TERMTIMEOUT=${EMPTTY_TERMTIMEOUT:-"TERM/60/KILL/15"} retry="${EMPTTY_TERMTIMEOUT}" depend() { after local before getty } emptty-0.13.0/res/pam000066400000000000000000000007651467633363300144340ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_succeed_if.so user ingroup nopasswdlogin auth include system-login -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so account include system-login password include system-login session include system-login -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start force_run emptty-0.13.0/res/pam-alpine000066400000000000000000000004111467633363300156660ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_succeed_if.so user ingroup nopasswdlogin auth include base-auth account include base-account password include base-password session include base-session emptty-0.13.0/res/pam-debian000066400000000000000000000007731467633363300156530ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_succeed_if.so user ingroup nopasswdlogin @include common-auth -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so @include common-account @include common-session -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start force_run @include common-password emptty-0.13.0/res/pam-fedora000066400000000000000000000007711467633363300156670ustar00rootroot00000000000000#%PAM-1.0 auth sufficient pam_succeed_if.so user ingroup nopasswdlogin auth include password-auth -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so account include password-auth password include password-auth session include password-auth -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start force_run emptty-0.13.0/res/pam-suse000066400000000000000000000004001467633363300153730ustar00rootroot00000000000000#%PAM-1.0 auth include common-auth account include common-account password include common-password session required pam_loginuid.so session include common-session session optional pam_keyinit.so revoke force emptty-0.13.0/res/runit-run000066400000000000000000000000511467633363300156060ustar00rootroot00000000000000#!/bin/sh exec setsid /usr/bin/emptty -d emptty-0.13.0/res/s6-dependencies000066400000000000000000000000071467633363300166200ustar00rootroot00000000000000udevadmemptty-0.13.0/res/s6-run000066400000000000000000000000571467633363300150030ustar00rootroot00000000000000#!/usr/bin/execlineb -P exec /usr/bin/emptty -demptty-0.13.0/res/s6-type000066400000000000000000000000071467633363300151530ustar00rootroot00000000000000longrunemptty-0.13.0/res/systemd-service000066400000000000000000000004651467633363300170020ustar00rootroot00000000000000[Unit] Description=emptty display manager After=systemd-user-sessions.service [Service] EnvironmentFile=/etc/emptty/conf Type=idle ExecStart=/usr/bin/emptty -d Restart=always TTYPath=/dev/tty${TTY_NUMBER} TTYReset=yes KillMode=process IgnoreSIGPIPE=no SendSIGHUP=yes [Install] Alias=display-manager.service emptty-0.13.0/res/testing/000077500000000000000000000000001467633363300154015ustar00rootroot00000000000000emptty-0.13.0/res/testing/conf000066400000000000000000000015331467633363300162530ustar00rootroot00000000000000# Testing config file. Only for test purpose! TTY_NUMBER=14 SWITCH_TTY=true PRINT_ISSUE=true PRINT_MOTD=false DEFAULT_USER=emptty-user DEFAULT_SESSION=/usr/bin/no-login DEFAULT_SESSION_ENV=waYLand AUTOLOGIN=false AUTOLOGIN_SESSION=none LANG=en_US.UTF-8 DBUS_LAUNCH=true XINITRC_LAUNCH=true VERTICAL_SELECTION=true LOGGING=disabled XORG_ARGS=-none LOGGING_FILE=/dev/null DYNAMIC_MOTD=true DYNAMIC_MOTD_PATH=/dev/null/dynamic MOTD_PATH="/dev/null/static" FG_COLOR=RED BG_COLOR=BLUE DISPLAY_START_SCRIPT='/usr/bin/none-start' DISPLAY_STOP_SCRIPT=/usr/bin/none ENABLE_NUMLOCK=true SESSION_ERROR_LOGGING=appending SESSION_ERROR_LOGGING_FILE=/dev/null ROOTLESS_XORG=true IDENTIFY_ENVS=true AUTOLOGIN_MAX_RETRY=-1 HIDE_ENTER_LOGIN=true ALWAYS_DBUS_LAUNCH=true XORG_SESSIONS_PATH=/dev/null WAYLAND_SESSIONS_PATH=/dev/zero SELECT_LAST_USER=per-tty AUTO_SELECTION=trueemptty-0.13.0/res/testing/desktops/000077500000000000000000000000001467633363300172355ustar00rootroot00000000000000emptty-0.13.0/res/testing/desktops/desktop1.desktop000066400000000000000000000001021467633363300223530ustar00rootroot00000000000000Name=Desktop1 Exec=/usr/bin/desktop1 DesktopNames=Desk1;DESKTOP_1;emptty-0.13.0/res/testing/desktops/desktop2.desktop000066400000000000000000000000601467633363300223570ustar00rootroot00000000000000Name=Desktop2 Exec=/usr/bin/desktop2 env=waylANDemptty-0.13.0/res/testing/issue000066400000000000000000000000161467633363300164510ustar00rootroot00000000000000Hello there \demptty-0.13.0/res/testing/issue_anything000066400000000000000000000001061467633363300203520ustar00rootroot00000000000000\d \l \m \n \r \s \S \S{id_like} \t \4 \6 \4{eth0} \6{eth0} \\t \5 \O emptty-0.13.0/res/testing/issue_more_new_lines000066400000000000000000000000301467633363300215320ustar00rootroot00000000000000Hello with new lines emptty-0.13.0/res/testing/motd-dynamic.sh000066400000000000000000000000471467633363300203230ustar00rootroot00000000000000#!/bin/sh echo "This is dynamic motd."emptty-0.13.0/res/testing/motd-static000066400000000000000000000000241467633363300175500ustar00rootroot00000000000000This is static motd.emptty-0.13.0/res/testing/motd-static-empty000066400000000000000000000000001467633363300206760ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome/000077500000000000000000000000001467633363300171705ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome/.cache/000077500000000000000000000000001467633363300203115ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome/.cache/emptty/000077500000000000000000000000001467633363300216335ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome/.cache/emptty/last-session000066400000000000000000000000251467633363300241770ustar00rootroot00000000000000/usr/bin/none;waylandemptty-0.13.0/res/testing/userHome/.config/000077500000000000000000000000001467633363300205135ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome/.config/emptty000066400000000000000000000002071467633363300217570ustar00rootroot00000000000000#!/bin/fish set env waylAND set COMMAND none set SELECTION false set EXEC none set NAME window-manager set LOGINSHELL /bin/fish --loginemptty-0.13.0/res/testing/userHome2/000077500000000000000000000000001467633363300172525ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome2/.config/000077500000000000000000000000001467633363300205755ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome2/.config/emptty-custom-sessions/000077500000000000000000000000001467633363300252735ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome2/.config/emptty-custom-sessions/custom-desktop.desktop000066400000000000000000000001051467633363300316430ustar00rootroot00000000000000Name=CustomDesktop1 Exec=/usr/bin/custom-desktop1 Environment=waylandemptty-0.13.0/res/testing/userHome2/.config/emptty-custom-sessions/custom-desktop2.desktop000066400000000000000000000001021467633363300317220ustar00rootroot00000000000000Name=CustomDesktop2 Exec=/usr/bin/custom-desktop2 Environment=xorgemptty-0.13.0/res/testing/userHome2/.emptty000066400000000000000000000001341467633363300205730ustar00rootroot00000000000000ENVIRONMENT=xorg COMMAND=none SELECTION=false EXEC=none NAME=window-manager LANG=en_US.UTF-8emptty-0.13.0/res/testing/userHome3/000077500000000000000000000000001467633363300172535ustar00rootroot00000000000000emptty-0.13.0/res/testing/userHome3/.emptty000066400000000000000000000001371467633363300205770ustar00rootroot00000000000000ENVIRONMENT=xorg COMMAND=none SELECTION=true EXEC=sfdgfdgd NAME=window-manager LANG=en_US.UTF-8emptty-0.13.0/res/testing/userHome3/.xinitrc000066400000000000000000000000231467633363300207270ustar00rootroot00000000000000exec dbus-launch $@emptty-0.13.0/screenshot.png000066400000000000000000000074241467633363300160250ustar00rootroot00000000000000PNG  IHDRu8*sBIT|dIDATxn8PfkErt66HfJGR؅?4}OOЇ@@@@@@@@@@@@@@@@?4M^ck~S%m8οq}@ymϺm5n?ݿQα#rοT2bs V#mcT%PGSb[Oͽ?z{v/_9LJY{s.HF?uY٩{Su)i?VZI߻5Mh Ӗ#k4F:nsSm5FKN E&fȽљ~JG 6P.Q;_Tz\};-%y6mdL{/_,X.x)5'uà 崟j{yTY{-6Aw'c<j7qAdn`)ܓ@?D!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P_\\\Oչןc~y?ZR^~EQEQEW1e ( ( ( ( ( ( C0CK/~%%%%%ԣ%Fwfo޳&7 O@b/[=l?8ÔQ2lx"h}37 !U(F`Di g(@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P!@P?pink~sy|ճ/u2z#B'PW ?}W|rߵWIRJ|ݙz %~ygg~/G_wtZl7OQEQEQ6%{0U2fY \Zxe@LMw@8@*5˨)qLwXX9KM<^)G|#[Ѷ2 f<2sTje(@P!@P_ӔX'qO |# L*"n-^צ^bT$;M)MҠZ`jck}~Jc=@$7YJkp)))b 0w]w@$Y#kS>atkN9M ?4}3e ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ?yפ3u&wl틒Vmoj?G}Vh=?u~쵽Ww߫m|lkk}k;Fx=?o{?ulSp~w|sϟ~|r^sõiϲl{i%gyNd_9f=moqsu?ZLh˟,_L>=vg-ϯ;?c߅OysG2:+j=SZoz6-~_s3R|8?9=L_̴)GSNGiu{{S {\lMIJMz]锆9},rFWoW#GSz]۩o9g{p~7oLa*Homhws̖{T8;&%)|Juxo~@nζ?.à/\K9>ZߍG}ס@xx֦OP]x_>]a𴋠x~1:wo.gZ;p~ug[opk;gfɾWZݩ?>´7k~y֚ pV]a" "wy"}tj쟜] %j^̮H};?\]qC=XZ yE.Ϸo}G.O86O؆Qm_L))P*#Ӗ=Ώ9>uһ} c3* SFZm9Gokw^X۾UlI{~UEQEQ)esʨQ0g~D)[-_.jji.oJ~R맖ョwgC!]z]F;9=eGz`}MS^* KP۰}Dq g~./}V9lwe)%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%/]u3IENDB`emptty-0.13.0/src/000077500000000000000000000000001467633363300137225ustar00rootroot00000000000000emptty-0.13.0/src/auth.go000066400000000000000000000047261467633363300152230ustar00rootroot00000000000000package src import ( "bufio" "fmt" "os" "strings" ) const ( pathLastLoggedInUser = "/var/cache/emptty/lastuser" ) type enSelectLastUser byte const ( // Do not preselect any last user False enSelectLastUser = iota // Preselect last successfully logged in user per tty PerTty // Preselect last successfully logged in user per system Global ) type authBase struct { command string } func (a *authBase) getCommand() string { return a.command } // Performs input selection user. If saving last user is enabled (PerTty/Global), user is read from defined path and used as predefined value. func (a *authBase) selectUser(c *config) (string, error) { if c.DefaultUser != "" { if !c.HideEnterLogin { hostname, _ := os.Hostname() fmt.Printf("%s login: %s\n", hostname, c.DefaultUser) } return c.DefaultUser, nil } lastUser := a.getLastSelectedUser(c) if !c.HideEnterLogin { hostname, _ := os.Hostname() lastUserDisplay := "" if lastUser != "" { lastUserDisplay = " [" + lastUser + "]" } fmt.Printf("%s login%s: ", hostname, lastUserDisplay) } input, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil { return "", err } username := input[:len(input)-1] if c.AllowCommands && strings.HasPrefix(strings.ReplaceAll(username, "\x1b", ""), ":") { a.command = strings.ReplaceAll(username, "\x1b", "")[1:] return "", nil } if lastUser != "" && username == "" { username = lastUser } return username, nil } // Gets last selected user with respect to configuration. func (a *authBase) getLastSelectedUser(c *config) string { switch c.SelectLastUser { case PerTty: return a.readLastUser(pathLastLoggedInUser + "-" + c.strTTY()) case Global: return a.readLastUser(pathLastLoggedInUser) } return "" } // Reads last user from file on path. func (a *authBase) readLastUser(path string) string { b, err := os.ReadFile(path) if err != nil { if !os.IsNotExist(err) { logPrint(err) } return "" } lastUser := strings.TrimSpace(string(b)) if strings.Contains(lastUser, "\n") { return "" } return lastUser } // Saves last selected user with respect to configuration. func (a *authBase) saveLastSelectedUser(c *config, username string) { if c.SelectLastUser == False { return } path := pathLastLoggedInUser if c.SelectLastUser == PerTty { path += "-" + c.strTTY() } if err := mkDirsForFile(path, 0700); err != nil { logPrint(err) } if err := os.WriteFile(path, []byte(username), 0600); err != nil { logPrint(err) } } emptty-0.13.0/src/auth_nopam.go000066400000000000000000000033061467633363300164060ustar00rootroot00000000000000//go:build nopam package src import ( "fmt" "os" "os/user" ) const tagPam = "nopam" // PamHandle defines structure of handle specifically designed for not using PAM authorization type nopamHandle struct { *authBase u *sysuser } // Creates authHandle and handles authorization func auth(conf *config) *nopamHandle { h := &nopamHandle{authBase: &authBase{}} h.authUser(conf) return h } // Handle authentication of user without PAM. // If user is successfully authorized, it returns sysuser. // // If autologin is enabled, it behaves as user has been authorized. func (n *nopamHandle) authUser(conf *config) { if conf.Autologin && conf.DefaultUser != "" { usr, err := user.Lookup(conf.DefaultUser) handleErr(err) n.u = getSysuser(usr) return } username, err := n.selectUser(conf) handleErr(err) if n.command != "" { return } if !conf.HideEnterPassword { fmt.Print("Password: ") } password, err := readPassword() handleErr(err) if n.authPassword(username, password) { n.saveLastSelectedUser(conf, username) usr, err := user.Lookup(username) username = "" handleErr(err) n.u = getSysuser(usr) return } addBtmpEntry(username, os.Getpid(), conf.strTTY()) handleStrErr("Authentication failure") } // Gets sysuser func (n *nopamHandle) usr() *sysuser { return n.u } // Handles close of authentication func (n *nopamHandle) closeAuth() { // Nothing to do here } // Defines specific environmental variables defined by PAM func (n *nopamHandle) defineSpecificEnvVariables() { // Nothing to do here } // Opens auth session with XDG_SESSION_TYPE set directly into PAM environments func (n *nopamHandle) openAuthSession(sessionType string) error { // Nothing to do here return nil } emptty-0.13.0/src/auth_nopam_linux.go000066400000000000000000000014651467633363300176310ustar00rootroot00000000000000//go:build nopam package src // #include // #include // #include // #include // #include // #cgo linux LDFLAGS: -lcrypt import "C" import "unsafe" // Tries to authorize user with password. func (n *nopamHandle) authPassword(username string, password string) bool { usr := C.CString(username) defer C.free(unsafe.Pointer(usr)) passwd := C.CString(password) defer C.free(unsafe.Pointer(passwd)) var passhash *C.char if pwd := C.getspnam(usr); pwd != nil { passhash = pwd.sp_pwdp } if passhash == nil { if pwd := C.getpwnam(usr); pwd != nil { passhash = pwd.pw_passwd } } if passhash == nil { return false } encrypted := C.crypt(passwd, passhash) if encrypted == nil || C.strcmp(encrypted, passhash) != 0 { return false } return true } emptty-0.13.0/src/auth_pam.go000066400000000000000000000055351467633363300160570ustar00rootroot00000000000000//go:build !nopam package src import ( "errors" "fmt" "os" "os/user" "github.com/msteinert/pam/v2" ) const tagPam = "" // PamHandle defines structure of handle specifically designed for using PAM authorization type pamHandle struct { *authBase trans *pam.Transaction u *sysuser } // Creates authHandle and handles authorization func auth(conf *config) *pamHandle { h := &pamHandle{authBase: &authBase{}} h.authUser(conf) return h } // Handle PAM authentication of user. // If user is successfully authorized, it returns sysuser. // // If autologin is enabled, it behaves as user has been authorized. func (h *pamHandle) authUser(conf *config) { username, err := h.selectUser(conf) handleErr(err) if h.command != "" { return } h.trans, _ = pam.StartFunc("emptty", username, func(s pam.Style, msg string) (string, error) { switch s { case pam.PromptEchoOff: if conf.Autologin { break } if !conf.HideEnterPassword { fmt.Print("Password: ") } return readPassword() case pam.PromptEchoOn: return "", nil case pam.ErrorMsg: logPrint(msg) return "", nil case pam.TextInfo: fmt.Println(msg) return "", nil } return "", errors.New("unrecognized message style") }) if err := h.trans.Authenticate(pam.DisallowNullAuthtok); err != nil { bkpErr := errors.New(err.Error()) username, _ := h.trans.GetItem(pam.User) addBtmpEntry(username, os.Getpid(), conf.strTTY()) h.handleErr(bkpErr) } logPrint("Authenticate OK") h.handleErr(h.trans.AcctMgmt(pam.Silent)) h.handleErr(h.trans.SetItem(pam.Tty, "tty"+conf.strTTY())) h.handleErr(h.trans.SetCred(pam.EstablishCred)) pamUsr, _ := h.trans.GetItem(pam.User) usr, _ := user.Lookup(pamUsr) h.u = getSysuser(usr) h.saveLastSelectedUser(conf, pamUsr) } func (h *pamHandle) handleErr(err error) { if err != nil { h.closeAuth() handleErr(err) } } // Gets sysuser func (h *pamHandle) usr() *sysuser { return h.u } // Handles close of PAM authentication func (h *pamHandle) closeAuth() { if h != nil && h.trans != nil { logPrint("Closing PAM auth") if err := h.trans.SetCred(pam.DeleteCred); err != nil { logPrint(err) } if err := h.trans.CloseSession(pam.Silent); err != nil { logPrint(err) } h.trans.End() h.trans = nil h.u = nil } } // Defines specific environmental variables defined by PAM func (h *pamHandle) defineSpecificEnvVariables() { if h.trans != nil && h.u != nil { envs, _ := h.trans.GetEnvList() for key, value := range envs { h.u.setenv(key, value) } } } // Opens auth session with XDG_SESSION_TYPE set directly into PAM environments func (h *pamHandle) openAuthSession(sessionType string) error { if h.trans != nil { if err := h.trans.PutEnv(fmt.Sprintf("XDG_SESSION_TYPE=%s", sessionType)); err != nil { return err } return h.trans.OpenSession(pam.Silent) } return errors.New("no active transaction") } emptty-0.13.0/src/config.go000066400000000000000000000172021467633363300155200ustar00rootroot00000000000000package src import ( "os" "reflect" "strconv" "strings" ) const ( pathConfigFile = "/etc/emptty/conf" ) // config defines structure of application configuration. type config struct { DaemonMode bool Autologin bool `config:"AUTOLOGIN" parser:"ParseBool" default:"false"` SwitchTTY bool `config:"SWITCH_TTY" parser:"ParseBool" default:"true"` PrintIssue bool `config:"PRINT_ISSUE" parser:"ParseBool" default:"true"` PrintMotd bool `config:"PRINT_MOTD" parser:"ParseBool" default:"true"` DbusLaunch bool `config:"DBUS_LAUNCH" parser:"ParseBool" default:"true"` AlwaysDbusLaunch bool `config:"ALWAYS_DBUS_LAUNCH" parser:"ParseBool" default:"false"` XinitrcLaunch bool `config:"XINITRC_LAUNCH" parser:"ParseBool" default:"false"` VerticalSelection bool `config:"VERTICAL_SELECTION" parser:"ParseBool" default:"false"` DynamicMotd bool `config:"DYNAMIC_MOTD" parser:"ParseBool" default:"false"` EnableNumlock bool `config:"ENABLE_NUMLOCK" parser:"ParseBool" default:"false"` NoXdgFallback bool `config:"NO_XDG_FALLBACK" parser:"ParseBool" default:"false"` DefaultXauthority bool `config:"DEFAULT_XAUTHORITY" parser:"ParseBool" default:"false"` RootlessXorg bool `config:"ROOTLESS_XORG" parser:"ParseBool" default:"false"` IdentifyEnvs bool `config:"IDENTIFY_ENVS" parser:"ParseBool" default:"false"` HideEnterLogin bool `config:"HIDE_ENTER_LOGIN" parser:"ParseBool" default:"false"` HideEnterPassword bool `config:"HIDE_ENTER_PASSWORD" parser:"ParseBool" default:"false"` AutoSelection bool `config:"AUTO_SELECTION" parser:"ParseBool" default:"false"` AllowCommands bool `config:"ALLOW_COMMANDS" parser:"ParseBool" default:"true"` DefaultSessionEnv enEnvironment `config:"DEFAULT_SESSION_ENV" parser:"ParseEnv" default:""` AutologinSessionEnv enEnvironment `config:"AUTOLOGIN_SESSION_ENV" parser:"ParseEnv" default:""` Logging enLogging `config:"LOGGING" parser:"ParseLogging" default:"rotate"` SessionErrLog enLogging `config:"SESSION_ERROR_LOGGING" parser:"ParseLogging" default:"disabled"` AutologinMaxRetry int `config:"AUTOLOGIN_MAX_RETRY" parser:"ParseInt" default:"2"` Tty int `config:"TTY_NUMBER" parser:"ParseTTY" default:"0"` DefaultUser string `config:"DEFAULT_USER" parser:"SanitizeValue" default:""` DefaultSession string `config:"DEFAULT_SESSION" parser:"SanitizeValue" default:""` AutologinSession string `config:"AUTOLOGIN_SESSION" parser:"SanitizeValue" default:""` Lang string `config:"LANG" parser:"SanitizeValue" default:""` LoggingFile string `config:"LOGGING_FILE" parser:"SanitizeValue" default:"/var/log/emptty/[TTY_NUMBER].log"` XorgArgs string `config:"XORG_ARGS" parser:"SanitizeValue" default:""` DynamicMotdPath string `config:"DYNAMIC_MOTD_PATH" parser:"SanitizeValue" default:"/etc/emptty/motd-gen.sh"` MotdPath string `config:"MOTD_PATH" parser:"SanitizeValue" default:"/etc/emptty/motd"` FgColor string `config:"FG_COLOR" parser:"ConvertFgColor" default:""` BgColor string `config:"BG_COLOR" parser:"ConvertBgColor" default:""` DisplayStartScript string `config:"DISPLAY_START_SCRIPT" parser:"SanitizeValue" default:""` DisplayStopScript string `config:"DISPLAY_STOP_SCRIPT" parser:"SanitizeValue" default:""` SessionErrLogFile string `config:"SESSION_ERROR_LOGGING_FILE" parser:"SanitizeValue" default:"/var/log/emptty/session-errors.[TTY_NUMBER].log"` XorgSessionsPath string `config:"XORG_SESSIONS_PATH" parser:"SanitizeValue" default:"/usr/share/xsessions/"` WaylandSessionsPath string `config:"WAYLAND_SESSIONS_PATH" parser:"SanitizeValue" default:"/usr/share/wayland-sessions/"` SelectLastUser enSelectLastUser `config:"SELECT_LAST_USER" parser:"ParseSelectLastUser" default:"false"` CmdPoweroff string `config:"CMD_POWEROFF" parser:"SanitizeValue" default:"poweroff"` CmdReboot string `config:"CMD_REBOOT" parser:"SanitizeValue" default:"reboot"` CmdSuspend string `config:"CMD_SUSPEND" parser:"SanitizeValue" default:""` } // LoadConfig handles loading of application configuration. func loadConfig(path string) *config { c := config{} var configMap map[string]string var err error if path != "" && fileExists(path) { configMap, err = readPropertiesToMap(path) if err != nil { logFatal(err) } } configType := reflect.TypeOf(c) configValue := reflect.ValueOf(&c) for i := 0; i < configType.NumField(); i++ { field := configType.Field(i) configParam := field.Tag.Get("config") parserName := field.Tag.Get("parser") defaultValue := field.Tag.Get("default") if parserName != "" && configParam != "" { settingValue, exists := configMap[configParam] if !exists { settingValue = defaultValue } parser := configValue.MethodByName(parserName) if parser.Kind() != reflect.Invalid { val := parser.Call([]reflect.Value{reflect.ValueOf(settingValue), reflect.ValueOf(defaultValue)})[0] configValue.Elem().Field(i).Set(val) } } } if c.Lang == "" { defaultLang := os.Getenv(envLang) if defaultLang != "" { c.Lang = defaultLang } else { c.Lang = "en_US.UTF-8" } } return &c } // Parse TTY number. func parseTTY(tty, defaultValue string) int { val, err := strconv.ParseInt(sanitizeValue(tty, defaultValue), 10, 32) if err != nil { return 0 } return int(val) } // Parses TTY from string to int. func (c *config) ParseTTY(value, defaultValue string) int { return parseTTY(value, defaultValue) } // Sanitezes the string value, if value is empty, the defaultValue is returned. func (c *config) SanitizeValue(value, defaultValue string) string { return sanitizeValue(value, defaultValue) } // Parses bool value from string. func (c *config) ParseBool(value, defaultValue string) bool { return parseBool(value, defaultValue) } // Parses int value from string. func (c *config) ParseInt(value, defaultValue string) int { result, _ := strconv.Atoi(sanitizeValue(value, defaultValue)) return result } // Parses logging type from string. func (c *config) ParseLogging(value, defaultValue string) enLogging { return parseLogging(value, defaultValue) } // Parses environment type from string func (c *config) ParseEnv(value, defaultValue string) enEnvironment { if value == "" { return Undefined } return parseEnv(value, defaultValue) } // Coverts string foreground color name into ANSI color value. func (c *config) ConvertFgColor(value, defaultValue string) string { return convertColor(sanitizeValue(value, defaultValue), true) } // Converts string background color name into ANSI color value. func (c *config) ConvertBgColor(value, defaultValue string) string { return convertColor(sanitizeValue(value, defaultValue), false) } // Returns TTY number converted to string func (c *config) strTTY() string { return strconv.Itoa(c.Tty) } // Returns path to TTY func (c *config) ttyPath() string { return "/dev/tty" + c.strTTY() } // Parses select last user config option. func (c *config) ParseSelectLastUser(value, defaultValue string) enSelectLastUser { switch strings.ToLower(sanitizeValue(value, defaultValue)) { case "per-tty": return PerTty case "global": return Global } return False } emptty-0.13.0/src/config_test.go000066400000000000000000000121011467633363300165500ustar00rootroot00000000000000package src import ( "os" "testing" ) func TestLoadConfig(t *testing.T) { loadConfig(getTestingPath("conf")) conf := loadConfig(loadConfigPath([]string{"-c", getTestingPath("conf")})) if conf.Tty != 14 || conf.strTTY() != "14" { t.Error("TestLoadConfig: TTY value is not correct") } if !conf.SwitchTTY { t.Error("TestLoadConfig: SWITCH_TTY value is not correct") } if !conf.PrintIssue { t.Error("TestLoadConfig: PRINT_ISSUE value is not correct") } if conf.PrintMotd { t.Error("TestLoadConfig: PRINT_MOTD value is not correct") } if conf.DefaultUser != "emptty-user" { t.Error("TestLoadConfig: DEFAULT_USER value is not correct") } if conf.Autologin { t.Error("TestLoadConfig: AUTOLOGIN value is not correct") } if conf.AutologinSession != "none" { t.Error("TestLoadConfig: AUTOLOGIN_SESSION value is not correct") } if conf.AutologinSessionEnv != Undefined { t.Error("TestLoadConfig: AUTOLOGIN_SESSION_ENV value is not correct") } if conf.Lang != "en_US.UTF-8" { t.Error("TestLoadConfig: LANG value is not correct") } if !conf.DbusLaunch { t.Error("TestLoadConfig: DBUS_LAUNCH value is not correct") } if !conf.XinitrcLaunch { t.Error("TestLoadConfig: XINITRC_LAUNCH value is not correct") } if !conf.VerticalSelection { t.Error("TestLoadConfig: VERTICAL_SELECTION value is not correct") } if conf.Logging != Disabled { t.Error("TestLoadConfig: LOGGING value is not correct") } if conf.XorgArgs != "-none" { t.Error("TestLoadConfig: XORG_ARGS value is not correct") } if conf.LoggingFile != "/dev/null" { t.Error("TestLoadConfig: LOGGING_FILE value is not correct") } if !conf.DynamicMotd { t.Error("TestLoadConfig: DYNAMIC_MOTD value is not correct") } if conf.FgColor != "31" { t.Error("TestLoadConfig: FG_COLOR value is not correct") } if conf.BgColor != "44" { t.Error("TestLoadConfig: BG_COLOR value is not correct") } if conf.DisplayStartScript != "/usr/bin/none-start" { t.Error("TestLoadConfig: DISPLAY_START_SCRIPT value is not correct") } if conf.DisplayStopScript != "/usr/bin/none" { t.Error("TestLoadConfig: DISPLAY_STOP_SCRIPT value is not correct") } if !conf.EnableNumlock { t.Error("TestLoadConfig: ENABLE_NUMLOCK value is not correct") } if conf.SessionErrLog != Appending { t.Error("TestLoadConfig: SESSION_ERROR_LOGGING value is not correct") } if conf.SessionErrLogFile != "/dev/null" { t.Error("TestLoadConfig: SESSION_ERROR_LOGGING_FILE value is not correct") } if conf.NoXdgFallback { t.Error("TestLoadConfig: NO_XDG_FALLBACK value is not correct") } if conf.DefaultXauthority { t.Error("TestLoadConfig: DEFAULT_XAUTHORITY value is not correct") } if !conf.RootlessXorg { t.Error("TestLoadConfig: ROOTLESS_XORG value is not correct") } if !conf.IdentifyEnvs { t.Error("TestLoadConfig: IDENTIFY_ENVS value is not correct") } if conf.AutologinMaxRetry != -1 { t.Error("TestLoadConfig: AUTOLOGIN_MAX_RETRY value is not correct") } if conf.MotdPath != "/dev/null/static" { t.Error("TestLoadConfig: MOTD_PATH value is not correct") } if conf.DynamicMotdPath != "/dev/null/dynamic" { t.Error("TestLoadConfig: DYNAMIC_MOTD_PATH value is not correct") } if conf.DefaultSession != "/usr/bin/no-login" { t.Error("TestLoadConfig: DEFAULT_SESSION value is not correct") } if conf.DefaultSessionEnv != Wayland { t.Error("TestLoadConfig: DEFAULT_SESSION_ENV value is not correct") } if !conf.HideEnterLogin { t.Error("TestLoadConfig: HIDE_ENTER_LOGIN value is not correct") } if conf.HideEnterPassword { t.Error("TestLoadConfig: HIDE_ENTER_PASSWORD value is not correct") } if !conf.AlwaysDbusLaunch { t.Error("TestLoadConfig: ALWAYS_DBUS_LAUNCH value is not correct") } if conf.XorgSessionsPath != "/dev/null" { t.Error("TestLoadConfig: XORG_SESSIONS_PATH value is not correct") } if conf.WaylandSessionsPath != "/dev/zero" { t.Error("TestLoadConfig: WAYLAND_SESSIONS_PATH value is not correct") } if conf.SelectLastUser != PerTty { t.Error("TestLoadConfig: SELECT_LAST_USER value is not correct") } if !conf.AutoSelection { t.Error("TestLoadConfig: AUTO_SELECTION value is not correct") } } func TestLangLoadConfig(t *testing.T) { lang := os.Getenv(envLang) if lang == "" { lang = "C.UTF-8" } os.Setenv(envLang, "") conf := loadConfig(getTestingPath("non-existing-conf")) if conf.Lang != "en_US.UTF-8" { t.Error("TestLangLoadConfig: fallback language is not correct -", conf.Lang) } os.Setenv(envLang, lang) conf = loadConfig(getTestingPath("non-existing-conf")) if conf.Lang != lang { t.Error("TestLangLoadConfig: fallback language is not correct -", conf.Lang) } } func TestParseTTY(t *testing.T) { var tty int tty = parseTTY("", "6") if tty != 6 { t.Error("TestParseTTY: wrong default value") } tty = parseTTY("7", "6") if tty != 7 { t.Error("TestParseTTY: wrong parsed value") } tty = parseTTY("aaa", "bbb") if tty != 0 { t.Error("TestParseTTY: wrong fallback value") } } func TestTtyPath(t *testing.T) { c := &config{Tty: 15} if c.ttyPath() != "/dev/tty15" { t.Error("TestTtyPath: unexpected result from ttyPath()") } } emptty-0.13.0/src/daemon.go000066400000000000000000000102321467633363300155120ustar00rootroot00000000000000package src import ( "fmt" "io" "os" "sort" "strings" ) const ( strCleanScreen = "\x1b[H\x1b[2J" pathIssue = "/etc/issue" ) // IssueVariable defines list of all escape sequences found in issue file type issueVariable struct { issue string char byte arg string } // Starts emptty as daemon spawning emptty on defined TTY, if allowed. func startDaemon(conf *config) *os.File { if !conf.DaemonMode { return nil } fTTY, err := os.OpenFile(conf.ttyPath(), os.O_RDWR, 0700) if err != nil { logFatal(err) } if conf.EnableNumlock { setKeyboardLeds(fTTY, false, true, false) } clearScreen(fTTY) os.Stdout = fTTY os.Stderr = fTTY os.Stdin = fTTY setColors(conf.FgColor, conf.BgColor) clearScreen(fTTY) if conf.PrintIssue { fmt.Println() printIssue(pathIssue, conf.strTTY()) setColors(conf.FgColor, conf.BgColor) } switchTTY(conf) return fTTY } // Stops daemon mode and closes opened TTY, if allowed func stopDaemon(conf *config, fTTY *os.File) { if !conf.DaemonMode { return } resetColors() clearScreen(fTTY) if fTTY != nil { fTTY.Close() } } // Clears terminal screen func clearScreen(w io.Writer) { if w == nil { fmt.Print(strCleanScreen) } else { w.Write([]byte(strCleanScreen)) } } // Perform switch to defined TTY, if switchTTY is true and tty is greater than 0. func switchTTY(conf *config) bool { if conf.SwitchTTY && conf.Tty > 0 { return chvt(conf.Tty) } return false } // Prints getty issue func printIssue(path, strTTY string) { if fileExists(path) { bIssue, err := os.ReadFile(path) if err == nil { issue := string(bIssue) issue = evaluateIssueVars(issue, findUniqueIssueVars(issue), strTTY) for issue[len(issue)-2:] == "\n\n" { issue = issue[:len(issue)-1] } fmt.Print(revertColorEscaping(issue)) } } } // Finds all unique issue escape sequences func findUniqueIssueVars(issue string) []*issueVariable { var result []*issueVariable var knownIssues []string for i := 0; i < len(issue); i++ { var issueVar *issueVariable issueVar, i = findIssueVar(issue, i) if issueVar != nil && !contains(knownIssues, issueVar.issue) { result = append(result, issueVar) knownIssues = append(knownIssues, issueVar.issue) } } return result } // Finds single issue escape sequence func findIssueVar(issue string, i int) (*issueVariable, int) { saveData := false var j int var buffer strings.Builder var varName byte var arg strings.Builder for j = i; j < len(issue); j++ { b := issue[j] if b == '\\' { saveData = true buffer.Reset() arg.Reset() } if saveData { if j > 0 { if issue[j-1] == '\\' { varName = b } else if b != '{' && b != '}' && b != '\\' { arg.WriteByte(b) } } buffer.WriteByte(b) if j == (len(issue)-1) || (j < len(issue) && j > 0 && issue[j-1] == '\\' && issue[j+1] != '{') || b == '}' { return &issueVariable{buffer.String(), varName, arg.String()}, j } } } return nil, j } // Evaluates outputs for all known escape sequences and return replaced issue func evaluateIssueVars(issue string, issueVars []*issueVariable, strTTY string) string { result := issue sort.Slice(issueVars, func(i int, j int) bool { return len(issueVars[i].arg) > len(issueVars[j].arg) }) for _, issueVar := range issueVars { if output, processed := evaluateIssueVar(issueVar, strTTY); processed { result = strings.ReplaceAll(result, issueVar.issue, output) } } return result } // Evaluate single issue variable and return its result value func evaluateIssueVar(issueVar *issueVariable, strTTY string) (output string, processed bool) { output = "" processed = true switch issueVar.char { case 'd': output = runSimpleCmd("date") case 'l': output = getCurrentTTYName(strTTY, false) case 'm': output = runSimpleCmd("uname", "-m") case 'n': output = runSimpleCmd("uname", "-n") case 'O': output = getDnsDomainName() case 'r': output = runSimpleCmd("uname", "-r") case 's': output = runSimpleCmd("uname", "-s") case 'S': output = getOsReleaseValue(issueVar.arg) case 't': output = runSimpleCmd("date", "+%T") case '4', '6': output = getIpAddress(issueVar.arg, issueVar.char) default: processed = false } return } emptty-0.13.0/src/daemon_test.go000066400000000000000000000023721467633363300165570ustar00rootroot00000000000000package src import ( "bytes" "testing" ) func TestPrintIssue(t *testing.T) { readOutput(func() { printIssue(getTestingPath("issue"), "X") }) } func TestPrintIssueFull(t *testing.T) { readOutput(func() { printIssue(getTestingPath("issue_anything"), "X") }) } func TestPrintIssueWithMoreLines(t *testing.T) { output := readOutput(func() { printIssue(getTestingPath("issue_more_new_lines"), "X") }) if output != "Hello with new lines\n" { t.Error("TestPrintIssueWithMoreLines: issue_more_new_lines after being parsed does not equals to expected value.") } } func TestClearScreen(t *testing.T) { output := readOutput(func() { clearScreen(nil) }) if output != strCleanScreen { t.Error("TestClearScreen: screen was not cleared") } } func TestClearScreenWithOutput(t *testing.T) { buf := new(bytes.Buffer) clearScreen(buf) if buf.String() != strCleanScreen { t.Error("TestClearScreenWithOutput: screen was not cleared") } } func TestSwitchTTY(t *testing.T) { conf := &config{} conf.SwitchTTY = false conf.Tty = -99 if switchTTY(conf) { t.Error("TestSwitchTTY: attempt to switch tty, even it is disabled") } conf.SwitchTTY = true if switchTTY(conf) { t.Error("TestSwitchTTY: attempt to switch tty with negative number") } } emptty-0.13.0/src/dbus.go000066400000000000000000000027201467633363300152070ustar00rootroot00000000000000package src import ( "bufio" "os" "strconv" "strings" ) const ( dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" dbusSessionBusPid = "DBUS_SESSION_BUS_PID" ) type dbus struct { pid int address string } // Launches dbus-launch to start daemon, parses its address and pid for further usage in environments. func (d *dbus) launch(usr *sysuser) { logPrint("Starting dbus-launch") dbusOutput := runSimpleCmdAsUser(usr, "dbus-launch") if dbusOutput == "" { logPrint("No output from dbus-launch") return } scanner := bufio.NewScanner(strings.NewReader(dbusOutput)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) readPropertyLine(line, func(key, value string) { switch key { case dbusSessionBusPid: var err error d.pid, err = strconv.Atoi(value) if err != nil { logPrint(err) } case dbusSessionBusAddress: d.address = value usr.setenv(dbusSessionBusAddress, value) } }, false) } if scanner.Err() != nil { logPrint("Reading output from dbus-launch error: ", scanner.Err()) } } // Interrupts dbus by defined pid. func (d *dbus) interrupt() { if d.pid <= 0 { logPrint("Trying to interrupt non-existing process") return } proc, err := os.FindProcess(d.pid) if err != nil { logPrint(err) } if proc != nil { logPrint("Interrupting dbus-daemon, pid: ", d.pid) if err := proc.Signal(os.Interrupt); err != nil { logPrint("Could not interrupt dbus-daemon (pid: ", d.pid, ")") } } } emptty-0.13.0/src/desktop.go000066400000000000000000000270301467633363300157240ustar00rootroot00000000000000package src import ( "bufio" "fmt" "os" "path/filepath" "strconv" "strings" ) const ( confCommand = "COMMAND" confSelection = "SELECTION" desktopExec = "EXEC" desktopName = "NAME" desktopEnvironment = "ENVIRONMENT" desktopEnv = "ENV" desktopLang = "LANG" desktopLoginShell = "LOGINSHELL" desktopNames = "DESKTOPNAMES" desktopNoDisplay = "NODISPLAY" desktopHidden = "HIDDEN" constEnvXorg = "xorg" constEnvWayland = "wayland" constEnvSUndefined = "Undefined" constEnvSXorg = "Xorg" constEnvSWayland = "Wayland" constEnvSCustom = "Custom" constEnvSUserCustom = "User Custom" constEnvSTXorg = "x11" constEnvSTWayland = "wayland" constTrue = "true" constFalse = "false" constAuto = "auto" pathLastSession = "/.cache/emptty/last-session" pathCustomSessions = "/etc/emptty/custom-sessions/" pathUserCustomSession = "/.config/emptty-custom-sessions/" pathLocalWaylandSessions = "/.local/share/wayland-sessions/" pathLocalXSessions = "/.local/share/xsessions/" ) // enEnvironment defines possible Environments. type enEnvironment byte const ( // Undefined represents no environment Undefined enEnvironment = iota // Xorg represents Xorg environment Xorg // Wayland represents Wayland environment Wayland // Custom represents custom desktops, only helper before real env is loaded Custom // UserCustom represents user's desktops, only helper before real env is loaded UserCustom ) type enSelection byte const ( // Never show selection SelectionFalse enSelection = iota // Always show selection SelectionTrue // Show selection only if necessary SelectionAuto ) // desktop defines structure for display environments and window managers. type desktop struct { name string exec string env enEnvironment envOrigin enEnvironment isUser bool path string selection enSelection child *desktop loginShell string desktopNames string noDisplay bool hidden bool } // Gets exec path from desktop and returns true, if command allows dbus-launch. func (d *desktop) getStrExec() (string, bool) { if d.selection != SelectionFalse && d.child != nil { return d.path + " " + d.child.exec, false } else if d.exec != "" { return d.exec, true } return d.path, false } // Gets correct desktop name, if is available. func (d *desktop) getDesktopName() string { if d.desktopNames != "" { names := strings.Split(d.desktopNames, ":") if len(names) > 0 { return names[0] } } return d.name } // Sets desktop names in expected format func (d *desktop) setDesktopNames(desktopNames string) { val := sanitizeValue(desktopNames, "") if val != "" { var names []string for _, name := range strings.Split(strings.ReplaceAll(val, ";", ":"), ":") { if name != "" { names = append(names, name) } } if len(names) > 0 { d.desktopNames = strings.Join(names, ":") } } } // lastSession defines structure for last used session on user login. type lastSession struct { exec string env enEnvironment } // Allows to select desktop, which could be selected. func selectDesktop(usr *sysuser, conf *config, d *desktop) (*desktop, *desktop) { allowAutoselectDesktop := d == nil || d.selection == SelectionFalse desktops := listAllDesktops(usr, conf.XorgSessionsPath, conf.WaylandSessionsPath) if len(desktops) == 0 { handleStrErr("Not found any installed desktop.") } lastDesktop := getLastDesktop(usr, desktops) if conf.Autologin && conf.AutologinSession != "" { if d := findAutoselectDesktop(conf.AutologinSession, conf.AutologinSessionEnv, desktops); d != nil { return d, desktops[lastDesktop] } } if conf.DefaultSession != "" && allowAutoselectDesktop { if d := findAutoselectDesktop(conf.DefaultSession, conf.DefaultSessionEnv, desktops); d != nil { return d, desktops[lastDesktop] } } // If there is just one desktop and AutoSelection is set or selection is set to Auto, select first desktop if len(desktops) == 1 && (conf.AutoSelection || (d != nil && d.selection == SelectionAuto)) { return desktops[0], desktops[lastDesktop] } // Otherwise go through selection process for { fmt.Printf("\n") printDesktops(conf, desktops) fmt.Printf("\nSelect [%d]: ", lastDesktop) selection, _ := bufio.NewReader(os.Stdin).ReadString('\n') selection = strings.TrimSpace(selection) if selection == "" { selection = strconv.Itoa(lastDesktop) } id, err := strconv.ParseUint(selection, 10, 32) if err != nil { continue } if int(id) < len(desktops) { return desktops[id], desktops[lastDesktop] } } } // Prints list of desktops on screen func printDesktops(conf *config, desktops []*desktop) { dSeparator := ", " eSeparator := " " if conf.VerticalSelection { dSeparator = "\n" eSeparator = "\n" } lastEnv := Undefined for i, v := range desktops { printSeparator := true if conf.IdentifyEnvs && v.envOrigin != lastEnv { if i > 0 { fmt.Print(eSeparator) fmt.Print(eSeparator) } lastEnv = v.envOrigin fmt.Printf("|%s|%s", lastEnv.string(), eSeparator) printSeparator = false } if printSeparator && i > 0 { fmt.Print(dSeparator) } fmt.Printf("[%d] %s", i, v.name) } } // Finds defined autologinSession in array of desktops by its exec or its name and environment, if defined. func findAutoselectDesktop(autologinSession string, env enEnvironment, desktops []*desktop) *desktop { exec, args := getDesktopBaseExec(autologinSession) for _, d := range desktops { desktopExec, _ := getDesktopBaseExec(d.exec) if (exec == desktopExec || strings.EqualFold(autologinSession, d.name)) && (env == Undefined || env == d.env) { if args != "" { d.exec = d.exec + " " + args } return d } } return nil } // Gets base executable name of desktop func getDesktopBaseExec(exec string) (string, string) { parts := strings.Split(strings.TrimSpace(exec), "/") value := strings.TrimSpace(parts[len(parts)-1]) sep := strings.Index(value, " ") if sep > -1 { return value[:sep], value[sep+1:] } return value, "" } // List all installed desktops and return their exec commands. func listAllDesktops(usr *sysuser, pathXorgDesktops, pathWaylandDesktops string) []*desktop { var result []*desktop // load Xorg desktops result = append(result, listDesktops(Xorg, pathXorgDesktops, usr.homedir+pathLocalXSessions)...) // load Wayland desktops result = append(result, listDesktops(Wayland, pathWaylandDesktops, usr.homedir+pathLocalWaylandSessions)...) // load custom desktops result = append(result, listDesktops(Custom, pathCustomSessions)...) // load custom user desktops result = append(result, listDesktops(UserCustom, usr.homedir+pathUserCustomSession)...) return result } // List desktops, that could be found on defined paths. func listDesktops(env enEnvironment, paths ...string) []*desktop { var result []*desktop for _, path := range paths { if strings.HasSuffix(path, "/") { path += "/" } if fileExists(path) { err := filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error { if !fileInfo.IsDir() && strings.HasSuffix(filePath, ".desktop") { d := getDesktop(filePath, env) if !d.noDisplay && !d.hidden { result = append(result, d) } } return nil }) handleErr(err) } } return result } // Inits desktop object from .desktop file on defined path. func getDesktop(path string, env enEnvironment) *desktop { d := desktop{env: env, envOrigin: env, isUser: false, path: path} if env == Custom || env == UserCustom { d.env = Xorg } readProperties(path, func(key string, value string) { switch key { case desktopName: d.name = value case desktopExec: d.exec = value case desktopEnvironment, desktopEnv: d.env = parseEnv(value, constEnvXorg) case desktopNames: d.setDesktopNames(value) case desktopNoDisplay: d.noDisplay = parseBool(value, "false") case desktopHidden: d.hidden = parseBool(value, "false") } }) return &d } // Parses user-specified configuration from file and returns it as desktop structure. func loadUserDesktop(homeDir string) (d *desktop, lang string) { homeDirConf := homeDir + "/.emptty" confDirConf := homeDir + "/.config/emptty" for _, confFile := range []string{confDirConf, homeDirConf} { if !fileExists(confFile) { continue } d := &desktop{isUser: true, path: confFile, env: Xorg, selection: SelectionFalse} err := readPropertiesWithSupport(confFile, func(key string, value string) { switch key { case desktopName: d.name = value case desktopExec, confCommand: d.exec = sanitizeValue(value, "") case desktopEnvironment, desktopEnv: d.env = parseEnv(value, constEnvXorg) case desktopLang: lang = value case confSelection: d.selection = parseSelection(value, "false") case desktopLoginShell: d.loginShell = sanitizeValue(value, "") case desktopNames: d.setDesktopNames(value) } }, true) handleErr(err) if d.selection != SelectionFalse { d.exec = "" d.name = "" d.desktopNames = "" } return d, lang } return nil, lang } // Gets index of last used desktop. func getLastDesktop(usr *sysuser, desktops []*desktop) int { l := getUserLastSession(usr) if l != nil { for i, d := range desktops { if d.exec == l.exec && d.env == l.env { return i } } } return 0 } // Gets user last session stored in his own home directory. func getUserLastSession(usr *sysuser) *lastSession { path := usr.homedir + pathLastSession if fileExists(path) { if content, err := os.ReadFile(path); err == nil { arrContent := strings.Split(strings.TrimSpace(string(content)), ";") l := lastSession{} l.exec = strings.TrimSpace(arrContent[0]) if len(arrContent) > 1 { l.env = parseEnv(arrContent[1], constEnvXorg) return &l } } } return nil } // Sets Last session for declared sysuser and saves it into user's home directory. func setUserLastSession(usr *sysuser, d *desktop) { doAsUser(usr, func() { path := usr.homedir + pathLastSession data := fmt.Sprintf("%s;%s\n", d.exec, d.env.stringify()) if err := mkDirsForFile(path, 0744); err != nil { logPrint(err) } if err := os.WriteFile(path, []byte(data), 0600); err != nil { logPrint(err) } }) } // Checks, if user last session file already exists. func isLastDesktopForSave(usr *sysuser, lastDesktop, currentDesktop *desktop) bool { return !fileExists(usr.homedir+pathLastSession) || lastDesktop.exec != currentDesktop.exec || lastDesktop.env != currentDesktop.env } // Parse input env and selects corresponding environment. func parseEnv(env, defaultValue string) enEnvironment { switch strings.ToLower(sanitizeValue(env, defaultValue)) { case constEnvXorg: return Xorg case constEnvWayland: return Wayland } return Xorg } // Stringify enEnvironment value. func (e enEnvironment) stringify() string { switch e { case Xorg: return constEnvXorg case Wayland: return constEnvWayland } return constEnvXorg } // String value of enEnvironment func (e enEnvironment) string() string { strings := []string{constEnvSUndefined, constEnvSXorg, constEnvSWayland, constEnvSCustom, constEnvSUserCustom} return strings[e] } // Session type of enEnvironment func (e enEnvironment) sessionType() string { strings := []string{"", constEnvSTXorg, constEnvSTWayland, "", ""} return strings[e] } // Parse input selection func parseSelection(selection, defaultValue string) enSelection { switch strings.ToLower(sanitizeValue(selection, defaultValue)) { case constTrue: return SelectionTrue case constFalse: return SelectionFalse case constAuto: return SelectionAuto } return SelectionFalse } emptty-0.13.0/src/desktop_test.go000066400000000000000000000242721467633363300167700ustar00rootroot00000000000000package src import ( "fmt" "os" "os/user" "testing" ) func TestStringifyEnv(t *testing.T) { if Xorg.stringify() != constEnvXorg { t.Error("TestStringifyEnv: wrong value for Xorg env") } if Wayland.stringify() != constEnvWayland { t.Error("TestStringifyEnv: wrong value for Wayland env") } if Custom.stringify() != constEnvXorg { t.Error("TestStringifyEnv: wrong value for Custom env") } } func TestStringEnv(t *testing.T) { if Undefined.string() != constEnvSUndefined { t.Error("TestStringEnv: wrong value for Xorg env") } if Xorg.string() != constEnvSXorg { t.Error("TestStringEnv: wrong value for Xorg env") } if Wayland.string() != constEnvSWayland { t.Error("TestStringEnv: wrong value for Xorg env") } if Custom.string() != constEnvSCustom { t.Error("TestStringEnv: wrong value for Xorg env") } if UserCustom.string() != constEnvSUserCustom { t.Error("TestStringEnv: wrong value for Xorg env") } } func TestPrintDesktops(t *testing.T) { desktops := []*desktop{{name: "a", envOrigin: Xorg}, {name: "b", envOrigin: Wayland}, {name: "c", envOrigin: Custom}, {name: "d", envOrigin: UserCustom}} var result string conf := &config{} conf.VerticalSelection = false conf.IdentifyEnvs = false result = readOutput(func() { printDesktops(conf, desktops) }) if result != "[0] a, [1] b, [2] c, [3] d" { t.Error("TestPrintDesktops: wrong output for VerticalSelection=false, IdentifyEnvs=false") } conf.VerticalSelection = true conf.IdentifyEnvs = false result = readOutput(func() { printDesktops(conf, desktops) }) if result != "[0] a\n[1] b\n[2] c\n[3] d" { t.Error("TestPrintDesktops: wrong output for VerticalSelection=true, IdentifyEnvs=false") } conf.VerticalSelection = false conf.IdentifyEnvs = true result = readOutput(func() { printDesktops(conf, desktops) }) if result != "|Xorg| [0] a |Wayland| [1] b |Custom| [2] c |User Custom| [3] d" { t.Error("TestPrintDesktops: wrong output for VerticalSelection=false, IdentifyEnvs=true") } conf.VerticalSelection = true conf.IdentifyEnvs = true result = readOutput(func() { printDesktops(conf, desktops) }) if result != "|Xorg|\n[0] a\n\n|Wayland|\n[1] b\n\n|Custom|\n[2] c\n\n|User Custom|\n[3] d" { t.Error("TestPrintDesktops: wrong output for VerticalSelection=true, IdentifyEnvs=true") } } func TestParseEnv(t *testing.T) { var env enEnvironment env = parseEnv("", "xorg") if env != Xorg { t.Error("TestParseEnv: wrong default value") } env = parseEnv("xorg", "wayland") if env != Xorg { t.Error("TestParseEnv: wrong parsed value for wayland") } env = parseEnv("wayland", "xorg") if env != Wayland { t.Error("TestParseEnv: wrong parsed value for wayland") } env = parseEnv("aaa", "bbb") if env != Xorg { t.Error("TestParseEnv: wrong fallback value") } } func TestLoadUserDesktop(t *testing.T) { loadUserDesktop(getTestingPath("userHome2")) d, _ := loadUserDesktop(getTestingPath("userHome")) fmt.Println(d.exec) if d.exec != "none" { t.Error("TestLoadUserDesktop: wrong EXEC value") } if d.selection != SelectionFalse { t.Error("TestLoadUserDesktop: wrong SELECTION value") } if d.env != Wayland { t.Error("TestLoadUserDesktop: wrong ENVIRONMENT value") } if d.name != "window-manager" { t.Error("TestLoadUserDesktop: wrong NAME value") } if !d.isUser { t.Error("TestLoadUserDesktop: wrong isUser value") } if d.loginShell == "" { t.Error("TestLoadUserDesktop: wrong loginShell value") } readOutput(func() { d, _ = loadUserDesktop(getTestingPath("userHome3")) if d == nil || d.exec != "" || d.name != "" { t.Error("TestLoadUserDesktop: No desktop returned, selection does not return empty desktop or exec/name are not empty.") } }) d, _ = loadUserDesktop("/dev/null") if d != nil { t.Error("TestLoadUserDesktop: No desktop should be returned, no data available") } } func TestGetDesktop(t *testing.T) { d := getDesktop(getTestingPath("desktops/desktop2.desktop"), Custom) if d.exec != "/usr/bin/desktop2" { t.Error("TestLoadUserDesktop: wrong EXEC value") } if d.selection != SelectionFalse { t.Error("TestLoadUserDesktop: wrong SELECTION value") } if d.env != Wayland { t.Error("TestLoadUserDesktop: wrong ENVIRONMENT value") } if d.name != "Desktop2" { t.Error("TestLoadUserDesktop: wrong NAME value") } if d.isUser { t.Error("TestLoadUserDesktop: wrong isUser value") } } func TestGetUserLastSession(t *testing.T) { usr := &sysuser{} usr.homedir = getTestingPath("userHome2") getUserLastSession(usr) usr.homedir = getTestingPath("userHome") s := getUserLastSession(usr) if s.env != Wayland { t.Error("TestGetUserLastSession: wrong env value") } if s.exec != "/usr/bin/none" { t.Error("TestGetUserLastSession: wrong exec value") } } func TestGetLastDesktop(t *testing.T) { usr := &sysuser{} usr.homedir = getTestingPath("userHome") desktops := []*desktop{{exec: "/usr/bin/none", env: Xorg}, {exec: "/usr/bin/none", env: Wayland}, {exec: "/usr/bin/none2", env: Wayland}} if getLastDesktop(usr, desktops) != 1 { t.Error("TestGetLastDesktop: expected different index") } } func TestListDesktops(t *testing.T) { desktops := listDesktops(Custom, getTestingPath("userHome")) if len(desktops) > 0 { t.Error("TestListDesktops: no desktop was expected") } desktops = listDesktops(Custom, getTestingPath("desktops")) if len(desktops) == 0 { t.Error("TestListDesktops: desktops were expected") } for _, d := range desktops { if d.name == "Desktop1" && (d.exec != "/usr/bin/desktop1" || d.desktopNames != "Desk1:DESKTOP_1") { t.Error("TestListDesktops: wrongly loaded desktop") } if d.name == "Desktop2" && d.env != Wayland { t.Error("TestListDesktops: wrongly loaded desktop, environment is not parsed correctly") } } } func TestIsLastDesktopForSave(t *testing.T) { currentDesktop := &desktop{exec: "/usr/bin/none", env: Wayland} lastDesktop := &desktop{exec: "/usr/bin/none", env: Wayland} usr := &sysuser{} usr.homedir = "/dev/null" if !isLastDesktopForSave(usr, lastDesktop, currentDesktop) { t.Error("TestIsLastDesktopForSave: file not exists and doesn't need to save") } usr.homedir = getTestingPath("userHome") if isLastDesktopForSave(usr, lastDesktop, currentDesktop) { t.Error("TestIsLastDesktopForSave: desktops should not need to save") } lastDesktop.env = Xorg if !isLastDesktopForSave(usr, lastDesktop, currentDesktop) { t.Error("TestIsLastDesktopForSave: desktop should be saved, env is different") } } func TestSetUserLastSession(t *testing.T) { d := &desktop{exec: "/usr/bin/none", env: Wayland} currentUser, _ := user.Current() usr := getSysuser(currentUser) usr.homedir = "/tmp/emptty-test/" setUserLastSession(usr, d) if !fileExists(usr.homedir + pathLastSession) { t.Error("TestSetUserLastSession: last session is not being saved") } os.RemoveAll(usr.homedir) } func TestListAllDesktops(t *testing.T) { usr := &sysuser{} usr.homedir = getTestingPath("userHome2") desktops := listAllDesktops(usr, getTestingPath("desktops"), getTestingPath("desktops")) if len(desktops) != 6 { t.Error("TestListAllDesktops: unexpected count of desktops, 6 expected") } usr.homedir = "/dev/null" desktops = listAllDesktops(usr, "/dev/null", "/dev/null") if len(desktops) != 0 { t.Error("TestListAllDesktops: unexpected count of desktops, 0 expected") } } func TestFindAutoselectDesktop(t *testing.T) { usr := &sysuser{} usr.homedir = getTestingPath("userHome2") desktops := listAllDesktops(usr, getTestingPath("desktops"), getTestingPath("desktops")) d1 := findAutoselectDesktop("CustomDesktop1", Undefined, desktops) if d1 == nil || d1.name != "CustomDesktop1" { t.Error("TestFindAutoselectDesktop: could not find desktop by its name") } d2 := findAutoselectDesktop("custom-desktop2", Xorg, desktops) if d2 == nil || d2.name != "CustomDesktop2" { t.Error("TestFindAutoselectDesktop: could not find desktop by its exec") } d3 := findAutoselectDesktop("unknowndESktop", Undefined, desktops) if d3 != nil { t.Error("TestFindAutoselectDesktop: found desktop, that should be unknown") } d4 := findAutoselectDesktop("UnknownDesktop", Wayland, desktops) if d4 != nil { t.Error("TestFindAutoselectDesktop: found desktop, that should be unknown") } } func TestGetStrExec(t *testing.T) { d := &desktop{path: "/dev/null", exec: "/usr/bin/none"} cmd, isExec := d.getStrExec() if !isExec || cmd != "/usr/bin/none" { t.Errorf("TestGetStrExec: unexpected result: %s; %t", cmd, isExec) } d.exec = "" cmd, isExec = d.getStrExec() if isExec || cmd != "/dev/null" { t.Errorf("TestGetStrExec: unexpected result: %s; %t", cmd, isExec) } d = &desktop{path: "/dev/null", exec: "/usr/bin/none", selection: SelectionAuto, child: d} cmd, isExec = d.getStrExec() if isExec || cmd != d.path+" "+d.child.exec { t.Errorf("TestGetStrExec: unexpected result: %s; %t", cmd, isExec) } } func TestGetDesktopBaseExec(t *testing.T) { exec1, args1 := getDesktopBaseExec("/usr/bin/shell ") if exec1 != "shell" || args1 != "" { t.Error("TestGetDesktopBaseExec: wrong value for exec1") } exec2, args2 := getDesktopBaseExec("/usr/bin/shell --argument1 --argument2='none' -a3 some") if exec2 != "shell" || args2 != "--argument1 --argument2='none' -a3 some" { t.Error("TestGetDesktopBaseExec: wrong value for exec2") } exec3, args3 := getDesktopBaseExec("shell --argument1 --argument2='none' -a3 some") if exec3 != "shell" || args3 != "--argument1 --argument2='none' -a3 some" { t.Error("TestGetDesktopBaseExec: wrong value for exec3") } exec4, args4 := getDesktopBaseExec("shell") if exec4 != "shell" || args4 != "" { t.Error("TestGetDesktopBaseExec: wrong value for exec4") } exec5, args5 := getDesktopBaseExec(" / ") if exec5 != "" || args5 != "" { t.Error("TestGetDesktopBaseExec: wrong value for exec5") } } func TestGetDesktopName(t *testing.T) { d := getDesktop(getTestingPath("desktops/desktop1.desktop"), Custom) if desktopName := d.getDesktopName(); desktopName != "Desk1" { t.Errorf("TestGetDesktopName: desktop1 got unexpected desktop name '%s'", d.getDesktopName()) } d = getDesktop(getTestingPath("desktops/desktop2.desktop"), Custom) if desktopName := d.getDesktopName(); desktopName != "Desktop2" { t.Errorf("TestGetDesktopName: desktop2 got unexpected desktop name '%s'", d.getDesktopName()) } } emptty-0.13.0/src/emptty.go000066400000000000000000000122421467633363300155740ustar00rootroot00000000000000package src import ( "fmt" "os" "os/signal" "runtime" "strings" "syscall" ) const version = "0.13.0" var buildVersion string type sessionHandle struct { session *commonSession auth authHandle } func init() { runtime.LockOSThread() } // Main handles the functionality of whole application. func Main() { TEST_MODE = false processCoreArgs(os.Args) conf := loadConfig(loadConfigPath(os.Args)) processArgs(os.Args, conf) fTTY := startDaemon(conf) initLogger(conf) printMotd(conf) if command := login(conf, initSessionHandle()); command != "" { processCommand(command, conf) } stopDaemon(conf, fTTY) } // Initialize session handle with common interrupt handler func initSessionHandle() *sessionHandle { h := &sessionHandle{} c := make(chan os.Signal, 10) signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) go handleInterrupt(c, h) return h } // Catch interrupt signal chan and interrupts Cmd. func handleInterrupt(c chan os.Signal, h *sessionHandle) { <-c logPrint("Caught interrupt signal") setTerminalEcho(os.Stdout.Fd(), true) if h.session != nil && h.session.cmd != nil { h.session.interrupted = true h.session.cmd.Process.Signal(os.Interrupt) h.session.cmd.Wait() } else { if h.auth != nil { h.auth.closeAuth() } os.Exit(1) } } // Process core arguments for help and version, because they don't require any further application run func processCoreArgs(args []string) { if contains(args, "-h", "--help") { printHelp() os.Exit(0) } if contains(args, "-v", "--version") { fmt.Printf("emptty %s\nhttps://github.com/tvrzna/emptty\n\nReleased under the MIT License.\n", getVersion()) os.Exit(0) } } // Loads config from path according to arguments func loadConfigPath(args []string) (configPath string) { configPath = pathConfigFile for i, arg := range args { switch arg { case "-c", "--config": nextArg(args, i, func(val string) { if fileExists(val) { configPath = val } }) return configPath case "-i", "--ignore-config": return "" } } return configPath } // Process arguments with affection on configuration func processArgs(args []string, conf *config) { for i, arg := range args { switch arg { case "-t", "--tty": nextArg(args, i, func(val string) { tty := parseTTY(val, "0") if tty > 0 { conf.Tty = tty } else { ttynum := strings.SplitAfterN(val, "tty", 2) if len(ttynum) == 2 { tty := parseTTY(ttynum[1], "0") if tty > 0 { conf.Tty = tty } } } }) case "-u", "--default-user": nextArg(args, i, func(val string) { conf.DefaultUser = val }) case "-d", "--daemon": conf.DaemonMode = true case "-a", "--autologin": conf.Autologin = true nextArg(args, i, func(val string) { conf.AutologinSession = val }) } } } // Gets next argument, if available func nextArg(args []string, i int, callback func(value string)) { if callback != nil && len(args) > i+1 { val := sanitizeValue(args[i+1], "") if !strings.HasPrefix(val, "-") { callback(args[i+1]) } } } // Prints help func printHelp() { fmt.Print(`Usage: emptty [options] Options: -h, --help print this help -v, --version print version -d, --daemon start in daemon mode -c, --config PATH load configuration from specified path -i, --ignore-config skips loading of configuration from file, loads only argument configuration -t, --tty NUMBER overrides configured TTY number -u, --default-user USER_NAME overrides configured Default User -a, --autologin [SESSION] overrides configured autologin to true and if next argument is defined, it defines also Autologin Session `) } // Gets current version func getVersion() string { tags := strings.Builder{} for _, tag := range []string{tagPam, tagUtmp, tagXlib} { if tags.Len() > 0 { tags.WriteString(", ") } tags.WriteString(tag) } if buildVersion != "" { if tags.Len() == 0 { return buildVersion[1:] } return buildVersion[1:] + " (" + tags.String() + ")" } return version } // Process commands input in login buffer func processCommand(command string, c *config) { switch command { case "help", "?": fmt.Print(` Available commands: :help, :? print this help :poweroff, :shutdown process poweroff command :reboot process reboot command :suspend, :zzz process suspend command `) waitForReturnToExit(0) case "poweroff", "shutdown": if err := processCommandAsCmd(c.CmdPoweroff); err != nil { handleErr(err) } else { waitForReturnToExit(0) } case "reboot": if err := processCommandAsCmd(c.CmdReboot); err != nil { handleErr(err) } else { waitForReturnToExit(0) } case "suspend", "zzz": var variants []string if c.CmdSuspend != "" { variants = append(variants, c.CmdSuspend) } variants = append(variants, "zzz") variants = append(variants, "systemctl suspend") variants = append(variants, "loginctl suspend") var err error for _, v := range variants { if err = processCommandAsCmd(v); err != nil { continue } else { break } } if err != nil { handleErr(err) } else { waitForReturnToExit(0) } default: handleStrErr(fmt.Sprintf("Unknown command '%s'", command)) } } emptty-0.13.0/src/emptty_test.go000066400000000000000000000066041467633363300166400ustar00rootroot00000000000000package src import ( "strings" "testing" ) func TestGetVersion(t *testing.T) { buildVersion = "" if !strings.HasPrefix(getVersion(), version) { t.Error("TestGetVersion: version does not start with constant") } buildVersion = "testing-version" if !strings.HasPrefix(getVersion(), buildVersion[1:]) { t.Error("TestGetVersion: version does not start with defined version") } } func TestPrintHelp(t *testing.T) { output := readOutput(func() { printHelp() }) if output == "" { t.Error("TestPrintHelp: help does not return text") } } func TestProcessArgs(t *testing.T) { conf1 := &config{} processArgs([]string{"-d"}, conf1) if !conf1.DaemonMode { t.Error("TestProcessArgs: daemon mode was expected") } conf2 := &config{Tty: 77} processArgs([]string{"-t"}, conf2) if conf2.DaemonMode { t.Error("TestProcessArgs: daemon mode was not expected") } if conf2.Tty != 77 { t.Error("TestProcessArgs: tty number should not be touched") } processArgs([]string{"-t", "2"}, conf2) if conf2.Tty != 2 { t.Errorf("TestProcessArgs: expected tty number was 2, but was %d", conf2.Tty) } processArgs([]string{"-t", "tty16"}, conf2) if conf2.Tty != 16 { t.Errorf("TestProcessArgs: expected tty number was 16, but was %d", conf2.Tty) } conf3 := &config{} processArgs([]string{"-u"}, conf3) if conf3.DefaultUser != "" { t.Error("TestProcessArgs: no default user was expected") } processArgs([]string{"-u", "emptty"}, conf3) if conf3.DefaultUser != "emptty" { t.Errorf("TestProcessArgs: expected default user was 'emptty', but was '%s'", conf3.DefaultUser) } conf4 := &config{} processArgs([]string{}, conf4) if conf4.Autologin || conf4.AutologinSession != "" { t.Error("TestProcessArgs: unexpected value for autologin or autologinSession") } processArgs([]string{"-a"}, conf4) if !conf4.Autologin || conf4.AutologinSession != "" { t.Error("TestProcessArgs: unexpected value for autologin or autologinSession") } conf4.Autologin = false processArgs([]string{"-a", "-t", "7"}, conf4) if !conf4.Autologin || conf4.AutologinSession != "" { t.Error("TestProcessArgs: unexpected value for autologin or autologinSession") } conf4.Autologin = false processArgs([]string{"--autologin", "sway"}, conf4) if !conf4.Autologin || conf4.AutologinSession != "sway" { t.Errorf("TestProcessArgs: unexpected value for autologin (is '%t') or autologinSession (is '%s')", conf4.Autologin, conf4.AutologinSession) } } func TestNextArg(t *testing.T) { args := []string{"one", "two", "three", "four"} nextArg(args, 0, func(val string) { if val != "two" { t.Error("TestNextArg: unexpected next argument") } }) nextArg(args, 0, nil) nextArg(args, 5, func(val string) { t.Error("TestNextArg: index out of bound") }) nextArg(args, 3, func(val string) { t.Error("TestNextArg: unexpected next argument") }) } func TestLoadConfigPath(t *testing.T) { path := loadConfigPath([]string{}) if path != pathConfigFile { t.Errorf("TestLoadConfigPath: '%s' was expected, but was '%s'", pathConfigFile, path) } expected := "/dev/null" path = loadConfigPath([]string{"-c", expected, "-c", "unexpected", "-i"}) if path != expected { t.Errorf("TestLoadConfigPath: '%s' was expected, but was '%s'", expected, path) } unexpected := "/dev/null" path = loadConfigPath([]string{"-i", "-c", unexpected}) if path != "" { t.Errorf("TestLoadConfigPath: no path was expected, but was '%s'", path) } } emptty-0.13.0/src/logging.go000066400000000000000000000061271467633363300157050ustar00rootroot00000000000000package src import ( "errors" "fmt" "log" "os" "strings" ) // TEST_MODE Defines if logging is in test mode var TEST_MODE bool const ( pathLogFileNull = "/dev/null" pathLogFileOldSuffix = ".old" constTTYplaceholder = "[TTY_NUMBER]" constLogDefault = "default" constLogRotate = "rotate" constLogAppending = "appending" constLogDisabled = "disabled" ) // enLogging defines possible option how to handle configuration. type enLogging byte const ( // Rotate represents saving into new file and backing up older with suffix Rotate enLogging = iota + 1 // Appending represents saving all logs into same file Appending // Disabled represents disabled logging Disabled ) // Log simple information func logPrint(v ...interface{}) { log.Print(v...) } // Log simple information with format func logPrintf(format string, v ...interface{}) { log.Printf(format, v...) } // Log fatal information func logFatal(v ...interface{}) { log.Fatal(v...) } // Handles error passed as string and calls handleErr function. func handleStrErr(err string) { if err != "" { handleErr(errors.New(err)) } } // If error is not nil, otherwise it prints error, waits for user input and then exits the program. func handleErr(err error) { if err != nil { logPrint(err) fmt.Printf("Error: %s\n", err) waitForReturnToExit(1) } } // Initialize logger to file defined by pathLogFile. func initLogger(conf *config) { f, err := prepareLogFile(conf.LoggingFile, conf.strTTY(), conf.Logging) if err == nil { log.SetOutput(f) } } // Initialize logger to file for session-errors. func initSessionErrorLogger(conf *config) (*os.File, error) { return prepareLogFile(conf.SessionErrLogFile, conf.strTTY(), conf.SessionErrLog) } // Prepares logging file according to defined configuration. func prepareLogFile(path, tty string, method enLogging) (*os.File, error) { logFilePath := strings.ReplaceAll(path, constTTYplaceholder, tty) if method == Rotate && logFilePath != pathLogFileNull { // Temporary workaround to allow create new folder backupFileIfNotFolder(logFilePath) if err := mkDirsForFile(logFilePath, 0755); err != nil { return nil, err } if fileExists(logFilePath) { os.Remove(logFilePath + pathLogFileOldSuffix) os.Rename(logFilePath, logFilePath+pathLogFileOldSuffix) } } else if method == Disabled { logFilePath = pathLogFileNull } return os.OpenFile(logFilePath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644) } // Temporal solution to avoid issues with names of logging folder, if there is already file with same name. func backupFileIfNotFolder(path string) { fileName := path[:strings.LastIndex(path, "/")] if f, err := os.Stat(fileName); err == nil && f != nil && !f.IsDir() { os.Remove(fileName + pathLogFileOldSuffix) os.Rename(fileName, fileName+pathLogFileOldSuffix) } } // Parse logging option func parseLogging(strLogging, defaultValue string) enLogging { switch strings.ToLower(sanitizeValue(strLogging, defaultValue)) { case constLogDisabled: return Disabled case constLogAppending: return Appending case constLogDefault, constLogRotate: return Rotate } return Rotate } emptty-0.13.0/src/logging_test.go000066400000000000000000000072771467633363300167530ustar00rootroot00000000000000package src import ( "errors" "os" "strings" "testing" ) func TestParseLogging(t *testing.T) { var logging enLogging logging = parseLogging("", constLogRotate) if logging != Rotate { t.Error("TestParseLogging: wrong default value") } logging = parseLogging("", constLogDefault) if logging != Rotate { t.Error("TestParseLogging: wrong default value") } logging = parseLogging(constLogDefault, constLogDefault) if logging != Rotate { t.Error("TestParseLogging: wrong parsed value for default") } logging = parseLogging("APPending", constLogDefault) if logging != Appending { t.Error("TestParseLogging: wrong parsed value for appending") } logging = parseLogging("DISABLED", constLogDefault) if logging != Disabled { t.Error("TestParseLogging: wrong parsed value for disabled") } logging = parseLogging("aaa", "bbb") if logging != Rotate { t.Error("TestParseLogging: wrong fallback value") } } func TestInitSessionErrorLogger(t *testing.T) { f, _ := os.CreateTemp(os.TempDir(), "emptty-session-log-file") fileName := f.Name() f.Close() conf := &config{SessionErrLogFile: f.Name(), SessionErrLog: Rotate} sessFile, sessErr := initSessionErrorLogger(conf) sessFile.Close() os.Remove(fileName + pathLogFileOldSuffix) os.Remove(fileName) if sessErr != nil { t.Error("TestInitSessionErrorLogger: unexpected error", sessErr) } conf.SessionErrLog = Appending sessFile, sessErr = initSessionErrorLogger(conf) sessFile.Close() if sessErr != nil { t.Error("TestInitSessionErrorLogger: unexpected error", sessErr) } conf.SessionErrLog = Disabled sessFile, sessErr = initSessionErrorLogger(conf) sessFile.Close() os.Remove(fileName) if sessErr != nil { t.Error("TestInitSessionErrorLogger: unexpected error", sessErr) } } func TestInitLogger(t *testing.T) { f, _ := os.CreateTemp(os.TempDir(), "emptty-log-file.[TTY_NUMBER]") fileName := f.Name() f.Close() conf := &config{LoggingFile: f.Name(), Logging: Rotate} initLogger(conf) os.Remove(fileName + pathLogFileOldSuffix) os.Remove(fileName) conf.Logging = Appending initLogger(conf) conf.Logging = Disabled initLogger(conf) os.Remove(fileName) } func TestLogPrint(t *testing.T) { expected := "expected message" output := readOutput(func() { logPrint(expected) }) if !strings.Contains(output, expected) { t.Errorf("TestLogPrint: '%s' was expected, but was '%s'", expected, output) } } func TestLogPrintf(t *testing.T) { expected := "expected message" output := readOutput(func() { logPrintf("expected %s", "message") }) if !strings.Contains(output, expected) { t.Errorf("TestLogPrint: '%s' was expected, but was '%s'", expected, output) } } func TestHandleErr(t *testing.T) { TEST_MODE = true output := readOutput(func() { handleErr(nil) }) if output != "" { t.Errorf("TestHandleErr: output should have been empty, but was '%s'", output) } output = readOutput(func() { handleErr(errors.New("THIS IS ERROR")) }) if !strings.Contains(output, "THIS IS ERROR") { t.Errorf("TestHandleErr: 'THIS IS ERROR' was expected, but was '%s'", output) } } func TestHandleStrErr(t *testing.T) { TEST_MODE = true output := readOutput(func() { handleStrErr("") }) if output != "" { t.Errorf("TestHandleStrErr: output should have been empty, but was '%s'", output) } output = readOutput(func() { handleStrErr("THIS IS ERROR") }) if !strings.Contains(output, "THIS IS ERROR") { t.Errorf("TestHandleStrErr: 'THIS IS ERROR' was expected, but was '%s'", output) } } func TestBackupFileIfNotFolder(t *testing.T) { f, _ := os.CreateTemp(os.TempDir(), "emptty-data") fileName := f.Name() f.Close() backupFileIfNotFolder(fileName + "/file") backupFileIfNotFolder(fileName + "/file") os.Remove(fileName) } emptty-0.13.0/src/login.go000066400000000000000000000062041467633363300153630ustar00rootroot00000000000000package src import ( "errors" "os" "os/exec" "strconv" "strings" "time" ) // AuthHandle interface defines handle for authorization type authHandle interface { usr() *sysuser authUser(*config) closeAuth() defineSpecificEnvVariables() openAuthSession(string) error getCommand() string } // Login into graphical environment func login(conf *config, h *sessionHandle) string { h.auth = auth(conf) if h.auth != nil && h.auth.getCommand() != "" { return h.auth.getCommand() } if err := handleLoginRetries(conf, h.auth.usr()); err != nil { h.auth.closeAuth() handleStrErr("Exceeded maximum number of allowed login retries in short period.") return "" } d := processDesktopSelection(h.auth.usr(), conf) runDisplayScript(conf.DisplayStartScript) if err := h.auth.openAuthSession(d.env.sessionType()); err != nil { h.auth.closeAuth() handleStrErr("No active transaction") return "" } h.session = createSession(h.auth, d, conf) h.session.start() h.auth.closeAuth() runDisplayScript(conf.DisplayStopScript) return "" } // Process whole desktop load, selection and last used save. func processDesktopSelection(usr *sysuser, conf *config) *desktop { d, usrLang := loadUserDesktop(usr.homedir) if d == nil || d.selection != SelectionFalse { selectedDesktop, lastDesktop := selectDesktop(usr, conf, d) if isLastDesktopForSave(usr, lastDesktop, selectedDesktop) { setUserLastSession(usr, selectedDesktop) } if d != nil && d.selection != SelectionFalse { d.child = selectedDesktop d.env = d.child.env } else { d = selectedDesktop } } if usrLang != "" { conf.Lang = usrLang } return d } // Runs display script, if defined func runDisplayScript(scriptPath string) { if scriptPath != "" { if fileIsExecutable(scriptPath) { if err := exec.Command(scriptPath).Run(); err != nil { logPrint(err) } } else { logPrint(scriptPath + " is not executable.") } } } // Handles keeping information about last login with retry. func handleLoginRetries(conf *config, usr *sysuser) (result error) { // infinite allowed retries, return to avoid writing into file if conf.AutologinMaxRetry < 0 { return nil } if conf.Autologin && conf.AutologinSession != "" && conf.AutologinMaxRetry >= 0 { retries := 0 doAsUser(usr, func() { if err := mkDirsForFile(usr.getLoginRetryPath(), 0744); err != nil { logPrint(err) } }) file, err := os.Open(usr.getLoginRetryPath()) if err != nil { logPrint(err) } defer file.Close() // Check if last retry was within last 2 seconds limit := time.Now().Add(-2 * time.Second) if info, err := file.Stat(); err == nil { if info.ModTime().After(limit) { content, err := os.ReadFile(usr.getLoginRetryPath()) if err == nil { retries, _ = strconv.Atoi(strings.TrimSpace(string(content))) } retries++ if retries >= conf.AutologinMaxRetry { result = errors.New("exceeded maximum number of allowed login retries in short period") retries = 0 } } } doAsUser(usr, func() { if err := os.WriteFile(usr.getLoginRetryPath(), []byte(strconv.Itoa(retries)), 0600); err != nil { logPrint(err) } }) } return result } emptty-0.13.0/src/login_test.go000066400000000000000000000022641467633363300164240ustar00rootroot00000000000000package src import ( "os" "testing" ) func TestHandleLoginRetriesInfinite(t *testing.T) { c := &config{Autologin: true, AutologinSession: "/dev/null", AutologinMaxRetry: -1} u := &sysuser{homedir: "/tmp/emptty-test"} for i := 0; i < 5; i++ { err := handleLoginRetries(c, u) if err != nil { t.Error("TestHandleLoginRetriesInfinite: No error from handleLoginRetries was expected") } } os.RemoveAll(u.homedir) } func TestHandleLoginRetriesNoRetry(t *testing.T) { c := &config{Autologin: true, AutologinSession: "/dev/null", AutologinMaxRetry: 0} u := &sysuser{homedir: "/tmp/emptty-test"} for i := 0; i < 5; i++ { err := handleLoginRetries(c, u) if err != nil { break } if i > 0 { t.Error("TestHandleLoginRetriesNoRetry: No retry was expected") } } os.RemoveAll(u.homedir) } func TestHandleLoginRetries2Retries(t *testing.T) { c := &config{Autologin: true, AutologinSession: "/dev/null", AutologinMaxRetry: 2} u := &sysuser{homedir: "/tmp/emptty-test"} for i := 0; i < 5; i++ { err := handleLoginRetries(c, u) if err != nil { break } if i > 3 { t.Error("TestHandleLoginRetriesNoRetry: No retry was expected") } } os.RemoveAll(u.homedir) } emptty-0.13.0/src/motd.go000066400000000000000000000040361467633363300152170ustar00rootroot00000000000000package src import ( "fmt" "os" "os/exec" "strings" ) const ( defaultMotd = `┌─┐┌┬┐┌─┐┌┬┐┌┬┐┬ ┬ ├┤ │││├─┘ │ │ └┬┘ └─┘┴ ┴┴ ┴ ┴ ┴ ` + version ) // Prints dynamic motd, if configured; otherwise prints motd, if pathMotd exists; otherwise it prints default motd. func printMotd(conf *config) { if !conf.PrintMotd { return } if !printDynamicMotd(conf) { if !printStaticMotd(conf) { printDefaultMotd() } } } // Prints dynamic motd. If something was printed, returns true. func printDynamicMotd(conf *config) bool { if conf.DynamicMotd && fileIsExecutable(conf.DynamicMotdPath) { motd, err := exec.Command(conf.DynamicMotdPath).Output() return printCommonMotd(conf, motd, err) } return false } // Prints static motd. If something was printed, returns true. func printStaticMotd(conf *config) bool { if fileExists(conf.MotdPath) { motd, err := os.ReadFile(conf.MotdPath) return printCommonMotd(conf, motd, err) } return false } // Handles common part of printing motd func printCommonMotd(conf *config, motd []byte, err error) bool { if err != nil { logPrint(err) return false } if len(motd) > 0 { fmt.Println(revertColorEscaping(string(motd))) if conf.DaemonMode { setColors(conf.FgColor, conf.BgColor) } else { resetColors() } } return true } // Prints default motd. func printDefaultMotd() { fmt.Printf("%s\n\n", defaultMotd) } // Reverts escaped color definitions to real color values. func revertColorEscaping(value string) string { if value != "" { result := strings.ReplaceAll(value, "\\x1b", "\x1b") result = strings.ReplaceAll(result, "\\033", "\x1b") return result } return value } // Sets defined colors. func setColors(fg, bg string) { color := "" if fg != "" { color += fg } if fg != "" && bg != "" { color += ";" } if bg != "" { color += bg } if fg == "" && bg == "" { color = "0" } fmt.Print("\x1b[0;" + color + "m\n") } // Resets colors to default. func resetColors() { setColors("", "") } emptty-0.13.0/src/motd_test.go000066400000000000000000000070741467633363300162630ustar00rootroot00000000000000package src import ( "os" "strings" "testing" ) func TestPrintDefaultMotd(t *testing.T) { output := readOutput(func() { printDefaultMotd() }) if output != defaultMotd+"\n\n" { t.Error("TestPrintDefaultMotd: default motd does not match") } } func TestMotdDynamicNotEnabled(t *testing.T) { c := &config{PrintMotd: true, DynamicMotd: true, DynamicMotdPath: getTestingPath("motd-dynamic.sh"), MotdPath: getTestingPath("motd-static")} output := readOutput(func() { printMotd(c) }) if !strings.HasPrefix(output, "This is static motd") { t.Error("TestMotdDynamicNotEnabled: unexpected result") } f, _ := os.Stat(getTestingPath("motd-dynamic.sh")) originalMode := f.Mode() defer os.Chmod(getTestingPath("motd-dynamic.sh"), originalMode) os.Chmod(getTestingPath("motd-dynamic.sh"), 0755) c.DynamicMotd = false c.MotdPath = "" output = readOutput(func() { printMotd(c) }) if !strings.HasPrefix(output, defaultMotd) { t.Error("TestMotdDynamicNotEnabled: unexpected result") } } func TestMotdDynamic(t *testing.T) { c := &config{PrintMotd: true, DynamicMotd: true, DynamicMotdPath: getTestingPath("motd-dynamic.sh"), MotdPath: getTestingPath("motd-static")} f, _ := os.Stat(getTestingPath("motd-dynamic.sh")) originalMode := f.Mode() defer os.Chmod(getTestingPath("motd-dynamic.sh"), originalMode) os.Chmod(getTestingPath("motd-dynamic.sh"), 0755) os.Stat(getTestingPath("motd-dynamic.sh")) output := readOutput(func() { printMotd(c) }) if !strings.HasPrefix(output, "This is dynamic motd") { t.Error("TestMotdDynamic: result does not match expected value") } } func TestMotdStatic(t *testing.T) { c := &config{PrintMotd: true, DynamicMotd: false, DynamicMotdPath: getTestingPath("motd-dynamic.sh"), MotdPath: getTestingPath("motd-static"), DaemonMode: true} output := readOutput(func() { printMotd(c) }) if !strings.HasPrefix(output, "This is static motd.") { t.Error("TestMotdStatic: result does not match expected value") } } func TestMotdStaticEmpty(t *testing.T) { c := &config{PrintMotd: true, DynamicMotd: false, DynamicMotdPath: getTestingPath("motd-dynamic.sh"), MotdPath: getTestingPath("motd-static-empty")} output := readOutput(func() { printMotd(c) }) if output != "" { t.Error("TestMotdStaticEmpty: result does not match expected value") } } func TestMotdDefault(t *testing.T) { c := &config{PrintMotd: true} output := readOutput(func() { printMotd(c) }) if !strings.HasPrefix(output, defaultMotd) { t.Error("TestMotdDefault: result does not match expected value") } } func TestRevertColorEscaping(t *testing.T) { if revertColorEscaping("") != "" { t.Error("TestRevertColorEscaping: there should not be nothing to be handled") } value := "\\033[0;m\\x1b[0;m" expected := "\033[0;m\x1b[0;m" if revertColorEscaping(value) != expected { t.Error("TestRevertColorEscaping: result does not match expected value") } } func TestSetColors(t *testing.T) { output := readOutput(func() { resetColors() }) if output != "\x1b[0;0m\n" { t.Error("TestSetColors: result does not match to resetting value") } output = readOutput(func() { setColors("31", "") }) if output != "\x1b[0;31m\n" { t.Error("TestSetColors: result does not match to defined foreground value") } output = readOutput(func() { setColors("", "41") }) if output != "\x1b[0;41m\n" { t.Error("TestSetColors: result does not match to defined background value") } output = readOutput(func() { setColors("31", "41") }) if output != "\x1b[0;31;41m\n" { t.Error("TestSetColors: result does not match to defined foreground and background value") } } emptty-0.13.0/src/password.go000066400000000000000000000006151467633363300161150ustar00rootroot00000000000000package src import ( "bufio" "fmt" "os" ) // Reads password without echoing it func readPassword() (string, error) { fd := os.Stdout.Fd() if err := setTerminalEcho(fd, false); err != nil { return "", err } defer setTerminalEcho(fd, true) input, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil { return "", err } fmt.Println() return input[:len(input)-1], nil } emptty-0.13.0/src/session.go000066400000000000000000000155731467633363300157470ustar00rootroot00000000000000package src import ( "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" ) const ( envXdgConfigHome = "XDG_CONFIG_HOME" envXdgRuntimeDir = "XDG_RUNTIME_DIR" envXdgSessionId = "XDG_SESSION_ID" envXdgSessionType = "XDG_SESSION_TYPE" envXdgSessionClass = "XDG_SESSION_CLASS" envXdgSeat = "XDG_SEAT" envHome = "HOME" envPwd = "PWD" envUser = "USER" envLogname = "LOGNAME" envXauthority = "XAUTHORITY" envDisplay = "DISPLAY" envShell = "SHELL" envLang = "LANG" envPath = "PATH" envDesktopSession = "DESKTOP_SESSION" envXdgSessDesktop = "XDG_SESSION_DESKTOP" envUid = "UID" envXdgCurrDesktop = "XDG_CURRENT_DESKTOP" userExitScript = ".config/emptty-exit" exitScriptKey = "TIMEOUT" exitScriptTimeout = 3 ) // session defines basic functions expected from desktop session type session interface { startCarrier() getCarrierPid() int finishCarrier() error } // commonSession defines structure with data required for starting the session type commonSession struct { session auth authHandle d *desktop conf *config dbus *dbus cmd *exec.Cmd interrupted bool } // Starts user's session func createSession(h authHandle, d *desktop, conf *config) *commonSession { s := &commonSession{auth: h, d: d, conf: conf} switch d.env { case Wayland: s.session = &waylandSession{s} case Xorg: s.session = &xorgSession{s, nil} } return s } // Performs common start of session func (s *commonSession) start() { s.defineEnvironment() applyRlimits() s.startCarrier() if !s.conf.NoXdgFallback { s.auth.usr().setenv(envXdgSessionType, s.d.env.sessionType()) } if s.conf.AlwaysDbusLaunch { s.dbus = &dbus{} } session, strExec := s.prepareGuiCommand() s.cmd = session if sessionErrLog, sessionErrLogErr := initSessionErrorLogger(s.conf); sessionErrLogErr == nil { session.Stderr = sessionErrLog defer sessionErrLog.Close() } else { logPrint(sessionErrLogErr) } if s.dbus != nil { if s.auth.usr().getenv(dbusSessionBusAddress) == "" || s.conf.AlwaysDbusLaunch { s.dbus.launch(s.auth.usr()) } else { logPrint("DBUS_SESSION_BUS_ADDRESS is already set, skipping start of DBUS_LAUNCH") } } logPrint("Starting " + strExec) session.Env = s.auth.usr().environ() if err := session.Start(); err != nil { s.finishCarrier() handleErr(err) } pid := s.getCarrierPid() if pid <= 0 { pid = session.Process.Pid } utmpEntry := addUtmpEntry(s.auth.usr().username, pid, s.conf.strTTY(), s.auth.usr().getenv(envDisplay)) logPrint("Added utmp entry") err := session.Wait() if s.dbus != nil && s.dbus.pid > 0 { s.dbus.interrupt() } carrierErr := s.finishCarrier() s.runExitScript() endUtmpEntry(utmpEntry) logPrint("Ended utmp entry") if !s.interrupted && err != nil { logPrint(strExec + " finished with error: " + err.Error() + ". For more details see `SESSION_ERROR_LOGGING` in configuration.") handleStrErr(s.d.env.string() + " session finished with error, please check logs") } if !s.interrupted && carrierErr != nil { logPrint(s.d.env.string() + " finished with error: " + carrierErr.Error()) handleStrErr(s.d.env.string() + " finished with error, please check logs") } } // Prepares environment and env variables for authorized user. func (s *commonSession) defineEnvironment() { s.auth.defineSpecificEnvVariables() s.auth.usr().setenv(envHome, s.auth.usr().homedir) s.auth.usr().setenv(envPwd, s.auth.usr().homedir) s.auth.usr().setenv(envUser, s.auth.usr().username) s.auth.usr().setenv(envLogname, s.auth.usr().username) s.auth.usr().setenv(envUid, s.auth.usr().strUid()) if !s.conf.NoXdgFallback { s.auth.usr().setenvIfEmpty(envXdgConfigHome, s.auth.usr().homedir+"/.config") s.auth.usr().setenvIfEmpty(envXdgRuntimeDir, "/run/user/"+s.auth.usr().strUid()) s.auth.usr().setenvIfEmpty(envXdgSeat, "seat0") s.auth.usr().setenv(envXdgSessionClass, "user") } s.auth.usr().setenv(envShell, s.auth.usr().getShell()) s.auth.usr().setenvIfEmpty(envLang, s.conf.Lang) s.auth.usr().setenvIfEmpty(envPath, os.Getenv(envPath)) if !s.conf.NoXdgFallback { if s.d.name != "" { s.auth.usr().setenv(envDesktopSession, s.d.name) s.auth.usr().setenv(envXdgSessDesktop, s.d.getDesktopName()) } else if s.d.child != nil && s.d.child.name != "" { s.auth.usr().setenv(envDesktopSession, s.d.child.name) s.auth.usr().setenv(envXdgSessDesktop, s.d.child.getDesktopName()) } if s.d.desktopNames != "" { s.auth.usr().setenv(envXdgCurrDesktop, s.d.desktopNames) } else if s.d.child != nil && s.d.child.desktopNames != "" { s.auth.usr().setenv(envXdgCurrDesktop, s.d.child.desktopNames) } } logPrint("Defined Environment") // create XDG folder if !s.conf.NoXdgFallback { if !fileExists(s.auth.usr().getenv(envXdgRuntimeDir)) { handleErr(os.MkdirAll(s.auth.usr().getenv(envXdgRuntimeDir), 0700)) // Set owner of XDG folder os.Chown(s.auth.usr().getenv(envXdgRuntimeDir), s.auth.usr().uid, s.auth.usr().gid) logPrint("Created XDG folder") } else { logPrint("XDG folder already exists, no need to create") } } os.Chdir(s.auth.usr().getenv(envPwd)) } // Prepares command for starting GUI. func (s *commonSession) prepareGuiCommand() (cmd *exec.Cmd, strExec string) { strExec, allowStartupPrefix := s.d.getStrExec() startScript := s.d.isUser && !allowStartupPrefix if allowStartupPrefix && s.conf.XinitrcLaunch && s.d.env == Xorg && !strings.Contains(strExec, ".xinitrc") && fileExists(s.auth.usr().homedir+"/.xinitrc") { startScript = true strExec = s.auth.usr().homedir + "/.xinitrc " + strExec } else if allowStartupPrefix && s.conf.DbusLaunch && !strings.Contains(strExec, "dbus-launch") { s.dbus = &dbus{} } if startScript { cmd = cmdAsUser(s.auth.usr(), s.getLoginShell()+" "+strExec) } else { cmd = cmdAsUser(s.auth.usr(), strExec) } return cmd, strExec } // Gets preferred login shell func (s *commonSession) getLoginShell() string { if s.d.loginShell != "" { return s.d.loginShell } return "/bin/sh" } // Runs session exit script func (s *commonSession) runExitScript() { filePath := filepath.Join(s.auth.usr().homedir, userExitScript) if fileExists(filePath) { timeout := exitScriptTimeout err := readProperties(filePath, func(key, value string) { if key == exitScriptKey { if v, err := strconv.Atoi(value); err == nil { timeout = v } } }) if err != nil { logPrint(err) return } c := make(chan error) cmd := cmdAsUser(s.auth.usr(), s.getLoginShell(), filePath) if err := cmd.Start(); err != nil { logPrint("error during start of exit script", err) return } go func(c chan error) { c <- cmd.Wait() }(c) select { case <-time.After(time.Duration(timeout) * time.Second): syscall.Kill(cmd.Process.Pid, syscall.SIGKILL) case err := <-c: if err != nil { logPrint(err) } } close(c) } } emptty-0.13.0/src/session_test.go000066400000000000000000000065611467633363300170030ustar00rootroot00000000000000package src import ( "strings" "testing" ) type testAuth struct { *authBase u *sysuser } func (t *testAuth) usr() *sysuser { return t.u } func (t *testAuth) authUser(conf *config) { //nothing to do } func (t *testAuth) closeAuth() { // nothing to do } func (t *testAuth) defineSpecificEnvVariables() { // nothing to do } func (t *testAuth) openAuthSession(sessionType string) error { // nothing to do return nil } func TestPrepareGuiCommandWithChild(t *testing.T) { c := &config{} u := &sysuser{uid: 3000, gid: 2000} a := &testAuth{&authBase{}, u} d := &desktop{path: "/dev/null", exec: "/usr/bin/none"} d.child = d s := &commonSession{nil, a, d, c, nil, nil, false} _, exec := s.prepareGuiCommand() if exec != "/usr/bin/none" { t.Errorf("TestPrepareGuiCommandWithChild: result exec command is unexpected: '%s'", exec) } d.selection = SelectionTrue _, exec = s.prepareGuiCommand() if exec != "/dev/null /usr/bin/none" { t.Errorf("TestPrepareGuiCommandWithChild: result exec command is unexpected: '%s'", exec) } } func TestPrepareGuiCommandXinitrc(t *testing.T) { c := &config{} u := &sysuser{uid: 3000, gid: 2000, homedir: getTestingPath("userHome3")} a := &testAuth{&authBase{}, u} d := &desktop{path: "/dev/null", exec: "/usr/bin/none", loginShell: "/bin/login-shell"} s := &commonSession{nil, a, d, c, nil, nil, false} // No config _, exec := s.prepareGuiCommand() if exec != "/usr/bin/none" { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command is unexpected: '%s'", exec) } // Should be correct d.env = Xorg c.XinitrcLaunch = true _, exec = s.prepareGuiCommand() if !strings.Contains(exec, ".xinitrc") { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command does not contain .xinitrc: '%s'", exec) } // Expects .xinitrc from homedir d.env = Wayland _, exec = s.prepareGuiCommand() if strings.Contains(exec, u.homedir+".xinitrc") { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command contains .xinitrc without homedir: '%s'", exec) } // Does not expects .xinitrc from homedir d.env = Xorg d.exec = "" c.XinitrcLaunch = true _, exec = s.prepareGuiCommand() if strings.Contains(exec, "userHome3") { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command should not be from homedir: '%s'", exec) } // Expects no dbus-launch c.DbusLaunch = true d.exec = "/usr/bin/none dbus-launch" cmd, exec := s.prepareGuiCommand() if strings.HasPrefix(exec, "dbus-launch") || s.dbus != nil { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command should not start with dbus-launch: '%s'", exec) } if !strings.HasPrefix(cmd.String(), d.loginShell) { t.Errorf("TestPrepareGuiCommandXinitrc: result cmd command should start with /bin/login-shell: '%s'", cmd.String()) } // Expects no dbus-launch d.exec = "/usr/bin/none" d.loginShell = "" cmd, exec = s.prepareGuiCommand() if strings.HasPrefix(exec, "dbus-launch") || s.dbus != nil { t.Errorf("TestPrepareGuiCommandXinitrc: result exec command should not start with dbus-launch: '%s'", exec) } if !strings.HasPrefix(cmd.String(), "/bin/sh") { t.Errorf("TestPrepareGuiCommandXinitrc: result cmd command should start with /bin/sh: '%s'", cmd.String()) } // Expects dbus-launch c.XinitrcLaunch = false d.exec = "/usr/bin/none" _, exec = s.prepareGuiCommand() if s.dbus == nil { t.Errorf("TestPrepareGuiCommandXinitrc: dbus-launch should be enabled: '%s'", exec) } } emptty-0.13.0/src/session_wayland.go000066400000000000000000000005541467633363300174570ustar00rootroot00000000000000package src // waylandSession defines structure for wayland type waylandSession struct { *commonSession } // Starts no wayland carrier func (w *waylandSession) startCarrier() { } // Gets -1 as carrier Pid func (w *waylandSession) getCarrierPid() int { return -1 } // Finishes no wayland carrier func (w *waylandSession) finishCarrier() error { return nil } emptty-0.13.0/src/session_xorg.go000066400000000000000000000064311467633363300167770ustar00rootroot00000000000000package src import ( "fmt" "os" "os/exec" "strconv" "syscall" ) // xorgSession defines structure for xorg type xorgSession struct { *commonSession xorg *exec.Cmd } // Starts Xorg as carrier for Xorg Session. func (x *xorgSession) startCarrier() { if !x.conf.DefaultXauthority { x.auth.usr().setenv(envXauthority, x.auth.usr().getenv(envXdgRuntimeDir)+"/.emptty-xauth") os.Remove(x.auth.usr().getenv(envXauthority)) } x.auth.usr().setenv(envDisplay, ":"+x.getFreeXDisplay()) // generate mcookie cmd := cmdAsUser(x.auth.usr(), lookPath("mcookie", "/usr/bin/mcookie")) mcookie, err := cmd.Output() handleErr(err) logPrint("Generated mcookie") // generate xauth cmd = cmdAsUser(x.auth.usr(), lookPath("xauth", "/usr/bin/xauth"), "add", x.auth.usr().getenv(envDisplay), ".", string(mcookie)) _, err = cmd.Output() handleErr(err) logPrint("Generated xauthority") // start X logPrint("Starting Xorg") xorgArgs := []string{"vt" + x.conf.strTTY(), x.auth.usr().getenv(envDisplay)} if x.allowRootlessX() { xorgArgs = append(xorgArgs, "-keeptty") } if x.conf.XorgArgs != "" { arrXorgArgs := parseExec(x.conf.XorgArgs) xorgArgs = append(xorgArgs, arrXorgArgs...) } if x.allowRootlessX() { x.xorg = cmdAsUser(x.auth.usr(), lookPath("Xorg", "/usr/bin/Xorg"), xorgArgs...) x.xorg.Env = x.auth.usr().environ() if err := x.setTTYOwnership(x.conf, x.auth.usr().uid); err != nil { logPrint(err) } } else { x.xorg = exec.Command(lookPath("Xorg", "/usr/bin/Xorg"), xorgArgs...) os.Setenv(envDisplay, x.auth.usr().getenv(envDisplay)) os.Setenv(envXauthority, x.auth.usr().getenv(envXauthority)) x.xorg.Env = os.Environ() } x.xorg.Start() if x.xorg.Process == nil { handleStrErr("Xorg is not running") } logPrint("Started Xorg") if err := openXDisplay(x.auth.usr().getenv(envDisplay)); err != nil { handleStrErr("Could not open X Display.") } } // Gets Xorg Pid as int func (x *xorgSession) getCarrierPid() int { if x.xorg == nil { handleStrErr("Xorg is not running") } return x.xorg.Process.Pid } // Finishes Xorg as carrier for Xorg Session func (x *xorgSession) finishCarrier() error { // Stop Xorg x.xorg.Process.Signal(os.Interrupt) err := x.xorg.Wait() logPrint("Interrupted Xorg") // Remove auth os.Remove(x.auth.usr().getenv(envXauthority)) logPrint("Cleaned up xauthority") // Revert rootless TTY ownership if x.allowRootlessX() { if err := x.setTTYOwnership(x.conf, os.Getuid()); err != nil { logPrint(err) } } return err } // Sets TTY ownership to defined uid, but keeps the original gid. func (x *xorgSession) setTTYOwnership(conf *config, uid int) error { info, err := os.Stat(conf.ttyPath()) if err != nil { return err } stat := info.Sys().(*syscall.Stat_t) err = os.Chown(conf.ttyPath(), uid, int(stat.Gid)) if err != nil { return err } err = os.Chmod(conf.ttyPath(), 0620) return err } // Finds free display for spawning Xorg instance. func (x *xorgSession) getFreeXDisplay() string { for i := 0; i < 32; i++ { filename := fmt.Sprintf("/tmp/.X%d-lock", i) if !fileExists(filename) { return strconv.Itoa(i) } } return "0" } // Checks is rootless Xorg is allowed to be used func (x *xorgSession) allowRootlessX() bool { return x.conf.RootlessXorg && (x.conf.DaemonMode || x.conf.ttyPath() == getCurrentTTYName("", true)) } emptty-0.13.0/src/sysuser.go000066400000000000000000000043771467633363300160010ustar00rootroot00000000000000package src // #include import "C" import ( "os/user" "strconv" "strings" ) const ( pathUserRetryFile = "/.cache/emptty/login-retry" ) // Type sysuser defines default structure of user to easier passing of all values. type sysuser struct { username string homedir string uid int gid int gids []int gidsu32 []uint32 env map[string]string } // Loads all necessary info about user into sysuser struct. func getSysuser(usr *user.User) *sysuser { u := &sysuser{} u.username = usr.Username u.homedir = usr.HomeDir u.uid, _ = strconv.Atoi(usr.Uid) u.gid, _ = strconv.Atoi(usr.Gid) u.env = make(map[string]string) if strGids, err := usr.GroupIds(); err == nil { for _, val := range strGids { value, _ := strconv.Atoi(val) u.gids = append(u.gids, int(value)) u.gidsu32 = append(u.gidsu32, uint32(value)) } } return u } // returns uid as uint32. func (u *sysuser) uidu32() uint32 { return uint32(u.uid) } // returns gid as uint32. func (u *sysuser) gidu32() uint32 { return uint32(u.gid) } // returns uid as string. func (u *sysuser) strUid() string { return strconv.Itoa(u.uid) } // returns gid as string. func (u *sysuser) strGid() string { return strconv.Itoa(u.gid) } // gets user's environmental variable by key. func (u *sysuser) getenv(key string) string { if strings.TrimSpace(key) == "" { return "" } return u.env[key] } // sets user's environmental variable. func (u *sysuser) setenv(key, value string) { if strings.TrimSpace(key) != "" { u.env[strings.TrimSpace(key)] = value } } // sets user's environmental variable only if is not already defined with same key func (u *sysuser) setenvIfEmpty(key, value string) { if strings.TrimSpace(u.getenv(key)) == "" { u.setenv(key, value) } } // returns a copy of environmental variables. func (u *sysuser) environ() []string { var result []string for key, value := range u.env { result = append(result, key+"="+value) } return result } // Reads default shell of user. func (u *sysuser) getShell() string { if pwd := C.getpwuid(C.uint(u.uid)); pwd != nil && pwd.pw_shell != nil { return C.GoString(pwd.pw_shell) } return "/bin/sh" } // Gets path to login retry file func (u *sysuser) getLoginRetryPath() string { return u.homedir + pathUserRetryFile } emptty-0.13.0/src/sysuser_test.go000066400000000000000000000046441467633363300170350ustar00rootroot00000000000000package src import ( "os/user" "strings" "testing" ) func TestGetSysuser(t *testing.T) { usr := &user.User{Uid: "3000", Gid: "2000", Username: "Dummy", Name: "There is no name", HomeDir: "/dev/null"} u := getSysuser(usr) if u.strUid() != usr.Uid { t.Error("TestGetSysuser: uid does not match") } if u.strGid() != usr.Gid { t.Error("TestGetSysuser: gid does not match") } if u.uidu32() != 3000 { t.Error("TestGetSysuser: uid32 does not match") } if u.gidu32() != 2000 { t.Error("TestGetSysuser: gid32 does not match") } if !strings.HasPrefix(u.getLoginRetryPath(), "/dev/null") || !strings.HasSuffix(u.getLoginRetryPath(), pathUserRetryFile) { t.Error("TestGetSysuser: unexpected login retry path") } } func TestSysuserEnviron(t *testing.T) { u := &sysuser{} u.env = make(map[string]string) if len(u.environ()) != 0 { t.Error("TestSysuserEnviron: no environmental variable was expected") } u.setenv("", "value") if len(u.environ()) != 0 { t.Error("TestSysuserEnviron: inserted variable with empty name") } u.setenv(" ", "value") if len(u.environ()) != 0 { t.Error("TestSysuserEnviron: inserted variable with blank name") } if u.getenv(" ") != "" { t.Error("TestSysuserEnviron: variable with blank name could not be accessible") } if u.getenv("non-existent") != "" { t.Error("TestSysuserEnviron: found non-existent variable") } u.setenv("key", "value") if u.getenv("key") != "value" { t.Error("TestSysuserEnviron: environmental variable does not contain expected value") } if len(u.environ()) != 1 { t.Error("TestSysuserEnviron: 1 environmental variable was expected") } u.setenv("key", "value2") if u.getenv("key") == "value" { t.Error("TestSysuserEnviron: environmental variable is not being updated") } if len(u.environ()) != 1 { t.Error("TestSysuserEnviron: 1 environmental variable was expected after update") } u.setenv("key2", "value") if u.getenv("key") == "value" { t.Error("TestSysuserEnviron: environmental variable is not being updated") } if len(u.environ()) == 1 { t.Error("TestSysuserEnviron: 2 environmental variable were expected") } u.setenvIfEmpty("key3", "value1") if u.getenv("key3") != "value1" { t.Errorf("TestSysuserEnviron: key3 has unexpected value '%s'", u.getenv("key3")) } u.setenvIfEmpty("key3", "value2") if u.getenv("key3") != "value1" { t.Errorf("TestSysuserEnviron: key3 has unexpected value '%s'", u.getenv("key3")) } } emptty-0.13.0/src/utils.go000066400000000000000000000300631467633363300154130ustar00rootroot00000000000000package src import ( "bufio" "errors" "fmt" "net" "os" "os/exec" "os/user" "path/filepath" "strconv" "strings" "syscall" "unsafe" ) const ( pathOsReleaseFile = "/etc/os-release" osReleasePrettyName = "PRETTY_NAME" osReleaseName = "NAME" devPath = "/dev/" _KDGKBTYPE = 0x4B33 _VT_ACTIVATE = 0x5606 _VT_WAITACTIVE = 0x5607 _KB_101 = 0x02 _KB_84 = 0x01 currentTty = "/dev/tty" devConsole = "/dev/console" ) // propertyFunc defines method to be invoked during readProperties method for each record. type propertyFunc func(key, value string) // readProperties reads defined filePath per line and parses each key-value pair. // These pairs are used as parameters for invoking propertyFunc func readProperties(filePath string, method propertyFunc) error { return readPropertiesWithSupport(filePath, method, false) } // readPropertiesWithSupport reads defined filePath per line and parses each key-value pair with possible fish shell support. // These pairs are used as parameters for invoking propertyFunc func readPropertiesWithSupport(filePath string, method propertyFunc, fishSupport bool) error { file, err := os.Open(filePath) if err != nil { return errors.New("Could not open file " + filePath) } defer file.Close() scanner := bufio.NewScanner(file) requiresFishSupport := false isFirstLine := true for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if isFirstLine { if fishSupport && strings.HasPrefix(line, "#!") && strings.Contains(line, "/fish") { requiresFishSupport = true } isFirstLine = false } readPropertyLine(line, method, requiresFishSupport) } return scanner.Err() } // Reads single property line and parses its content into key-value pair. // The pair is used as parameter for invoking propertyFunc. func readPropertyLine(line string, method propertyFunc, fishSupport bool) { if !strings.HasPrefix(line, "#") && ((!fishSupport && strings.Contains(line, "=")) || fishSupport && strings.HasPrefix(line, "set")) { var splitIndex int if fishSupport { line = line[4:] splitIndex = strings.Index(line, " ") } else { splitIndex = strings.Index(line, "=") } key := strings.ReplaceAll(line[:splitIndex], "export ", "") value := line[splitIndex+1:] if strings.Contains(value, "#") { value = value[:strings.Index(value, "#")] } key = strings.ToUpper(strings.TrimSpace(key)) value = strings.TrimSpace(value) for (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) { value = value[1 : len(value)-1] } method(key, value) } } // Reads properties from defined filePath into key-value map pair. // The result map is returned, if no error appears. func readPropertiesToMap(filePath string) (result map[string]string, err error) { result = make(map[string]string) err = readProperties(filePath, func(key, value string) { result[key] = value }) if err != nil { return nil, err } return result, nil } // Checks, if file on path exists. func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } // Checks, if file on path exists and is executable. func fileIsExecutable(path string) bool { stat, err := os.Stat(path) return err == nil && (stat.Mode()&0100 == 0100) } // Sanitize value. func sanitizeValue(value, defaultValue string) string { if value == "" { return defaultValue } return strings.TrimSpace(value) } // Makes directories up to last part of path (to make sure to not make dir, that is named as result file) func mkDirsForFile(path string, perm os.FileMode) error { if !fileExists(path) && path != "" { return os.MkdirAll(path[:strings.LastIndex(path, "/")], perm) } return nil } // Converts color by name into ANSI color number. func convertColor(name string, isForeground bool) string { colorName := strings.ToUpper(name) isLight := strings.HasPrefix(colorName, "LIGHT_") colorName = strings.Replace(colorName, "LIGHT_", "", -1) colorNumber := 0 switch colorName { case "": colorNumber = 0 case "BLACK": colorNumber = 30 case "RED": colorNumber = 31 case "GREEN": colorNumber = 32 case "YELLOW": colorNumber = 33 case "BLUE": colorNumber = 34 case "MAGENTA": colorNumber = 35 case "CYAN": colorNumber = 36 case "WHITE": colorNumber = 37 default: return "" } if colorNumber > 0 { if !isForeground { colorNumber += 10 } if isLight { colorNumber += 60 } } return strconv.Itoa(colorNumber) } // Prepares *exec.Cmd to be started as sysuser. func cmdAsUser(usr *sysuser, name string, arg ...string) *exec.Cmd { if strings.Contains(name, " ") { nameArgs := parseExec(name) name = nameArgs[0] arg = append(nameArgs[1:], arg...) } cmd := exec.Command(name, arg...) cmd.Env = usr.environ() cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{Uid: usr.uidu32(), Gid: usr.gidu32(), Groups: usr.gidsu32} return cmd } // Processes selected command as exec.Cmd func processCommandAsCmd(name string) error { var arg []string if strings.Contains(name, " ") { nameArgs := parseExec(name) name = nameArgs[0] arg = append(nameArgs[1:], arg...) } name, _ = exec.LookPath(name) cmd := exec.Command(name, arg...) return cmd.Run() } // Splits execString by spaces respecting double quotes. func parseExec(execString string) (result []string) { var sb strings.Builder inQuotes := false escapeNext := false for _, r := range execString { switch { case escapeNext: escapeNext = false case r == '\\': escapeNext = true case r == '"': inQuotes = !inQuotes case r == ' ' && !inQuotes: if sb.Len() > 0 { result = append(result, sb.String()) sb.Reset() continue } } sb.WriteRune(r) } if sb.Len() > 0 { result = append(result, sb.String()) } return } // Applies current resource limits func applyRlimits() { rlimits := []int{syscall.RLIMIT_AS, syscall.RLIMIT_CORE, syscall.RLIMIT_CPU, syscall.RLIMIT_DATA, syscall.RLIMIT_FSIZE, syscall.RLIMIT_NOFILE, syscall.RLIMIT_STACK} rlimit := &syscall.Rlimit{} for _, r := range rlimits { if err := syscall.Getrlimit(r, rlimit); err != nil { logPrintf("could not get rlimit %d", r) continue } if err := syscall.Setrlimit(r, rlimit); err != nil { logPrintf("could not set rlimit %d(soft: %d, max: %d)", r, rlimit.Cur, rlimit.Max) } } } // Checks, if array contains any of values func contains(array []string, values ...string) bool { for _, a := range array { for _, v := range values { if a == v { return true } } } return false } // Parse boolean values. func parseBool(strBool, defaultValue string) bool { val, err := strconv.ParseBool(sanitizeValue(strBool, defaultValue)) if err != nil { return false } return val } // Runs simple command and returns its output as string func runSimpleCmd(cmd ...string) string { return runSimpleCmdAsUser(nil, cmd...) } // Runs simple command as user and returns its output as string func runSimpleCmdAsUser(usr *sysuser, cmd ...string) string { path, err := exec.LookPath(cmd[0]) if err != nil { logPrintf("Could not find command '%s' on PATH", cmd[0]) return "" } execCmd := exec.Command(path, cmd[1:]...) if usr != nil { execCmd.Env = usr.environ() execCmd.SysProcAttr = &syscall.SysProcAttr{} execCmd.SysProcAttr.Credential = &syscall.Credential{Uid: usr.uidu32(), Gid: usr.gidu32(), Groups: usr.gidsu32} } if output, err := execCmd.Output(); err == nil { return strings.TrimSpace(string(output)) } return "" } // Look for path of cmd, if not found, use fallback func lookPath(cmd string, fallback string) string { path, err := exec.LookPath(cmd) if err != nil { logPrintf("Could not find command '%s' on PATH, using fallback '%s'", cmd, fallback) return fallback } return path } // Tries to find corresponding interface and its IP address func getIpAddress(name string, ipType byte) string { if name == "" { ifaces, err := net.Interfaces() if err != nil { logPrint(err) return "" } for _, iface := range ifaces { if iface.Flags&net.FlagUp > 0 && iface.Flags&net.FlagLoopback == 0 { return getIpAddressFromIface(&iface, ipType) } } } else { iface, err := net.InterfaceByName(name) if err != nil { logPrint(err) return "" } return getIpAddressFromIface(iface, ipType) } return "" } // Gets corresponding IP address from interface func getIpAddressFromIface(iface *net.Interface, ipType byte) string { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { return "" } addrs, err := iface.Addrs() if err != nil { logPrint(err) return "" } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil { return "" } if ipType == '4' { if ip.To4() != nil { return ip.To4().String() } } else { if ip.To4() == nil { return ip.To16().String() } } } return "" } // Gets value from /etc/os-release. If no name is defined, it assumes PRETTY_NAME or NAME, if PRETTY_NAME is not defined. func getOsReleaseValue(name string) string { var values = make(map[string]string) readProperties(pathOsReleaseFile, func(key, value string) { if len(value) > 1 { values[key] = value } }) if name == "" { if values[osReleasePrettyName] != "" { return values[osReleasePrettyName] } return values[osReleaseName] } return values[name] } // Do operation as user and then reverts to previous user. func doAsUser(usr *sysuser, fce func()) { currentUser, _ := user.Current() previousUser := getSysuser(currentUser) setFsUser(usr) fce() setFsUser(previousUser) } // Gets current TTY name func getCurrentTTYName(fallback string, fullname bool) string { if name, err := filepath.EvalSymlinks(os.Stdout.Name()); err == nil { if fullname { return name } return name[strings.LastIndex(name, devPath)+len(devPath):] } // if tty name fails, try to run ps command if result := runSimpleCmd("ps", "-p", strconv.Itoa(os.Getpid()), "-o", "tty", "--no-headers"); result != "" { if fullname { return filepath.Join(devPath, result) } return result } if fullname { return filepath.Join(devPath, fallback) } return fallback } // Gets DNS domain name of current machine func getDnsDomainName() string { if host, err := os.Hostname(); err == nil { var domain string if canonname, err := net.LookupCNAME(host); err == nil { domain = canonname[strings.Index(canonname, ".")+1:] } if domain == "" { if ip, err := net.LookupHost(host); err == nil && len(ip) > 0 { if domains, err := net.LookupAddr(ip[0]); err == nil { for _, d := range domains { if d[len(d)-1:] == "." { domain = d[strings.Index(d, ".")+1:] break } } } } } if domain != "" && domain[len(domain)-1:] == "." { return domain[:len(domain)-1] } } return "unknown_domain" } // Opens console by its path func openConsole(path string) *os.File { for _, flag := range []int{os.O_RDWR, os.O_RDONLY, os.O_WRONLY} { if c, err := os.OpenFile(path, flag, 0700); err == nil { return c } } return nil } // Checks, if used fd is a console func isConsole(fd uintptr) bool { flag := 0 if _, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(_KDGKBTYPE), uintptr(unsafe.Pointer(&flag))); errNo == 0 { return flag == _KB_101 || flag == _KB_84 } return false } // Gets console to change the TTY func getConsole() *os.File { for _, name := range []string{currentTty, currentVc, devConsole} { if c := openConsole(name); c != nil { if isConsole(c.Fd()) { return c } c.Close() } } return nil } // Performs chvt command using ioctl func chvt(tty int) bool { if c := getConsole(); c != nil { defer c.Close() if _, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, uintptr(c.Fd()), uintptr(_VT_ACTIVATE), uintptr(tty)); errNo > 0 { return false } if _, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, uintptr(c.Fd()), uintptr(_VT_WAITACTIVE), uintptr(tty)); errNo > 0 { return false } } return true } func waitForReturnToExit(code int) { fmt.Printf("\nPress Enter to continue...") if !TEST_MODE { bufio.NewReader(os.Stdin).ReadString('\n') os.Exit(code) } } emptty-0.13.0/src/utils_linux.go000066400000000000000000000032071467633363300166320ustar00rootroot00000000000000package src import ( "os" "syscall" "unsafe" ) const ( _KDSETLED = 0x4B32 _KDGKBLED = 0x4B64 _KDSKBLED = 0x4B65 _K_SCROLLLOCK = 0x01 _K_NUMLOCK = 0x02 _K_CAPSLOCK = 0x04 currentVc = "/dev/tty0" ) // Sets fsuid, fsgid and fsgroups according sysuser func setFsUser(usr *sysuser) { handleErr(syscall.Setfsuid(usr.uid)) handleErr(syscall.Setfsgid(usr.gid)) handleErr(syscall.Setfsgid(usr.gid)) } // Sets keyboard LEDs func setKeyboardLeds(tty *os.File, scrolllock, numlock, capslock bool) { var flags uint64 // Read current keyboards flags syscall.Syscall(syscall.SYS_IOCTL, uintptr(tty.Fd()), uintptr(_KDGKBLED), uintptr(unsafe.Pointer(&flags))) if scrolllock { flags |= _K_SCROLLLOCK } if numlock { flags |= _K_NUMLOCK } if capslock { flags |= _K_CAPSLOCK } if scrolllock || numlock || capslock { // Magic constant that allows user changes flags |= 0x30 // Flags are used also for leds to keep flag valid to led syscall.Syscall(syscall.SYS_IOCTL, uintptr(tty.Fd()), uintptr(_KDSKBLED), uintptr(flags)) syscall.Syscall(syscall.SYS_IOCTL, uintptr(tty.Fd()), uintptr(_KDSETLED), uintptr(flags)) } } // Enables or disables echo depending on status func setTerminalEcho(fd uintptr, status bool) error { var termios = &syscall.Termios{} if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 { return err } if status { termios.Lflag |= syscall.ECHO } else { termios.Lflag &^= syscall.ECHO } if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(termios))); err != 0 { return err } return nil } emptty-0.13.0/src/utils_test.go000066400000000000000000000137001467633363300164510ustar00rootroot00000000000000package src import ( "io" "log" "os" "testing" ) // Testing method to get current working directory func getCwd() string { cwd, err := os.Getwd() if err != nil { logFatal(err) } return cwd } // Testing method to get path of file/directory in testing directory func getTestingPath(path string) string { return getCwd() + "/../res/testing/" + path } // Testing method to steal os.Stdout and check printed output. func readOutput(method func()) string { original := os.Stdout r, w, _ := os.Pipe() os.Stdout = w log.SetOutput(w) method() w.Close() output, _ := io.ReadAll(r) os.Stdout = original log.SetOutput(original) if testing.Verbose() { os.Stdout.Write(output) } return string(output) } func TestConvertColorBlack(t *testing.T) { if convertColor("UNKNOWN", true) != "" { t.Error("TestConvertColor: unexpected color for unknown") } // BLACK - 30 if convertColor("BLACK", true) != "30" { t.Error("TestConvertColor: unexpected color for BLACK foreground") } if convertColor("BLACK", false) != "40" { t.Error("TestConvertColor: unexpected color for BLACK background") } if convertColor("LIGHT_BLACK", true) != "90" { t.Error("TestConvertColor: unexpected color for LIGHT BLACK foreground") } if convertColor("LIGHT_BLACK", false) != "100" { t.Error("TestConvertColor: unexpected color for LIGHT BLACK background") } } func TestConvertColorRed(t *testing.T) { // RED - 31 if convertColor("RED", true) != "31" { t.Error("TestConvertColor: unexpected color for RED foreground") } if convertColor("RED", false) != "41" { t.Error("TestConvertColor: unexpected color for RED background") } if convertColor("LIGHT_RED", true) != "91" { t.Error("TestConvertColor: unexpected color for LIGHT RED foreground") } if convertColor("LIGHT_RED", false) != "101" { t.Error("TestConvertColor: unexpected color for LIGHT RED background") } } func TestConvertColorGreen(t *testing.T) { // GREEN - 32 if convertColor("GREEN", true) != "32" { t.Error("TestConvertColor: unexpected color for GREEN foreground") } if convertColor("GREEN", false) != "42" { t.Error("TestConvertColor: unexpected color for GREEN background") } if convertColor("LIGHT_GREEN", true) != "92" { t.Error("TestConvertColor: unexpected color for LIGHT GREEN foreground") } if convertColor("LIGHT_GREEN", false) != "102" { t.Error("TestConvertColor: unexpected color for LIGHT GREEN background") } } func TestConvertColorYellow(t *testing.T) { // YELLOW - 33 if convertColor("YELLOW", true) != "33" { t.Error("TestConvertColor: unexpected color for YELLOW foreground") } if convertColor("YELLOW", false) != "43" { t.Error("TestConvertColor: unexpected color for YELLOW background") } if convertColor("LIGHT_YELLOW", true) != "93" { t.Error("TestConvertColor: unexpected color for LIGHT YELLOW foreground") } if convertColor("LIGHT_YELLOW", false) != "103" { t.Error("TestConvertColor: unexpected color for LIGHT YELLOW background") } } func TestConvertColorBlue(t *testing.T) { // BLUE - 34 if convertColor("BLUE", true) != "34" { t.Error("TestConvertColor: unexpected color for BLUE foreground") } if convertColor("BLUE", false) != "44" { t.Error("TestConvertColor: unexpected color for BLUE background") } if convertColor("LIGHT_BLUE", true) != "94" { t.Error("TestConvertColor: unexpected color for LIGHT BLUE foreground") } if convertColor("LIGHT_BLUE", false) != "104" { t.Error("TestConvertColor: unexpected color for LIGHT BLUE background") } } func TestConvertColorMagenta(t *testing.T) { // MAGENTA - 35 if convertColor("MAGENTA", true) != "35" { t.Error("TestConvertColor: unexpected color for MAGENTA foreground") } if convertColor("MAGENTA", false) != "45" { t.Error("TestConvertColor: unexpected color for MAGENTA background") } if convertColor("LIGHT_MAGENTA", true) != "95" { t.Error("TestConvertColor: unexpected color for LIGHT MAGENTA foreground") } if convertColor("LIGHT_MAGENTA", false) != "105" { t.Error("TestConvertColor: unexpected color for LIGHT MAGENTA background") } } func TestConvertColorCyan(t *testing.T) { // CYAN - 36 if convertColor("CYAN", true) != "36" { t.Error("TestConvertColor: unexpected color for CYAN foreground") } if convertColor("CYAN", false) != "46" { t.Error("TestConvertColor: unexpected color for CYAN background") } if convertColor("LIGHT_CYAN", true) != "96" { t.Error("TestConvertColor: unexpected color for LIGHT CYAN foreground") } if convertColor("LIGHT_CYAN", false) != "106" { t.Error("TestConvertColor: unexpected color for LIGHT CYAN background") } } func TestConvertColorWhite(t *testing.T) { // WHITE - 37 if convertColor("WHITE", true) != "37" { t.Error("TestConvertColor: unexpected color for WHITE foreground") } if convertColor("WHITE", false) != "47" { t.Error("TestConvertColor: unexpected color for WHITE background") } if convertColor("LIGHT_WHITE", true) != "97" { t.Error("TestConvertColor: unexpected color for LIGHT WHITE foreground") } if convertColor("LIGHT_WHITE", false) != "107" { t.Error("TestConvertColor: unexpected color for LIGHT WHITE background") } } func TestCmdAsUser(t *testing.T) { u := &sysuser{uid: 3000, gid: 2000} cmd := cmdAsUser(u, "/dev/null", "another", "and_another") if cmd.SysProcAttr.Credential.Uid != 3000 { t.Error("TestCmdAsUser: unexpected UID") } if cmd.SysProcAttr.Credential.Gid != 2000 { t.Error("TestCmdAsUser: unexpected UID") } } func TestSetKeyboardLeds(t *testing.T) { f, err := os.CreateTemp(os.TempDir(), "emptty-led-test") if err != nil { t.Error("TestSetKeyboardLeds: could not open test file") } setKeyboardLeds(f, true, true, true) setKeyboardLeds(f, false, false, false) f.Close() err = os.Remove(f.Name()) if err != nil { t.Error("TestSetKeyboardLeds: could not remove test file") } } func TestParseExec(t *testing.T) { str := `echo "this is just an \"argument" ok` res := parseExec(str) if len(res) != 3 { t.Error("TestParseExec: unexpected lenght of parsed parts of executable") } } emptty-0.13.0/src/utmp.go000066400000000000000000000035441467633363300152440ustar00rootroot00000000000000//go:build !noutmp package src /* #include #include #include #include void putTimeToUtmpEntry(struct utmpx * utmp) { struct timeval tv; gettimeofday (&tv, NULL); utmp->ut_tv.tv_sec = tv.tv_sec; utmp->ut_tv.tv_usec = tv.tv_usec; } void prepareUtmpEntry(struct utmpx * utmp, int pid, char* id, char* line, char* username, char* host) { utmp->ut_pid = pid; strncpy (utmp->ut_id, id, sizeof (utmp->ut_id)); strncpy (utmp->ut_line, line, sizeof (utmp->ut_line)); strncpy (utmp->ut_user, username, sizeof (utmp->ut_user)); strncpy (utmp->ut_host, host, sizeof (utmp->ut_host)); putTimeToUtmpEntry(utmp); } */ import "C" import ( "unsafe" ) const tagUtmp = "" // Prepares UTMPx entry func prepareUtmpEntry(username string, pid int, ttyNo string, xdisplay string) *C.struct_utmpx { utmp := &C.struct_utmpx{} id := xdisplay if id == "" { id = ttyNo } utPid := C.int(pid) utId := C.CString(id) utLine := C.CString("tty" + ttyNo) utUser := C.CString(username) utHost := C.CString(xdisplay) utmp.ut_type = C.USER_PROCESS C.prepareUtmpEntry(utmp, utPid, utId, utLine, utUser, utHost) C.free(unsafe.Pointer(utId)) C.free(unsafe.Pointer(utLine)) C.free(unsafe.Pointer(utUser)) C.free(unsafe.Pointer(utHost)) return utmp } // Adds UTMPx entry as user process func addUtmpEntry(username string, pid int, ttyNo string, xdisplay string) *C.struct_utmpx { utmp := prepareUtmpEntry(username, pid, ttyNo, xdisplay) putUtmpEntry(utmp) return utmp } // End UTMPx entry by marking as dead process func endUtmpEntry(utmp *C.struct_utmpx) { utmp.ut_type = C.DEAD_PROCESS C.putTimeToUtmpEntry(utmp) putUtmpEntry(utmp) } // Puts UTMPx entry into utmp file func putUtmpEntry(utmp *C.struct_utmpx) { C.setutxent() if C.pututxline(utmp) == nil { logPrint("Could not write into utmp.") } C.endutxent() updwtmpx(utmp) } emptty-0.13.0/src/utmp_linux.go000066400000000000000000000020701467633363300164540ustar00rootroot00000000000000//go:build !noutmp package src // #include // #include // #include // #include import "C" import "unsafe" // Converts UTMPx entry into UTMP structure. func convertUtmpxToUtmp(utmpx *C.struct_utmpx) *C.struct_utmp { utmp := &C.struct_utmp{} utmp.ut_type = utmpx.ut_type utmp.ut_pid = utmpx.ut_pid utmp.ut_line = utmpx.ut_line utmp.ut_id = utmpx.ut_id utmp.ut_tv.tv_sec = utmpx.ut_tv.tv_sec utmp.ut_tv.tv_usec = utmpx.ut_tv.tv_usec utmp.ut_user = utmpx.ut_user utmp.ut_host = utmpx.ut_host utmp.ut_addr_v6 = utmpx.ut_addr_v6 return utmp } // Puts UTMP entry into wtmp file. func updwtmpx(utmpx *C.struct_utmpx) { wtmpPath := C.CString(C._PATH_WTMP) C.updwtmp(wtmpPath, convertUtmpxToUtmp(utmpx)) C.free(unsafe.Pointer(wtmpPath)) } // Adds BTMP entry to log unsuccessful login attempt. func addBtmpEntry(username string, pid int, ttyNo string) { btmpPath := C.CString("/var/log/btmp") utmpx := prepareUtmpEntry(username, pid, ttyNo, "") C.updwtmp(btmpPath, convertUtmpxToUtmp(utmpx)) C.free(unsafe.Pointer(btmpPath)) } emptty-0.13.0/src/utmp_noutmp.go000066400000000000000000000006621467633363300166440ustar00rootroot00000000000000//go:build noutmp package src const tagUtmp = "noutmp" // Adds UTMP entry as user process func addUtmpEntry(username string, pid int, ttyNo string, xdisplay string) bool { return false } // End UTMP entry by marking as dead process func endUtmpEntry(value bool) { // Nothing to do here } // Adds BTMP entry to log unsuccessful login attempt. func addBtmpEntry(username string, pid int, ttyNo string) { // Nothing to do here } emptty-0.13.0/src/xlib.go000066400000000000000000000007761467633363300152210ustar00rootroot00000000000000//go:build !noxlib package src // #cgo LDFLAGS: -lX11 // #include // #include import "C" import ( "errors" "time" "unsafe" ) const tagXlib = "" // Opens XDisplay with xlib. func openXDisplay(dispName string) error { displayName := C.CString(dispName) defer C.free(unsafe.Pointer(displayName)) for i := 0; i < 50; i++ { d := C.XOpenDisplay(displayName) if d != nil { return nil } time.Sleep(50 * time.Millisecond) } return errors.New("could not open X Display") } emptty-0.13.0/src/xlib_noxlib.go000066400000000000000000000005141467633363300165620ustar00rootroot00000000000000//go:build noxlib package src import ( "time" ) const tagXlib = "noxlib" // Slows down start by waiting to create X lock file func openXDisplay(dispName string) error { for i := 0; i < 50; i++ { if fileExists("/tmp/.X11-unix/X" + dispName[1:]) { break } else { time.Sleep(10 * time.Millisecond) } } return nil }