0
0
Fork 0
mirror of https://github.com/boltgolt/howdy.git synced 2024-09-12 09:41:18 +02:00

Merge pull request #803 from musikid/meson

build: add meson build system
This commit is contained in:
boltgolt 2023-09-14 10:34:04 +02:00 committed by GitHub
commit 95df4d2d48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 530 additions and 135 deletions

7
.clang-tidy Normal file
View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
#!/bin/sh
env python3 "@script_path@" "$@"

82
howdy-gtk/meson.build Normal file
View file

@ -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',
)

View file

@ -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)

View file

@ -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()

13
howdy-gtk/src/paths.py.in Normal file
View file

@ -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@")

View file

@ -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

View file

@ -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"))

View file

@ -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()

View file

@ -1 +1 @@
debian/howdy.1
howdy.1

1
howdy/meson.build Normal file
View file

@ -0,0 +1 @@
subdir('src')

View file

@ -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

3
howdy/src/bin/howdy.in Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
env python3 "@script_path@" "$@"

View file

@ -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:

View file

@ -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"))

View file

@ -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()])

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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"]

View file

@ -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

170
howdy/src/meson.build Normal file
View file

@ -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',
)

View file

@ -1,6 +0,0 @@
Name: Howdy
Default: yes
Priority: 512
Auth-Type: Primary
Auth:
[success=end default=ignore] /lib/security/howdy/pam_howdy.so

View file

@ -0,0 +1,6 @@
Name: Howdy
Default: yes
Priority: 512
Auth-Type: Primary
Auth:
[success=end default=ignore] @pamdir@/pam_howdy.so

View file

@ -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

1
howdy/src/pam/.clang-tidy Symbolic link
View file

@ -0,0 +1 @@
../.clang-tidy

View file

@ -9,6 +9,7 @@
#include <spawn.h>
#include <stdexcept>
#include <sys/signalfd.h>
#include <sys/stat.h>
#include <sys/syslog.h>
#include <sys/types.h>
#include <sys/wait.h>
@ -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<int, std::chrono::milliseconds::period>(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<const char **>(&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<const char **>(&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<int, char *>(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<std::mutex> 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

View file

@ -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: ''
)

View file

@ -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@";

View file

@ -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' ]
)

16
howdy/src/paths.py.in Normal file
View file

@ -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@")

View file

@ -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")

View file

@ -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

19
meson.build Normal file
View file

@ -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')

9
meson.options Normal file
View file

@ -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)')

1
meson_options.txt Symbolic link
View file

@ -0,0 +1 @@
meson.options