diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d83ff6eb..4f37de5e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,8 +11,8 @@ _Please describe the issue in as much detail as possible, including any errors a ---- -- [ ] I've searched for similar issues already, and my issue has not been reported yet. +I've searched for similar issues already, and my issue has not been reported yet. -Linux distribution (if applicable): +Linux distribution (if applicable): -Howdy version: +Howdy version: diff --git a/.travis.yml b/.travis.yml index 24f64115..e3eb3ce1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,26 @@ 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 +python: "3.6" script: # Build the binary (.deb) - debuild -i -us -uc -b - # Install the binary, also fireing the debian scripts + # Install the binary, running the debian scripts in the process - sudo apt install ../*.deb -y + + # Confirm the cv2 module has been installed correctly + - sudo /usr/bin/env python3 -c "import cv2; print(cv2.__version__);" + # Confirm the face_recognition module has been installed correctly + - sudo /usr/bin/env python3 -c "import face_recognition; print(face_recognition.__version__);" + # 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 @@ -27,3 +29,8 @@ notifications: addons: apt: update: true + packages: + - dh-make + - ack-grep + - devscripts + - fakeroot diff --git a/debian/changelog b/debian/changelog index 2c541225..6e8f3097 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +howdy (2.4.0) xenial; urgency=medium + + * Cameras are now selected by path instead of by video device number (thanks @Rhiyo!) + * Added fallbacks to $EDITOR for the config command (thanks @yassineim!) + * Fixed missing cv2 module after installation (thanks @bendandersen and many others!) + * Fixed file permissions crashing Howdy in some cases (thanks @GJDitchfield!) + * Fixed howdy using python3 from local virtual environment (thanks @EdwardJB!) + + -- boltgolt Fri, 09 Nov 2018 20:59:45 +0100 + howdy (2.3.1) xenial; urgency=high * Fixed issue where `frame_width` and `frame_height` would be completely ignored (thanks @janecz-n!) diff --git a/debian/control b/debian/control index 0bf354c4..8d2efa16 100644 --- a/debian/control +++ b/debian/control @@ -2,14 +2,14 @@ Source: howdy Section: misc Priority: optional Standards-Version: 3.9.7 -Build-Depends: python, dh-python, devscripts, dh-make, debhelper +Build-Depends: python, dh-python, devscripts, dh-make, debhelper, fakeroot Maintainer: boltgolt 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 +Depends: ${misc:Depends}, git, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, python-opencv, cmake, streamer Description: Howdy: Windows Hello style authentication for Linux. Use your built-in IR emitters and camera in combination with face recognition to prove who you are. diff --git a/debian/howdy.lintian-overrides b/debian/howdy.lintian-overrides new file mode 100644 index 00000000..2160448a --- /dev/null +++ b/debian/howdy.lintian-overrides @@ -0,0 +1,7 @@ +# W: Don't require ugly linebreaks in last 5 chars +howdy: debian-changelog-line-too-long + +# E: Allows the name Howdy to show up in Ubuntu updater +howdy: description-starts-with-package-name +# E: Allows python for installation scripts +howdy: unknown-control-interpreter diff --git a/debian/postinst b/debian/postinst index 665b2f3a..dffcc5d2 100755 --- a/debian/postinst +++ b/debian/postinst @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # Installation script to install howdy # Executed after primary apt install @@ -53,6 +53,11 @@ if not os.path.exists("/tmp/howdy_picked_device"): # Go through every setting in the old config and apply it to the new file for section in oldConf.sections(): for (key, value) in oldConf.items(section): + # If config is still using the old device_id parameter, convert it to a path + if key == "device_id": + key = "device_path" + value = "/dev/video" + value + try: newConf.set(section, key, value) # Add a new section where needed @@ -98,7 +103,7 @@ print("Temporary dlib files removed") log("Installing python dependencies") -# Install face_recognition though pip +# Install direct dependencies so pip does not freak out with the manual dlib install 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") @@ -106,19 +111,26 @@ 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"])) +try: + import cv2 +except Exception as e: + log("Reinstalling opencv2") + handleStatus(subprocess.call(["pip3", "install", "opencv-python"])) + 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="") + print(line.replace("device_path = none", "device_path = " + picked), end="") +print("Camera ID saved") # Secure the howdy folder -handleStatus(subprocess.call(["chmod 600 -R /lib/security/howdy/"], shell=True)) +handleStatus(subprocess.call(["chmod 744 -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)) +handleStatus(subprocess.call(["chmod 755 /lib/security/howdy/cli.py"], shell=True)) +handleStatus(subprocess.call(["chmod 755 -R /lib/security/howdy/cli"], shell=True)) print("Permissions set") # Make the CLI executable as howdy @@ -198,33 +210,5 @@ 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 +# Sign off 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)) diff --git a/debian/preinst b/debian/preinst index a918b1b7..4acfb392 100755 --- a/debian/preinst +++ b/debian/preinst @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # Used to check cameras before commiting to install # Executed before primary apt install of files @@ -35,7 +35,7 @@ if "install" not in sys.argv: sys.exit(0) # The default picked video device id -picked = -1 +picked = "none" print(col(1) + "Starting IR camera check...\n" + col(0)) @@ -45,64 +45,62 @@ if "HOWDY_NO_PROMPT" in os.environ: # Write the default device to disk and exit with open("/tmp/howdy_picked_device", "w") as out_file: - out_file.write("0") + out_file.write("none") sys.exit(0) # Get all devices -devices = os.listdir("/dev") +devices = os.listdir("/dev/v4l/by-path") # 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 + time.sleep(.5) + + # The full path to the device is the default name + device_name = "/dev/v4l/by-path/" + 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(["streamer -t 1:0:0 -c /dev/v4l/by-path/" + dev + " -b 16 -f rgb24 -o /dev/null 1>/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().strip() == "y" or ans.lower().strip() == "yes": - picked = dev[5:] - break - else: - print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0)) + # Set this camera as picked if the answer was yes, go to the next one if no + if ans.lower().strip() == "y" or ans.lower().strip() == "yes": + picked = dev + break + else: + print("Interpreting as a " + col(3) + "\"NO\"\n" + col(0)) # Abort if no camera was picked -if picked == -1: +if picked == "none": 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)) + out_file.write("/dev/v4l/by-path/" + picked) # Add a line break print("") diff --git a/debian/prerm b/debian/prerm index 542a63c7..e0c51269 100755 --- a/debian/prerm +++ b/debian/prerm @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # Executed on deinstallation # Completely remove howdy from the system diff --git a/debian/rules b/debian/rules index 6d3bdcb1..08b08809 100755 --- a/debian/rules +++ b/debian/rules @@ -6,6 +6,3 @@ include /usr/share/dpkg/default.mk %: dh $@ - -binary: - dh binary diff --git a/src/cli/add.py b/src/cli/add.py index 2d82c069..8df9c656 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -79,7 +79,7 @@ } # Open the camera -video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) +video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true if config.get("video", "force_mjpeg") == "true": diff --git a/src/cli/config.py b/src/cli/config.py index 9606f243..aaa55182 100644 --- a/src/cli/config.py +++ b/src/cli/config.py @@ -1,12 +1,20 @@ -# Open the config file in gedit +# Open the config file in an editor # Import required modules import os -import time import subprocess # Let the user know what we're doing print("Opening config.ini in the default editor") -# Open gedit as a subprocess and fork it -subprocess.call(["/etc/alternatives/editor", os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"]) +# Default to the nano editor +editor = "/bin/nano" + +# Use the user preferred editor if available +if os.path.isfile("/etc/alternatives/editor"): + editor = "/etc/alternatives/editor" +elif "EDITOR" in os.environ: + editor = os.environ["EDITOR"] + +# Open the editor as a subprocess and fork it +subprocess.call([editor, os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"]) diff --git a/src/cli/test.py b/src/cli/test.py index 6321d987..3a550531 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -18,7 +18,7 @@ config.read(path + "/../config.ini") # Start capturing from the configured webcam -video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) +video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true if config.get("video", "force_mjpeg") == "true": diff --git a/src/compare.py b/src/compare.py index 03d89dce..62824d64 100644 --- a/src/compare.py +++ b/src/compare.py @@ -58,7 +58,7 @@ def stop(status): timings.append(time.time()) # Start video capture on the IR camera -video_capture = cv2.VideoCapture(int(config.get("video", "device_id"))) +video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true if config.get("video", "force_mjpeg") == "true": diff --git a/src/config.ini b/src/config.ini index 4e4416e8..1f577e97 100644 --- a/src/config.ini +++ b/src/config.ini @@ -29,9 +29,9 @@ certainty = 3.5 # The number of seconds to search before timing out timout = 4 -# The /dev/videoX id to capture frames from +# The path of the device to capture frames from # Should be set automatically by the installer -device_id = 1 +device_path = none # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise diff --git a/src/pam.py b/src/pam.py index 5fd462e5..42ee9199 100644 --- a/src/pam.py +++ b/src/pam.py @@ -13,7 +13,7 @@ config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") def doAuth(pamh): - """Start authentication in a seperate process""" + """Starts authentication in a seperate process""" # Abort is Howdy is disabled if config.get("core", "disabled") == "true": @@ -25,7 +25,7 @@ def doAuth(pamh): sys.exit(0) # 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(["/usr/bin/python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()]) # Status 10 means we couldn't find any face models if status == 10: @@ -62,9 +62,9 @@ def pam_sm_open_session(pamh, flags, args): return doAuth(pamh) def pam_sm_close_session(pamh, flags, argv): - """We don't need to clean anyting up at the end of a session, so return true""" + """We don't need to clean anyting up at the end of a session, so returns true""" return pamh.PAM_SUCCESS def pam_sm_setcred(pamh, flags, argv): - """We don't need set any credentials, so return true""" + """We don't need set any credentials, so returns true""" return pamh.PAM_SUCCESS