New CLI and debian package structure
This commit is contained in:
commit
e700de775a
33 changed files with 759 additions and 417 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -101,4 +101,10 @@ ENV/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
# generated models
|
# generated models
|
||||||
/models
|
/src/models
|
||||||
|
|
||||||
|
# build files
|
||||||
|
debian/howdy.substvars
|
||||||
|
debian/files
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/howdy
|
||||||
|
|
|
||||||
25
.travis.yml
Normal file
25
.travis.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
sudo: required
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.4"
|
||||||
|
- "3.6"
|
||||||
|
|
||||||
|
install:
|
||||||
|
# Install build tools and ack-grep for checks
|
||||||
|
- sudo apt install devscripts dh-make ack-grep -y
|
||||||
|
|
||||||
|
script:
|
||||||
|
# Build the binary (.deb)
|
||||||
|
- debuild -i -us -uc -b
|
||||||
|
# Install the binary, also fireing the debian scripts
|
||||||
|
- sudo apt install ../*.deb -y
|
||||||
|
# Check if the username passthough works correctly with sudo
|
||||||
|
- 'howdy | ack-grep --passthru --color "current active user: travis"'
|
||||||
|
- 'sudo howdy | ack-grep --passthru --color "current active user: travis"'
|
||||||
|
# Remove howdy from the installation
|
||||||
|
- sudo apt purge howdy -y
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: never
|
||||||
|
on_failure: always
|
||||||
41
README.md
41
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
# Howdy for Ubuntu
|
# Howdy for Ubuntu [](https://travis-ci.org/Boltgolt/howdy) [](https://github.com/Boltgolt/howdy/releases) [](https://github.com/Boltgolt/howdy/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
||||||
|
|
||||||
Windows Hello™ style authentication for Ubuntu. Use your built-in IR emitters and camera in combination with face recognition to prove who you are.
|
Windows Hello™ style authentication for Ubuntu. Use your built-in IR emitters and camera in combination with face recognition to prove who you are.
|
||||||
|
|
||||||
|
|
@ -6,21 +6,40 @@ Using the central authentication system in Linux (PAM), this works everywhere yo
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Run the installer by pasting (`ctrl+shift+V`) the following command into the terminal:
|
Run the installer by pasting (`ctrl+shift+V`) the following commands into the terminal one at a time:
|
||||||
|
|
||||||
```
|
```
|
||||||
wget -O /tmp/howdy_install.py https://raw.githubusercontent.com/Boltgolt/howdy/master/installer.py && sudo python3 /tmp/howdy_install.py
|
sudo add-apt-repository ppa:boltgolt/howdy
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install howdy
|
||||||
```
|
```
|
||||||
|
|
||||||
This will guide you through the installation. When that's done run `sudo howdy USER add` and replace `USER` with your username to add a face model.
|
|
||||||
|
|
||||||
If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action.
|
|
||||||
|
|
||||||
**Note:** The build of dlib can hang on 100% for over a minute, give it time.
|
**Note:** The build of dlib can hang on 100% for over a minute, give it time.
|
||||||
|
|
||||||
|
This will guide you through the installation. When that's done run `sudo howdy add` to add a face model.
|
||||||
|
|
||||||
|
If nothing went wrong we should be able to run sudo by just showing your face. Open a new terminal and run `sudo -i` to see it in action.
|
||||||
|
|
||||||
|
If you're curious you can run `sudo howdy config` to open the central config file and see the options Howdy has.
|
||||||
|
|
||||||
### Command line
|
### Command line
|
||||||
|
|
||||||
The installer adds a `howdy` command to manage face models for the current user. Use `howdy help` to list the available options.
|
The installer adds a `howdy` command to manage face models for the current user. Use `howdy --help` or `man howdy` to list the available options.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```
|
||||||
|
howdy [-U user] [-y] command [argument]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|-----------|-----------------------------------------------|
|
||||||
|
| `add` | Add a new face model for an user |
|
||||||
|
| `clear` | Remove all face models for an user |
|
||||||
|
| `config` | Open the config file in gedit |
|
||||||
|
| `disable` | Disable or enable howdy |
|
||||||
|
| `list` | List all saved face models for an user |
|
||||||
|
| `remove` | Remove a specific model for an user |
|
||||||
|
| `test` | Test the camera and recognition methods |
|
||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
|
|
@ -28,14 +47,10 @@ Any python errors get logged directly into the console and should indicate what
|
||||||
|
|
||||||
If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.
|
If you encounter an error that hasn't been reported yet, don't be afraid to open a new issue.
|
||||||
|
|
||||||
### Uninstalling
|
|
||||||
|
|
||||||
There is an uninstaller available, run `sudo python3 /lib/security/howdy/uninstall.py` to remove Howdy from your system.
|
|
||||||
|
|
||||||
### A note on security
|
### A note on security
|
||||||
|
|
||||||
This script is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you or well-printed photo of you could be enough to do it.
|
This script is in no way as secure as a password and will never be. Although it's harder to fool than normal face recognition, a person who looks similar to you or well-printed photo of you could be enough to do it.
|
||||||
|
|
||||||
To minimize the chance of this script being compromised, it's recommend to leave this repo in /lib/security and to keep it read only.
|
To minimize the chance of this program being compromised, it's recommend to leave Howdy in /lib/security and to keep it read only.
|
||||||
|
|
||||||
DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.
|
DO NOT USE HOWDY AS THE SOLE AUTHENTICATION METHOD FOR YOUR SYSTEM.
|
||||||
|
|
|
||||||
1
autocomplete.sh → autocomplete/howdy
Normal file → Executable file
1
autocomplete.sh → autocomplete/howdy
Normal file → Executable file
|
|
@ -1,3 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
# Autocomplete file run in bash
|
# Autocomplete file run in bash
|
||||||
# Will sugest arguments on tab
|
# Will sugest arguments on tab
|
||||||
|
|
||||||
53
cli.py
53
cli.py
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# CLI directly called by running the howdy command
|
|
||||||
|
|
||||||
# Import required modules
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Check if if a command has been given and print help otherwise
|
|
||||||
if (len(sys.argv) < 2):
|
|
||||||
print("Howdy IR face recognition help")
|
|
||||||
import cli.help
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# The command given
|
|
||||||
cmd = sys.argv[1]
|
|
||||||
|
|
||||||
# Call the right files for commands that don't need root
|
|
||||||
if cmd == "help":
|
|
||||||
print("Howdy IR face recognition")
|
|
||||||
import cli.help
|
|
||||||
elif cmd == "test":
|
|
||||||
import cli.test
|
|
||||||
else:
|
|
||||||
# Check if the minimum of 3 arugemnts has been met and print help otherwise
|
|
||||||
if (len(sys.argv) < 3):
|
|
||||||
print("Howdy IR face recognition help")
|
|
||||||
import cli.help
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Requre sudo for comamnds that need root rights to read the model files
|
|
||||||
if os.getenv("SUDO_USER") is None:
|
|
||||||
print("Please run this command with sudo")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Frome here on we require the second argument to be the username,
|
|
||||||
# switching the command to the 3rd
|
|
||||||
cmd = sys.argv[2]
|
|
||||||
|
|
||||||
if cmd == "list":
|
|
||||||
import cli.list
|
|
||||||
elif cmd == "add":
|
|
||||||
import cli.add
|
|
||||||
elif cmd == "remove":
|
|
||||||
import cli.remove
|
|
||||||
elif cmd == "clear":
|
|
||||||
import cli.clear
|
|
||||||
else:
|
|
||||||
# If the comand is invalid, check if the user hasn't swapped the username and command
|
|
||||||
if sys.argv[1] in ["list", "add", "remove", "clear"]:
|
|
||||||
print("Usage: howdy <user> <command>")
|
|
||||||
else:
|
|
||||||
print('Unknown command "' + cmd + '"')
|
|
||||||
import cli.help
|
|
||||||
17
cli/help.py
17
cli/help.py
|
|
@ -1,17 +0,0 @@
|
||||||
# Prints a simple help page for the CLI
|
|
||||||
|
|
||||||
print("""
|
|
||||||
Usage:
|
|
||||||
howdy <user> <command> [argument]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
help Show this help page
|
|
||||||
list List all saved face models for the current user
|
|
||||||
add Add a new face model for the current user
|
|
||||||
remove [id] Remove a specific model
|
|
||||||
clear Remove all face models for the current user
|
|
||||||
test Test the camera and recognition methods
|
|
||||||
|
|
||||||
For support please visit
|
|
||||||
https://github.com/Boltgolt/howdy\
|
|
||||||
""")
|
|
||||||
25
debian/changelog
vendored
Normal file
25
debian/changelog
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
howdy (2.1.0) xenial; urgency=medium
|
||||||
|
|
||||||
|
* First complete PPA release
|
||||||
|
* Reworked CLI
|
||||||
|
|
||||||
|
-- boltgolt <boltgolt@gmail.com> Fri, 13 Apr 2018 22:22:27 +0200
|
||||||
|
|
||||||
|
howdy (2.0.0-alpha+3) xenial; urgency=medium
|
||||||
|
|
||||||
|
* Fixed issue where dlib dependency failed to install on some installations
|
||||||
|
* Added preinst script for camera detection
|
||||||
|
|
||||||
|
-- boltgolt <boltgolt@gmail.com> Thu, 12 Apr 2018 21:42:42 +0000
|
||||||
|
|
||||||
|
howdy (2.0.0-alpha+2) xenial; urgency=medium
|
||||||
|
|
||||||
|
* Fixed build dependency issue
|
||||||
|
|
||||||
|
-- boltgolt <boltgolt@gmail.com> Sat, 07 Apr 2018 21:30:48 +0200
|
||||||
|
|
||||||
|
howdy (2.0.0-alpha+1) xenial; urgency=low
|
||||||
|
|
||||||
|
* Initial packaged release.
|
||||||
|
|
||||||
|
-- boltgolt <boltgolt@gmail.com> Wed, 04 Apr 2018 18:13:15 +0200
|
||||||
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
10
|
||||||
15
debian/control
vendored
Normal file
15
debian/control
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Source: howdy
|
||||||
|
Section: misc
|
||||||
|
Priority: optional
|
||||||
|
Standards-Version: 3.9.7
|
||||||
|
Build-Depends: python, dh-python, devscripts, dh-make, debhelper
|
||||||
|
Maintainer: boltgolt <boltgolt@gmail.com>
|
||||||
|
Vcs-Git: https://github.com/Boltgolt/howdy
|
||||||
|
|
||||||
|
Package: howdy
|
||||||
|
Homepage: https://github.com/Boltgolt/howdy
|
||||||
|
Architecture: all
|
||||||
|
Depends: ${misc:Depends}, git, python3, python3-pip, python3-dev, python3-setuptools, build-essential, libpam-python, fswebcam, libopencv-dev, python-opencv, cmake
|
||||||
|
Description: Windows Hello style authentication for Ubuntu.
|
||||||
|
Use your built-in IR emitters and camera in combination with face recognition
|
||||||
|
to prove who you are.
|
||||||
21
debian/copyright
vendored
Normal file
21
debian/copyright
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 boltgolt
|
||||||
|
|
||||||
|
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.
|
||||||
47
debian/howdy.1
vendored
Normal file
47
debian/howdy.1
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
.\" Please adjust this date whenever revising the manpage.
|
||||||
|
.TH HOWDY 1 "April 9, 2018" "Howdy help" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
howdy \- Windows Hello style authentication for Ubuntu
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Howdy IR face recognition implements a PAM module to use your face as a authentication method.
|
||||||
|
.SS "Usage:"
|
||||||
|
.IP
|
||||||
|
howdy [\-U USER] [\-y] [\-h] command [argument]
|
||||||
|
.SS "Commands:"
|
||||||
|
.TP
|
||||||
|
add
|
||||||
|
Add a new face model for an user.
|
||||||
|
.TP
|
||||||
|
clear
|
||||||
|
Remove all face models for an user.
|
||||||
|
.TP
|
||||||
|
config
|
||||||
|
Open the config file in gedit.
|
||||||
|
.TP
|
||||||
|
disable
|
||||||
|
Disable or enable howdy.
|
||||||
|
.TP
|
||||||
|
list
|
||||||
|
List all saved face models for an user.
|
||||||
|
.TP
|
||||||
|
remove
|
||||||
|
Remove a specific model for an user.
|
||||||
|
.TP
|
||||||
|
clear
|
||||||
|
Remove all face models for an user.
|
||||||
|
.TP
|
||||||
|
test
|
||||||
|
Test the camera and recognition methods.
|
||||||
|
.SS "Optional arguments:"
|
||||||
|
.TP
|
||||||
|
\fB\-U\fR USER, \fB\-\-user\fR USER
|
||||||
|
Set the user account to use.
|
||||||
|
.TP
|
||||||
|
\fB\-y\fR
|
||||||
|
Skip all questions.
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
Show this help message and exit.
|
||||||
|
.PP
|
||||||
|
.SH AUTHOR
|
||||||
|
Howdy was written by boltgolt. For more information visit https://github.com/Boltgolt/howdy
|
||||||
1
debian/howdy.manpages
vendored
Normal file
1
debian/howdy.manpages
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
debian/howdy.1
|
||||||
2
debian/install
vendored
Normal file
2
debian/install
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
src/. lib/security/howdy
|
||||||
|
autocomplete/. usr/share/bash-completion/completions
|
||||||
204
debian/postinst
vendored
Executable file
204
debian/postinst
vendored
Executable file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Installation script to install howdy
|
||||||
|
# Executed after primary apt install
|
||||||
|
|
||||||
|
def col(id):
|
||||||
|
"""Add color escape sequences"""
|
||||||
|
if id == 1: return "\033[32m"
|
||||||
|
if id == 2: return "\033[33m"
|
||||||
|
if id == 3: return "\033[31m"
|
||||||
|
return "\033[0m"
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import fileinput
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
# Don't run unless we need to configure the install
|
||||||
|
# Will also happen on upgrade but we will catch that later on
|
||||||
|
if "configure" not in sys.argv:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def log(text):
|
||||||
|
"""Print a nicely formatted line to stdout"""
|
||||||
|
print("\n>>> " + col(1) + text + col(0) + "\n")
|
||||||
|
|
||||||
|
def handleStatus(status):
|
||||||
|
"""Abort if a command fails"""
|
||||||
|
if (status != 0):
|
||||||
|
print(col(3) + "Error while running last command" + col(0))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# We're not in fresh configuration mode (probably an upgrade), so exit
|
||||||
|
if not os.path.exists("/tmp/howdy_picked_device"):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Open the temporary file containing the device ID
|
||||||
|
in_file = open("/tmp/howdy_picked_device", "r")
|
||||||
|
# Load it in, it should be a string
|
||||||
|
picked = in_file.read()
|
||||||
|
in_file.close()
|
||||||
|
|
||||||
|
# Remove the temporary file
|
||||||
|
subprocess.call(["rm /tmp/howdy_picked_device"], shell=True)
|
||||||
|
|
||||||
|
log("Upgrading pip to the latest version")
|
||||||
|
|
||||||
|
# Update pip
|
||||||
|
handleStatus(subprocess.call(["pip3 install --upgrade pip"], shell=True))
|
||||||
|
|
||||||
|
log("Cloning dlib")
|
||||||
|
|
||||||
|
# Clone the dlib git to /tmp, but only the last commit
|
||||||
|
handleStatus(subprocess.call(["git", "clone", "--depth", "1", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"]))
|
||||||
|
|
||||||
|
log("Building dlib")
|
||||||
|
|
||||||
|
# Start the build without GPU
|
||||||
|
handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True))
|
||||||
|
|
||||||
|
log("Cleaning up dlib")
|
||||||
|
|
||||||
|
# Remove the no longer needed git clone
|
||||||
|
handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"]))
|
||||||
|
print("Temporary dlib files removed")
|
||||||
|
|
||||||
|
log("Installing python dependencies")
|
||||||
|
|
||||||
|
# Install face_recognition though pip
|
||||||
|
handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "face_recognition_models==0.3.0", "Click>=6.0", "numpy", "Pillow"]))
|
||||||
|
|
||||||
|
log("Installing face_recognition")
|
||||||
|
|
||||||
|
# Install face_recognition though pip
|
||||||
|
handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "--no-deps", "face_recognition==1.2.2"]))
|
||||||
|
|
||||||
|
log("Configuring howdy")
|
||||||
|
|
||||||
|
# Manually change the camera id to the one picked
|
||||||
|
for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1):
|
||||||
|
print(line.replace("device_id = 1", "device_id = " + picked), end="")
|
||||||
|
|
||||||
|
# Secure the howdy folder
|
||||||
|
handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True))
|
||||||
|
|
||||||
|
# Allow anyone to execute the python CLI
|
||||||
|
handleStatus(subprocess.call(["chmod 755 /lib/security/howdy"], shell=True))
|
||||||
|
handleStatus(subprocess.call(["chmod 744 /lib/security/howdy/cli.py"], shell=True))
|
||||||
|
handleStatus(subprocess.call(["chmod 744 -R /lib/security/howdy/cli"], shell=True))
|
||||||
|
print("Permissions set")
|
||||||
|
|
||||||
|
# Make the CLI executable as howdy
|
||||||
|
handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/local/bin/howdy"], shell=True))
|
||||||
|
handleStatus(subprocess.call(["chmod +x /usr/local/bin/howdy"], shell=True))
|
||||||
|
print("Howdy command installed")
|
||||||
|
|
||||||
|
log("Adding howdy as PAM module")
|
||||||
|
|
||||||
|
# Will be filled with the actual output lines
|
||||||
|
outlines = []
|
||||||
|
# Will be fillled with lines that contain coloring
|
||||||
|
printlines = []
|
||||||
|
# Track if the new lines have been insterted yet
|
||||||
|
inserted = False
|
||||||
|
|
||||||
|
# Open the PAM config file
|
||||||
|
with open("/etc/pam.d/common-auth") as fp:
|
||||||
|
# Read the first line
|
||||||
|
line = fp.readline()
|
||||||
|
|
||||||
|
while line:
|
||||||
|
# Add the line to the output directly, we're not deleting anything
|
||||||
|
outlines.append(line)
|
||||||
|
|
||||||
|
# Print the comments in gray and don't insert into comments
|
||||||
|
if line[:1] == "#":
|
||||||
|
printlines.append("\033[37m" + line + "\033[0m")
|
||||||
|
else:
|
||||||
|
printlines.append(line)
|
||||||
|
|
||||||
|
# If it's not a comment and we haven't inserted yet
|
||||||
|
if not inserted:
|
||||||
|
# Set both the comment and the linking line
|
||||||
|
line_comment = "# Howdy IR face recognition\n"
|
||||||
|
line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n"
|
||||||
|
|
||||||
|
# Add them to the output without any markup
|
||||||
|
outlines.append(line_comment)
|
||||||
|
outlines.append(line_link)
|
||||||
|
|
||||||
|
# Make the print orange to make it clear what's being added
|
||||||
|
printlines.append("\033[33m" + line_comment + "\033[0m")
|
||||||
|
printlines.append("\033[33m" + line_link + "\033[0m")
|
||||||
|
|
||||||
|
# Mark as inserted
|
||||||
|
inserted = True
|
||||||
|
|
||||||
|
# Go to the next line
|
||||||
|
line = fp.readline()
|
||||||
|
|
||||||
|
# Print a file Header
|
||||||
|
print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m")
|
||||||
|
|
||||||
|
# Loop though all printing lines and use the enters from the file
|
||||||
|
for line in printlines:
|
||||||
|
print(line, end="")
|
||||||
|
|
||||||
|
# Print a footer
|
||||||
|
print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n")
|
||||||
|
|
||||||
|
# Do not prompt for a yes if we're in no promt mode
|
||||||
|
if "HOWDY_NO_PROMPT" not in os.environ:
|
||||||
|
# Ask the user if this change is okay
|
||||||
|
print("Lines will be insterted in /etc/pam.d/common-auth as shown above")
|
||||||
|
ans = input("Apply this change? [y/N]: ")
|
||||||
|
|
||||||
|
# Abort the whole thing if it's not
|
||||||
|
if (ans.lower() != "y"):
|
||||||
|
print("Inerpeting as a \"NO\", aborting")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Adding lines to PAM\n")
|
||||||
|
|
||||||
|
# Write to PAM
|
||||||
|
common_auth = open("/etc/pam.d/common-auth", "w")
|
||||||
|
common_auth.write("".join(outlines))
|
||||||
|
common_auth.close()
|
||||||
|
|
||||||
|
# From here onwards the installation is complete
|
||||||
|
# We want to gather more information about the types or IR camera's
|
||||||
|
# used though, and the following lines are data gathering
|
||||||
|
# No data is ever uploaded without permission
|
||||||
|
|
||||||
|
if "HOWDY_NO_PROMPT" not in os.environ:
|
||||||
|
# List all video devices
|
||||||
|
diag_out = "Video devices [IR=" + picked + "]\n"
|
||||||
|
diag_out += "```\n"
|
||||||
|
diag_out += subprocess.check_output(['ls /dev/ | grep video'], shell=True).decode("utf-8")
|
||||||
|
diag_out += "```\n"
|
||||||
|
|
||||||
|
# Get some info from the USB kernel listings
|
||||||
|
diag_out += "Lsusb output\n"
|
||||||
|
diag_out += "```\n"
|
||||||
|
diag_out += subprocess.check_output(['lsusb -vvvv | grep -i "Camera\|iFunction"'], shell=True).decode("utf-8")
|
||||||
|
diag_out += "```\n"
|
||||||
|
|
||||||
|
# Get camera information from video4linux
|
||||||
|
diag_out += "Udevadm\n"
|
||||||
|
diag_out += "```\n"
|
||||||
|
diag_out += subprocess.check_output(['udevadm info -r --query=all -n /dev/video' + picked + ' | grep -i "ID_BUS\|ID_MODEL_ID\|ID_VENDOR_ID\|ID_V4L_PRODUCT\|ID_MODEL"'], shell=True).decode("utf-8")
|
||||||
|
diag_out += "```"
|
||||||
|
|
||||||
|
# Print it all as a clickable link to a new github issue
|
||||||
|
print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n")
|
||||||
|
|
||||||
|
# Let the user know what to do with the link
|
||||||
|
print("Installation complete.")
|
||||||
|
print(col(2) + "If you want to help the development, please use the link above to post some camera-related information to github!" + col(0))
|
||||||
94
debian/preinst
vendored
Executable file
94
debian/preinst
vendored
Executable file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Used to check cameras before commiting to install
|
||||||
|
# Executed before primary apt install of files
|
||||||
|
|
||||||
|
def col(id):
|
||||||
|
"""Add color escape sequences"""
|
||||||
|
if id == 1: return "\033[32m"
|
||||||
|
if id == 2: return "\033[33m"
|
||||||
|
if id == 3: return "\033[31m"
|
||||||
|
return "\033[0m"
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
|
||||||
|
# Don't run if we're not trying to install fresh
|
||||||
|
if "install" not in sys.argv:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# The default picked video device id
|
||||||
|
picked = -1
|
||||||
|
|
||||||
|
print(col(1) + "Starting IR camera check...\n" + col(0))
|
||||||
|
|
||||||
|
# If prompting has been disabled, skip camera check
|
||||||
|
if "HOWDY_NO_PROMPT" in os.environ:
|
||||||
|
print(col(2) + "AUTOMATED INSTALL, YOU WILL NOT BE ASKED FOR INPUT AND CHECKS WILL BE SKIPPED" + col(0))
|
||||||
|
|
||||||
|
# Write the default device to disk and exit
|
||||||
|
with open("/tmp/howdy_picked_device", "w") as out_file:
|
||||||
|
out_file.write("0")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Get all devices
|
||||||
|
devices = os.listdir("/dev")
|
||||||
|
|
||||||
|
# Loop though all devices
|
||||||
|
for dev in devices:
|
||||||
|
# Only use the video devices
|
||||||
|
if (dev[:5] == "video"):
|
||||||
|
time.sleep(.5)
|
||||||
|
|
||||||
|
# The full path to the device is the default name
|
||||||
|
device_name = "/dev/" + dev
|
||||||
|
# Get the udevadm details to try to get a better name
|
||||||
|
udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8")
|
||||||
|
|
||||||
|
# Loop though udevadm to search for a better name
|
||||||
|
for line in udevadm.split("\n"):
|
||||||
|
# Match it and encase it in quotes
|
||||||
|
re_name = re.search('product.*=(.*)$', line, re.IGNORECASE)
|
||||||
|
if re_name:
|
||||||
|
device_name = '"' + re_name.group(1) + '"'
|
||||||
|
|
||||||
|
# Show what device we're using
|
||||||
|
print("Trying " + device_name)
|
||||||
|
|
||||||
|
# Let fswebcam keep the camera open in the background
|
||||||
|
sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ask the user if this is the right one
|
||||||
|
print(col(2) + "One of your cameras should now be on." + col(0))
|
||||||
|
ans = input("Did your IR emitters turn on? [y/N]: ")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Kill fswebcam if the user aborts
|
||||||
|
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# The user has answered, kill fswebcam
|
||||||
|
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
|
||||||
|
|
||||||
|
# Set this camera as picked if the answer was yes, go to the next one if no
|
||||||
|
if ans.lower() == "y" or ans.lower() == "yes":
|
||||||
|
picked = dev[5:]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0))
|
||||||
|
|
||||||
|
# Abort if no camera was picked
|
||||||
|
if picked == -1:
|
||||||
|
print(col(3) + "No suitable IR camera found, aborting install." + col(0))
|
||||||
|
sys.exit(23)
|
||||||
|
|
||||||
|
# Write the result to disk so postinst can have a look at it
|
||||||
|
with open("/tmp/howdy_picked_device", "w") as out_file:
|
||||||
|
out_file.write(str(picked))
|
||||||
|
|
||||||
|
# Add a line break
|
||||||
|
print("")
|
||||||
47
debian/prerm
vendored
Executable file
47
debian/prerm
vendored
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Executed on deinstallation
|
||||||
|
# Completely remove howdy from the system
|
||||||
|
|
||||||
|
def col(id):
|
||||||
|
"""Add color escape sequences"""
|
||||||
|
if id == 1: return "\033[32m"
|
||||||
|
if id == 2: return "\033[33m"
|
||||||
|
if id == 3: return "\033[31m"
|
||||||
|
return "\033[0m"
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Only run when we actually want to remove
|
||||||
|
if "remove" not in sys.argv and "purge" not in sys.argv:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Don't try running this if it's already gome
|
||||||
|
if not os.path.exists("/lib/security/howdy/cli"):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Remove files and symlinks
|
||||||
|
try:
|
||||||
|
subprocess.call(["rm /usr/local/bin/howdy"], shell=True)
|
||||||
|
except e:
|
||||||
|
print("Can't remove executable")
|
||||||
|
try:
|
||||||
|
subprocess.call(["rm /usr/share/bash-completion/completions/howdy"], shell=True)
|
||||||
|
except e:
|
||||||
|
print("Can't remove autocompletion script")
|
||||||
|
try:
|
||||||
|
subprocess.call(["rm -rf /lib/security/howdy"], shell=True)
|
||||||
|
except e:
|
||||||
|
# This error is normal
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Remove face_recognition and dlib
|
||||||
|
subprocess.call(["pip3 uninstall face_recognition face_recognition_models dlib -y --no-cache-dir"], shell=True)
|
||||||
|
|
||||||
|
# Print a tearbending message
|
||||||
|
print(col(2) + """
|
||||||
|
There are still lines in /etc/pam.d/common-auth that can't be removed automatically
|
||||||
|
Run "nano /etc/pam.d/common-auth" to remove them by hand\
|
||||||
|
""" + col(0))
|
||||||
11
debian/rules
vendored
Executable file
11
debian/rules
vendored
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
DH_VERBOSE = 1
|
||||||
|
|
||||||
|
DPKG_EXPORT_BUILDFLAGS = 1
|
||||||
|
include /usr/share/dpkg/default.mk
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
binary:
|
||||||
|
dh binary
|
||||||
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.0 (native)
|
||||||
6
debian/source/options
vendored
Normal file
6
debian/source/options
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
tar-ignore = ".git"
|
||||||
|
tar-ignore = "models"
|
||||||
|
tar-ignore = ".gitignore"
|
||||||
|
tar-ignore = "README.md"
|
||||||
|
tar-ignore = "LICENSE"
|
||||||
|
tar-ignore = ".travis.yml"
|
||||||
252
installer.py
252
installer.py
|
|
@ -1,252 +0,0 @@
|
||||||
# Installation script to install howdy
|
|
||||||
# Runs completely independent of the others
|
|
||||||
|
|
||||||
# Import required modules
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import signal
|
|
||||||
import fileinput
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
def log(text):
|
|
||||||
"""Print a nicely formatted line to stdout"""
|
|
||||||
print("\n>>> \033[32m" + text + "\033[0m\n")
|
|
||||||
|
|
||||||
def handleStatus(status):
|
|
||||||
"""Abort if a command fails"""
|
|
||||||
if (status != 0):
|
|
||||||
print("\033[31mError while running last command\033[0m")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Check if we're running as root
|
|
||||||
user = os.getenv("SUDO_USER")
|
|
||||||
if user is None:
|
|
||||||
print("Please run this script as a sudo user")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Print some nice intro text
|
|
||||||
print("\n\033[33m HOWDY INSTALLER FOR UBUNTU\033[0m")
|
|
||||||
print(" Version 1, 2016/02/05\n")
|
|
||||||
|
|
||||||
# Let it sink in
|
|
||||||
time.sleep(.5)
|
|
||||||
log("Installing required apt packages")
|
|
||||||
|
|
||||||
# Install packages though apt
|
|
||||||
handleStatus(subprocess.call(["apt", "install", "-y", "git",
|
|
||||||
"python3-pip",
|
|
||||||
"python3-dev",
|
|
||||||
"python3-setuptools",
|
|
||||||
"build-essential",
|
|
||||||
"libpam-python",
|
|
||||||
"fswebcam",
|
|
||||||
"libopencv-dev",
|
|
||||||
"python-opencv",
|
|
||||||
"cmake"]))
|
|
||||||
|
|
||||||
# Update pip
|
|
||||||
handleStatus(subprocess.call(["pip3 install --upgrade pip"], shell=True))
|
|
||||||
|
|
||||||
log("Starting camera check")
|
|
||||||
|
|
||||||
# Get all devices
|
|
||||||
devices = os.listdir("/dev")
|
|
||||||
# The picked video device id
|
|
||||||
picked = False
|
|
||||||
|
|
||||||
# Loop though all devices
|
|
||||||
for dev in devices:
|
|
||||||
# Only use the video devices
|
|
||||||
if (dev[:5] == "video"):
|
|
||||||
time.sleep(.5)
|
|
||||||
|
|
||||||
# The full path to the device is the default name
|
|
||||||
device_name = "/dev/" + dev
|
|
||||||
# Get the udevadm details to try to get a better name
|
|
||||||
udevadm = subprocess.check_output(["udevadm info -r --query=all -n " + device_name], shell=True).decode("utf-8")
|
|
||||||
|
|
||||||
# Loop though udevadm to search for a better name
|
|
||||||
for line in udevadm.split("\n"):
|
|
||||||
# Match it and encase it in quotes
|
|
||||||
re_name = re.search('product.*=(.*)$', line, re.IGNORECASE)
|
|
||||||
if re_name:
|
|
||||||
device_name = '"' + re_name.group(1) + '"'
|
|
||||||
|
|
||||||
# Show what device we're using
|
|
||||||
print("Trying " + device_name)
|
|
||||||
|
|
||||||
# Let fswebcam keep the camera open in the background
|
|
||||||
sub = subprocess.Popen(["fswebcam -S 9999999999 -d /dev/" + dev + " /dev/null 2>/dev/null"], shell=True, preexec_fn=os.setsid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Ask the user if this is the right one
|
|
||||||
print("\033[33mOne of your cameras should now be on.\033[0m")
|
|
||||||
ans = input("Did your IR emitters turn on? [y/N]: ")
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# Kill fswebcam if the user aborts
|
|
||||||
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
|
|
||||||
raise
|
|
||||||
|
|
||||||
# The user has answered, kill fswebcam
|
|
||||||
os.killpg(os.getpgid(sub.pid), signal.SIGTERM)
|
|
||||||
|
|
||||||
# Set this camera as picked if the answer was yes, go to the next one if no
|
|
||||||
if (ans.lower() == "y"):
|
|
||||||
picked = dev[5:]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("Inerpeting as a \"NO\"\n")
|
|
||||||
|
|
||||||
# Abort if no camera was picked
|
|
||||||
if (picked == False):
|
|
||||||
print("\033[31mNo suitable IR camera found\033[0m")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
log("Cloning dlib")
|
|
||||||
|
|
||||||
# Clone the git to /tmp
|
|
||||||
handleStatus(subprocess.call(["git", "clone", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"]))
|
|
||||||
|
|
||||||
log("Building dlib")
|
|
||||||
|
|
||||||
# Start the build without GPU
|
|
||||||
handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True))
|
|
||||||
|
|
||||||
log("Cleaning up dlib")
|
|
||||||
|
|
||||||
# Remove the no longer needed git clone
|
|
||||||
handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"]))
|
|
||||||
|
|
||||||
log("Installing face_recognition")
|
|
||||||
|
|
||||||
# Install face_recognition though pip
|
|
||||||
handleStatus(subprocess.call(["pip3", "install", "face_recognition"]))
|
|
||||||
|
|
||||||
log("Cloning howdy")
|
|
||||||
|
|
||||||
# Make sure /lib/security exists
|
|
||||||
if not os.path.exists("/lib/security"):
|
|
||||||
os.makedirs("/lib/security")
|
|
||||||
|
|
||||||
# Clone howdy into it
|
|
||||||
handleStatus(subprocess.call(["git", "clone", "https://github.com/Boltgolt/howdy.git", "/lib/security/howdy"]))
|
|
||||||
|
|
||||||
# Manually change the camera id to the one picked
|
|
||||||
for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1):
|
|
||||||
print(line.replace("device_id = 1", "device_id = " + picked), end="")
|
|
||||||
|
|
||||||
# Secure the howdy folder
|
|
||||||
handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True))
|
|
||||||
|
|
||||||
# Make the CLI executable as howdy
|
|
||||||
handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/bin/howdy"], shell=True))
|
|
||||||
handleStatus(subprocess.call(["chmod +x /usr/bin/howdy"], shell=True))
|
|
||||||
|
|
||||||
# Install the command autocomplete, don't error on failure
|
|
||||||
subprocess.call(["sudo cp /lib/security/howdy/autocomplete.sh /etc/bash_completion.d/howdy"], shell=True)
|
|
||||||
|
|
||||||
log("Adding howdy as PAM module")
|
|
||||||
|
|
||||||
# Will be filled with the actual output lines
|
|
||||||
outlines = []
|
|
||||||
# Will be fillled with lines that contain coloring
|
|
||||||
printlines = []
|
|
||||||
# Track if the new lines have been insterted yet
|
|
||||||
inserted = False
|
|
||||||
|
|
||||||
# Open the PAM config file
|
|
||||||
with open("/etc/pam.d/common-auth") as fp:
|
|
||||||
# Read the first line
|
|
||||||
line = fp.readline()
|
|
||||||
|
|
||||||
while line:
|
|
||||||
# Add the line to the output directly, we're not deleting anything
|
|
||||||
outlines.append(line)
|
|
||||||
|
|
||||||
# Print the comments in gray and don't insert into comments
|
|
||||||
if line[:1] == "#":
|
|
||||||
printlines.append("\033[37m" + line + "\033[0m")
|
|
||||||
else:
|
|
||||||
printlines.append(line)
|
|
||||||
|
|
||||||
# If it's not a comment and we haven't inserted yet
|
|
||||||
if not inserted:
|
|
||||||
# Set both the comment and the linking line
|
|
||||||
line_comment = "# Howdy IR face recognition\n"
|
|
||||||
line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n"
|
|
||||||
|
|
||||||
# Add them to the output without any markup
|
|
||||||
outlines.append(line_comment)
|
|
||||||
outlines.append(line_link)
|
|
||||||
|
|
||||||
# Make the print orange to make it clear what's being added
|
|
||||||
printlines.append("\033[33m" + line_comment + "\033[0m")
|
|
||||||
printlines.append("\033[33m" + line_link + "\033[0m")
|
|
||||||
|
|
||||||
# Mark as inserted
|
|
||||||
inserted = True
|
|
||||||
|
|
||||||
# Go to the next line
|
|
||||||
line = fp.readline()
|
|
||||||
|
|
||||||
# Print a file Header
|
|
||||||
print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m")
|
|
||||||
|
|
||||||
# Loop though all printing lines and use the enters from the file
|
|
||||||
for line in printlines:
|
|
||||||
print(line, end="")
|
|
||||||
|
|
||||||
# Print a footer
|
|
||||||
print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n")
|
|
||||||
|
|
||||||
# Ask the user if this change is okay
|
|
||||||
print("Lines will be insterted in /etc/pam.d/common-auth as shown above")
|
|
||||||
ans = input("Apply this change? [y/N]: ")
|
|
||||||
|
|
||||||
# Abort the whole thing if it's not
|
|
||||||
if (ans.lower() != "y"):
|
|
||||||
print("Inerpeting as a \"NO\", aborting")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
print("Adding lines to PAM\n")
|
|
||||||
|
|
||||||
# Write to PAM
|
|
||||||
common_auth = open("/etc/pam.d/common-auth", "w")
|
|
||||||
common_auth.write("".join(outlines))
|
|
||||||
common_auth.close()
|
|
||||||
|
|
||||||
# From here onwards the installation is complete
|
|
||||||
# We want to gather more information about the types or IR camera's
|
|
||||||
# used though, and the following lines are data collection
|
|
||||||
|
|
||||||
# List all video devices
|
|
||||||
diag_out = "Video devices [IR=" + picked + "]\n"
|
|
||||||
diag_out += "```\n"
|
|
||||||
diag_out += subprocess.check_output(['ls /dev/ | grep video'], shell=True).decode("utf-8")
|
|
||||||
diag_out += "```\n"
|
|
||||||
|
|
||||||
# Get some info from the USB kernel listings
|
|
||||||
diag_out += "Lsusb output\n"
|
|
||||||
diag_out += "```\n"
|
|
||||||
diag_out += subprocess.check_output(['lsusb -vvvv | grep -i "Camera\|iFunction"'], shell=True).decode("utf-8")
|
|
||||||
diag_out += "```\n"
|
|
||||||
|
|
||||||
# Get camera information from video4linux
|
|
||||||
diag_out += "Udevadm\n"
|
|
||||||
diag_out += "```\n"
|
|
||||||
diag_out += subprocess.check_output(['udevadm info -r --query=all -n /dev/video' + picked + ' | grep -i "ID_BUS\|ID_MODEL_ID\|ID_VENDOR_ID\|ID_V4L_PRODUCT\|ID_MODEL"'], shell=True).decode("utf-8")
|
|
||||||
diag_out += "```"
|
|
||||||
|
|
||||||
# Print it all as a clickable link to a new github issue
|
|
||||||
print("https://github.com/Boltgolt/howdy-reports/issues/new?title=Post-installation%20camera%20information&body=" + urllib.parse.quote_plus(diag_out) + "\n")
|
|
||||||
|
|
||||||
# Let the user know what to do with the link
|
|
||||||
print("Installation complete.")
|
|
||||||
print("If you want to help the development, please use the link above to post some camera-related information to github")
|
|
||||||
|
|
||||||
# Remove the installer if it was downloaded to /tmp
|
|
||||||
if os.path.exists("/tmp/howdy_install.py"):
|
|
||||||
os.remove("/tmp/howdy_install.py")
|
|
||||||
98
src/cli.py
Executable file
98
src/cli.py
Executable file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# CLI directly called by running the howdy command
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import getpass
|
||||||
|
import argparse
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
# Try to get the original username (not "root") from shell
|
||||||
|
user = subprocess.check_output("echo $(logname 2>/dev/null || echo $SUDO_USER)", shell=True).decode("ascii").strip()
|
||||||
|
|
||||||
|
# If that fails, try to get the direct user
|
||||||
|
if user == "root" or user == "":
|
||||||
|
env_user = getpass.getuser().strip()
|
||||||
|
|
||||||
|
# If even that fails, error out
|
||||||
|
if env_user == "":
|
||||||
|
print("Could not determine user, please use the --user flag")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
user = env_user
|
||||||
|
|
||||||
|
# Basic command setup
|
||||||
|
parser = argparse.ArgumentParser(description="Command line interface for Howdy face authentication.",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
add_help=False,
|
||||||
|
prog="howdy",
|
||||||
|
epilog="For support please visit\nhttps://github.com/Boltgolt/howdy")
|
||||||
|
|
||||||
|
# Add an argument for the command
|
||||||
|
parser.add_argument("command",
|
||||||
|
help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.",
|
||||||
|
metavar="command",
|
||||||
|
choices=["add", "clear", "config", "disable", "list", "remove", "test"])
|
||||||
|
|
||||||
|
# Add an argument for the extra arguments of diable and remove
|
||||||
|
parser.add_argument("argument",
|
||||||
|
help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.",
|
||||||
|
nargs="?")
|
||||||
|
|
||||||
|
# Add the user flag
|
||||||
|
parser.add_argument("-U", "--user",
|
||||||
|
default=user,
|
||||||
|
help="Set the user account to use.")
|
||||||
|
|
||||||
|
# Add the -y flag
|
||||||
|
parser.add_argument("-y",
|
||||||
|
help="Skip all questions.",
|
||||||
|
action="store_true")
|
||||||
|
|
||||||
|
# Overwrite the default help message so we can use a uppercase S
|
||||||
|
parser.add_argument("-h", "--help",
|
||||||
|
action="help",
|
||||||
|
default=argparse.SUPPRESS,
|
||||||
|
help="Show this help message and exit.")
|
||||||
|
|
||||||
|
# If we only have 1 argument we print the help text
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("current active user: " + user + "\n")
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Parse all arguments above
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Save the args and user as builtins which can be accessed by the imports
|
||||||
|
builtins.howdy_args = args
|
||||||
|
builtins.howdy_user = args.user
|
||||||
|
|
||||||
|
# Check if we have rootish rights
|
||||||
|
# This is this far down the file so running the command for help is always possible
|
||||||
|
if os.getenv("SUDO_USER") is None:
|
||||||
|
print("Please run this command with sudo")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Beond this point the user can't change anymore, if we still have root as user we need to abort
|
||||||
|
if args.user == "root":
|
||||||
|
print("Can't run howdy commands as root, please run this command with the --user flag")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Execute the right command
|
||||||
|
if args.command == "add":
|
||||||
|
import cli.add
|
||||||
|
elif args.command == "clear":
|
||||||
|
import cli.clear
|
||||||
|
elif args.command == "config":
|
||||||
|
import cli.config
|
||||||
|
elif args.command == "disable":
|
||||||
|
import cli.disable
|
||||||
|
elif args.command == "list":
|
||||||
|
import cli.list
|
||||||
|
elif args.command == "remove":
|
||||||
|
import cli.remove
|
||||||
|
elif args.command == "test":
|
||||||
|
import cli.test
|
||||||
|
|
@ -8,6 +8,7 @@ import sys
|
||||||
import json
|
import json
|
||||||
import cv2
|
import cv2
|
||||||
import configparser
|
import configparser
|
||||||
|
import builtins
|
||||||
|
|
||||||
# Try to import face_recognition and give a nice error if we can't
|
# Try to import face_recognition and give a nice error if we can't
|
||||||
# Add should be the first point where import issues show up
|
# Add should be the first point where import issues show up
|
||||||
|
|
@ -18,7 +19,7 @@ except ImportError as err:
|
||||||
|
|
||||||
print("\nCan't import the face_recognition module, check the output of")
|
print("\nCan't import the face_recognition module, check the output of")
|
||||||
print("pip3 show face_recognition")
|
print("pip3 show face_recognition")
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# Get the absolute path to the current file
|
# Get the absolute path to the current file
|
||||||
path = os.path.dirname(os.path.abspath(__file__))
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
@ -27,8 +28,7 @@ path = os.path.dirname(os.path.abspath(__file__))
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(path + "/../config.ini")
|
config.read(path + "/../config.ini")
|
||||||
|
|
||||||
# The current user
|
user = builtins.howdy_user
|
||||||
user = sys.argv[1]
|
|
||||||
# The permanent file to store the encoded model in
|
# The permanent file to store the encoded model in
|
||||||
enc_file = path + "/../models/" + user + ".dat"
|
enc_file = path + "/../models/" + user + ".dat"
|
||||||
# Known encodings
|
# Known encodings
|
||||||
|
|
@ -46,11 +46,11 @@ except FileNotFoundError:
|
||||||
encodings = []
|
encodings = []
|
||||||
|
|
||||||
# Print a warning if too many encodings are being added
|
# Print a warning if too many encodings are being added
|
||||||
if len(encodings) > 2:
|
if len(encodings) > 3:
|
||||||
print("WARNING: Every additional model slows down the face recognition engine")
|
print("WARNING: Every additional model slows down the face recognition engine")
|
||||||
print("Press ctrl+C to cancel")
|
print("Press ctrl+C to cancel\n")
|
||||||
|
|
||||||
print("Adding face model for the user account " + user)
|
print("Adding face model for the user " + user)
|
||||||
|
|
||||||
# Set the default label
|
# Set the default label
|
||||||
label = "Initial model"
|
label = "Initial model"
|
||||||
|
|
@ -59,12 +59,16 @@ label = "Initial model"
|
||||||
if len(encodings) > 0:
|
if len(encodings) > 0:
|
||||||
label = "Model #" + str(len(encodings) + 1)
|
label = "Model #" + str(len(encodings) + 1)
|
||||||
|
|
||||||
# Ask the user for a custom label
|
# Keep de default name if we can't ask questions
|
||||||
label_in = input("Enter a label for this new model [" + label + "]: ")
|
if builtins.howdy_args.y:
|
||||||
|
print("Using default label \"" + label + "\" because of -y flag")
|
||||||
|
else:
|
||||||
|
# Ask the user for a custom label
|
||||||
|
label_in = input("Enter a label for this new model [" + label + "]: ")
|
||||||
|
|
||||||
# Set the custom label (if any) and limit it to 24 characters
|
# Set the custom label (if any) and limit it to 24 characters
|
||||||
if label_in != "":
|
if label_in != "":
|
||||||
label = label_in[:24]
|
label = label_in[:24]
|
||||||
|
|
||||||
# Prepare the metadata for insertion
|
# Prepare the metadata for insertion
|
||||||
insert_model = {
|
insert_model = {
|
||||||
|
|
@ -106,12 +110,12 @@ while frames < 60:
|
||||||
# If 0 faces are detected we can't continue
|
# If 0 faces are detected we can't continue
|
||||||
if len(enc) == 0:
|
if len(enc) == 0:
|
||||||
print("No face detected, aborting")
|
print("No face detected, aborting")
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# If more than 1 faces are detected we can't know wich one belongs to the user
|
# If more than 1 faces are detected we can't know wich one belongs to the user
|
||||||
if len(enc) > 1:
|
if len(enc) > 1:
|
||||||
print("Multiple faces detected, aborting")
|
print("Multiple faces detected, aborting")
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# Totally clean array that can be exported as JSON
|
# Totally clean array that can be exported as JSON
|
||||||
clean_enc = []
|
clean_enc = []
|
||||||
|
|
@ -130,5 +134,6 @@ with open(enc_file, "w") as datafile:
|
||||||
json.dump(encodings, datafile)
|
json.dump(encodings, datafile)
|
||||||
|
|
||||||
# Give let the user know how it went
|
# Give let the user know how it went
|
||||||
print("Scan complete")
|
print("""Scan complete
|
||||||
print("\nAdded a new model to " + user)
|
|
||||||
|
Added a new model to """ + user)
|
||||||
|
|
@ -3,30 +3,33 @@
|
||||||
# Import required modules
|
# Import required modules
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import builtins
|
||||||
|
|
||||||
# Get the full path to this file
|
# Get the full path to this file
|
||||||
path = os.path.dirname(os.path.abspath(__file__))
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
# Get the passed user
|
# Get the passed user
|
||||||
user = sys.argv[1]
|
user = builtins.howdy_user
|
||||||
|
|
||||||
# Check if the models folder is there
|
# Check if the models folder is there
|
||||||
if not os.path.exists(path + "/../models"):
|
if not os.path.exists(path + "/../models"):
|
||||||
print("No models created yet, can't clear them if they don't exist")
|
print("No models created yet, can't clear them if they don't exist")
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# Check if the user has a models file to delete
|
# Check if the user has a models file to delete
|
||||||
if not os.path.isfile(path + "/../models/" + user + ".dat"):
|
if not os.path.isfile(path + "/../models/" + user + ".dat"):
|
||||||
print(user + " has no models or they have been cleared already")
|
print(user + " has no models or they have been cleared already")
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# Double check with the user
|
# Only ask the user if there's no -y flag
|
||||||
print("This will clear all models for " + user)
|
if not builtins.howdy_args.y:
|
||||||
ans = input("Do you want to continue [y/N]: ")
|
# Double check with the user
|
||||||
|
print("This will clear all models for " + user)
|
||||||
|
ans = input("Do you want to continue [y/N]: ")
|
||||||
|
|
||||||
# Abort if they don't answer y or Y
|
# Abort if they don't answer y or Y
|
||||||
if (ans.lower() != "y"):
|
if (ans.lower() != "y"):
|
||||||
print('\nInerpeting as a "NO"')
|
print('\nInerpeting as a "NO"')
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
# Delete otherwise
|
# Delete otherwise
|
||||||
os.remove(path + "/../models/" + user + ".dat")
|
os.remove(path + "/../models/" + user + ".dat")
|
||||||
15
src/cli/config.py
Normal file
15
src/cli/config.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Open the config file in gedit
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Let the user know what we're doing
|
||||||
|
print("Opening condig.ini in gedit")
|
||||||
|
|
||||||
|
# Open gedit as a subprocess and fork it
|
||||||
|
subprocess.Popen(["gedit", os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"],
|
||||||
|
cwd="/",
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
41
src/cli/disable.py
Normal file
41
src/cli/disable.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Set the disable flag
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import builtins
|
||||||
|
import fileinput
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
# Get the absolute filepath
|
||||||
|
config_path = os.path.dirname(os.path.abspath(__file__)) + "/../config.ini"
|
||||||
|
|
||||||
|
# Read config from disk
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(config_path)
|
||||||
|
|
||||||
|
# Check if enough arguments have been passed
|
||||||
|
if builtins.howdy_args.argument == None:
|
||||||
|
print("Please add a 0 (enable) or a 1 (disable) as an argument")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Translate the argument to the right string
|
||||||
|
if builtins.howdy_args.argument == "1":
|
||||||
|
out_value = "true"
|
||||||
|
elif builtins.howdy_args.argument == "0":
|
||||||
|
out_value = "false"
|
||||||
|
else:
|
||||||
|
# Of it's not a 0 or a 1, it's invalid
|
||||||
|
print("Please only use a 0 (enable) or a 1 (disable) as an argument")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Loop though the config file and only replace the line containing the disable config
|
||||||
|
for line in fileinput.input([config_path], inplace=1):
|
||||||
|
print(line.replace("disabled = " + config.get("core", "disabled"), "disabled = " + out_value), end="")
|
||||||
|
|
||||||
|
# Print what we just did
|
||||||
|
if builtins.howdy_args.argument == "1":
|
||||||
|
print("Howdy has been disabled")
|
||||||
|
else:
|
||||||
|
print("Howdy has been enabled")
|
||||||
|
|
@ -5,10 +5,11 @@ import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import builtins
|
||||||
|
|
||||||
# Get the absolute path and the username
|
# Get the absolute path and the username
|
||||||
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
||||||
user = sys.argv[1]
|
user = builtins.howdy_user
|
||||||
|
|
||||||
# Check if the models file has been created yet
|
# Check if the models file has been created yet
|
||||||
if not os.path.exists(path + "/models"):
|
if not os.path.exists(path + "/models"):
|
||||||
|
|
@ -4,22 +4,23 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import builtins
|
||||||
|
|
||||||
# Get the absolute path and the username
|
# Get the absolute path and the username
|
||||||
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
path = os.path.dirname(os.path.realpath(__file__)) + "/.."
|
||||||
user = sys.argv[1]
|
user = builtins.howdy_user
|
||||||
|
|
||||||
# Check if enough arguments have been passed
|
# Check if enough arguments have been passed
|
||||||
if len(sys.argv) == 3:
|
if builtins.howdy_args.argument == None:
|
||||||
print("Please add the ID of the model to remove as an argument")
|
print("Please add the ID of the model you want to remove as an argument")
|
||||||
print("You can find the IDs by running:")
|
print("You can find the IDs by running:")
|
||||||
print("\n\thowdy " + user + " list\n")
|
print("\n\thowdy list\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Check if the models file has been created yet
|
# Check if the models file has been created yet
|
||||||
if not os.path.exists(path + "/models"):
|
if not os.path.exists(path + "/models"):
|
||||||
print("Face models have not been initialized yet, please run:")
|
print("Face models have not been initialized yet, please run:")
|
||||||
print("\n\thowdy " + user + " add\n")
|
print("\n\thowdy add\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Path to the models file
|
# Path to the models file
|
||||||
|
|
@ -30,7 +31,7 @@ try:
|
||||||
encodings = json.load(open(enc_file))
|
encodings = json.load(open(enc_file))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("No face model known for the user " + user + ", please run:")
|
print("No face model known for the user " + user + ", please run:")
|
||||||
print("\n\thowdy " + user + " add\n")
|
print("\n\thowdy add\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Tracks if a encoding with that id has been found
|
# Tracks if a encoding with that id has been found
|
||||||
|
|
@ -38,24 +39,28 @@ found = False
|
||||||
|
|
||||||
# Loop though all encodings and check if they match the argument
|
# Loop though all encodings and check if they match the argument
|
||||||
for enc in encodings:
|
for enc in encodings:
|
||||||
if str(enc["id"]) == sys.argv[3]:
|
if str(enc["id"]) == builtins.howdy_args.argument:
|
||||||
# Double check with the user
|
# Only ask the user if there's no -y flag
|
||||||
print('This will remove the model called "' + enc["label"] + '" for ' + user)
|
if not builtins.howdy_args.y:
|
||||||
ans = input("Do you want to continue [y/N]: ")
|
# Double check with the user
|
||||||
|
print('This will remove the model called "' + enc["label"] + '" for ' + user)
|
||||||
|
ans = input("Do you want to continue [y/N]: ")
|
||||||
|
|
||||||
# Abort if the answer isn't yes
|
# Abort if the answer isn't yes
|
||||||
if (ans.lower() != "y"):
|
if (ans.lower() != "y"):
|
||||||
print('\nInerpeting as a "NO"')
|
print('\nInerpeting as a "NO"')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
# Add a padding empty line
|
||||||
|
print()
|
||||||
|
|
||||||
# Mark as found and print an enter
|
# Mark as found and print an enter
|
||||||
found = True
|
found = True
|
||||||
print()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Abort if no matching id was found
|
# Abort if no matching id was found
|
||||||
if not found:
|
if not found:
|
||||||
print("No model with ID " + sys.argv[3] + " exists for " + user)
|
print("No model with ID " + builtins.howdy_args.argument + " exists for " + user)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
# Remove the entire file if this encoding is the only one
|
# Remove the entire file if this encoding is the only one
|
||||||
|
|
@ -68,11 +73,11 @@ else:
|
||||||
|
|
||||||
# Loop though all encodin and only add thos that don't need to be removed
|
# Loop though all encodin and only add thos that don't need to be removed
|
||||||
for enc in encodings:
|
for enc in encodings:
|
||||||
if str(enc["id"]) != sys.argv[3]:
|
if str(enc["id"]) != builtins.howdy_args.argument:
|
||||||
new_encodings.append(enc)
|
new_encodings.append(enc)
|
||||||
|
|
||||||
# Save this new set to disk
|
# Save this new set to disk
|
||||||
with open(enc_file, "w") as datafile:
|
with open(enc_file, "w") as datafile:
|
||||||
json.dump(new_encodings, datafile)
|
json.dump(new_encodings, datafile)
|
||||||
|
|
||||||
print("Removed model " + sys.argv[3])
|
print("Removed model " + builtins.howdy_args.argument)
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# Howdy config file
|
||||||
|
|
||||||
[core]
|
[core]
|
||||||
# Do not print anything when a face verification succeeds
|
# Do not print anything when a face verification succeeds
|
||||||
no_confirmation = false
|
no_confirmation = false
|
||||||
|
|
@ -11,6 +13,10 @@ suppress_unknown = false
|
||||||
# Expirimental, can behave incorrectly on some systems
|
# Expirimental, can behave incorrectly on some systems
|
||||||
dismiss_lockscreen = false
|
dismiss_lockscreen = false
|
||||||
|
|
||||||
|
# Disable howdy in the PAM
|
||||||
|
# The howdy command will still function
|
||||||
|
disabled = false
|
||||||
|
|
||||||
[video]
|
[video]
|
||||||
# The certainty of the detected face belonging to the user of the account
|
# The certainty of the detected face belonging to the user of the account
|
||||||
# On a scale from 1 to 10, values above 5 are not recommended
|
# On a scale from 1 to 10, values above 5 are not recommended
|
||||||
|
|
@ -15,6 +15,10 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini")
|
||||||
def doAuth(pamh):
|
def doAuth(pamh):
|
||||||
"""Start authentication in a seperate process"""
|
"""Start authentication in a seperate process"""
|
||||||
|
|
||||||
|
# Abort is Howdy is disabled
|
||||||
|
if config.get("core", "disabled") == "true":
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# Run compare as python3 subprocess to circumvent python version and import issues
|
# Run compare as python3 subprocess to circumvent python version and import issues
|
||||||
status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()])
|
status = subprocess.call(["python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()])
|
||||||
|
|
||||||
36
uninstall.py
36
uninstall.py
|
|
@ -1,36 +0,0 @@
|
||||||
# Completely remove howdy from the system
|
|
||||||
|
|
||||||
# Import required modules
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Check if we're running as root
|
|
||||||
user = os.getenv("SUDO_USER")
|
|
||||||
if user is None:
|
|
||||||
print("Please run the uninstaller as a sudo user")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Double check with the user for the last time
|
|
||||||
print("This will remove Howdy and all models generated with it")
|
|
||||||
ans = input("Do you want to continue? [y/N]: ")
|
|
||||||
|
|
||||||
# Abort if they don't say yes
|
|
||||||
if (ans.lower() != "y"):
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
# Remove files and symlinks
|
|
||||||
subprocess.call(["rm -rf /lib/security/howdy/"], shell=True)
|
|
||||||
subprocess.call(["rm /usr/bin/howdy"], shell=True)
|
|
||||||
subprocess.call(["rm /etc/bash_completion.d/howdy"], shell=True)
|
|
||||||
|
|
||||||
# Remove face_recognition and dlib
|
|
||||||
subprocess.call(["pip3 uninstall face_recognition dlib -y"], shell=True)
|
|
||||||
|
|
||||||
# Print a tearbending message
|
|
||||||
print("""
|
|
||||||
Howdy has been uninstalled :'(
|
|
||||||
|
|
||||||
There are still lines in /etc/pam.d/common-auth that can't be removed automatically
|
|
||||||
Run "nano /etc/pam.d/common-auth" to remove them by hand\
|
|
||||||
""")
|
|
||||||
Loading…
Reference in a new issue