diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..887f4c3 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,7 @@ +Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' +WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: '50' + +# vim:syntax=yaml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1843f5d..994880d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -8,16 +8,19 @@ jobs: - name: Install required libraries run: > sudo apt-get update && sudo apt-get install -y - python3 python3-pip python3-setuptools python3-wheel ninja-build meson + python3 python3-pip python3-setuptools python3-wheel cmake make build-essential clang-tidy libpam0g-dev libinih-dev libevdev-dev python3-dev libopencv-dev + + - name: Install meson + run: sudo python3 -m pip install meson ninja - uses: actions/checkout@v2 - name: Build run: | - meson setup build howdy/src/pam + meson setup build ninja -C build - name: Check source code diff --git a/howdy-gtk/bin/howdy-gtk.in b/howdy-gtk/bin/howdy-gtk.in new file mode 100644 index 0000000..1ea89f7 --- /dev/null +++ b/howdy-gtk/bin/howdy-gtk.in @@ -0,0 +1,3 @@ +#!/bin/sh + +env python3 "@script_path@" "$@" \ No newline at end of file diff --git a/howdy-gtk/meson.build b/howdy-gtk/meson.build new file mode 100644 index 0000000..26b18b5 --- /dev/null +++ b/howdy-gtk/meson.build @@ -0,0 +1,82 @@ +if meson.is_subproject() +project('howdy-gtk', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') +endif + +datadir = get_option('prefix') / get_option('datadir') / 'howdy-gtk' +py_conf = configuration_data(paths_dict) +py_conf.set('data_dir', datadir) + + +py_paths = configure_file( + input: 'src/paths.py.in', + output: 'paths.py', + configuration: py_conf, +) + +sources = files( + 'src/authsticky.py', + 'src/i18n.py', + 'src/init.py', + 'src/onboarding.py', + 'src/paths_factory.py', + 'src/tab_models.py', + 'src/tab_video.py', + 'src/window.py', +) + +py = import('python').find_installation( + # modules: ['gi', 'elevate'] +) +py.dependency() + +if get_option('install_in_site_packages') + pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy-gtk') +else + pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') : join_paths(get_option('prefix'), get_option('libdir'), 'howdy-gtk') +endif + +if get_option('install_in_site_packages') + py.install_sources( + sources, + py_paths, + subdir: 'howdy-gtk', + install_mode: 'r--r--r--', + install_tag: 'py_sources', + ) +else + install_data( + sources, + py_paths, + install_dir: pysourcesinstalldir, + install_mode: 'r--r--r--', + install_tag: 'py_sources', + ) +endif + +logos = files( + 'src/logo.png', + 'src/logo_about.png', +) +install_data(logos, install_dir: datadir) + +interface_files = files( + 'src/main.glade', + 'src/onboarding.glade', +) +install_data(interface_files, install_dir: datadir) + +cli_path = join_paths(pysourcesinstalldir, 'init.py') +conf_data = configuration_data({ 'script_path': cli_path }) + +bin_name = 'howdy-gtk' +bin = configure_file( + input: 'bin/howdy-gtk.in', + output: bin_name, + configuration: conf_data +) +install_data( + bin, + install_mode: 'rwxr-xr-x', + install_dir: get_option('prefix') / get_option('bindir'), + install_tag: 'bin', +) \ No newline at end of file diff --git a/howdy-gtk/src/authsticky.py b/howdy-gtk/src/authsticky.py index 9d80867..4a8c8a3 100644 --- a/howdy-gtk/src/authsticky.py +++ b/howdy-gtk/src/authsticky.py @@ -3,6 +3,7 @@ import cairo import gi import signal import sys +import paths_factory import os from i18n import _ @@ -32,9 +33,7 @@ class StickyWindow(gtk.Window): gtk.Window.__init__(self) # Get the absolute or relative path to the logo file - logo_path = "/usr/lib/howdy-gtk/logo.png" - if not os.access(logo_path, os.R_OK): - logo_path = "./logo.png" + logo_path = paths_factory.logo_path() # Create image and calculate scale size based on image size self.logo_surface = cairo.ImageSurface.create_from_png(logo_path) diff --git a/howdy-gtk/src/onboarding.py b/howdy-gtk/src/onboarding.py index a941493..e6a4aa7 100644 --- a/howdy-gtk/src/onboarding.py +++ b/howdy-gtk/src/onboarding.py @@ -3,6 +3,7 @@ import os import re import time import subprocess +import paths_factory from i18n import _ @@ -22,7 +23,7 @@ class OnboardingWindow(gtk.Window): self.connect("delete_event", self.exit) self.builder = gtk.Builder() - self.builder.add_from_file("./onboarding.glade") + self.builder.add_from_file(paths_factory.onboarding_wireframe_path()) self.builder.connect_signals(self) self.window = self.builder.get_object("onboardingwindow") @@ -67,29 +68,17 @@ class OnboardingWindow(gtk.Window): self.execute_slide6() def execute_slide1(self): - conf_path = "/etc/howdy" - self.downloadoutputlabel = self.builder.get_object("downloadoutputlabel") eventbox = self.builder.get_object("downloadeventbox") eventbox.modify_bg(gtk.StateType.NORMAL, gdk.Color(red=0, green=0, blue=0)) - for lib_site in ("/lib", "/usr/lib", "/lib64", "/usr/lib64"): - if os.path.exists(lib_site + "/security/howdy/"): - break - else: - lib_site = None - - if lib_site is None: - self.downloadoutputlabel.set_text(_("Unable to find Howdy's installation location")) - return - - - if os.path.exists(conf_path + "/dlib-data/shape_predictor_5_face_landmarks.dat"): + # TODO: Better way to do this? + if os.path.exists(paths_factory.dlib_data_dir_path() / "shape_predictor_5_face_landmarks.dat"): self.downloadoutputlabel.set_text(_("Datafiles have already been downloaded!\nClick Next to continue")) self.enable_next() return - self.proc = subprocess.Popen("./install.sh", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=conf_path + "/howdy/dlib-data") + self.proc = subprocess.Popen("./install.sh", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=paths_factory.dlib_data_dir_path()) self.download_lines = [] self.read_download_line() diff --git a/howdy-gtk/src/paths.py.in b/howdy-gtk/src/paths.py.in new file mode 100644 index 0000000..91f2bf5 --- /dev/null +++ b/howdy-gtk/src/paths.py.in @@ -0,0 +1,13 @@ +from pathlib import PurePath + +# Define the absolute path to the config directory +config_dir = PurePath("@config_dir@") + +# Define the absolute path to the DLib models data directory +dlib_data_dir = PurePath("@dlib_data_dir@") + +# Define the absolute path to the Howdy user models directory +user_models_dir = PurePath("@user_models_dir@") + +# Define the absolute path to the Howdy data directory +data_dir = PurePath("@data_dir@") \ No newline at end of file diff --git a/howdy-gtk/src/paths_factory.py b/howdy-gtk/src/paths_factory.py new file mode 100644 index 0000000..b02a900 --- /dev/null +++ b/howdy-gtk/src/paths_factory.py @@ -0,0 +1,32 @@ +from pathlib import PurePath +import paths + + +def config_file_path() -> str: + """Return the path to the config file""" + return str(paths.config_dir / "config.ini") + + +def user_models_dir_path() -> PurePath: + """Return the path to the user models directory""" + return paths.user_models_dir + + +def logo_path() -> str: + """Return the path to the logo file""" + return str(paths.data_dir / "logo.png") + + +def onboarding_wireframe_path() -> str: + """Return the path to the onboarding wireframe file""" + return str(paths.data_dir / "onboarding.glade") + + +def main_window_wireframe_path() -> str: + """Return the path to the main window wireframe file""" + return str(paths.data_dir / "main.glade") + + +def dlib_data_dir_path() -> PurePath: + """Return the path to the dlib data directory""" + return paths.dlib_data_dir diff --git a/howdy-gtk/src/tab_video.py b/howdy-gtk/src/tab_video.py index 2a8617a..4f151b2 100644 --- a/howdy-gtk/src/tab_video.py +++ b/howdy-gtk/src/tab_video.py @@ -1,6 +1,7 @@ import configparser from i18n import _ +import paths_factory from gi.repository import Gtk as gtk from gi.repository import Gdk as gdk @@ -17,7 +18,7 @@ def on_page_switch(self, notebook, page, page_num): try: self.config = configparser.ConfigParser() - self.config.read("/etc/howdy/config.ini") + self.config.read(paths_factory.config_file_path()) except Exception: print(_("Can't open camera")) diff --git a/howdy-gtk/src/window.py b/howdy-gtk/src/window.py index 737b7a4..f43e35d 100644 --- a/howdy-gtk/src/window.py +++ b/howdy-gtk/src/window.py @@ -7,6 +7,7 @@ import elevate import subprocess from i18n import _ +import paths_factory # Make sure we have the libs we need gi.require_version("Gtk", "3.0") @@ -26,7 +27,7 @@ class MainWindow(gtk.Window): self.connect("delete_event", self.exit) self.builder = gtk.Builder() - self.builder.add_from_file("./main.glade") + self.builder.add_from_file(paths_factory.main_window_wireframe_path()) self.builder.connect_signals(self) self.window = self.builder.get_object("mainwindow") @@ -49,7 +50,7 @@ class MainWindow(gtk.Window): # Add the treeview self.modellistbox.add(self.treeview) - filelist = os.listdir("/etc/howdy/models") + filelist = os.listdir(paths_factory.user_models_dir_path()) self.active_user = "" self.userlist.items = 0 @@ -120,7 +121,7 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) elevate.elevate() # If no models have been created yet or when it is forced, start the onboarding -if "--force-onboarding" in sys.argv or not os.path.exists("/etc/howdy/models"): +if "--force-onboarding" in sys.argv or not os.path.exists(paths_factory.user_models_dir_path()): import onboarding onboarding.OnboardingWindow() diff --git a/howdy/debian/howdy.manpages b/howdy/debian/howdy.manpages index 2578acd..a27b8e7 100644 --- a/howdy/debian/howdy.manpages +++ b/howdy/debian/howdy.manpages @@ -1 +1 @@ -debian/howdy.1 +howdy.1 diff --git a/howdy/debian/howdy.1 b/howdy/howdy.1 similarity index 100% rename from howdy/debian/howdy.1 rename to howdy/howdy.1 diff --git a/howdy/meson.build b/howdy/meson.build new file mode 100644 index 0000000..7edf76f --- /dev/null +++ b/howdy/meson.build @@ -0,0 +1 @@ +subdir('src') \ No newline at end of file diff --git a/howdy/src/autocomplete/howdy b/howdy/src/autocomplete/howdy.in similarity index 86% rename from howdy/src/autocomplete/howdy rename to howdy/src/autocomplete/howdy.in index 0ee737c..38d4fed 100755 --- a/howdy/src/autocomplete/howdy +++ b/howdy/src/autocomplete/howdy.in @@ -4,6 +4,8 @@ _howdy() { local cur prev opts + local config_path="@config_path@" + source _variables COMPREPLY=() # The argument typed so far cur="${COMP_WORDS[COMP_CWORD]}" @@ -20,7 +22,7 @@ _howdy() { ;; # For disable, grab the current "disabled" config option and give the reverse "disable") - local status=$(cut -d'=' -f2 <<< $(cat /etc/howdy/config.ini | grep 'disabled =') | xargs echo -n) + local status=$(cut -d'=' -f2 <<< $(cat $config_path | grep 'disabled =') | xargs echo -n) [ "$status" == "false" ] && COMPREPLY="true" || COMPREPLY="false" return 0 diff --git a/howdy/src/bin/howdy.in b/howdy/src/bin/howdy.in new file mode 100644 index 0000000..1ea89f7 --- /dev/null +++ b/howdy/src/bin/howdy.in @@ -0,0 +1,3 @@ +#!/bin/sh + +env python3 "@script_path@" "$@" \ No newline at end of file diff --git a/howdy/src/cli/add.py b/howdy/src/cli/add.py index 8baa0f1..9a2c9a0 100644 --- a/howdy/src/cli/add.py +++ b/howdy/src/cli/add.py @@ -8,6 +8,7 @@ import json import configparser import builtins import numpy as np +import paths_factory from recorders.video_capture import VideoCapture from i18n import _ @@ -26,39 +27,36 @@ except ImportError as err: # OpenCV needs to be imported after dlib import cv2 -# Define the absolute path to the config directory -config_path = "/etc/howdy" - # Test if at lest 1 of the data files is there and abort if it's not -if not os.path.isfile(config_path + "/dlib-data/shape_predictor_5_face_landmarks.dat"): +if not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()): print(_("Data files have not been downloaded, please run the following commands:")) - print("\n\tcd " + config_path + "/dlib-data") + print("\n\tcd " + paths_factory.dlib_data_dir_path()) print("\tsudo ./install.sh\n") sys.exit(1) # Read config from disk config = configparser.ConfigParser() -config.read(config_path + "/config.ini") +config.read(paths_factory.config_file_path()) use_cnn = config.getboolean("core", "use_cnn", fallback=False) if use_cnn: - face_detector = dlib.cnn_face_detection_model_v1(config_path + "/dlib-data/mmod_human_face_detector.dat") + face_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path()) else: face_detector = dlib.get_frontal_face_detector() -pose_predictor = dlib.shape_predictor(config_path + "/dlib-data/shape_predictor_5_face_landmarks.dat") -face_encoder = dlib.face_recognition_model_v1(config_path + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat") +pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) +face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) user = builtins.howdy_user # The permanent file to store the encoded model in -enc_file = config_path + "/models/" + user + ".dat" +enc_file = paths_factory.user_model_path(user) # Known encodings encodings = [] # Make the ./models folder if it doesn't already exist -if not os.path.exists(config_path + "/models"): +if not os.path.exists(paths_factory.user_models_dir_path()): print(_("No face model folder found, creating one")) - os.makedirs(config_path + "/models") + os.makedirs(paths_factory.user_models_dir_path()) # To try read a premade encodings file if it exists try: diff --git a/howdy/src/cli/clear.py b/howdy/src/cli/clear.py index 6fa5f3e..64941ba 100644 --- a/howdy/src/cli/clear.py +++ b/howdy/src/cli/clear.py @@ -4,21 +4,20 @@ import os import sys import builtins +import paths_factory from i18n import _ -# Get the full path to this file -path = "/etc/howdy/models" # Get the passed user user = builtins.howdy_user # Check if the models folder is there -if not os.path.exists(path): +if not os.path.exists(paths_factory.user_models_dir_path()): print(_("No models created yet, can't clear them if they don't exist")) sys.exit(1) # Check if the user has a models file to delete -if not os.path.isfile(path + "/" + user + ".dat"): +if not os.path.isfile(paths_factory.user_model_path(user)): print(_("{} has no models or they have been cleared already").format(user)) sys.exit(1) @@ -34,5 +33,5 @@ if not builtins.howdy_args.y: sys.exit(1) # Delete otherwise -os.remove(path + "/" + user + ".dat") +os.remove(paths_factory.user_model_path(user)) print(_("\nModels cleared")) diff --git a/howdy/src/cli/config.py b/howdy/src/cli/config.py index 7106483..fae7b08 100644 --- a/howdy/src/cli/config.py +++ b/howdy/src/cli/config.py @@ -3,6 +3,7 @@ # Import required modules import os import subprocess +import paths_factory from i18n import _ @@ -19,4 +20,4 @@ elif os.path.isfile("/etc/alternatives/editor"): editor = "/etc/alternatives/editor" # Open the editor as a subprocess and fork it -subprocess.call([editor, "/etc/howdy/config.ini"]) +subprocess.call([editor, paths_factory.config_file_path()]) diff --git a/howdy/src/cli/disable.py b/howdy/src/cli/disable.py index e6b635d..dc87e23 100644 --- a/howdy/src/cli/disable.py +++ b/howdy/src/cli/disable.py @@ -6,11 +6,12 @@ import os import builtins import fileinput import configparser +import paths_factory from i18n import _ # Get the absolute filepath -config_path = os.path.dirname("/etc/howdy/") + "/config.ini" +config_path = paths_factory.config_file_path() # Read config from disk config = configparser.ConfigParser() diff --git a/howdy/src/cli/list.py b/howdy/src/cli/list.py index 3532e9f..01628de 100644 --- a/howdy/src/cli/list.py +++ b/howdy/src/cli/list.py @@ -6,21 +6,20 @@ import os import json import time import builtins +import paths_factory from i18n import _ -# Get the absolute path and the username -path = "/etc/howdy" user = builtins.howdy_user # Check if the models file has been created yet -if not os.path.exists(path + "/models"): +if not os.path.exists(paths_factory.user_models_dir_path()): print(_("Face models have not been initialized yet, please run:")) print("\n\tsudo howdy -U " + user + " add\n") sys.exit(1) # Path to the models file -enc_file = path + "/models/" + user + ".dat" +enc_file = paths_factory.user_model_path(user) # Try to load the models file and abort if the user does not have it yet try: diff --git a/howdy/src/cli/remove.py b/howdy/src/cli/remove.py index 6321e0b..cec96c7 100644 --- a/howdy/src/cli/remove.py +++ b/howdy/src/cli/remove.py @@ -5,11 +5,10 @@ import sys import os import json import builtins +import paths_factory from i18n import _ -# Get the absolute path and the username -path = "/etc/howdy" user = builtins.howdy_user # Check if enough arguments have been passed @@ -22,13 +21,13 @@ if not builtins.howdy_args.arguments: sys.exit(1) # Check if the models file has been created yet -if not os.path.exists(path + "/models"): +if not os.path.exists(paths_factory.user_models_dir_path()): print(_("Face models have not been initialized yet, please run:")) print("\n\thowdy add\n") sys.exit(1) # Path to the models file -enc_file = path + "/models/" + user + ".dat" +enc_file = paths_factory.user_model_path(user) # Try to load the models file and abort if the user does not have it yet try: @@ -72,7 +71,7 @@ if not found: # Remove the entire file if this encoding is the only one if len(encodings) == 1: - os.remove(path + "/models/" + user + ".dat") + os.remove(paths_factory.user_model_path(user)) print(_("Removed last model, howdy disabled for user")) else: # A place holder to contain the encodings that will remain diff --git a/howdy/src/cli/set.py b/howdy/src/cli/set.py index 0f36817..ba981c2 100644 --- a/howdy/src/cli/set.py +++ b/howdy/src/cli/set.py @@ -5,11 +5,12 @@ import sys import os import builtins import fileinput +import paths_factory from i18n import _ # Get the absolute filepath -config_path = os.path.dirname("/etc/howdy/") + "/config.ini" +config_path = paths_factory.config_file_path() # Check if enough arguments have been passed if len(builtins.howdy_args.arguments) < 2: diff --git a/howdy/src/cli/snap.py b/howdy/src/cli/snap.py index cbcae50..4d0d2ca 100644 --- a/howdy/src/cli/snap.py +++ b/howdy/src/cli/snap.py @@ -5,15 +5,14 @@ import os import configparser import datetime import snapshot +import paths_factory from recorders.video_capture import VideoCapture from i18n import _ -path = "/etc/howdy" - # Read the config config = configparser.ConfigParser() -config.read(path + "/config.ini") +config.read(paths_factory.config_file_path()) # Start video capture video_capture = VideoCapture(config) diff --git a/howdy/src/cli/test.py b/howdy/src/cli/test.py index 3a6e4d1..f18c4eb 100644 --- a/howdy/src/cli/test.py +++ b/howdy/src/cli/test.py @@ -10,16 +10,14 @@ import time import dlib import cv2 import numpy as np +import paths_factory from i18n import _ from recorders.video_capture import VideoCapture -# The absolute path to the config directory -path = "/etc/howdy" - # Read config from disk config = configparser.ConfigParser() -config.read(path + "/config.ini") +config.read(paths_factory.config_file_path()) if config.get("video", "recording_plugin", fallback="opencv") != "opencv": print(_("Howdy has been configured to use a recorder which doesn't support the test command yet, aborting")) @@ -59,20 +57,20 @@ use_cnn = config.getboolean('core', 'use_cnn', fallback=False) if use_cnn: face_detector = dlib.cnn_face_detection_model_v1( - path + "/dlib-data/mmod_human_face_detector.dat" + paths_factory.mmod_human_face_detector_path() ) else: face_detector = dlib.get_frontal_face_detector() -pose_predictor = dlib.shape_predictor(path + "/dlib-data/shape_predictor_5_face_landmarks.dat") -face_encoder = dlib.face_recognition_model_v1(path + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat") +pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) +face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) encodings = [] models = None try: user = builtins.howdy_user - models = json.load(open(path + "/models/" + user + ".dat")) + models = json.load(open(paths_factory.user_model_path(user))) for model in models: encodings += model["data"] diff --git a/howdy/src/compare.py b/howdy/src/compare.py index 99f5285..0b2213f 100644 --- a/howdy/src/compare.py +++ b/howdy/src/compare.py @@ -23,10 +23,7 @@ import subprocess import snapshot import numpy as np import _thread as thread - -# Allow imports from the local howdy folder -sys.path.append('/lib/security/howdy') - +import paths_factory from recorders.video_capture import VideoCapture from i18n import _ @@ -48,22 +45,22 @@ def init_detector(lock): global face_detector, pose_predictor, face_encoder # Test if at lest 1 of the data files is there and abort if it's not - if not os.path.isfile(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat"): + if not os.path.isfile(paths_factory.shape_predictor_5_face_landmarks_path()): print(_("Data files have not been downloaded, please run the following commands:")) - print("\n\tcd " + PATH + "/dlib-data") + print("\n\tcd " + paths_factory.dlib_data_dir_path()) print("\tsudo ./install.sh\n") lock.release() exit(1) # Use the CNN detector if enabled if use_cnn: - face_detector = dlib.cnn_face_detection_model_v1(PATH + "/dlib-data/mmod_human_face_detector.dat") + face_detector = dlib.cnn_face_detection_model_v1(paths_factory.mmod_human_face_detector_path()) else: face_detector = dlib.get_frontal_face_detector() # Start the others regardless - pose_predictor = dlib.shape_predictor(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat") - face_encoder = dlib.face_recognition_model_v1(PATH + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat") + pose_predictor = dlib.shape_predictor(paths_factory.shape_predictor_5_face_landmarks_path()) + face_encoder = dlib.face_recognition_model_v1(paths_factory.dlib_face_recognition_resnet_model_v1_path()) # Note the time it took to initialize detectors timings["ll"] = time.time() - timings["ll"] @@ -103,9 +100,6 @@ def send_to_ui(type, message): if len(sys.argv) < 2: exit(12) -# Get the absolute path to the config directory -PATH = "/etc/howdy" - # The username of the user being authenticated user = sys.argv[1] # The model file contents @@ -129,7 +123,7 @@ face_encoder = None # Try to load the face model from the models folder try: - models = json.load(open(PATH + "/models/" + user + ".dat")) + models = json.load(open(paths_factory.user_model_path(user))) for model in models: encodings += model["data"] @@ -142,7 +136,7 @@ if len(models) < 1: # Read config from disk config = configparser.ConfigParser() -config.read(PATH + "/config.ini") +config.read(paths_factory.config_file_path()) # Get all config values needed use_cnn = config.getboolean("core", "use_cnn", fallback=False) @@ -160,7 +154,7 @@ gtk_pipe = sys.stdout if gtk_stdout else subprocess.DEVNULL # Start the auth ui, register it to be always be closed on exit try: - gtk_proc = subprocess.Popen(["../howdy-gtk/src/init.py", "--start-auth-ui"], stdin=subprocess.PIPE, stdout=gtk_pipe, stderr=gtk_pipe) + gtk_proc = subprocess.Popen(["howdy-gtk", "--start-auth-ui"], stdin=subprocess.PIPE, stdout=gtk_pipe, stderr=gtk_pipe) atexit.register(exit) except FileNotFoundError: pass diff --git a/howdy/src/meson.build b/howdy/src/meson.build new file mode 100644 index 0000000..6533951 --- /dev/null +++ b/howdy/src/meson.build @@ -0,0 +1,170 @@ +if meson.is_subproject() +project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') +endif + +py = import('python').find_installation() +py.dependency() + +datadir = get_option('prefix') / get_option('datadir') / 'howdy' +py_conf = configuration_data(paths_dict) +py_conf.set('data_dir', datadir) + +py_paths = configure_file( + input: 'paths.py.in', + output: 'paths.py', + configuration: py_conf, +) + +py_sources = [ + 'cli/__init__.py', + 'cli/add.py', + 'cli/clear.py', + 'cli/config.py', + 'cli/disable.py', + 'cli/list.py', + 'cli/remove.py', + 'cli/set.py', + 'cli/snap.py', + 'cli/test.py', + 'cli.py', + 'compare.py', + 'i18n.py', + 'paths_factory.py', + 'recorders/__init__.py', + 'recorders/ffmpeg_reader.py', + 'recorders/pyv4l2_reader.py', + 'recorders/v4l2.py', + 'recorders/video_capture.py', + 'rubberstamps/__init__.py', + 'rubberstamps/hotkey.py', + 'rubberstamps/nod.py', + 'snapshot.py', + py_paths, +] + +# Include PAM module +if get_option('install_in_site_packages') + pysourcesinstalldir = join_paths(py.get_install_dir(), 'howdy') +else + pysourcesinstalldir = get_option('py_sources_dir') != '' ? get_option('py_sources_dir') : join_paths(get_option('prefix'), get_option('libdir'), 'howdy') +endif + +pam_module_conf_data = configuration_data(paths_dict) +pam_module_conf_data.set('compare_script_path', join_paths(pysourcesinstalldir, 'compare.py')) +pam_module_conf_data.set('config_file_path', config_path) +subdir('pam') +if get_option('install_pam_config') + # pamdir is inherited from the pam subproject + pam_config = configure_file( + input: 'pam-config/howdy.in', + output: 'pam-config', + configuration: {'pamdir': pamdir} + ) + install_data( + pam_config, + install_dir: get_option('prefix') / get_option('datadir') / 'pam-configs', + install_mode: 'rwxr-xr-x', + install_tag: 'pam', + rename: 'howdy', + ) +endif + +if get_option('install_in_site_packages') + py.install_sources( + py_sources, + subdir: 'howdy', + preserve_path: true, + install_mode: 'r--r--r--', + install_tag: 'py_sources', + ) +else + install_data( + py_sources, + preserve_path: true, + install_dir: pysourcesinstalldir, + install_mode: 'r--r--r--', + install_tag: 'py_sources', + ) +endif + +install_data('logo.png', install_tag: 'meta') +autocomplete = configure_file( + input: 'autocomplete/howdy.in', + output: 'autocomplete', + configuration: configuration_data({ 'config_path': config_path }) +) +install_data( + autocomplete, + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'bash-completion', 'completions'), + install_mode: 'rwxr--r--', + install_tag: 'bash_completion', + rename: 'howdy', +) + +fs = import('fs') +if not fs.exists(config_path) + install_data('config.ini', install_dir: confdir, install_mode: 'rwxr--r--', install_tag: 'config') +endif + +install_data('dlib-data/install.sh', 'dlib-data/Readme.md', install_dir: dlibdatadir, install_mode: 'rwxr--r--') + +install_man('../howdy.1') + +# if get_option('fetch_dlib_data') +# downloader = find_program('wget') +# bunzip2 = find_program('bunzip2') + +# links = [ +# 'https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2', +# 'https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2', +# 'https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2' +# ] + +# archived_model_files = [ +# 'dlib_face_recognition_resnet_model_v1.dat.bz2', +# 'shape_predictor_5_face_landmarks.dat.bz2', +# 'mmod_human_face_detector.dat.bz2' +# ] + +# download = run_command( +# 'download', +# links, +# output: archived_model_files, +# command: [downloader, '-O', '@OUTPUT@', '@INPUT@'] +# ) + +# model_files = [ +# 'dlib_face_recognition_resnet_model_v1.dat', +# 'shape_predictor_5_face_landmarks.dat', +# 'mmod_human_face_detector.dat' +# ] + +# models = custom_target( +# 'models', +# input: archived_model_files, +# output: model_files, +# command: [bunzip2, '-k', '@INPUT@'], +# ) + +# install_data( +# model_files, +# install_dir: join_paths(get_option('prefix'), get_option('libdir'), 'dlib_models'), +# ) + +# endif + +cli_path = join_paths(pysourcesinstalldir, 'cli.py') +conf_data = configuration_data({ 'script_path': cli_path }) + +bin_name = 'howdy' +bin = configure_file( + input: 'bin/howdy.in', + output: bin_name, + configuration: conf_data +) +install_data( + bin, + install_mode: 'rwxr-xr-x', + install_dir: get_option('bindir'), + install_tag: 'bin', +) \ No newline at end of file diff --git a/howdy/src/pam-config/howdy b/howdy/src/pam-config/howdy deleted file mode 100644 index ed19039..0000000 --- a/howdy/src/pam-config/howdy +++ /dev/null @@ -1,6 +0,0 @@ -Name: Howdy -Default: yes -Priority: 512 -Auth-Type: Primary -Auth: - [success=end default=ignore] /lib/security/howdy/pam_howdy.so diff --git a/howdy/src/pam-config/howdy.in b/howdy/src/pam-config/howdy.in new file mode 100644 index 0000000..b447b25 --- /dev/null +++ b/howdy/src/pam-config/howdy.in @@ -0,0 +1,6 @@ +Name: Howdy +Default: yes +Priority: 512 +Auth-Type: Primary +Auth: + [success=end default=ignore] @pamdir@/pam_howdy.so diff --git a/howdy/src/pam/.clang-tidy b/howdy/src/pam/.clang-tidy deleted file mode 100644 index 887f4c3..0000000 --- a/howdy/src/pam/.clang-tidy +++ /dev/null @@ -1,7 +0,0 @@ -Checks: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' -WarningsAsErrors: 'clang-diagnostic-*,clang-analyser-*,-clang-diagnostic-unused-command-line-argument,google-*,bugprone-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-*,-readability-magic-numbers,-google-readability-todo' -CheckOptions: - - key: readability-function-cognitive-complexity.Threshold - value: '50' - -# vim:syntax=yaml diff --git a/howdy/src/pam/.clang-tidy b/howdy/src/pam/.clang-tidy new file mode 120000 index 0000000..878a8ae --- /dev/null +++ b/howdy/src/pam/.clang-tidy @@ -0,0 +1 @@ +../.clang-tidy \ No newline at end of file diff --git a/howdy/src/pam/main.cc b/howdy/src/pam/main.cc index 69d9d53..d1b8e34 100644 --- a/howdy/src/pam/main.cc +++ b/howdy/src/pam/main.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -41,12 +42,12 @@ #include "enter_device.hh" #include "main.hh" #include "optional_task.hh" +#include "paths.hh" const auto DEFAULT_TIMEOUT = std::chrono::duration(100); const auto MAX_RETRIES = 5; const auto PYTHON_EXECUTABLE = "python3"; -const auto COMPARE_PROCESS_PATH = "/lib/security/howdy/compare.py"; #define S(msg) gettext(msg) @@ -80,7 +81,8 @@ auto howdy_error(int status, syslog(LOG_ERR, "Failure, image too dark"); break; case CompareError::INVALID_DEVICE: - syslog(LOG_ERR, "Failure, not possible to open camera at configured path"); + syslog(LOG_ERR, + "Failure, not possible to open camera at configured path"); break; default: conv_function(PAM_ERROR_MSG, @@ -133,10 +135,11 @@ auto howdy_status(char *username, int status, const INIReader &config, * Check if Howdy should be enabled according to the configuration and the * environment. * @param config INI configuration + * @param username Username * @return Returns PAM_AUTHINFO_UNAVAIL if it shouldn't be enabled, * PAM_SUCCESS otherwise */ -auto check_enabled(const INIReader &config) -> int { +auto check_enabled(const INIReader &config, const char* username) -> int { // Stop executing if Howdy has been disabled in the config if (config.GetBoolean("core", "disabled", false)) { syslog(LOG_INFO, "Skipped authentication, Howdy is disabled"); @@ -182,6 +185,13 @@ auto check_enabled(const INIReader &config) -> int { globfree(&glob_result); } + // pre-check if this user has face model file + auto model_path = std::string(USER_MODELS_DIR) + "/" + username + ".dat"; + struct stat stat_; + if (stat(model_path.c_str(), &stat_) != 0) { + return PAM_AUTHINFO_UNAVAIL; + } + return PAM_SUCCESS; } @@ -191,12 +201,12 @@ auto check_enabled(const INIReader &config) -> int { * @param flags Flags passed on to us by PAM, XORed * @param argc Amount of rules in the PAM config (disregared) * @param argv Options defined in the PAM config - * @param auth_tok True if we should ask for a password too + * @param ask_auth_tok True if we should ask for a password too * @return Returns a PAM return code */ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, - bool auth_tok) -> int { - INIReader config("/etc/howdy/config.ini"); + bool ask_auth_tok) -> int { + INIReader config(CONFIG_FILE_PATH); openlog("pam_howdy", 0, LOG_AUTHPRIV); // Error out if we could not read the config file @@ -209,8 +219,16 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Will contain the responses from PAM functions int pam_res = PAM_IGNORE; + // Get the username from PAM, needed to match correct face model + char *username = nullptr; + if ((pam_res = pam_get_user(pamh, const_cast(&username), + nullptr)) != PAM_SUCCESS) { + syslog(LOG_ERR, "Failed to get username"); + return pam_res; + } + // Check if we should continue - if ((pam_res = check_enabled(config)) != PAM_SUCCESS) { + if ((pam_res = check_enabled(config, username)) != PAM_SUCCESS) { return pam_res; } @@ -243,21 +261,6 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); textdomain(GETTEXT_PACKAGE); - // Get the username from PAM, needed to match correct face model - char *username = nullptr; - if ((pam_res = pam_get_user(pamh, const_cast(&username), - nullptr)) != PAM_SUCCESS) { - syslog(LOG_ERR, "Failed to get username"); - return pam_res; - } - - // pre-check if this user has face model file - auto model_path = std::string("/etc/howdy/models/") + username + ".dat"; - if (!std::ifstream(model_path)) { - return howdy_status(username, CompareError::NO_FACE_MODEL, config, - conv_function); - } - if (config.GetBoolean("core", "detection_notice", true)) { if ((conv_function(PAM_TEXT_INFO, S("Attempting facial authentication"))) != PAM_SUCCESS) { @@ -316,7 +319,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, return std::tuple(pam_res, auth_tok_ptr); }); - auto ask_pass = auth_tok && workaround != Workaround::Off; + auto ask_pass = ask_auth_tok && workaround != Workaround::Off; // We ask for the password if the function requires it and if a workaround is // set @@ -327,7 +330,8 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, // Wait for the end either of the child or the password input { std::unique_lock lock(mutx); - convar.wait(lock, [&] { return confirmation_type != ConfirmationType::Unset; }); + convar.wait(lock, + [&] { return confirmation_type != ConfirmationType::Unset; }); } // The password has been entered or an error has occurred diff --git a/howdy/src/pam/meson.build b/howdy/src/pam/meson.build index be93919..35cdb03 100644 --- a/howdy/src/pam/meson.build +++ b/howdy/src/pam/meson.build @@ -1,5 +1,3 @@ -project('pam_howdy', 'cpp', version: '0.1.0', default_options: ['cpp_std=c++14']) - inih_cpp = dependency('INIReader', fallback: ['inih', 'INIReader_dep']) libevdev = dependency('libevdev') libpam = meson.get_compiler('cpp').find_library('pam') @@ -8,6 +6,15 @@ threads = dependency('threads') # Translations subdir('po') +# Paths +paths_h = configure_file( + input: 'paths.hh.in', + output: 'paths.hh', + configuration: pam_module_conf_data +) + +pamdir = get_option('pam_dir') != '' ? get_option('pam_dir') : join_paths(get_option('prefix'), get_option('libdir'), 'security') + shared_library( 'pam_howdy', 'main.cc', @@ -18,7 +25,11 @@ shared_library( threads, libevdev, ], + link_depends: [ + paths_h, + ], install: true, - install_dir: '/lib/security', + install_dir: pamdir, + install_tag: 'pam_module', name_prefix: '' ) diff --git a/howdy/src/pam/paths.hh.in b/howdy/src/pam/paths.hh.in new file mode 100644 index 0000000..771db2a --- /dev/null +++ b/howdy/src/pam/paths.hh.in @@ -0,0 +1,3 @@ +const auto COMPARE_PROCESS_PATH = "@compare_script_path@"; +const auto CONFIG_FILE_PATH = "@config_file_path@"; +const auto USER_MODELS_DIR = "@user_models_dir@"; \ No newline at end of file diff --git a/howdy/src/pam/po/meson.build b/howdy/src/pam/po/meson.build index 762f7a4..58e47a4 100644 --- a/howdy/src/pam/po/meson.build +++ b/howdy/src/pam/po/meson.build @@ -6,5 +6,5 @@ localedir = '-DLOCALEDIR="@0@"'.format(get_option('prefix') / get_option('locale add_project_arguments(gettext_package, localedir, language: 'cpp') i18n.gettext(meson.project_name(), - args: [ '--directory=' + meson.source_root(), '--keyword=S:1' ] + args: [ '--directory=' + meson.current_source_dir(), '--keyword=S:1' ] ) \ No newline at end of file diff --git a/howdy/src/paths.py.in b/howdy/src/paths.py.in new file mode 100644 index 0000000..68aca6c --- /dev/null +++ b/howdy/src/paths.py.in @@ -0,0 +1,16 @@ +from pathlib import PurePath + +# Define the absolute path to the config directory +config_dir = PurePath("@config_dir@") + +# Define the absolute path to the DLib models data directory +dlib_data_dir = PurePath("@dlib_data_dir@") + +# Define the absolute path to the Howdy user models directory +user_models_dir = PurePath("@user_models_dir@") + +# Define path to any howdy logs +log_path = PurePath("@log_path@") + +# Define the absolute path to the Howdy data directory +data_dir = PurePath("@data_dir@") \ No newline at end of file diff --git a/howdy/src/paths_factory.py b/howdy/src/paths_factory.py new file mode 100644 index 0000000..5716ca5 --- /dev/null +++ b/howdy/src/paths_factory.py @@ -0,0 +1,48 @@ +from pathlib import PurePath +import paths + +models = [ + "shape_predictor_5_face_landmarks.dat", + "mmod_human_face_detector.dat", + "dlib_face_recognition_resnet_model_v1.dat", +] + + +def dlib_data_dir_path() -> str: + return str(paths.dlib_data_dir) + + +def shape_predictor_5_face_landmarks_path() -> str: + return str(paths.dlib_data_dir / models[0]) + + +def mmod_human_face_detector_path() -> str: + return str(paths.dlib_data_dir / models[1]) + + +def dlib_face_recognition_resnet_model_v1_path() -> str: + return str(paths.dlib_data_dir / models[2]) + + +def user_model_path(user: str) -> str: + return str(paths.user_models_dir / f"{user}.dat") + + +def config_file_path() -> str: + return str(paths.config_dir / "config.ini") + + +def snapshots_dir_path() -> PurePath: + return paths.log_path / "snapshots" + + +def snapshot_path(snapshot: str) -> str: + return str(snapshots_dir_path() / snapshot) + + +def user_models_dir_path() -> PurePath: + return paths.user_models_dir + + +def logo_path() -> str: + return str(paths.data_dir / "logo.png") diff --git a/howdy/src/snapshot.py b/howdy/src/snapshot.py index 324b578..ae40d47 100644 --- a/howdy/src/snapshot.py +++ b/howdy/src/snapshot.py @@ -5,6 +5,7 @@ import cv2 import os import datetime import numpy as np +import paths_factory def generate(frames, text_lines): @@ -14,8 +15,6 @@ def generate(frames, text_lines): if len(frames) == 0: return - # Get the path to the containing folder - core_path = os.path.dirname(os.path.abspath(__file__)) # Get frame dimensions frame_height, frame_width, cc = frames[0].shape # Spread the given frames out horizontally @@ -31,7 +30,7 @@ def generate(frames, text_lines): # Add the Howdy logo if there's space to do so if len(frames) > 1: # Load the logo from file - logo = cv2.imread(core_path + "/logo.png") + logo = cv2.imread(paths_factory.logo_path()) # Calculate the position of the logo logo_y = frame_height + 20 logo_x = frame_width * len(frames) - 210 @@ -49,19 +48,15 @@ def generate(frames, text_lines): line_number += 1 - # Define path to any howdy logs - log_path = "/var/log/howdy" - # Made sure a snapshot folder exist - if not os.path.exists(log_path): - os.makedirs(log_path) - if not os.path.exists(log_path + "/snapshots"): - os.makedirs(log_path + "/snapshots") + if not os.path.exists(paths_factory.snapshots_dir_path()): + os.makedirs(paths_factory.snapshots_dir_path()) # Generate a filename based on the current time filename = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S.jpg") + filepath = paths_factory.snapshot_path(filename) # Write the image to that file - cv2.imwrite(log_path + "/snapshots/" + filename, snap) + cv2.imwrite(filepath, snap) # Return the saved file location - return log_path + "/snapshots/" + filename + return filepath diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..c2150df --- /dev/null +++ b/meson.build @@ -0,0 +1,19 @@ +project('howdy', 'cpp', license: 'MIT', version: 'beta', meson_version: '>= 0.64.0') + +dlibdatadir = get_option('dlib_data_dir') != '' ? get_option('dlib_data_dir') : join_paths(get_option('prefix'), get_option('datadir'), 'dlib-data') +confdir = get_option('config_dir') != '' ? get_option('config_dir') : join_paths(get_option('prefix'), get_option('sysconfdir'), 'howdy') +usermodelsdir = get_option('user_models_dir') != '' ? get_option('user_models_dir') : join_paths(confdir, 'models') +logpath = get_option('log_path') + +config_path = join_paths(confdir, 'config.ini') + +paths_dict = { + 'config_dir': confdir, + 'dlib_data_dir': dlibdatadir, + 'user_models_dir': usermodelsdir, + 'log_path': logpath, +} + +# We need to keep this order beause howdy-gtk defines the gtk script path +subdir('howdy-gtk') +subdir('howdy') \ No newline at end of file diff --git a/meson.options b/meson.options new file mode 100644 index 0000000..1a35b81 --- /dev/null +++ b/meson.options @@ -0,0 +1,9 @@ +option('pam_dir', type: 'string', value: '', description: 'Set the pam_howdy destination directory') +#option('fetch_dlib_data', type: 'boolean', value: false, description: 'Download dlib data files') +option('config_dir', type: 'string', value: '', description: 'Set the howdy config directory') +option('dlib_data_dir', type: 'string', value: '', description: 'Set the dlib data directory') +option('user_models_dir', type: 'string', value: '', description: 'Set the user models directory') +option('log_path', type: 'string', value: '/var/log/howdy', description: 'Set the log file path') +option('install_in_site_packages', type: 'boolean', value: false, description: 'Install howdy python files in site packages') +option('py_sources_dir', type: 'string', value: '', description: 'Set the python sources directory') +option('install_pam_config', type: 'boolean', value: false, description: 'Install pam config file (for Debian/Ubuntu)') \ No newline at end of file diff --git a/meson_options.txt b/meson_options.txt new file mode 120000 index 0000000..7b28df2 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +meson.options \ No newline at end of file